Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VFD-0001 — Command vs feedback deviation

Statusverified — engine e2ff2f8, cxf:fnv1a128:5c7f261d8eb5790babf4c55afa671f06, 2026-08-17
Severity2
Methodrule
Phase2
CategoryPROTECTIVE
ConfidenceMEDIUM
EstimationQUALITATIVE_ONLY
G36
Clusters
SuppressesVFD-0002, VFD-0003, VFD-0004
Suppressed by
RelatedVFD-0002, VFD-0003, VFD-0004, VFD-0005, PMP-0006
Playbooksvfd-pump-faults
SourceHVAC FDD Reference v1.0 §15, VFD-0001; Ali et al. 2020; Engineering best practice
Operating statesdrive commanded to run above its minimum speed

Preconditions (host-enforced): vfd_speed_cmd and vfd_speed must be the same drive’s command and feedback, both scaled 0-100% of rated speed. The rule does no unit conversion: a site trending feedback in Hz against a percent command reads as a permanent 40-point deviation on a 60 Hz drive. Both points must be fresh — a stale feedback value held at its last reading is a communication fault (diagnosis 4), which this rule reports as a drive fault, the right alarm for the wrong reason. Where the site trends both a drive-reported speed and a tachometer, prefer the drive-reported value, since diagnosis 5 is the tachometer itself. Command evaluability is signalled in-rule by yCmdOk: when it is false the verdict is NO_EVAL, not healthy, and in particular a drive commanded off is not evaluated at all.

Points: vfd_speed_cmd, vfd_speed

Outputs:

  • yFault — True while the drive is commanded above min_cmd_for_eval and its feedback has stayed more than speed_error_threshold away from the command, for deviation_duration plus alarm_delay
  • yCmdOk — Evaluability signal — true when vfd_speed_cmd exceeds min_cmd_for_eval; false means NO_EVAL and the host must ignore yFault

Parameters:

NameDefaultUnitCXF pathDescription
speed_error_threshold5.0%devHigh.tCommand-versus-feedback tolerance, in points of rated speed, in either direction
min_cmd_for_eval20.0%cmdOk.tSpeed command below which the drive is not obliged to track and the comparison is not evaluable. ADOPTED — the reference states no such parameter; the default is VFD-0002’s min_speed
deviation_duration300.0strack.delayTimeContinuous deviation required before it counts as sustained rather than a drive ramping to a new command (5 min)
alarm_delay300.0spersist.delayTimeFurther persistence required after deviation_duration before the alarm asserts (5 min)

Description

A variable frequency drive is asked for a speed and reports back the speed it is running. When those two numbers separate and stay separated, something between the control loop and the shaft has stopped working — the drive derating itself on a hot heatsink, a motor loading up on a failing bearing, a belt slipping so the driven equipment never reaches the speed the motor does, or a command that never arrived. The rule is deliberately indifferent to which: two points, one subtraction and a tolerance, which is why the same rule covers fan and pump drives without change. What it buys is early warning on a component whose failures are progressive — a derating drive or a slipping belt deteriorates for weeks before it stops — so the alarm is about the equipment, not about energy.

Detection Logic

deviation = |vfd_speed_cmd − vfd_speed|

yCmdOk = vfd_speed_cmd > min_cmd_for_eval        (false ⇒ host reports NO_EVAL)
yFault = (deviation > speed_error_threshold AND yCmdOk)
         sustained for deviation_duration, then held a further alarm_delay

Block graph (rule.cxf.jsonld):

VFD-0001 block graph

err and absErr form the unsigned deviation, so the test is symmetric: a drive falling short of its command and a drive overrunning it are both faults and both alarm. Belt slip and a failing tachometer sit on opposite sides of that same comparison.

