The bounds script checks whether sensor values fall outside valid ranges (e.g. temperature too high or too low). Like the flatline check, it runs in episodic fashion—finding each contiguous run of flagged rows and reporting which BRICK sensor(s) were out of bounds in that episode. OOB means “out of bounds.” In this context, an OOB sensor is one whose readings fall outside the allowed range (e.g. temperature or pressure outside the configured bounds).

Metric conversion

Imperial uses °F for temperature and inH2O for pressure. Metric uses °C and Pa.

  • Temperature: °F → °C: (F - 32) * 5/9
  • Pressure: 1 inH2O ≈ 249 Pa

Example: [-0.5, 2.5] inH2O → [-125, 623] Pa

name: bad_sensor_check
type: bounds
flag: bad_sensor_flag

params:
  units: imperial

inputs:
  Supply_Air_Temperature_Sensor:
    brick: Supply_Air_Temperature_Sensor
    column: Supply_Air_Temperature_Sensor
    bounds:
      imperial: [50, 90]
      metric: [10, 32]
  Mixed_Air_Temperature_Sensor:
    brick: Mixed_Air_Temperature_Sensor
    column: Mixed_Air_Temperature_Sensor
    bounds:
      imperial: [50, 100]
      metric: [10, 38]
  Return_Air_Temperature_Sensor:
    brick: Return_Air_Temperature_Sensor
    column: Return_Air_Temperature_Sensor
    bounds:
      imperial: [60, 100] 
      metric: [16, 38] 
  Outside_Air_Temperature_Sensor:
    brick: Outside_Air_Temperature_Sensor
    column: Outside_Air_Temperature_Sensor
    bounds:
      imperial: [-40, 120] 
      metric: [-40, 49]    
  Supply_Air_Static_Pressure_Sensor:
    brick: Supply_Air_Static_Pressure_Sensor
    column: Supply_Air_Static_Pressure_Sensor
    bounds:
      imperial: [-0.5, 2.5] 
      metric: [-125, 623]  

Run it. Rules load from my_rules/ — create that folder on your desktop and run from there if you prefer; it doesn’t need to be inside the repo.

cd examples
python check_faults_ahu7_bounds.py

What the script does

What it does: RuleRunner loads sensor_bounds.yaml and parses it into a rule dict with type bounds, inputs with bounds.imperial or bounds.metric, and params.units. For each input, the runner uses column_map to map BRICK class to DataFrame column (e.g. Supply_Air_Temperature_Sensor to SAT (°F)). It calls check_bounds(df[col], low, high) using the bounds for the active unit system. A row is flagged if any sensor value is outside [low, high]. The masks from each sensor are combined with =, so a row is flagged if any sensor is out of bounds. The combined mask is stored as bad_sensor_flag on the result DataFrame. analyze_bounds_episodes finds each contiguous run of bad_sensor_flag=1 and determines which BRICK sensor(s) had values outside bounds in that episode. print_bounds_episodes formats and prints them (first and last N when there are many), including average readings per OOB sensor.

Bounds script code



from pathlib import Path

import pandas as pd

from open_fdd import RuleRunner
from open_fdd.engine import bounds_map_from_rule, load_rule
from open_fdd.reports import (
    analyze_bounds_episodes,
    print_bounds_episodes,
    print_column_mapping,
    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 (bounds check: temp + static pressure sensors)
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_Air_Static_Pressure_Sensor": "SA Static Press (inH₂O)",
    "Supply_Fan_Speed_Command": "SF Spd Cmd (%)",
}

# Bounds from my_rules (your rules for your deployment)
bounds_rule = load_rule(rules_dir / "sensor_bounds.yaml")
bounds_map = bounds_map_from_rule(bounds_rule, units="imperial")

df = pd.read_csv(csv_path)
df["timestamp"] = pd.to_datetime(df["timestamp"])

