Files
eco/docs/examples/pipeline_offload.md
T
2026-08-18 21:56:17 +02:00

132 lines
4.4 KiB
Markdown

# Offloading code to the pipeline server
Some per-shot calculations are too heavy to run comfortably in an analysis
client — especially on fast beam-synchronous (BS) streams. The **pipeline
server** (`cam_server`) can run *your own Python code* on a stream, right next
to the data source, and publish the results as a new stream. eco's
{py:class}`~eco.pipeline.pipeline_server.PipelineServer` makes that upload
ergonomic.
:::{note}
This module is experimental and kept separate from the main eco device tree.
It wraps the external `cam_server` `PipelineClient`.
:::
## How it works
![eco uploads a small script; the server runs it per message and streams results back](../images/pipeline_offload.svg)
1. **You upload a small processing script** to the server (from a file, or
straight from a function in your shell/notebook).
2. **The server hot-reloads it** and runs it on every message of the pipeline
instance, close to the source — off your client.
3. **Results come back as a new output stream** you can read like any other.
The script must be **self-contained**: it does its own imports at the top,
because the server executes the file on its own.
## The two script contracts
Which function your script defines depends on the pipeline *type*:
`process` — a **custom** pipeline (arbitrary streaming source, e.g. BS channels).
The natural fit for offloading BS-stream calculations:
```python
def process(parameters, init=False):
# `init` is True on the very first call — set up state once here.
# `parameters` is the live pipeline config dict.
stream_data = {"my_result": 42.0}
timestamp = ... # seconds since epoch for this message
pulse_id = ... # pulse id, or None
data_size = 1
return stream_data, timestamp, pulse_id, data_size
```
`process_image` — a **processing** pipeline (camera image, plus optional
synchronous BS data in `bsdata`):
```python
def process_image(image, pulse_id, timestamp, x_axis, y_axis, parameters, bsdata=None):
return {"my_intensity": float(image.sum())}
```
Each key you return becomes a channel on the output stream.
Ready-to-edit starting points ship with the module:
```python
from eco.pipeline import PROCESS_IMAGE_TEMPLATE, CUSTOM_PROCESS_TEMPLATE
print(CUSTOM_PROCESS_TEMPLATE) # copy into a .py file and edit
```
## Attaching a script to a running instance
Write a self-contained script to a file, then hand it to an existing pipeline
instance. {py:meth}`~eco.pipeline.pipeline_server.PipelineServer.set_function`
uploads the file *and* tells the instance to reload — so your new code starts
running immediately:
```python
from eco.pipeline import PipelineServer
ps = PipelineServer()
ps.set_function("SARFE10-PSSS059_sp1", "/sf/bernina/.../my_proc.py")
```
Manage what is on the server with the script helpers:
```python
ps.list_scripts() # names of user scripts on the server
ps.read_script("my_proc.py") # fetch a script's source back
ps.delete_script("my_proc.py")
```
## Offloading a function straight from the shell
For a quick, self-contained function you can skip the file and offload it
directly — the source is captured with `inspect.getsource` and uploaded:
```python
def process(parameters, init=False):
import time
return {"answer": 42.0}, time.time(), None, 1
ps.offload_function("my_custom_instance", process)
```
The function must be named `process` or `process_image` (to match a contract),
and — since only its body is uploaded — must import everything it needs *inside
itself*. For anything needing module-level imports or helper functions, write a
`.py` file and use `set_function` instead.
## Creating a custom pipeline from scratch
To spin up a new **custom** pipeline driven entirely by your script:
```python
instance_id, stream_address = ps.create_custom_pipeline(
"/sf/bernina/.../my_stream_proc.py",
additional_config={
# e.g. input stream / BS channel wiring goes here
},
)
```
## Reading the results
The output is a standard pipeline stream; get its address and read it however
you normally consume streams:
```python
ps.stream_address(instance_id) # e.g. "tcp://…:xxxx"
ps.instance_info(instance_id) # server-side status
ps.is_running(instance_id)
ps.stop(instance_id)
```
## Related
- {doc}`listening_monitor` — collect a channel's updates in the client instead.
- {doc}`archiver_stripchart` — historical data and live strip charts.