rbed

Conditions and Expressions

What a Rule actually compares: Channel values, rates, moving averages and arithmetic between Channels.

A Rule takes Conditions, and Conditions are built from two kinds of building blocks:

  • An Expression is a number that changes over time: a Channel's value, a rate, a moving average, a count of Events. temp is an Expression.
  • A Condition is true, false or undefined at each moment (a Sample or a time advance): temp.above(80) is a Condition.

Expressions are compared to get Conditions, and Conditions are combined with and, or and not. Every Expression supports every comparison and every arithmetic operation.

What the Condition sees

One Rule, AboveLimit, compares an Expression of a noisy temperature with a fixed threshold. Choose the Expression to see how each one turns the same data into a different Condition.

The temperature starts warm, has a single outlier at 15 s and ramps up from 30 s. The Channel ambient reports only from 5 s on.

The live chart and its controls need JavaScript. The tables show the Events at the default parameters.

The site cannot run the default parameters: (JavaScript) TypeError: Cannot read properties of undefined (reading 'length')

import io.github.romanhaendler.rbed.*;
import java.time.Duration;
import java.util.Map;

Channel temp = Channel.named("temp");
Channel ambient = Channel.named("ambient");

Rule aboveLimit = Rule.named("AboveLimit").when(temp.averageOverSamples(5).above(24));
Detector detector = Detector.builder()
    .rules(aboveLimit)
    .build();

double[] noise = {0.0, 0.2, -0.1, 0.1, -0.2, 0.1, 0.0, -0.1, 0.2, -0.1};
for (int s = 0; s < 60; s++) {   // one Sample per second
  double value = s < 6 ? 26 + noise[s % 10]
      : s == 15 ? 28                              // an outlier
      : s < 30 ? 20 + noise[s % 10]
      : s < 38 ? 20 + (s - 30) + noise[s % 10]    // a ramp
      : 28 + noise[s % 10];
  if (s < 5) detector.push(s * 1_000L, Map.of(temp, value));   // ambient reports from 5 s on
  else detector.push(s * 1_000L, Map.of(temp, value, ambient, 22.0));
}
detector.close();

At the default, a moving average over 5 Samples, AboveLimit runs from 4 s, where the average is first defined, to 7 s, and again from 37 s, once the ramp has lifted the average above 24, to the end of the data.

Things to try

  • Moving average over Samples: the temperature is above 24 from the first Sample, but the average of 5 Samples is undefined until the fifth. The first Event starts at 4 s, Truncated at its start. Raise the Samples averaged to 10 and the warm start is averaged away.
  • Rate: the outlier at 15 s is a single step of 8.2 °C in one second, so the rate crosses its threshold and makes an Event of its own; the ramp from 30 s makes a second one, from 31 s to 39 s. A rate shows noise in full.
  • Moving average over a duration: undefined until the values span the window. With the default 10 s, the warm start lies entirely in the warm-up and leaves no Event; the ramp makes one from 40 s.
  • Arithmetic between Channels: temp.minus(ambient) is undefined until ambient has a value. Arithmetic does not smooth: the outlier makes an Event here, too, and the ramp one from 38 s.

Comparisons

temp.above(80)              // temp > 80, strictly
temp.below(-5)              // temp < -5, strictly
temp.between(18, 24)        // 18 <= temp <= 24, both bounds included

above and below also compare two Expressions, so a threshold can itself be a Channel, such as weight.above(ratedLoad). To test "at least" or "at most", negate the opposite: temp.below(80).not() is temp >= 80.

Arithmetic

Expressions support plus, minus, times and dividedBy, each with another Expression or a number, and abs():

supplyTemp.minus(returnTemp).above(5)     // temperature spread
current.times(voltage).above(2_000)       // power from two Channels
setpoint.minus(actual).abs().above(2)     // deviation in either direction
errors.dividedBy(requests).above(0.01)    // error rate above 1 %

A division by zero, or any other operation without a finite result, is undefined.