vfd_speed_cmd fans out a second time into cmdOk, the evaluability branch, whose output is both the boundary output yCmdOk and the second input of gate. A drive commanded below its minimum speed therefore holds yFault down — and that false means unknown, not healthy. Below its own minimum a drive is under no obligation to track: a loop output of 10% on a drive whose minimum is 20% leaves the machine stopped, and the resulting 10-point “deviation” is correct behavior.

track and persist are two delays in series (the AHU-0027 pattern, also used by VAV-0004): track is the reference’s sustained for deviation_duration, persist its separate AlarmDelay, ten minutes total at the defaults and independently tunable. Any moment of tracking drops both timers and discards the accumulated time, so the alarm always describes one continuous deviation. Both comparisons are strict — a deviation of exactly 5.0 points is not a fault, a command of exactly 20.0% is not evaluable.

Possible Diagnoses

  1. VFD internal fault — overcurrent, overvoltage, or overtemperature, the last of which shows first as quiet derating rather than a trip
  2. Motor bearing failure, loading the drive until it can no longer hold commanded speed
  3. Belt slip on fan applications — the motor reaches its speed and the driven equipment does not, so which of the two the feedback reports decides whether this rule can see it at all
  4. Communication fault between the BAS and the drive, leaving the drive on its last received command or a local setpoint
  5. Speed sensor or tachometer failure — the drive is fine and the measurement is wrong, which is why the point dictionary prefers the drive-reported speed where both are trended

Energy Impact

PROTECTIVE, MEDIUM confidence, QUALITATIVE_ONLY. There is no per-fault energy model and the reference does not offer one; it points to the Energy Impact Reference §4.4 framework, which applies only once the failing component is known. The direction of the waste depends on the cause: a derating drive makes a pressure-controlled loop work longer for the same result, a slipping belt turns shaft power into heat in the belt, and a failed tachometer costs nothing until someone acts on its reading. Neutral climate sensitivity. The number worth quoting to an owner is the repair — the playbook puts a drive replacement at $500-$2,000 depending on motor horsepower.

Emissions Impact

Scope 2, QUALITATIVE_EMISSIONS, MEDIUM confidence, basis N/A. VFD-driven equipment is electric, so whatever additional draw the fault produces is purchased electricity. The reference declines to give a range — “protective; indirect via motor/VFD damage” — and that is the honest reading: the emissions consequence is dominated by the replacement hardware and the runtime of whatever the drive serves, neither of which this rule measures.

Deviations

  • min_cmd_for_eval is an adopted addition; the reference has no such parameter, only the bare deviation test. Without the gate the rule misfires wherever a drive is commanded below the speed it can physically hold: the drive sits stopped, the command sits at 10%, and the rule reads a 10-point fault on a machine behaving as designed. The 20.0% default is VFD-0002’s min_speed — same chapter, same family, same quantity — and hosts retune it to the drive’s actual minimum. Exposed as yCmdOk per SCHEMA.md so the host can tell NO_EVAL from healthy.
  • The gate creates a blind spot, and not a small one. A drive running while commanded off — a welded contactor, a hand-off-auto switch left in hand, the stuck last-command case of diagnosis 4 — has a command of 0% and is never evaluated at any positive min_cmd_for_eval. The check that would catch it is a run-status-versus-command comparison at equipment level, which this library has no drive rule for; AHU-0018 and RTU-0006 ask the nearest question (a fan running outside its schedule) and catch only the after-hours version. Read yCmdOk = false as this rule standing down, not as an idle drive being fine.
  • Two delays in series rather than one. The reference lists deviation_duration and AlarmDelay as separate tunables for the same condition, so both are kept and chained. A single 600 s delay would be indistinguishable as shipped, but a site wanting a two-minute deviation window and a ten-minute alarm hold can have it. Precedent: VAV-0004, whose reference tunables have the identical shape.
  • Strict > at the deviation threshold. The reference writes > too, so nothing is lost, and CDL Reals has no GreaterEqual to express the inclusive form anyway. A deviation of exactly 5.0 points reads healthy; the disagreement is measure-zero on a real-valued signal and both sides are pinned.
  • suppresses: [VFD-0002, VFD-0003, VFD-0004] is an authored relationship, not the reference’s. All three rules infer operating limits or motion from the same vfd_speed feedback; a drive not tracking its command cannot support those premises. Each card carries the matching suppressed_by. Suppression must be instance-scoped to the same physical drive.
  • Both points are percent of rated speed and the rule converts nothing. The point dictionary declares % for both; a drive trended in Hz must be scaled before binding. Stated in the preconditions because the failure mode is a permanent, plausible-looking deviation rather than an obvious error.
  • delayOnInit = true on both delays (CDL default is false): a drive already deviating when the controller starts waits out the full ten minutes rather than alarming on the first tick.
  • Frontmatter clusters is empty — the reference defines no cluster containing a VFD rule, and this card does not edit the cluster set. g36 is null: a research-backed 050-range rule sourced to Ali et al. 2020 and engineering best practice rather than to a G36 clause.
  • The reference publishes no test vectors for this card; every scenario in vectors.json is library-authored.

