For each sensor, the algorithm checks whether the reading changes within a specified tolerance over a rolling 12-sample window and flags rows where the difference between the maximum and minimum values is less than the tolerance.
The masks from each sensor are then combined using the |= operator, so a row is flagged if any sensor appears flat. Finally, the combined mask is written to the result DataFrame as a new column called flatline_flag.
In short, the YAML defines the sensors and parameters, the runner maps them to DataFrame columns through column_map, applies check_flatline to each signal, ORs the results together, and adds flatline_flag to the output.
name: sensor_flatline
type: flatline
flag: flatline_flag
inputs:
Supply_Air_Temperature_Sensor:
brick: Supply_Air_Temperature_Sensor
column: Supply_Air_Temperature_Sensor
Mixed_Air_Temperature_Sensor:
brick: Mixed_Air_Temperature_Sensor
column: Mixed_Air_Temperature_Sensor
Return_Air_Temperature_Sensor:
brick: Return_Air_Temperature_Sensor
column: Return_Air_Temperature_Sensor
Outside_Air_Temperature_Sensor:
brick: Outside_Air_Temperature_Sensor
column: Outside_Air_Temperature_Sensor
params:
tolerance: 0.000001
window: 12
For this tutorial the supply air static pressure is excluded from flatline as it is legitimately flat when the fan is off but included in the next tutorial for a sensor bounds check.
The tutorial uses data_ahu7.csv (~10k rows). Place it in the examples directory (see the README there for how to obtain it). The scripts load rules from my_rules/ — your rules folder. Create a my_rules folder on your desktop (or anywhere) and run the tutorial from there; it doesn’t need to be inside the repo. check_faults_ahu7_flatline.py and check_faults_ahu7_bounds.py run flatline and bounds checks on this data. Try it yourself.
The data set has been artificially modified for flat lined values on all rows in a 3 hour time frame which could mimic a BAS/BMS device being offline.
2025-01-01 02:00:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,65.06600189208984,45.50650024414063,62.3120002746582,61.64599990844727,,,,,,,,
2025-01-01 02:15:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,64.68800354003906,45.11940002441406,61.80799865722656,61.555999755859375,,,,,,,,
2025-01-01 02:30:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,64.50800323486328,44.67440032958984,61.46599960327149,61.46599960327149,,,,,,,,
...
2025-01-01 05:45:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,75.75800323486328,44.99039840698242,61.141998291015625,60.79999923706055,,,,,,,,
2025-01-01 06:00:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,75.75800323486328,44.99039840698242,61.141998291015625,60.79999923706055,,,,,,,,
2025-01-01 06:15:00,0.0,0.0,100.0,0.0,0.0,0.0,0.0268750004470348,70.0,75.75800323486328,44.99039840698242,61.141998291015625,60.79999923706055,,,,,,,,
Episodes
There are also real instances in the dataset where the BAS supervisory controller fails to update the networked outside-air temperature value on the AHU7 controller. The fault rule detects these conditions and reports them as episodes — each episode represents a separate period when the sensor stops updating.
When you run the script.
cd examples
python check_faults_ahu7_flatline.py
What the script does
What it does: It finds each contiguous flatline episode in the data, checks which BRICK sensors were flat lined in that episode, and returns a list of episode dicts with start/end times, which sensors were flat, and flags for “all sensors flat” (device offline) vs “single sensor flat” (controller not updating).
from pathlib import Path
import pandas as pd
from open_fdd import RuleRunner
from open_fdd.engine import load_rule
from open_fdd.reports import (
analyze_flatline_episodes,
flatline_period_range,
print_column_mapping,
print_flatline_episodes,
print_summary,
sensor_cols_from_column_map,
summarize_fault,
time_range,
)
script_dir = Path(__file__).parent
csv_path = script_dir / "data_ahu7.csv"
rules_dir = script_dir / "my_rules"
# BRICK class -> CSV column (flatline check: temp sensors only)
# Supply_Air_Static_Pressure_Sensor excluded - legitimately flat when fan off
column_map = {
"Supply_Air_Temperature_Sensor": "SAT (°F)",
"Mixed_Air_Temperature_Sensor": "MAT (°F)",
"Outside_Air_Temperature_Sensor": "OAT (°F)",
"Return_Air_Temperature_Sensor": "RAT (°F)",
"Supply_Fan_Speed_Command": "SF Spd Cmd (%)",
}
df = pd.read_csv(csv_path)
df["timestamp"] = pd.to_datetime(df["timestamp"])
runner = RuleRunner(rules=[load_rule(rules_dir / "sensor_flatline.yaml")])
result = runner.run(
df,
timestamp_col="timestamp",
params={"units": "imperial"},
skip_missing_columns=True,
column_map=column_map,
)
sensor_cols = sensor_cols_from_column_map(column_map)
flatline_count = int(result["flatline_flag"].sum())
print("Flatline check only")
print_column_mapping("Column mapping", column_map)
print()
print("Results")
print(" Flatline (stuck sensor):", flatline_count, "rows flagged")
print(" Time frame:", time_range(result, "flatline_flag"))
print()
# Per-episode: which BRICK sensor(s) were flat
episodes = analyze_flatline_episodes(
result,
flag_col="flatline_flag",
timestamp_col="timestamp",
sensor_cols=sensor_cols,
tolerance=0.000001,
)
print_flatline_episodes(episodes)
print()
print("Analytics")
flatline_range = flatline_period_range(result, window=12)
print_summary(
summarize_fault(
result,
"flatline_flag",
timestamp_col="timestamp",
motor_col=column_map["Supply_Fan_Speed_Command"],
sensor_cols=sensor_cols,
period_range=flatline_range,
),
title="Flatline fault",
)
In the console this will print back this below which was ran on PowerShell and Windows.
> python .\check_faults_ahu7_flatline.py
Flatline check only
Column mapping: {'Supply_Air_Temperature_Sensor': 'SAT (°F)', 'Mixed_Air_Temperature_Sensor': 'MAT (°F)', 'Outside_Air_Temperature_Sensor': 'OAT (°F)', 'Return_Air_Temperature_Sensor': 'RAT (°F)', 'Supply_Fan_Speed_Command': 'SF Spd Cmd (%)'}
Results
Flatline (stuck sensor): 3926 rows flagged
Time frame: 2025-01-01 06:00:00 to 2025-02-28 06:15:00
--- Flatline episodes ---
(76 episodes total, showing first 10 and last 10)
Episode 1: 2025-01-01 06:00:00 to 2025-01-01 06:15:00 (2 rows)
BRICK sensors flat: Supply_Air_Temperature_Sensor, Mixed_Air_Temperature_Sensor, Outside_Air_Temperature_Sensor, Return_Air_Temperature_Sensor
Last 3 values: Supply_Air_Temperature_Sensor: [60.8, 60.8], Mixed_Air_Temperature_Sensor: [75.76, 75.76], Outside_Air_Temperature_Sensor: [44.99, 44.99], Return_Air_Temperature_Sensor: [61.14, 61.14]
All sensors flat: Yes (device offline)
Episode 2: 2025-01-05 04:45:00 to 2025-01-05 08:45:00 (17 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [21.0, 21.0, 21.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 3: 2025-01-09 10:30:00 to 2025-01-09 18:00:00 (29 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 4: 2025-01-09 21:00:00 to 2025-01-10 06:15:00 (38 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 5: 2025-01-10 09:30:00 to 2025-01-10 18:00:00 (35 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 6: 2025-01-10 21:00:00 to 2025-01-13 05:15:00 (225 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 7: 2025-01-13 08:15:00 to 2025-01-13 18:00:00 (40 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 8: 2025-01-13 21:00:00 to 2025-01-14 06:15:00 (38 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 9: 2025-01-14 09:15:00 to 2025-01-14 18:00:00 (36 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 10: 2025-01-14 21:00:00 to 2025-01-15 06:15:00 (38 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
... (56 episodes omitted) ...
Episode 67: 2025-02-21 21:00:00 to 2025-02-21 21:00:00 (1 rows)
BRICK sensors flat: (none)
Episode 68: 2025-02-22 00:00:00 to 2025-02-24 05:15:00 (212 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 69: 2025-02-24 08:15:00 to 2025-02-24 18:00:00 (40 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 70: 2025-02-24 21:00:00 to 2025-02-25 06:15:00 (37 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 71: 2025-02-25 09:15:00 to 2025-02-25 16:00:00 (26 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 72: 2025-02-25 21:00:00 to 2025-02-26 06:15:00 (37 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 73: 2025-02-26 09:15:00 to 2025-02-26 18:00:00 (34 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 74: 2025-02-26 21:00:00 to 2025-02-27 06:15:00 (38 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 75: 2025-02-27 09:15:00 to 2025-02-27 18:00:00 (36 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Episode 76: 2025-02-27 21:00:00 to 2025-02-28 06:15:00 (38 rows)
BRICK sensors flat: Outside_Air_Temperature_Sensor
Last 3 values: Outside_Air_Temperature_Sensor: [70.0, 70.0, 70.0]
Single sensor flat: Outside_Air_Temperature_Sensor (controller not updating)
Analytics
--- Flatline fault ---
total days: 105.39
total hours: 2529
hours flatline mode: 992
percent true: 38.44
percent false: 61.56
percent hours true: 39.22
hours motor runtime: 860.3
flag true Supply Air Temperature Sensor: 93.74
flag true Mixed Air Temperature Sensor: 60.39
flag true Outside Air Temperature Sensor: 69.78
flag true Return Air Temperature Sensor: 71.13
fault period start: 2025-01-01 03:00:00
fault period end: 2025-02-28 06:15:00
fault period days: 58.14
fault period hours: 1395
fault period rows: 5636
fault period rows flagged: 3926
fault period percent true: 69.66
Next: Sensor Bounds Tutorial