Pipeline#
Define a pipeline once, then run it in batch or live with identical
results. Input names a source stream; calling functors and stream operators on
those handles records the pipeline. Pipeline(inputs=[...], outputs=[...]) compiles it
into a callable: pipe(arrays) runs it in batch, pipe(generators) runs it event
by event (lazy pull path), and pipe.live() opens an incremental session you drive
yourself. All three produce byte-identical output on the same events.
For the conceptual model (how the pipeline is built, when to reach for it, and the define-once-run-anywhere guarantee), see Pipelines. This page is the reference contract.
- class Input(name)[source]#
Create a source Node - a named placeholder for a timed stream.
- __call__(**kwargs)#
Call self as a function.
- __init__(**kwargs)#
- class Node(op, inputs=())[source]#
An immutable handle for a stream inside a pipeline.
You do not usually construct a Node directly:
Input(name)returns one, and applying a functor or a stream operator to a Node returns another.oprecords the operation (an input, a functor instance, or an operator) andinputsis a tuple of upstream Nodes.
- class Pipeline(inputs, outputs, align_outputs=True)[source]#
A reusable N-in / M-out function you define once and call on stored or live data.
Arguments:
inputs: ordered list ofInput(...)nodes that define the call signature. Feeds are bound positionally, or by name via a keyword call.outputs: ordered list of output nodes to evaluate.align_outputs(defaultTrue): whenTrue, co-index all M outputs onto a shared, sorted index axis, so each output carries its as-of value at every unique union index (combine_latest's per-event intermediate rows are collapsed to one row per index) and the returned (values, index) pairs have equal length. WhenFalse, return independent per-output streams whose lengths may differ.
Call
pipe(*feeds)(positional) orpipe(**named_feeds)(by Input name) to run the pipeline. Each feed may be a bare value array (positional, index = row-number), or a(values, index)pair (values-first). Pass generators of(value, index)pairs to run the pipeline lazily, event by event, with byte-identical results (the lazy pull path). Returns a single(values, index)pair when M == 1, or a tuple of pairs when M > 1.- live()[source]#
Open a live streaming session: push events and drive a clock yourself.
Bind data AFTER definition, event by event, on the same Pipeline that runs batch. Push events with .push(input, index, value); close windows whose boundary has passed with .advance(now) (e.g. on a clock tick, finalizing empty bars); force the current partial window with .flush(); collect aligned outputs with .result().
The session drives this Pipeline's single compiled engine (shared with __call__), resetting it on open, so use one session at a time: do not interleave a live session with a batch call or run two live sessions concurrently.
The three names#
Input(name): a function that returns aNode, a named source handle. Theinputslist of aPipelineis made of these, and their order and names define the pipeline's call signature.Node: the handle type for a stream inside the pipeline. It is user-facing as a type, but you rarely constructNode(...)by hand; you obtain nodes fromInput(...)and by applying functors and stream operators to existing nodes.Pipeline: the compiled, runnable object built frominputsandoutputs.
Constructor#
Pipeline(inputs, outputs, align_outputs=True)
inputs: a list ofInput(...)nodes. Defines the call signature; feeds are bound positionally in this order, or by the input names.outputs: a list of nodes to evaluate.align_outputs: whenTrue(default), all outputs are co-indexed onto a single shared index, so every output is an equal-length(values, index)pair. WhenFalse, each output is returned as an independent stream whose length may differ from the others.
The constructor validates the pipeline and raises a clear ValueError if: an
inputs entry is not an Input(...) node; an outputs entry is not a Node;
an output references an undeclared input; a declared input is never used; or a
single functor instance backs more than one node.
Calling the pipeline#
Feeds are passed positionally in input order, pipe(*feeds), or by input name,
pipe(**named_feeds). Each feed may be:
a bare array (positional, with the index taken as the row number), or
a
(values, index)pair.
The return is always (values, index) tuples:
one output, a single
(values, index)pair,multiple outputs, a tuple of such pairs, one per output.
Pass generators of (value, index) pairs instead of arrays to run the pipeline
lazily, event by event: pipe(gen_a, gen_b) returns an iterator that yields
output events byte-identical to the batch result.
Live, incremental sessions: pipe.live()#
pipe(arrays) and pipe(generators) both consume complete feeds. When you drive
the pipeline yourself, one event at a time, pipe.live() opens a session object. It
shares the pipeline's single engine and resets it on open, so use one
session at a time (do not interleave it with a pipe(...) call). Each method
returns the session, so calls chain.
.push(input, index, value)feeds one event.inputis anInputname (str) or its position (int) in theinputslist;indexis the event's integer index;valueis the float value..advance(now)moves logical time tonow(an integer index), closing every windowing node whose bucket boundary has passed bynow. This finalizes time-based bars even when no event fell in them, for example an empty minute bar closed by a clock tick. It is a no-op for event-count (count=) windows and before the first event; call it with non-decreasingnow..flush()finalizes the current partial window(s) on demand, for example at the end of a processing loop. The end-of-input flush thatpipe(...)performs implicitly is the special case..result()returns the output accumulated so far, in the same shapepipe(...)returns, and drains the internal buffers.
Feeding the same events in index order and then calling .flush() reproduces the
batch result exactly. .advance() (and a clock input wired into the pipeline)
additionally let a windowing node emit bars a purely event-driven pass would not,
the empty leading and trailing bars in
Resample, for instance.
import numpy as np
from screamer import Input, Pipeline, First, Last, Resample, CombineLatest
price = Input("price")
open_b = Resample(freq=10, fill="nan", agg=First())(price)
close_b = Resample(freq=10, fill="nan", agg=Last())(price)
bars = CombineLatest()(open_b, close_b)
pipe = Pipeline(inputs=[price], outputs=[bars])
live = pipe.live()
live.push("price", 0, 100.0) # one trade in bar [0, 10)
live.advance(30) # clock passes bars [10,20) and [20,30): empty -> NaN
live.flush()
values, index = live.result()
print(index)
print(values)
[ 0 10 20]
[[100. nan]
[ nan nan]
[ nan nan]]
Example#
Align two streams and take their difference. The same pipeline runs in batch and live.
import numpy as np
from screamer import Input, Pipeline, Sub, CombineLatest
a, b = Input("a"), Input("b")
pipe = Pipeline(inputs=[a, b], outputs=[Sub()(CombineLatest()(a, b))])
# feeds are (values, index) - values-first
fa = (np.array([10.0, 20.0, 30.0]), np.array([1, 2, 3]))
fb = (np.array([1.0, 2.0, 3.0]), np.array([1, 2, 3]))
spread, idx = pipe(fa, fb)
print(spread.reshape(-1))
# pipe(generators) returns the identical result, event by event (lazy path)
events = list(pipe(
((v, k) for v, k in zip(fa[0], fa[1])),
((v, k) for v, k in zip(fb[0], fb[1])),
))
sv = np.array([e[0] for e in events])
print(np.array_equal(spread.reshape(-1), sv, equal_nan=True))
[ 9. 18. 27.]
True
Inspecting a pipeline#
print(pipe) (or pipe.to_text()) shows the pipeline as an indented tree, rooted at
each output and descending to the inputs. A node shared by several consumers is
printed once and then referenced by id, so a diamond reads as a diamond. Node
labels carry the functor and operator parameters (RollingMean(window_size=20),
Resample(every=5, ...)).
For a diagram, pipe.to_dot() returns a Graphviz DOT string with no dependencies,
and pipe.to_graphviz() returns a rendered graphviz.Source when the optional
graphviz package is installed (pip install screamer[viz], plus the system
Graphviz dot binary). In a Jupyter notebook, displaying a Pipeline shows the diagram
inline, falling back to the text tree when graphviz is not available.
from screamer import Input, Pipeline, RollingMean, Sub, CombineLatest
a, b = Input("a"), Input("b")
pipe = Pipeline(inputs=[a, b], outputs=[RollingMean(20)(Sub()(CombineLatest()(a, b)))])
print(pipe.to_text())
Pipeline(2 input(s), 1 output(s), align_outputs=True)
out[0] = RollingMean(window_size=20) #4
└─ Sub() #3
└─ CombineLatest(emit='when_all') #2
├─ a #0 (input)
└─ b #1 (input)
Saving and loading a pipeline#
pipe.to_json() serializes the pipeline to JSON (its inputs, nodes with their
parameters, outputs, and align_outputs), and Pipeline.from_json(text) rebuilds a
runnable Pipeline from it. This round-trips exactly, so a pipeline can be saved as a
config file and reloaded. to_dict / from_dict give the same round-trip with a
plain dict.
import numpy as np
from screamer import Input, Pipeline, RollingMean, Sub, CombineLatest
a, b = Input("a"), Input("b")
pipe = Pipeline(inputs=[a, b], outputs=[RollingMean(20)(Sub()(CombineLatest()(a, b)))])
config = pipe.to_json() # save this string to a file
restored = Pipeline.from_json(config)
fa = (np.arange(1.0, 61), np.arange(1, 61))
fb = (np.arange(0.0, 60), np.arange(1, 61))
print(np.array_equal(pipe(fa, fb)[0], restored(fa, fb)[0], equal_nan=True))
True
See also#
Pipelines: the conceptual model and walkthrough.
Streams, values, and alignment: the alignment model the pipeline relies on.