Notes

Read yFault and yCmdOk together. An alarming drive can go quiet because the loop backed its command below the evaluation floor rather than because it recovered — both outputs fall on the same tick, and only the pair distinguishes that from a genuine recovery. Step 3.1 of the vfd-pump-faults playbook, verifying that the VFD output tracks the command within 5%, is this rule’s threshold read as an acceptance test: the same number that raises the alarm closes the work order. VFD-0002 asks a different question of the same drive — not whether it follows its command, but whether the command has run out of room. Clear this one first.

Test Vectors

11 scenarios, clock step 60 s over 2400 s.

ScenarioDescription
drive_tracking_commandHealthy drive: commanded to 60%, running at 58%. A 2-point offset is inside the 5% tolerance and the drive is well above the evaluation floor, so the rule is live and silent.
feedback_below_commandCommanded to 60%, running at 40%: the drive is 20 points short of what it was asked for. Both delays run from t=0 under delayOnInit, so the alarm lands 600 s in (deviation_duration + alarm_delay).
feedback_above_commandThe mirror case: commanded to 40%, running at 60%. Abs makes the test symmetric, so a drive overrunning its command alarms exactly as a drive falling short does.
deviation_exactly_at_thresholdBoundary: 60% commanded against 55% feedback is a deviation of exactly speed_error_threshold (5.0). CDL Reals has no GreaterEqual, so the comparison is strict and exactly 5 clears.
deviation_just_above_thresholdBoundary from the other side: 54.9% feedback against the same command is a 5.1-point deviation, and the alarm lands after the two delays.
command_below_run_floorCommanded to 10% — below the drive’s own minimum speed — with the drive stopped. The 10-point deviation clears the threshold, but a drive is not obliged to track a command below its minimum, so yCmdOk is false and the verdict is NO_EVAL rather than a fault.
command_exactly_at_run_floorEvaluability boundary: the command sits exactly on min_cmd_for_eval (20.0%). Strict > again, so exactly 20 is not evaluable.
command_just_above_run_floorEvaluability boundary from the other side: 20.1% commanded with the drive stopped. The rule becomes evaluable and the standing 20.1-point deviation alarms after the two delays.
deviation_shorter_than_both_delaysAn 8-minute deviation: long enough to mature track (5 min) but only 3 of the further 5 minutes persist needs. No alarm. With a single 300 s delay in place of the chain this scenario would fire at 300 s, which is what makes it the regression test for the two-delay structure.
drive_recovers_after_alarmRecovery: the alarm asserts at 600 s and the drive comes back to within 1 point of its command at t=1200 s. Both delays drop on that tick — TrueDelay only delays the rising edge.
command_drops_below_floor_after_alarmThe evaluability release: a deviating drive alarms at 600 s, then the loop backs the command down to 10% at t=1200 s. yCmdOk goes false and yFault follows — the host must read that pair as NO_EVAL, not as a drive that fixed itself.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 2400
  },
  "scenarios": [
    {
      "name": "drive_tracking_command",
      "description": "Healthy drive: commanded to 60%, running at 58%. A 2-point offset is inside the 5% tolerance and the drive is well above the evaluation floor, so the rule is live and silent.",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": 58.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "feedback_below_command",
      "description": "Commanded to 60%, running at 40%: the drive is 20 points short of what it was asked for. Both delays run from t=0 under delayOnInit, so the alarm lands 600 s in (deviation_duration + alarm_delay).",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": 40.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "feedback_above_command",
      "description": "The mirror case: commanded to 40%, running at 60%. Abs makes the test symmetric, so a drive overrunning its command alarms exactly as a drive falling short does.",
      "inputs": {
        "vfd_speed_cmd": 40.0,
        "vfd_speed": 60.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "deviation_exactly_at_threshold",
      "description": "Boundary: 60% commanded against 55% feedback is a deviation of exactly speed_error_threshold (5.0). CDL Reals has no GreaterEqual, so the comparison is strict and exactly 5 clears.",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": 55.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "deviation_just_above_threshold",
      "description": "Boundary from the other side: 54.9% feedback against the same command is a 5.1-point deviation, and the alarm lands after the two delays.",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": 54.9
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "command_below_run_floor",
      "description": "Commanded to 10% \u2014 below the drive's own minimum speed \u2014 with the drive stopped. The 10-point deviation clears the threshold, but a drive is not obliged to track a command below its minimum, so yCmdOk is false and the verdict is NO_EVAL rather than a fault.",
      "inputs": {
        "vfd_speed_cmd": 10.0,
        "vfd_speed": 0.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "command_exactly_at_run_floor",
      "description": "Evaluability boundary: the command sits exactly on min_cmd_for_eval (20.0%). Strict `>` again, so exactly 20 is not evaluable.",
      "inputs": {
        "vfd_speed_cmd": 20.0,
        "vfd_speed": 0.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "command_just_above_run_floor",
      "description": "Evaluability boundary from the other side: 20.1% commanded with the drive stopped. The rule becomes evaluable and the standing 20.1-point deviation alarms after the two delays.",
      "inputs": {
        "vfd_speed_cmd": 20.1,
        "vfd_speed": 0.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "deviation_shorter_than_both_delays",
      "description": "An 8-minute deviation: long enough to mature `track` (5 min) but only 3 of the further 5 minutes `persist` needs. No alarm. With a single 300 s delay in place of the chain this scenario would fire at 300 s, which is what makes it the regression test for the two-delay structure.",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": [
          {
            "t": 0,
            "value": 40.0
          },
          {
            "t": 480,
            "value": 60.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "drive_recovers_after_alarm",
      "description": "Recovery: the alarm asserts at 600 s and the drive comes back to within 1 point of its command at t=1200 s. Both delays drop on that tick \u2014 TrueDelay only delays the rising edge.",
      "inputs": {
        "vfd_speed_cmd": 60.0,
        "vfd_speed": [
          {
            "t": 0,
            "value": 40.0
          },
          {
            "t": 1200,
            "value": 59.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1260,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "command_drops_below_floor_after_alarm",
      "description": "The evaluability release: a deviating drive alarms at 600 s, then the loop backs the command down to 10% at t=1200 s. yCmdOk goes false and yFault follows \u2014 the host must read that pair as NO_EVAL, not as a drive that fixed itself.",
      "inputs": {
        "vfd_speed_cmd": [
          {
            "t": 0,
            "value": 60.0
          },
          {
            "t": 1200,
            "value": 10.0
          }
        ],
        "vfd_speed": 40.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 1260,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yCmdOk",
          "from_s": 0,
          "to_s": 1140,
          "equals": true
        },
        {
          "output": "yCmdOk",
          "from_s": 1200,
          "to_s": 2400,
          "equals": false
        }
      ]
    }
  ]
}