Skip to main content

Node

A Node is the smallest composable unit in SiMa.ai Neat. Nodes are the things a Graph wires together: decode stages, preprocess stages, model stages, postprocess stages, sources, sinks, and application boundaries.

Reference:

What a Node represents

A node contributes one logical piece of work plus the metadata the builder needs to wire it safely:

  • runtime work, such as decode, convert, preprocess, inference, postprocess, or sink behavior;
  • input/output contracts, such as expected media type, tensor shape, dtype, or caps;
  • deterministic backend naming, so describe(), diagnostics, metrics, and probes stay stable.

The main rule is simple:

Graph = nodes wired together

Model is the model-aware node source. A Model can be added directly to a Graph, or can provide reusable stage fragments such as preprocess, inference, and postprocess.

Pre-built node groups

Some common patterns are more than one low-level node. Neat exposes those as pre-built groups so users do not have to hand-wire every source, decoder, converter, queue, or sink.

Think of a group as a reusable mini-graph: a named bundle of nodes with a clear contract.

Examples:

  • VideoInputGroup(...): file/video source + decode path.
  • RtspDecodedInput(...): RTSP source + decode path.
  • model route fragments: preprocess + inference + decode/postprocess around a compiled model.
#include "neat/runtime.h"
#include "neat/node_groups.h"

simaai::neat::Graph graph;

simaai::neat::nodes::groups::VideoInputGroupOptions vopt;
vopt.path = "/data/sample.mp4";
graph.add(simaai::neat::nodes::groups::VideoInputGroup(vopt));

Use pre-built groups when the shape is standard and the interesting part of your app is not the internal media plumbing. If you need custom topology, build the equivalent Graph yourself from lower-level nodes.

Pick a node or group

Use this map before you open the full API reference.

TaskC++ starting pointPython starting point
App pushes tensors, images, or samplesnodes::Input("name", options)pyneat.nodes.input("name", options)
App pulls a full samplenodes::Output("name", options)pyneat.nodes.output("name", options)
Read frames from a filenodes::FileInput(...) plus decode nodes, or nodes::groups::VideoInputGroup(...)pyneat.nodes.file_input(...) plus decode nodes, or pyneat.groups.video_input(...)
Read one image as a sourcenodes::StillImageInput(...)pyneat.nodes.still_image_input(...)
Read an RTSP streamnodes::RTSPInput(...) or nodes::groups::RtspDecodedInput(...)pyneat.nodes.rtsp_input(...) or pyneat.groups.rtsp_decoded_input(...)
Decode imagesnodes::ImageDecode() or nodes::JpegDecode()pyneat.nodes.image_decode() or pyneat.nodes.jpeg_decode()
Scale or rate-limit videonodes::VideoScale(), VideoRate(), ImageFreeze()pyneat.nodes.video_scale(), video_rate(), image_freeze()
Build a reusable image input pathnodes::groups::ImageInputGroup(...)pyneat.groups.image_input(...)
Build a reusable video input pathnodes::groups::VideoInputGroup(...)pyneat.groups.video_input(...)
Send video outnodes::groups::VideoSender(...) or H.264/UDP output helperspyneat.groups.video_sender(...) or H.264/UDP output helpers
Send metadata outMetadataSenderpyneat.MetadataSender
Render detectionsnodes::SimaRender(...)pyneat.nodes.sima_render(...)
Convert logits to class indexnodes::SimaArgMax(...)pyneat.nodes.sima_argmax(...)

Use groups when you want a tested fragment. Use individual nodes when you need explicit topology. Use Graph helpers such as graphs::Branch(...) and graphs::Combine(...) for fan-out and fan-in instead of hiding topology behind duplicated outputs.

Source-owned and sink groups

Use source-owned groups when the graph should read media by itself. Build those graphs without app input, then pull outputs or let sink nodes handle delivery. Use sink groups when the graph should send video out without your app pulling every frame; a sink graph can still be app-pushed if it also has an Input boundary.

NeedGroupKey fields to set firstCommon follow-up fields
Still image fixturenodes::groups::ImageInputGroup(...) / pyneat.groups.image_input(...)pathfps, imagefreeze_num_buffers, output_caps
Video filenodes::groups::VideoInputGroup(...) / pyneat.groups.video_input(...)pathout_format, output_caps, sync_mode
Live RTSP H.264 streamnodes::groups::RtspDecodedInput(...) / pyneat.groups.rtsp_decoded_input(...)urllatency_ms, tcp, fallback H.264 width/height/FPS, out_format, output_caps
Raw or encoded video outputnodes::groups::VideoSender(...) / pyneat.groups.video_sender(...)VideoSenderOptions::H264RtpUdpFromRaw(...) or H264RtpUdpFromEncoded()host, channel, video_port_base, encoder/RTP options
RTP/H.264 over UDP outputnodes::groups::UdpH264OutputGroup(...) / pyneat.groups.udp_h264_output_group(...)destination host/port when the defaults are not righth264_caps, payload_type, config_interval

