Files
dynalab-core/docs/Processing.md
T
h3cx db5d509b49 Version 0.2.1
Version 0.2.1 now has derive unit context for derivative calculus
2026-10-19 19:35:10 +02:00

153 lines
7.6 KiB
Markdown

# DLUnit processing and derivatives
DLUnits can optionally request a `DeriveContext` to calculate time derivatives
of their declared inputs. The same interface works during realtime acquisition,
offline `Core.reprocess()`, and playback with `playback_mode="reprocess"`.
Playback with `playback_mode="read"` reads previously saved outputs instead.
## Example: speed to acceleration
Use this as the DLUnit's `unit.py` source:
```python
from dynalab_core.processing import DeriveContext
from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.protocols.packets.handshake import SignalDescriptor
def process(
speed: ValueDescriptor,
ctx: DeriveContext,
output: SignalDescriptor,
) -> ValueDescriptor | None:
acceleration = ctx.derivative(speed, order=1, max_gap_s=0.5)
if acceleration is None:
return None
return ValueDescriptor(output.id, acceleration, speed.timestamp)
```
Bind it using the existing API:
```python
from uuid import uuid4
from dynalab_core.dlunit import DLUnit
from dynalab_core.protocols.packets.handshake import SignalDescriptor
acceleration_signal = SignalDescriptor(
id=uuid4(), name="Acceleration (m/s²)", type="number", origin="derived"
)
unit = DLUnit()
unit.set_manifest(uuid4(), [speed_signal], acceleration_signal)
unit.set_data(source_text) # The unit.py source above.
core.bind_derive_unit(dlunit=unit)
# Optionally persist it with unit.write(directory, "acceleration.dlunit").
```
`speed_signal` must describe the existing speed input. If its values are in m/s,
the output values are in m/s². Core does not infer physical units or rename
signals automatically. Derivative outputs are ordinary derived signals: they
can feed another DLUnit, be recorded, and be saved in a DLPak.
## Function contract
A function takes its `ValueDescriptor` inputs in manifest order, optionally one
`DeriveContext`, then one final `SignalDescriptor` describing the output. Use
the actual imported classes in annotations, without postponed/string
annotations. The source must expose exactly one Python function; importing the
context class is supported.
Existing functions such as `process(a, b, output) -> ValueDescriptor` continue
to work. To skip output, explicitly annotate `-> ValueDescriptor | None` and
return `None`. This also works without a context. Returning `None` with the
original `-> ValueDescriptor` annotation remains an invalid result.
No placeholder is emitted when a function returns `None`: live values are not
updated, recording gets no sample, and downstream units receive no event for
that output. A previously published live output remains subject to its normal
timeout; skipping an output does not explicitly clear it.
An offline unit annotated to allow `None` may produce an empty series. Core logs
`derive.offline_no_values` and skips units requiring that empty output, logging
`derive.offline_upstream_empty`. Reprocessing still succeeds, and descriptors
are retained for the empty outputs. Units that do not allow `None` retain the
`DeriveError(code="no_values")` behavior if they execute but emit nothing.
## `ctx.derivative(value, order=1, *, max_gap_s=None) -> float | None`
- `value` is one of the function's supplied input descriptors.
- `order` is an integer: 1, 2, or 3. Other values raise `ValueError`.
- `max_gap_s` is an optional finite positive maximum interval between
consecutive samples in the derivative window. Invalid limits raise
`ValueError`. The default places no gap limit.
- The result uses seconds: input units/s, input units/s², or input units/s³.
- Warm-up requires 2, 3, or 4 distinct timestamps, respectively. Until then,
the result is `None`. A window containing a gap above `max_gap_s` also returns
`None`; estimates resume after enough samples arrive following that gap.
- Repeated calls are read-only and return the same estimate. You can request
different orders for the same input in one invocation.
The estimator differentiates the degree-`order` polynomial interpolating the
most recent `order + 1` samples. For order 1 this is the usual backward secant:
`(new_value - previous_value) / elapsed_seconds`. Orders 2 and 3 use the actual
unequal sample intervals rather than repeatedly dividing by a nominal period.
Timestamp differences are taken as integers before converting to seconds.
These are numerical estimates without smoothing. They use only present and
past samples, and higher orders can magnify measurement noise. Very uneven
spacing and floating-point precision also limit accuracy. A non-finite
computed estimate returns `None`.
## History and alignment
Core retains at most four samples per declared input, per context-enabled unit.
The router advances history only when that input itself updates and queues a
snapshot for each invocation. Later arrivals cannot alter an already queued
context, and calls to `derivative()` do not advance history.
Context-enabled units wait until all declared inputs have a sample at or before
the triggering event's timestamp. Each input retains its own original timestamp.
Only the most recent four accepted samples are available, so severely late
events may no longer have a companion sample in history and are skipped.
Equal or older timestamps for a signal are ignored by that unit's history and
do not trigger its processing function. A newer non-finite value clears that
signal's history and skips the invocation; finite samples restart warm-up.
These rules do not remove or alter raw recording samples.
When another input changes, a derivative of an unchanged input reuses that
input's existing estimate. Its history does not gain another sample. If the
output combines multiple inputs, choose its timestamp explicitly, for example
`max(a.timestamp, b.timestamp)`. Companion values are retained in the bounded
history; they do not expire through the realtime live-value timeout. Apply any
additional age policy in the function using the supplied timestamps.
Offline processing uses the same bounded history and causal alignment, with a
fresh history on every pass. Functions without a context retain the original
offline nearest-neighbor alignment, including possible future samples and
timestamps assigned from the triggering event.
Online and offline estimates match for the same chronological accepted input
sequence. Offline processing sorts stored samples; online out-of-order arrivals
can therefore differ. Queue overload can also change the observed sequence:
router drops omit samples from history, while a unit queue drop omits that
invocation but its sample remains in later history snapshots. Asynchronous
multi-input derived chains can observe a different event ordering online.
Offline processing does not drop samples due to queue capacity.
Starting or stopping recording does not reset ongoing online history. If a
recording begins midstream, its first online derivative outputs may depend on
samples preceding the recording. Offline recalculation warms up from the
recorded samples and can omit those early estimates.
## Lifecycle
Online histories are invalidated when Core changes mode, a source connector
registers/disconnects, processing or playback data is replaced, or a playback
control changes its generation (play/resume, pause, seek, or restart). Connector
changes conservatively invalidate all units' histories. Queued requests and
results from an older revision are discarded, including results completing
after the reset. New data warms up the estimator again.
Offline histories are local to the processing pass and never share online
state. DLUnit archives save source and manifest, not history. DLPak and connector
packet formats are unchanged. No numerical-library dependency is required.