Python Streaming API¶
doppler.stream moves sample blocks between processes (or hosts) over
NATS, in three subject-based patterns that share one wire format — a
dp_header_t (sample rate, centre frequency, sample type, an auto
timestamp_ns, and a per-socket sequence counter) followed by the raw
sample bytes. Because the header is shared, a C transmitter and a Python
receiver interoperate freely.
Every pattern needs a nats-server reachable at the endpoint's
host:port — plain nats-server is enough for PUB/SUB and REQ/REP;
PUSH/PULL rides the JetStream work-queue tier, so start it with
nats-server -js. Endpoints are nats://host:port/subject.
Source:
src/doppler/stream/__init__.py
For the C side and a runnable end-to-end walk-through, see the Streaming examples.
Socket patterns¶
| Pattern | Sender | Receiver | NATS tier | Use |
|---|---|---|---|---|
| PUB / SUB | Publisher |
Subscriber |
Core NATS | fan-out broadcast; subscribers may drop |
| PUSH/PULL | Push |
Pull |
JetStream work-queue | load-balanced pipeline; durable, acked |
| REQ / REP | Requester |
Replier |
NATS request/reply | request/response (lock-step) |
Every socket is a context manager and releases the GIL while blocked
waiting on NATS, so receive loops thread cleanly. The sender's
sample_type fixes the NumPy dtype on both ends:
| Constant | BLUE code | dtype | layout |
|---|---|---|---|
CF32 |
"CF" |
numpy.complex64 |
complex I/Q |
CF64 |
"CD" |
numpy.complex128 |
complex I/Q |
CI8 |
"CB" |
numpy.int8 |
interleaved I/Q, length 2n |
CI16 |
"CI" |
numpy.int16 |
interleaved I/Q, length 2n |
CI32 |
"CL" |
numpy.int32 |
interleaved I/Q, length 2n |
A format's value is its two-character Midas BLUE code, packed
little-endian — the same characters the same samples get in a BLUE file,
so nothing translates between the wire and the file. TLM16 is the
exception in the same argument slot: it is a frame kind, not a sample
format (telemetry records, Publisher only — see
Telemetry), because BLUE has no code for a record
stream.
recv() returns (samples, header) where header is a dict of the
decoded dp_header_t fields: format, kind, version, flags,
data_rep, payload_bytes, sequence, timestamp_ns, sample_rate,
center_freq and num_samples. The full byte layout, and what a
receiver validates before handing anything back, is in
Design: Streaming.
PUB / SUB — broadcast¶
The publisher fans out to every subscriber on the subject; a slow
subscriber drops frames rather than back-pressuring the sender. Core NATS
has no backlog, so a subscriber must already be listening before the
publish — start the Subscriber first (or add a brief warm-up sleep).
import time
import numpy as np
from doppler.stream import Publisher, Subscriber, CF64
# receiver: subscribe first. Core NATS drops a frame sent before the
# subscription reaches the broker, so give it a moment.
with Subscriber("nats://127.0.0.1:4222/iq") as sub:
time.sleep(0.5)
# transmitter
with Publisher("nats://127.0.0.1:4222/iq", CF64) as pub:
iq = np.exp(2j * np.pi * 1e3 * np.arange(1000) / 1e6) # complex128
pub.send(iq, sample_rate=1e6, center_freq=2.4e9)
# A send returns once the client holds the frame; close() then
# flushes for at most 500 ms and reports nothing. drain() waits
# for the server and raises if the tail could not get there.
pub.drain()
samples, header = sub.recv(timeout_ms=1000)
assert len(samples) == 1000
assert header["sample_rate"] == 1e6
print(header["sample_rate"], header["sequence"], len(samples))
PUSH / PULL — pipeline¶
PUSH publishes onto a durable JetStream work-queue subject; JetStream
load-balances each frame to exactly one connected PULL worker, giving a
back-pressured, at-least-once work queue. Call Pull.ack() once a frame
has been fully processed. Requires a JetStream-enabled broker
(nats-server -js).
import numpy as np
from doppler.stream import Push, Pull, CF64
with Push("nats://127.0.0.1:4222/work", CF64) as push:
push.send(np.zeros(4096, dtype=np.complex128), sample_rate=1e6)
with Pull("nats://127.0.0.1:4222/work") as pull:
samples, header = pull.recv() # blocks until a frame arrives
pull.ack(samples)
A worker may hold WORK_QUEUE_MAX_ACK_PENDING frames delivered and not
yet acked. Past that, the server hands it no new frames, only
redeliveries of the pending ones, each an AckWait after the last, so a
worker that never acks stalls at exactly that count. An ack is
fire-and-forget, so a lost one is a redelivery too: the header's
sequence, which counts each producer's frames from 0, is how a worker
can tell one from a new frame.
REQ / REP — request/response¶
A Requester sends a sample block and blocks for the Replier's response;
the two alternate strictly, over NATS's native request/reply.
import numpy as np
from doppler.stream import Requester, Replier, CF64
# server: receive a request, send a reply
with Replier("nats://127.0.0.1:4222/ctrl", CF64) as rep:
req, header = rep.recv()
rep.send(req * 2) # echo-and-scale, say
# client: send, then receive the reply
with Requester("nats://127.0.0.1:4222/ctrl", CF64) as req:
req.send(np.ones(256, dtype=np.complex128), sample_rate=1e6)
resp, header = req.recv()
Timestamps¶
get_timestamp_ns() returns the same CLOCK_REALTIME nanosecond stamp the
senders embed in each header, so a receiver can compute end-to-end latency
against header["timestamp_ns"].
PUB / SUB¶
Publisher
¶
NATS core PUB — one-to-many broadcast of signal frames.
Wraps dp_pub_t. Each :meth:send call stages one
[dp_header_t][raw data] buffer and publishes it to the
endpoint's iq.<subject>.<type> NATS subject. Multiple
:class:Subscriber sockets can subscribe to the same subject;
each subscriber receives every frame. NATS core has no
persistence or back-pressure: a slow subscriber simply misses
frames published while it isn't reading.
The endpoint identifies a subject on a NATS broker; both
Publisher and Subscriber connect to the same nats-server (no
bind/connect distinction — the broker mediates fan-out).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding. One of :data: |
...
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
Construct, use as a context manager, and verify the type:
>>> from doppler.stream import Publisher, CF64
>>> pub = Publisher("nats://127.0.0.1:4222/t19100", CF64)
>>> type(pub).__name__
'Publisher'
>>> pub.close()
Context-manager form (preferred — ensures close on exception):
Full send round-trip with a :class:Subscriber (requires a live
nats-server and a brief warm-up sleep so the subscription is
established before the first publish):
>>> import numpy as np, time
>>> from doppler.stream import Subscriber
>>> pub = Publisher("nats://127.0.0.1:4222/iq", CF64)
>>> sub = Subscriber("nats://127.0.0.1:4222/iq")
>>> time.sleep(0.1)
>>> pub.send(np.array([1+2j, 3+4j],
... dtype=np.complex128),
... sample_rate=int(1e6),
... center_freq=int(2.4e9))
>>> samples, hdr = sub.recv(timeout_ms=2000)
>>> pub.close(); sub.close()
__init__
¶
Create a Publisher and connect to endpoint's NATS broker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding: :data: |
...
|
__enter__
¶
send
¶
send(
samples: NDArray[Any],
sample_rate: float = 0,
center_freq: float = 0,
timestamp_ns: int | None = None,
) -> None
Broadcast one block of samples to all connected Subscribers.
Constructs a dp_header_t with the supplied metadata (plus a
per-socket monotonically increasing sequence number), stages
one [header][raw sample bytes] buffer (NATS has no scatter/
gather send, so header and data are copied into one contiguous
buffer first), and publishes it. The call releases the GIL
while blocked in the underlying NATS client, so other Python
threads can run concurrently.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
C-contiguous array whose dtype must match the socket's
|
required |
sample_rate
|
float
|
Samples per second written into the header (default 0). |
0
|
center_freq
|
float
|
Centre frequency in Hz written into the header (default 0). |
0
|
timestamp_ns
|
int
|
UNIX nanoseconds to stamp on |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
RuntimeError
|
If the publish fails (e.g. the broker connection dropped). |
Examples:
drain
¶
Shut down gracefully: stop new work, finish, flush, close.
This waits for the connection to close, which is the part worth having: the underlying drain returns immediately and finishes in the background, so a process that exits when it returns abandons exactly the work the drain was for.
Drain last, after you have stopped producing. It cannot be
reversed, and a send issued while one is in progress races its
phases -- it may slip through, or be refused. Because this waits,
a single-threaded caller need not reason about that: afterwards a
send raises RuntimeError deterministically.
Against :meth:flush: flush asks whether the server has what you
published and leaves the publisher usable; drain ends it. A
drained shutdown needs no flush of its own -- the drain's final
phase is that flush.
Size timeout_ms to the slowest thing the drain waits for,
with margin. doppler's receive is synchronous, so there is no
handler to finish and the wait is dominated by flushing what is
buffered; the default is generous for a link that is keeping up.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
How long to wait for the close (default 5000). |
5000
|
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If the drain did not complete in the budget. |
RuntimeError
|
If the publisher is already closed, or the drain failed. |
Examples:
send_eos
¶
Tell subscribers the stream has ended.
A subscriber's :meth:Subscriber.recv raises :class:EOFError
instead of waiting out a timeout -- which means only "nothing
yet", and cannot be told from "nothing ever" without this.
Send it before :meth:drain, not after: a drain cannot be
reversed and refuses sends once it reaches its publish-flushing
phase. The order is stop producing, send_eos(), drain(),
close().
Notes
PUB/SUB is at-most-once, so this frame can be dropped like any other. It makes the common case end promptly, not reliably, and a subscriber that must not hang on a lost marker still needs a timeout. PUSH/PULL delivers it at-least-once, so handling it must be idempotent.
Examples:
flush
¶
Wait until the server has everything published so far.
:meth:send hands the frame to the client and returns; the client
writes it in the background, so "the send returned" is not "the
server has it". This waits for a round trip.
You do not need it before :meth:close: the NATS client
flushes what is buffered when the connection closes. But it does
so best-effort with a 500 ms cap and no way to report failure, so
a backlog that cannot drain in half a second is dropped silently
-- and on a link slower than loopback that is not a large backlog.
Call this when losing the tail would matter, and you get a budget
you chose and an answer you can act on. It is also the only way
to ask the question without closing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
How long to wait (default 2000). |
2000
|
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If the budget ran out with data still buffered. |
RuntimeError
|
If the publisher is closed, or the flush failed outright. |
Examples:
close
¶
Destroy the underlying NATS handle and release all resources.
Calls dp_pub_destroy(). Safe to call multiple times —
subsequent calls are no-ops. After close() the object
must not be used for sending.
Examples:
Subscriber
¶
NATS core SUB — receives signal frames from one or more Publishers.
Wraps dp_sub_t. Subscribes to the endpoint's iq.<subject>.>
NATS subject, so it receives every frame any :class:Publisher bound
to that subject sends (fan-out: every Subscriber on the subject gets
every frame). There is no "connect to exactly one Publisher" concept
— the broker mediates delivery by subject, not by socket.
For load-balanced consumption (each frame delivered to exactly one
of several workers, with durable redelivery) use :class:Pull
instead, which is backed by NATS JetStream rather than NATS core.
The recv path is zero-copy: the returned NumPy array's memory is
owned by an internal dp_msg_t handle that points into the
received NATS message; the buffer is freed only when the array
(and all views of it) are garbage-collected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Examples:
>>> from doppler.stream import Subscriber
>>> sub = Subscriber("nats://127.0.0.1:4222/t19104")
>>> type(sub).__name__
'Subscriber'
>>> sub.close()
Context-manager form:
Receive one frame (requires a live :class:Publisher):
__init__
¶
Create a Subscriber socket and connect to endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g.
|
required |
__enter__
¶
recv
¶
Receive one signal frame from the connected Publisher.
Blocks until a frame arrives or the optional timeout expires. The returned NumPy array is a zero-copy view into the received NATS message buffer; the buffer is freed when the array is garbage-collected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
Milliseconds to wait before raising :exc: |
-1
|
Returns:
| Name | Type | Description |
|---|---|---|
samples |
ndarray
|
Decoded sample data. dtype follows the frame's own
|
header |
dict
|
Decoded
|
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If |
RuntimeError
|
If the underlying NATS recv fails for any other reason. |
Examples:
reasm_stats
¶
What reassembling chunked frames has given up on this subscriber.
A frame above the server's max_payload (1 MiB by default) arrives as chunks, and recv() rebuilds one frame at a time. A frame it can no longer complete is given up and counted here rather than hidden: the next frame still arrives intact. A frame recv() DOES return is complete and exact. The counts run from the subscriber's creation and are never reset. Read them from the thread that calls recv(): a recv() on another thread releases the GIL and updates them without a lock.
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the subscriber is closed. |
Examples:
close
¶
PUSH / PULL¶
Push
¶
NATS JetStream work-queue producer — durable pipeline sender.
Wraps dp_push_t. Each :meth:send is a synchronous, server-
acked JetStream publish: the frame is persisted (and replicated, on
a clustered broker) before the call returns, so a producer crash
never silently drops a frame. Each persisted frame is later
delivered to exactly one :class:Pull worker (competing-consumers
distribution), unlike :class:Publisher which fans out to every
Subscriber.
Use the PUSH/PULL pattern when you have a pool of workers pulling
from a shared, durable queue and want at-least-once delivery with
redelivery on worker crash (see :meth:Pull.ack).
Both ends address the same NATS work-queue subject; there is no bind/connect distinction (the broker mediates delivery).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding. One of :data: |
...
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
>>> from doppler.stream import Push, CF64
>>> push = Push("nats://127.0.0.1:4222/t19108", CF64)
>>> type(push).__name__
'Push'
>>> push.close()
Context-manager form:
Round-trip with a :class:Pull worker (requires a live connection):
>>> import numpy as np, time
>>> from doppler.stream import Pull
>>> push = Push("nats://127.0.0.1:4222/work", CF64)
>>> pull = Pull("nats://127.0.0.1:4222/work")
>>> time.sleep(0.05)
>>> push.send(np.ones(4, dtype=np.complex128))
>>> samples, hdr = pull.recv(timeout_ms=2000)
>>> push.close(); pull.close()
__init__
¶
Create a Push socket and bind to endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding: :data: |
...
|
__enter__
¶
send
¶
send(
samples: NDArray[Any],
sample_rate: float = 0,
center_freq: float = 0,
timestamp_ns: int | None = None,
) -> None
Durably publish one block of samples to the work-queue.
Blocks until the broker acks that the frame is persisted (and replicated, on a clustered broker) — not until a Pull worker is ready; the frame waits in the queue for the next available worker. Releases the GIL while blocked on the broker round trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
C-contiguous array whose dtype must match the socket's
|
required |
sample_rate
|
float
|
Samples per second written into the header (default 0). |
0
|
center_freq
|
float
|
Centre frequency in Hz written into the header (default 0). |
0
|
timestamp_ns
|
int
|
UNIX nanoseconds to stamp on |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
RuntimeError
|
If the publish fails (e.g. the broker connection dropped). |
Examples:
>>> from doppler.stream import Push, Pull, CF64
>>> import numpy as np, time
>>> push = Push("nats://127.0.0.1:4222/work", CF64)
>>> pull = Pull("nats://127.0.0.1:4222/work")
>>> time.sleep(0.05)
>>> push.send(np.array([1+2j, 3+4j],
... dtype=np.complex128),
... sample_rate=int(48000))
>>> samples, hdr = pull.recv(timeout_ms=2000)
>>> push.close(); pull.close()
delete_stream
¶
Destroy the work-queue stream backing this endpoint.
Every frame still in the queue goes with it, acked or not.
Administrative, and deliberately never automatic: a work queue is
shared infrastructure and outliving any one producer is the
feature, so closing a Push must not end it. Call this only
when you own the queue's lifetime — a test that invented the
subject, or a tool tearing down what it provisioned. A work queue
drops a frame only when a consumer acks it, so frames nobody
consumed are kept forever otherwise (doppler#1136).
Only a Push provisions the stream, so a Pull cannot attach
to the endpoint again until another Push re-creates it.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the push is already closed. |
RuntimeError
|
If the broker refuses; the message carries what it said. |
Examples:
close
¶
Pull
¶
NATS JetStream work-queue consumer — durable pipeline receiver.
Wraps dp_pull_t. Consumes frames persisted by one or more
:class:Push producers. Multiple Pull workers can share the same
durable consumer group; each frame is delivered to exactly one
worker. Call :meth:ack once a frame is fully processed — an
un-acked frame is redelivered if the worker dies first.
The recv path is zero-copy: see :class:Subscriber for the buffer
lifetime semantics.
Both ends address the same NATS work-queue subject; there is no bind/connect distinction (the broker mediates delivery).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Examples:
>>> from doppler.stream import Push, Pull, CF64
>>> _ = Push("nats://127.0.0.1:4222/t19112", CF64) # provisions the queue
>>> pull = Pull("nats://127.0.0.1:4222/t19112")
>>> type(pull).__name__
'Pull'
>>> pull.close()
Context-manager form:
>>> _ = Push("nats://127.0.0.1:4222/t19113", CF64) # provisions the queue
>>> with Pull("nats://127.0.0.1:4222/t19113") as pull:
... type(pull).__name__
'Pull'
Receive one frame (requires a live :class:Push):
__init__
¶
Create a Pull socket and connect to endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g.
|
required |
__enter__
¶
recv
¶
Receive one signal frame from the connected Push socket.
Blocks until a frame arrives or the optional timeout expires. The returned NumPy array is a zero-copy view into the received NATS message buffer; the buffer is freed when the array is garbage-collected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
Milliseconds to wait before raising :exc: |
-1
|
Returns:
| Name | Type | Description |
|---|---|---|
samples |
ndarray
|
Decoded sample data. dtype follows the frame's own
|
header |
dict
|
Decoded |
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If |
RuntimeError
|
If the underlying NATS recv fails for any other reason. |
Examples:
ack
¶
Acknowledge a frame consumed from the JetStream work-queue.
Delivery is at-least-once: a frame stays pending until acked
and is redelivered to another worker if this one dies first.
Pass the array returned by :meth:recv once it has been fully
processed, then drop the array. Ack before :meth:close: once
the Pull that received the frame is closed, nothing can be sent.
A frame that did not come from a Pull -- a :class:Subscriber's,
say -- has nothing to acknowledge: its ack is a no-op that never
raises, whether or not its receiver is closed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
The samples array returned by :meth: |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If the broker could not be told. |
Examples:
close
¶
Destroy the underlying NATS handle and release all resources.
Calls dp_pull_destroy(). Safe to call multiple times.
Examples:
REQ / REP¶
Requester
¶
NATS request/reply — sends a request frame, then waits for a reply.
Wraps dp_req_t. Built on a NATS request: :meth:send publishes
to the endpoint's subject with a reply-to address (a dedicated
inbox created for this Requester), and :meth:recv waits on that
same inbox for the :class:Replier's answer. Use this pattern
strictly alternating (send, then recv, then send again) — unlike
ZMQ's REQ socket there is no enforced state machine, but calling
recv before a matching send simply blocks/times out on an
empty inbox rather than raising immediately.
Complements :class:Replier. Use this pattern for control-plane
messages (tuning commands, metadata queries) or synchronous
signal-frame RPC where one peer processes each frame and returns a
result.
Both ends address the same NATS subject; there is no bind/connect distinction (the broker mediates delivery via the reply-to inbox).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding of frames sent by this socket. One of
:data: |
...
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
>>> from doppler.stream import Requester, CF64
>>> req = Requester("nats://127.0.0.1:4222/t19116", CF64)
>>> type(req).__name__
'Requester'
>>> req.close()
Context-manager form:
Full REQ/REP round-trip (requires a live :class:Replier):
>>> import numpy as np, time
>>> from doppler.stream import Replier
>>> rep = Replier("nats://127.0.0.1:4222/ctrl", CF64)
>>> req = Requester("nats://127.0.0.1:4222/ctrl", CF64)
>>> time.sleep(0.05)
>>> req.send(np.ones(4, dtype=np.complex128),
... sample_rate=int(1e6))
>>> request, hdr = rep.recv(timeout_ms=2000)
>>> rep.send(request)
>>> reply, hdr2 = req.recv(timeout_ms=2000)
>>> req.close(); rep.close()
__init__
¶
Create a Requester socket and connect to endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g.
|
required |
sample_type
|
int
|
Wire encoding of frames sent by this socket:
:data: |
...
|
__enter__
¶
send
¶
send(
samples: NDArray[Any],
sample_rate: float = 0,
center_freq: float = 0,
timestamp_ns: int | None = None,
) -> None
Publish a request frame with this Requester's reply-to inbox.
Use strictly alternating with :meth:recv (send, recv, send,
recv, ...) — sending again before consuming the previous reply
works mechanically but leaves an unread message in the inbox
for the next :meth:recv to pick up, which will desync the
request/reply pairing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
C-contiguous array whose dtype must match the socket's
|
required |
sample_rate
|
float
|
Samples per second written into the header (default 0). |
0
|
center_freq
|
float
|
Centre frequency in Hz written into the header (default 0). |
0
|
timestamp_ns
|
int
|
UNIX nanoseconds to stamp on |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
RuntimeError
|
If the publish fails (e.g. the broker connection dropped). |
Examples:
recv
¶
Receive the reply frame from the :class:Replier.
Waits on this Requester's reply-to inbox. Call after
:meth:send; calling without a prior send just blocks (or
times out) on an empty inbox rather than raising immediately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
Milliseconds to wait before raising :exc: |
-1
|
Returns:
| Name | Type | Description |
|---|---|---|
samples |
ndarray
|
Decoded reply data. dtype mirrors the :class: |
header |
dict
|
Decoded |
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If |
Examples:
close
¶
Replier
¶
NATS request/reply — receives a request frame, then sends a reply.
Wraps dp_rep_t. Subscribes to the endpoint's subject; each
:meth:recv captures the request's reply-to inbox, and the next
:meth:send publishes the reply directly to that inbox. Calling
send before recv has captured a request raises
:exc:RuntimeError immediately (there is no reply-to target yet)
— unlike ZMQ's REP FSM, recv itself has no such restriction and
simply waits for the next request.
Complements :class:Requester. Use for control-plane responses or
signal-frame RPC where the Replier processes each frame and returns
a result synchronously.
Both ends address the same NATS subject; there is no bind/connect distinction (the broker mediates delivery via the reply-to inbox).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding of frames sent by this socket (the reply). One
of :data: |
...
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
RuntimeError
|
If |
Examples:
>>> from doppler.stream import Replier, CF64
>>> rep = Replier("nats://127.0.0.1:4222/t19120", CF64)
>>> type(rep).__name__
'Replier'
>>> rep.close()
Context-manager form:
Full REQ/REP server loop (requires a live :class:Requester):
>>> from doppler.stream import Requester
>>> import numpy as np, time
>>> rep = Replier("nats://127.0.0.1:4222/ctrl", CF64)
>>> req = Requester("nats://127.0.0.1:4222/ctrl", CF64)
>>> time.sleep(0.05)
>>> req.send(np.ones(4, dtype=np.complex128))
>>> request, hdr = rep.recv(timeout_ms=2000)
>>> rep.send(request, sample_rate=hdr["sample_rate"])
>>> reply, _ = req.recv(timeout_ms=2000)
>>> req.close(); rep.close()
__init__
¶
Create a Replier socket and bind to endpoint.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
endpoint
|
str
|
NATS endpoint, e.g. |
required |
sample_type
|
int
|
Wire encoding of reply frames sent by this socket:
:data: |
...
|
__enter__
¶
recv
¶
Receive one request frame from a :class:Requester.
Blocks until a request arrives or the timeout expires. Captures
the request's reply-to inbox so the next :meth:send reaches
the right Requester.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout_ms
|
int
|
Milliseconds to wait before raising :exc: |
-1
|
Returns:
| Name | Type | Description |
|---|---|---|
samples |
ndarray
|
Decoded request data. dtype mirrors the
:class: |
header |
dict
|
Decoded |
Raises:
| Type | Description |
|---|---|
TimeoutError
|
If |
RuntimeError
|
If the underlying NATS recv fails for any other reason. |
Examples:
send
¶
send(
samples: NDArray[Any],
sample_rate: float = 0,
center_freq: float = 0,
timestamp_ns: int | None = None,
) -> None
Publish the reply to the request's captured reply-to inbox.
Must be called after :meth:recv; calling without a prior
recv raises :exc:RuntimeError immediately (there is no
reply-to target yet). After this call returns, the Replier is
ready to recv the next request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
C-contiguous array whose dtype must match the socket's
|
required |
sample_rate
|
float
|
Samples per second written into the reply header (default 0). |
0
|
center_freq
|
float
|
Centre frequency in Hz written into the reply header (default 0). |
0
|
timestamp_ns
|
int
|
UNIX nanoseconds to stamp on |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
RuntimeError
|
If |
Examples:
close
¶
Helpers¶
get_timestamp_ns
¶
Current wall-clock time in nanoseconds since the UNIX epoch.
Calls clock_gettime(CLOCK_REALTIME) in the C layer. Useful for
stamping outgoing frames when the caller does not supply its own
timestamp_ns, or for computing round-trip latency from the value
returned in the received header dict.
Returns:
| Type | Description |
|---|---|
int
|
Non-negative nanosecond timestamp. Guaranteed to be
monotonically non-decreasing within a single process on any
POSIX system that supports |
Examples:
Stopping a blocked receive¶
The interrupt is no longer part of this module. It was never a streaming feature — the same flag ends a ring wait and a capture read — so it now lives on its own, as an object rather than as six free functions:
import numpy as np
from doppler.interrupt import Interrupt
it = Interrupt(np.array([], dtype=np.int32))
# Requested from another thread, or from a SIGINT handler this guard
# installed. A Subscriber.recv() blocked in the NATS client raises
# KeyboardInterrupt; so does a ring wait, and so does a capture read.
it.interrupt()
assert it.interrupted()
it.resume()
doppler.stream.interrupt_on_sigint(), interrupt(), resume(),
interrupted(), set_interrupt_latency_ms() and interrupt_latency_ms()
are gone, not deprecated. See Interrupt for the
object that replaces them and
Ending a wait for the contract it serves on
all three transports.
mean_power
¶
Mean power of a complex sample block, normalised to full scale.
mean(|x|**2), with the integer formats divided by their full
scale first, so the answer means the same thing whatever the wire
carried and 10*log10() of it is dBFS in every case.
This is the same dp_mean_power() the C examples call -- one
implementation, so the Python and C receivers cannot report
different numbers for one frame, and neither has to spell the loop
out again.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
samples
|
ndarray
|
C-contiguous block: |
required |
Returns:
| Type | Description |
|---|---|
float
|
Mean power, 0.0 for an empty block. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If the dtype is not one of the wire formats. |
ValueError
|
If |
Examples:
format_name
¶
The name of a wire format code.
The same dp_sample_type_str() the C face prints, so a Python
receiver names a format exactly as the C one does rather than
carrying a private code-to-name table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
int
|
A wire format, e.g. :data: |
required |
Returns:
| Type | Description |
|---|---|
str
|
The format's name, or |
Examples:
Related pages¶
Gallery — Waveform I/O — One Capture, Four File Types Design — Spectrum Analyzer, The Spectrogram — the measurement record, Streaming — one envelope, six roles, two planes, Telemetry — zero-cost scalar taps for running pipelines