# rbed

Rule-based event detection in time series, for Java.

A small Java 17 library that finds **Events** in multi-channel time series by evaluating declarative **Rules**, on a live stream or over a recorded series. Zero runtime dependencies.

You describe *what* an Event is ("the weight is above 500 kg for 2 seconds", "the temperature is above 8 °C within 30 minutes after the door was closed"), and rbed tells you *when* it happened: when it started, when it was confirmed, when it ended, and what the data looked like in between. Every chart on this site is the real rbed, compiled to JavaScript and running in your browser.

The terms (Sample, Channel, Condition, Rule, Event, Detector, Pending, Confirmed, Truncated, ...) are defined in the [glossary](https://github.com/romanhaendler/rbed/blob/main/CONTEXT.md).

## A first Rule

`HighTemperature` is an Event while the Channel `temp` is above a threshold.

Parameters (in the page address as ?threshold=…):

- Threshold (`threshold`): 60 to 90 °C in steps of 1, default 80

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

Java source at the default parameters:

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

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

Rule highTemperature = Rule.named("HighTemperature").when(temp.above(80));
Detector detector = Detector.builder()
    .rules(highTemperature)
    .listener(new EventListener() {
      @Override public void onEnded(Event e) {
        System.out.println(e.name() + " from " + e.start() + " to " + e.end().getAsLong());
      }
    })
    .build();

detector.push(1_000, Map.of(temp, 75.0));   // timestamp in ms, then Channel values
detector.push(2_000, Map.of(temp, 85.0));
detector.push(3_000, Map.of(temp, 70.0));
detector.close();
```

At the default threshold of 80 °C, `HighTemperature` runs from 2 s to 3 s, and the listener prints "HighTemperature from 2000 to 3000": timestamps are milliseconds, and the site's tables show seconds.

### Things to try

- Lower the threshold to 70 °C: the Condition already holds at the first Sample (75 °C), so the Event starts at 1 s with a Truncated start.
- Raise it to 90 °C: no Sample is above it, and there is no Event.

### Three steps

Every use of rbed takes the same three steps:

1. **Build a Detector** from the Rules and a listener.
2. **Feed it Samples** with `push(timestamp, values)`, in time order.
3. **Close it** with `close()` when the data ends, so that an Event still in progress is reported, too. A Detector is `AutoCloseable`, so try-with-resources works.

For a recorded series, [`detect`](https://romanhaendler.github.io/rbed/sampling-rate/) does all three in one call.

Methods used: [`Channel.named`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Channel.html#named%28java.lang.String%29), [`Rule.named`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Rule.html#named%28java.lang.String%29), [`Rule.when`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Rule.html#when%28io.github.romanhaendler.rbed.Condition%29), [`Expression.above`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Expression.html#above%28double%29), [`Detector.builder`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.html#builder%28%29), [`Detector.Builder.rules`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.Builder.html#rules%28io.github.romanhaendler.rbed.Rule...%29), [`Detector.Builder.listener`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.Builder.html#listener%28io.github.romanhaendler.rbed.EventListener%29), [`EventListener.onEnded`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/EventListener.html#onEnded%28io.github.romanhaendler.rbed.Event%29), [`Event.name`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Event.html#name%28%29), [`Event.start`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Event.html#start%28%29), [`Event.end`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Event.html#end%28%29), [`Detector.Builder.build`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.Builder.html#build%28%29), [`Detector.push`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.html#push%28long,java.util.Map%29), [`Detector.close`](https://romanhaendler.github.io/rbed/api/io.github.romanhaendler.rbed/io/github/romanhaendler/rbed/Detector.html#close%28%29).

## Install

rbed 0.4.0 is on Maven Central and needs Java 17 or later.

```xml Maven
<dependency>
  <groupId>io.github.romanhaendler</groupId>
  <artifactId>rbed</artifactId>
  <version>0.4.0</version>
</dependency>
```

```kotlin Gradle
implementation("io.github.romanhaendler:rbed:0.4.0")
```

The library is the Java module `io.github.romanhaendler.rbed` (`requires io.github.romanhaendler.rbed;`) with a single package of the same name; it also works on the class path.

## The code on this site

The Java on every page can be copied and run: it starts with its imports, and its statements are the body of a method, such as `main`. Most sources leave out the listener to stay short, so they run without output: the Events they detect are the page's Event table. To print them, add a listener before `.build()`, as in [A first Rule](https://romanhaendler.github.io/rbed/#a-first-rule), or print the list that `detect` returns. Where it calls a method of its own, that method follows under `// In the enclosing class:`. Time values are written as `java.time.Duration`; every method that takes one also takes a `long` in milliseconds. Short snippets in the text assume the Channels, Rules, variables and methods they name, and these imports:

```java
import static io.github.romanhaendler.rbed.Conditions.*;   // during, after, count, totalDuration, latest

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

`EventListener` shares its simple name with `java.util.EventListener`: next to `import java.util.*;`, import `io.github.romanhaendler.rbed.EventListener` explicitly.

## Good to know

- A Rule is an immutable value: `when`, `forDuration`, `endWhen` and the other methods each return a new Rule, so a Rule can be shared by several Detectors, and a call whose result is not used changes nothing.
- A Detector is single-use and not thread-safe. Use one per data stream, and feed it from one thread at a time.
- Rules and the listener are fixed when the Detector is built. To change Rules, build a new Detector and pass it the Events so far as [History](https://romanhaendler.github.io/rbed/history/), or [resume the stream](https://romanhaendler.github.io/rbed/resuming-a-stream/) from a Snapshot.
- A pushed Sample costs one hash lookup per value, and in steady state (no Event starting or ending) a Detector allocates nothing. Throughput ranges from about 1.2 million Samples per second for 50 Rules over 20 Channels to over 40 million for a single threshold Rule. See the README's [Performance](https://github.com/romanhaendler/rbed#performance), and [Memory and the Retention Limit](https://romanhaendler.github.io/rbed/memory/) for what a Detector keeps.

The Guide takes one concept per page, starting with [Samples and Channels](https://romanhaendler.github.io/rbed/samples-and-channels/).

## Domain examples

Runnable end-to-end examples from different domains, each a test with its expected Events and a page of its own where it runs live.

- [**Cranes** `CraneScenarioTest` hysteresis, during, classifying a load, partial set-down via latest](https://romanhaendler.github.io/rbed/cranes/)
- [**Manufacturing** `MachineToolOverheatExample` sampling-rate mode, rate, combined durings](https://romanhaendler.github.io/rbed/manufacturing/)
- [**Building services** `HvacShortCyclingExample` after().within(), count().within()](https://romanhaendler.github.io/rbed/building-services/)
- [**Healthcare** `VitalSignsExample` advanceTo and age() for a silent sensor, maxGap](https://romanhaendler.github.io/rbed/healthcare/)
- [**Automotive** `FleetTelematicsExample` deceleration via rate, distance via integral](https://romanhaendler.github.io/rbed/automotive/)
- [**Finance** `TradingSignalsExample` moving-average crosses, Truncated start after warm-up](https://romanhaendler.github.io/rbed/finance/)
- [**Logistics** `ColdChainExample` attributing a cause with after, totalDuration limit](https://romanhaendler.github.io/rbed/logistics/)
- [**Environment** `FloodWarningExample` smoothed rate of a moving average, hysteresis](https://romanhaendler.github.io/rbed/environment/)
- [**Sports** `IntervalTrainingExample` after, rep count via count, listener-side timing](https://romanhaendler.github.io/rbed/sports/)
- [**IT operations** `DeploymentRegressionExample` a Pulse Channel reporting deployments](https://romanhaendler.github.io/rbed/it-operations/)

[Source on GitHub](https://github.com/romanhaendler/rbed) · [Maven Central](https://central.sonatype.com/artifact/io.github.romanhaendler/rbed) · [MIT licence](https://github.com/romanhaendler/rbed/blob/main/LICENSE)