A source-owned graph is a different runtime shape than an app-pushed graph:

source group -> model -> output

Run it with graph.build() or graph.run() without an input argument. Do not add an Input("image") in front of a source group unless your app really has a second public input to push.

simaai::neat::nodes::groups::RtspDecodedInputOptions source_options;
source_options.url = "rtsp://camera.example/live";

simaai::neat::Graph graph("rtsp_detector");
graph.add(simaai::neat::nodes::groups::RtspDecodedInput(source_options));
graph.add(model);
graph.add(simaai::neat::nodes::Output(
"detections",
simaai::neat::OutputOptions::Latest()));

auto run = graph.build();

Start with a group when the media plumbing is standard. Drop to individual nodes only when you need a custom topology or a source format the group does not describe.

Common SiMa nodes

Some SiMa nodes are common enough that they have focused reference or how-to pages:

  • Preproc: fused CVU image preprocessing for resize, color conversion, normalization, quantization, tessellation, and runtime ROI lists.
  • SimaBoxDecode: detection postprocessing that decodes model heads into bounding boxes and uses upstream preprocess metadata for coordinate mapping.

Boundary nodes: Input and Output

Input and Output are nodes too. They are special because they describe where data enters or leaves a graph fragment.

The important mental model:

Input("name") and Output("name") are named doors.

At the outside of the final app, those doors become public runtime APIs:

  • Input("image") becomes run.push("image", ...).
  • Output("classes") becomes run.pull("classes", ...).

Inside a larger graph, those same doors are just connection points. Neat removes the internal boundary nodes while building the executable runtime path and wires the real work directly.

simaai::neat::Graph route("route");
route.add(simaai::neat::nodes::Input("image"));
route.add(model);
route.add(simaai::neat::nodes::Output("classes"));

Used as a complete app:

auto run = route.build();
run.push("image", simaai::neat::TensorList{image_tensor});
auto classes = run.pull("classes");

Used inside a larger app:

simaai::neat::Graph app("app");
app.connect(camera, route);
app.connect(route, telemetry);

Conceptually, the internal boundary nodes lower to direct wiring:

camera -> model -> telemetry

The names are still preserved for diagnostics, endpoint inspection, metrics, and visualization. They just do not create extra queues, copies, or fake runtime sinks in the middle of the app.

Labels are not endpoints

Keep these names separate:

NameWhat it isDoes it create push(...) / pull(...)?
Graph("detector")Graph label for diagnostics, exports, and logs.No.
nodes::Input("image") / pyneat.nodes.input("image")Public input boundary when it remains on the outside of the final graph.Yes: run.push("image", ...).
nodes::Output("detections") / pyneat.nodes.output("detections")Public output boundary when it remains on the outside of the final graph.Yes: run.pull("detections", ...).
Node or element namesBackend names used in diagnostics and pipeline lowering.No, unless exposed through an Input or Output boundary.

If a runtime call says the endpoint name is unknown, inspect run.input_names() and run.output_names() instead of guessing. Names are cheap; wrong names are expensive.

App-pushed input

Use nodes::Input when your application already has the frame or tensor and wants to push it into Neat.

Use InputOptions when the boundary needs an explicit payload, format, size, timing, or allocation contract.

NeedField
Payload familypayload_type / PayloadType
Formatformat / FormatTag
Fixed dimensionswidth, height, depth
Dynamic input limitsmax_width, max_height, max_depth, max_bytes
Timing and capsfps_n, fps_d, caps_override
Live-source behavioris_live, do_timestamp, block, stream_type
Allocationuse_simaai_pool, pool_min_buffers, pool_max_buffers, memory_policy
Advanced metadatabuffer_name, preprocess_meta

Leave fields unset unless the graph boundary needs the contract. Guessing caps is how you summon the swamp.

simaai::neat::Graph graph;

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Image;
iopt.format = simaai::neat::FormatTag::RGB;
iopt.width = 224;
iopt.height = 224;
graph.add(simaai::neat::nodes::Input("image", iopt));

Input boundary recipes

Use these patterns when the boundary contract matters before the first push.