A threshold tells you where a Channel is; a trend tells you where it is going, so a Rule can react before a threshold is reached.

  • rising() and falling() are Conditions: the Channel's latest value is greater, or less, than its previous one. An unchanged value is neither. Combine them with forSamples for a sustained trend, or with and and a level, so that a high value that is still climbing counts and one that is already falling does not.
  • rate() is an Expression: the change per second between the two latest values, (latest - previous) / seconds between them, so irregular sampling is handled correctly.
// hard braking: speed in m/s drops by more than 4 m/s per second
Rule hardBraking = Rule.named("HardBraking").when(speed.rate().below(-4));

All three are undefined until the Channel has two values. They use the Channel's own values, not Samples: if a Channel is missing from a Sample, they still compare its two latest reported values. Because rate() looks at only two values, take the rate of a moving average for noisy Channels: level.averageOverSamples(4).rate().

Moving averages

A moving average replaces each value with the mean of the most recent ones, so a Rule sees the underlying trend instead of the noise.

level.averageOverSamples(5)                          // mean of the last 5 values
level.averageOverDuration(Duration.ofMinutes(10))    // mean of the last 10 minutes

averageOverSamples(n) is defined once n values exist; use it for regularly sampled data. averageOverDuration is defined once the values span the window; use it when Samples arrive irregularly or the window has a meaning in time. A longer window smooths more but reacts later. A moving average supports everything a Channel does, including rate and another average. Two averages of one Channel make a crossover:

Rule goldenCross = Rule.named("GoldenCross").when(
    price.averageOverSamples(50).above(price.averageOverSamples(200)));

While an average warms up it is undefined, so an Event that starts right after the warm-up is Truncated at its start: the Rule could not tell whether the Condition held earlier.

Logic: and, or, not

  • and: all criteria must hold, which makes a Rule specific.
  • or: any criterion suffices, which merges several ways into the same situation.
  • not: the opposite, for "outside a band" or "at least".
temp.above(80).and(fan.below(0.5))                          // cooling failure
temp.between(18, 24).not()                                   // outside the comfort band
rpm.above(100).and(vibration.above(4).or(temp.above(80)))   // calls nest explicitly

Undefined values

A Condition is undefined when the data it needs does not exist yet: a Channel without a value so far, a moving average still warming up, rate before two values, a division by zero, or latest(...) before the first such Event. Undefined never counts as true, and that includes not(): pressure.above(5).not() is false, not true, as long as pressure has no value. So a Rule never produces an Event because data is missing.

Read an Expression's value

To log, plot or check what your Rules compare, register the Expressions with watch when you build the Detector and read each with value after a Sample or a time advance. The value is an OptionalDouble, empty while the Expression is undefined:

import io.github.romanhaendler.rbed.*;
import java.util.Map;

Channel temp = Channel.named("temp");
ChannelExpression smoothTemp = temp.averageOverSamples(3);
Detector detector = Detector.builder()
    .rules(Rule.named("Warm").when(smoothTemp.above(24)))
    .watch(smoothTemp)
    .build();

double[] readings = {23, 25, 27, 26};
for (int i = 0; i < readings.length; i++) {
  detector.push(i * 1_000L, Map.of(temp, readings[i]));
  System.out.println(detector.value(smoothTemp));   // empty, empty, then 25.0 and 26.0
}
detector.close();

A watched Expression need not be used by any Rule. Reading also works from an EventListener callback (the moment being processed) and after close() (the last moment). Reading an Expression that was not watched throws IllegalArgumentException: a Channel matches by name, any other Expression must be the object you passed to watch. The dashed lines on these pages are read this way.

Reference

Methods used: Channel.named, Rule.named, Rule.when, ChannelExpression.rate, Expression.above, ChannelExpression.averageOverSamples, ChannelExpression.averageOverDuration, Expression.minus, Detector.builder, Detector.Builder.rules, Detector.Builder.build, Detector.push, Detector.close, Expression.below, Expression.between, Expression.times, Expression.abs, Expression.dividedBy, Condition.and, Condition.not, Condition.or, ChannelExpression.rising, ChannelExpression.falling, Expression.plus, Detector.Builder.watch, Detector.value.