153 lines
7.6 KiB
Markdown
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.
|