BoundarySetLeave unset when
Fixed-size decoded imagepayload_type, format, width, heightThe pushed tensor or sample already carries complete image metadata.
Dynamic decoded imagepayload_type, format, max_width, max_height, and optional max_bytesThe graph only accepts one fixed size.
Encoded H.264 inputpayload_type = Encoded, format = H264, and caps metadata on the pushed SampleThe graph source owns the stream through an RTSP or file group.
Detection coordinate mappingpreprocess_metaThe model route owns preprocessing and emits the metadata itself.

For dynamic image input, bound the stream without pretending every frame has the same size:

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Image;
iopt.format = simaai::neat::FormatTag::BGR;
iopt.max_width = 1920;
iopt.max_height = 1080;
iopt.max_bytes = 1920 * 1080 * 3;

graph.add(simaai::neat::nodes::Input("image", iopt));

For encoded H.264 that your app pushes, make the boundary encoded and put the caps on the sample:

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Encoded;
iopt.format = simaai::neat::FormatTag::H264;

graph.add(simaai::neat::nodes::Input("video", iopt));
auto run = graph.build();

std::vector<std::uint8_t> bytes = /* your H.264 access unit */;
auto sample = simaai::neat::make_encoded_sample(
std::move(bytes),
"video/x-h264,stream-format=byte-stream,alignment=au");
sample.port_name = "video";
run.push("video", sample);

If you already use RtspDecodedInput(...) or VideoInputGroup(...), do not add this app-pushed boundary in front of it. Source-owned groups own their input path.

Sample output

Use nodes::Output when the consumer needs a full Sample: payload plus stream/frame/timestamp metadata.

Use OutputOptions when pull behavior matters.

GoalUse
Keep the freshest outputOutputOptions::Latest() / pyneat.OutputOptions.latest()
Pull every frameOutputOptions::EveryFrame(...) / pyneat.OutputOptions.every_frame(...)
Pace output to the graph clockOutputOptions::Clocked(...) / pyneat.OutputOptions.clocked(...)
Combine multiple producerscombine_policy = CombinePolicy::ByFrame or ByPts
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input("image"));
graph.add(model);
graph.add(simaai::neat::nodes::Output(
"classes",
simaai::neat::OutputOptions::EveryFrame()));

const auto run = graph.build();

OutputOptions has four knobs, but the presets cover most apps:

FieldWhat it controlsPrefer
max_buffersHow many output samples may queue before backpressure or dropping.Latest(), EveryFrame(...), or Clocked(...) presets.
dropWhether overflow drops the oldest queued sample instead of blocking.Latest() for live preview-style output.
syncWhether output syncs to the graph clock.Clocked(...) for clock-paced output.
combine_policyHow one public Output combines several upstream producers.CombinePolicy::ByFrame only with valid frame_id; ByPts only with valid pts_ns.

Use EveryFrame(...) for records you must not lose. Use Latest() when freshness beats history. Do not make a live preview behave like a courtroom transcript.

Advanced: adapt image or video output to a tensor

For normal model or graph outputs, stop here: use nodes::Output. If the payload is tensor-only and you do not need the full Sample envelope, pull it with pull_tensors(...).

Use Graph::add_output_tensor(...) only when image or video output must be converted, scaled, or rate-adjusted into a CPU-friendly UInt8 tensor before the app pulls it. The helper inserts the adapter path and creates an unnamed output. With one output, pull it with run.pull_tensors(...).

OutputTensorOptions controls format, target_width, target_height, and target_fps. Keep dtype as UInt8; the current public path rejects other dtypes. Most model outputs do not need this helper.

Quick decision guide

  • Source is file, camera, or RTSP: use a pre-built input group from nodes::groups.
  • Source is app-produced tensor/frame: use nodes::Input.
  • Output consumer needs stream/frame/timestamp metadata: use nodes::Output and pull Sample objects.
  • Output is a model or tensor payload: use nodes::Output and pull tensors with pull_tensors(...) when you do not need the full Sample.
  • Output is image or video that must be resized, reformatted, or rate-adjusted before pull: use the advanced add_output_tensor(...) adapter.
  • Need a reusable bundle: make a Graph fragment from nodes and add/connect it like any other building block.

Why this matters

  • One concept covers atomic stages, boundary declarations, and pre-built groups.
  • Reusable graph fragments behave like functions: inputs in, outputs out, no accidental runtime sources/sinks in the middle.
  • Diagnostics stay friendly because node names and boundary names survive lowering.
  • Runtime stays efficient because internal boundaries do not force hidden copies or queues.

See also

Tutorials