runner = RuleRunner(rules=[load_rule(rules_dir / "sensor_bounds.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)

bounds_count = int(result["bad_sensor_flag"].sum())

print("Bounds check only")
print_column_mapping("Column mapping", column_map)
print()
print("Results")
print("  Bounds (out-of-range):", bounds_count, "rows flagged")
print("    Time frame:", time_range(result, "bad_sensor_flag"))
print()

# Per-episode: which BRICK sensor(s) were out of bounds
episodes = analyze_bounds_episodes(
    result,
    flag_col="bad_sensor_flag",
    timestamp_col="timestamp",
    sensor_cols=sensor_cols,
    bounds_map=bounds_map,
)
print_bounds_episodes(episodes)

print()
print("Analytics")
print_summary(
    summarize_fault(
        result,
        "bad_sensor_flag",
        timestamp_col="timestamp",
        motor_col=column_map["Supply_Fan_Speed_Command"],
        sensor_cols=sensor_cols,
    ),
    title="Bounds fault",
)


Bounds console output (episodic)

When you run the bounds script with tight tolerances for testing:


> python .\check_faults_ahu7_bounds.py  
Bounds 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_Air_Static_Pressure_Sensor': 'SA Static Press (inH?O)', 'Supply_Fan_Speed_Command': 'SF Spd Cmd (%)'}  

Results
  Bounds (out-of-range): 3146 rows flagged
    Time frame: 2025-01-02 06:30:00 to 2025-04-16 09:00:00


--- Bounds episodes ---
  (138 episodes total, showing first 10 and last 10)

  Episode 1: 2025-01-02 06:30:00 to 2025-01-02 06:30:00 (1 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 91.15
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 2: 2025-01-03 01:30:00 to 2025-01-03 05:30:00 (17 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.47
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 3: 2025-01-03 06:30:00 to 2025-01-03 06:30:00 (1 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 95.54
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 4: 2025-01-04 01:30:00 to 2025-01-04 08:30:00 (29 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.37
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 5: 2025-01-04 21:30:00 to 2025-01-04 21:30:00 (1 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.54
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 6: 2025-01-04 22:00:00 to 2025-01-04 22:30:00 (3 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.79
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 7: 2025-01-05 00:15:00 to 2025-01-05 09:00:00 (36 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 57.14
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 8: 2025-01-06 05:30:00 to 2025-01-06 05:30:00 (1 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 93.78
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 9: 2025-01-07 04:00:00 to 2025-01-07 04:15:00 (2 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.89
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 10: 2025-01-07 04:45:00 to 2025-01-07 06:15:00 (7 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 59.57
    Single sensor OOB: Return_Air_Temperature_Sensor

  ... (118 episodes omitted) ...

  Episode 129: 2025-04-05 17:30:00 to 2025-04-05 18:45:00 (6 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 91.82
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 130: 2025-04-08 06:00:00 to 2025-04-08 06:15:00 (2 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 58.18
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 131: 2025-04-10 23:15:00 to 2025-04-11 06:15:00 (29 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 54.29
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 132: 2025-04-11 19:15:00 to 2025-04-13 10:15:00 (156 rows)
    BRICK sensors out of bounds: Mixed_Air_Temperature_Sensor, Return_Air_Temperature_Sensor
    Avg readings: Mixed_Air_Temperature_Sensor: 51.24, Return_Air_Temperature_Sensor: 49.55

  Episode 133: 2025-04-13 20:45:00 to 2025-04-14 05:15:00 (35 rows)
    BRICK sensors out of bounds: Mixed_Air_Temperature_Sensor, Return_Air_Temperature_Sensor
    Avg readings: Mixed_Air_Temperature_Sensor: 52.99, Return_Air_Temperature_Sensor: 49.36

  Episode 134: 2025-04-14 05:30:00 to 2025-04-14 05:30:00 (1 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 57.29
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 135: 2025-04-14 18:00:00 to 2025-04-14 18:00:00 (1 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 90.14
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 136: 2025-04-14 18:15:00 to 2025-04-14 18:30:00 (2 rows)
    BRICK sensors out of bounds: Supply_Air_Temperature_Sensor
    Avg readings: Supply_Air_Temperature_Sensor: 90.32
    Single sensor OOB: Supply_Air_Temperature_Sensor

  Episode 137: 2025-04-15 22:15:00 to 2025-04-16 06:15:00 (31 rows)
    BRICK sensors out of bounds: Return_Air_Temperature_Sensor
    Avg readings: Return_Air_Temperature_Sensor: 54.24
    Single sensor OOB: Return_Air_Temperature_Sensor

  Episode 138: 2025-04-16 06:30:00 to 2025-04-16 09:00:00 (11 rows)
    BRICK sensors out of bounds: Mixed_Air_Temperature_Sensor, Return_Air_Temperature_Sensor
    Avg readings: Mixed_Air_Temperature_Sensor: 55.32, Return_Air_Temperature_Sensor: 52.43

Analytics

--- Bounds fault ---
  total days: 105.39
  total hours: 2529
  hours bad sensor mode: 785
  percent true: 30.8
  percent false: 69.2
  percent hours true: 31.03
  hours motor runtime: 860.3
  flag true Supply Air Temperature Sensor: 103.62
  flag true Mixed Air Temperature Sensor: 59.44
  flag true Outside Air Temperature Sensor: 64.22
  flag true Return Air Temperature Sensor: 67.41
  flag true Supply Air Static Pressure Sensor: 0.03
  fault period start: 2025-01-02 06:30:00
  fault period end: 2025-04-16 09:00:00
  fault period days: 104.1
  fault period hours: 2498
  fault period rows: 10090
  fault period rows flagged: 3146
  fault period percent true: 31.18


Next: SPARQL & Validate Prereq — test SPARQL, validate model before running faults


MIT License

This site uses Just the Docs, a documentation theme for Jekyll.