Skip to main content

Channels

A calculated channel is a derived time-series signal computed from existing channels and persisted alongside them — for example, speed_kmh = raw_speed * 3.6, or the sum of two sensors. Unlike an aggregation (which summarizes a signal into bins or statistics), a calculated channel materializes a new signal at the same per-sample grain as the silver channels table, so it can be queried and charted like any physical channel.

A calculated channel wraps a TSAL expression (see TSAL) that must evaluate to a SampleSeries. This is the persisted, labeled form of the ephemeral virtual signals you can build ad-hoc in a query.

Every calculated channel used in a report must be registered with the report via add_calculated_channel() before determine_report() is called.

my_report.add_calculated_channel(my_channel)

CalculatedChannel

A CalculatedChannel couples a TSAL expression with an identity — an arbitrary key-value dict that identifies the channel and seeds its deterministic channel_id. When the report is solved, the wrapped expression is evaluated per measurement container and exploded into narrow rows matching the silver channels shape. The identity itself is stored once on the channel dimension (as a map), not repeated on every fact row.

When to use a calculated channel

Reach for a calculated channel only when the derived time series itself is the deliverable — you want it materialized to a queryable gold table, or returned as narrow silver-shaped rows via solve_calculated_channels. To compute over a derived signal, you don't need one: aggregations and events accept any TSAL expression directly (e.g. StatsAggregator(input_expressions=[rpm * torque], ...), BasicEvent(expr=speed > 100)) and derive it on the fly at solve time.

from impulse_reporting.channels.calculated_channel import CalculatedChannel

speed_kmh = CalculatedChannel(
name="speed_kmh",
expr=veh_spd * 3.6,
identity={"channel_name": "speed_kmh", "data_key": "CALC"},
desc="Vehicle speed converted to km/h",
)

Parameters

ParameterTypeRequiredDescription
namestrYesHuman-readable channel name, stored in the channel dimension table.
exprTimeSeriesExpressionYesTSAL expression defining the derived signal. Must evaluate to SampleSeries; validated at construction (raises ValueError otherwise).
identitydict[str, str]YesChannel identity. Any non-empty dict (keys are arbitrary, e.g. channel_name / data_key). Seeds the deterministic channel_id and is stored as a map on calculated_channel_dimension (joined to the fact via channel_id); not repeated on fact rows.
descstrNoHuman-readable description stored in the channel dimension table.
attributesMapping[str, str]NoFree-form key-value metadata stored in the channel dimension table. Values are coerced to strings.

How it works

  1. The expr is evaluated per measurement container by the solver, yielding a SampleSeries for each container.
  2. Calculated channels take their own narrow solve path (QueryBuilder.solve_calculated_channels), distinct from the wide batch solve used by events and aggregations. Multi-channel expressions (e.g. rpm + speed) are time-base synchronized automatically; emitted intervals are the intersection.
  3. Each series is exploded into one row per sample interval: container_id, channel_id, tstart, tend, value. The identity is not on the fact — it lives on the dimension, joined via channel_id.
  4. The channel_id is a deterministic hash of the sorted identity — the same value appears in both the fact and dimension tables, so they join directly.
  5. The sample rows are written to the calculated_channel_fact table; the channel definition (name, description, expression, identity) is written to the calculated_channel_dimension table.
Requires a DefaultSolver

The calculated-channels solve path is implemented by DefaultSolver. A Report uses DefaultSolver automatically; if you call solve_calculated_channels directly, pass a DefaultSolver — the default BlobSolver raises NotImplementedError.

Examples

Unit conversion (single channel):

speed_kmh = CalculatedChannel(
name="speed_kmh",
expr=veh_spd * 3.6,
identity={"channel_name": "speed_kmh", "data_key": "CALC"},
desc="Vehicle speed converted to km/h",
)

Derived signal from multiple channels:

power = CalculatedChannel(
name="power_w",
expr=eng_rpm * torque,
identity={"channel_name": "power_w", "data_key": "CALC"},
desc="Mechanical power (RPM × torque), time-synchronized across both inputs",
)

Calculated channel output schema

calculated_channel_dimension

Stores channel definitions (one row per calculated channel per report).

ColumnTypeDescription
channel_idlongUnique channel identifier (deterministic hash of the sorted identity).
report_idintReport identifier.
channel_typestr"CALCULATED_CHANNEL".
channel_namestrChannel name (from identity["channel_name"]).
channel_descriptionstrChannel description.
channel_expressionstrString representation of the wrapped TSAL expression (encodes identity + expression).
identitymap[str, str]Full identity dict.
definition_hashlongHash of the channel definition; used by incremental processing to detect definition changes.
attributesmap[str, str]Free-form key-value metadata supplied via the attributes constructor argument.

calculated_channel_fact

Stores the materialized derived signal (one row per sample interval per container) — the same narrow, run-length-encoded shape as the silver channels table.

ColumnTypeDescription
container_idintContainer identifier. Type is inherited from the silver source.
channel_idlongForeign key to calculated_channel_dimension.
tstartlongSample interval start timestamp.
tendlongSample interval end timestamp.
valuedoubleComputed value over the interval.
channel_namestrIdentity column (from identity).
data_keystrIdentity column (from identity).

Both tables carry the configurable table_prefix (e.g. {prefix}_calculated_channel_fact). See the gold layer schema for how they fit the star schema, and incremental processing for how definition changes are reprocessed.