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

TOWER-0005 — Condenser water overcooling with fan energy

Statusverified — engine e2ff2f8, cxf:fnv1a128:b4c4b0066b62393e887a37208afffd2e, 2026-08-20
Severity3
Methodrule
Phase2
CategoryEXCESS_CONSUMPTION
ConfidenceMEDIUM
EstimationPROXY_ESTIMATION
G36
Clusters
Suppresses
Suppressed by
RelatedTOWER-0001, TOWER-0002, TOWER-0004, CHW-0005
Playbookscooling-tower-performance
SourceNREL, Refrigeration Playbook: Natural Refrigerants (2021), cooling-tower control discussion near p.45 — variable-speed fans seek leaving-water setpoint and turn off when leaving water is below setpoint; mechanism only; EnergyPlus Engineering Reference, Cooling Towers and Evaporative Fluid Coolers — fan-off free convection and fan modulation/cycling are normal ways to meet tower outlet setpoint; simulation semantics only; Library-authored thresholds and persistence; no cited source publishes 1 K, 30%, or 600 s as portable fault limits
Operating statesTower cell enabled for normal mechanical heat rejection with an active setpoint, proven fan operation, and normal automatic isolation/bypass control

Preconditions (host-enforced): Bind tower_leaving_temp to the cold water leaving this tower/cell and tower_leaving_temp_sp to the active target for that same stream; the latter is commonly named condenser-water supply or entering-chiller setpoint. Parallel cells may share a common setpoint only when that target truly applies to each cell and per-cell outlet sensing exists. Fan status and speed must describe the same cell: status is independent run proof, while speed is preferably feedback. A verified command proxy is allowed only with a documented limitation and must not be used where local control, current limiting, or drive overrides can separate command from delivered airflow. Configure minimum_fan_speed from the same-cell feedback/power relationship and effective drive minimum before adoption; the shipped 30% is NO_PORTABLE_DEFAULT. Points must be fresh and time-aligned. Exclude waterside economizer/free cooling, thermal-storage charging, emergency heat rejection, mandated low-condenser-water modes, startup/shutdown transients, drain-down, manual/local operation, and abnormal isolation or bypass states. Sensor calibration and topology must be valid; otherwise report NO_EVAL.

Points: tower_leaving_temp, tower_leaving_temp_sp, tower_fan_status, tower_fan_speed

Outputs:

  • yFault — True after material overcooling and proven loaded fan operation persist continuously for sustained_duration
  • yOvercooled — True while active tower-leaving setpoint exceeds measured leaving temperature by strictly more than overcooling_allowance
  • yFanLoaded — True while this fan is independently proven on and speed is strictly above minimum_fan_speed

Parameters:

NameDefaultUnitCXF pathDescription
overcooling_allowance1.0Kovercooled.tAllowed tower-leaving temperature undershoot. ADOPTED_TUNABLE: set outside combined sensor, setpoint-distribution, and control-loop uncertainty.
minimum_fan_speed30.0%speedHigh.tSpeed above which fan use is considered materially loaded. NO_PORTABLE_DEFAULT: 30% is an adoption-blocking placeholder; configure from measured same-cell fan feedback/power and the drive’s effective minimum. The strict graph comparison leaves exactly 30% clear.
sustained_duration600.0spersist.delayTimeContinuous overcooling with loaded fan required before alarm. ADOPTED_TUNABLE: longer than ordinary setpoint and cell-stage response.

Description

A healthy variable-speed tower reduces or stops fan work when leaving water is already colder than its active target. This rule finds the narrower waste case: the water is materially below setpoint while the same cell is proven running above a meaningful fan speed. Cold water with the fan off remains explicitly silent; natural convection is normal operation, not a fault.

Detection Logic

temperature_error = tower_leaving_temp_sp - tower_leaving_temp
overcooled         = temperature_error > overcooling_allowance
fan_loaded         = tower_fan_status AND tower_fan_speed > minimum_fan_speed
candidate          = overcooled AND fan_loaded

yOvercooled = overcooled
yFanLoaded  = fan_loaded
yFault      = candidate sustained for sustained_duration

TOWER-0005 block graph

The algebra avoids a second subtract block: setpoint minus measured leaving temperature is positive when the tower overcools. Both comparisons are strict, so exactly 1 K of undershoot or exactly 30% fan speed remains clear. TrueDelay(delayOnInit=true) resets immediately if temperature recovers, fan proof drops, or speed unloads.

Possible Diagnoses

  1. Leaving-water setpoint is reset upward but the tower sequence still follows a stale, local, or differently scaled setpoint
  2. Fan minimum speed, VFD floor, or cell-stage deadband is too high
  3. Fan is in hand/local, speed command is overridden, or drive feedback is wrong
  4. Isolation or bypass valve is stuck/mis-sequenced, making the sensed stream unrepresentative of the controlled common header
  5. Leaving-water sensor reads low or active setpoint is mapped to the wrong node
  6. Cell lead/lag logic keeps too many fans loaded after the heat-rejection load falls
  7. A legitimate low-water strategy was not excluded by the host

Energy Impact

EXCESS_CONSUMPTION with MEDIUM confidence. The direct waste is tower-fan electricity during the persisted condition. Integrating measured same-cell fan power gives a conservative upper bound, not guaranteed savings: the corrected sequence may still require some fan work. Chiller energy can move in either direction with condenser-water temperature and must not be added without a site-specific optimum-lift model.

Emissions Impact

Scope 2 from avoidable fan electricity, using the site’s marginal operating emissions rate. No portable savings or emissions fraction is claimed.

Deviations

  • No source establishes the three numeric defaults. The 1 K allowance and 10-minute persistence are adopted commissioning starts. The 30% loaded floor is NO_PORTABLE_DEFAULT: configure it from same-cell feedback/power before adoption because a speed percentage is not a universal meaningful-energy line.
  • The 1 K allowance must exceed measurement uncertainty. Common-header setpoints compared with individual-cell outlet temperatures can create a false undershoot on parallel towers; topology and setpoint distribution are preconditions, not hidden graph assumptions.
  • Speed feedback is preferred. A verified command proxy is weaker because a drive in local, limited, or overridden operation can deliver something else. The host must label that binding and avoid near-threshold conclusions.
  • This rule stays outside CLU-10. It is a control/energy finding, not the condenser-side degradation syndrome triggered by TOWER-0001.
  • No suppression is encoded. TOWER-0004 failure-to-start can invalidate the fan premise, while its unexpected-run direction can cause this exact fault; whole-rule suppression would erase a useful diagnosis.
  • Simulation validates healthy FPR only, not causal TPR. The recorded Denver July campaign uses native per-object outlet temperature, fan power, and airflow ratio at 60 s. Airflow ratio is an effective-airflow proxy rather than mechanical VFD feedback, and the parallel towers share one loop target.

Notes

Read both diagnostic outputs before dispatch. yOvercooled=true with yFanLoaded=false is often normal free convection. yFanLoaded=true without overcooling says the fan may simply be doing useful work.

Test Vectors

20 scenarios, clock step 60 s over 2400 s.

ScenarioDescription
on_setpoint_fan_loadedA loaded fan at the active leaving-water setpoint is not overcooling.
naturally_cold_fan_offCold water with the fan proven off exposes temperature context but never raises the energy finding.
cold_fan_below_minimumA running fan below the strict diagnostic speed floor is not treated as materially loaded.
cold_loaded_at_initializationA standing violation serves the complete initialization-safe sustained duration.
temperature_exact_boundary_clearExactly 1 K below setpoint is clear because the comparator is strict.
temperature_just_beyond_boundaryOne test increment beyond the adopted temperature allowance is overcooled.
speed_exact_boundary_clearExactly 30 percent is clear because loaded-fan comparison is strict.
speed_just_beyond_boundaryOne test increment above the speed floor is materially loaded.
violation_ends_one_tick_before_persistenceA 540-second candidate clears one evaluator tick before the 600-second delay and never alarms.
mature_alarm_recoversAn already-mature overcooling alarm clears immediately when leaving water returns to setpoint.
fan_unload_resets_timerDropping below the loaded-fan floor resets persistence even while water remains cold.
setpoint_change_resets_candidateA temporary active-setpoint increase creates a sub-duration candidate that clears when the setpoint returns.
status_false_blocks_high_speedA high speed value without independent run proof cannot satisfy the loaded-fan gate.
two_short_episodes_do_not_accumulateSeparated overcooling episodes each need a complete sustained duration.
threshold_chatter_never_maturesTemperature chatter across the strict allowance repeatedly resets persistence.
startup_violation_uses_full_delayA candidate beginning after healthy initialization receives the same complete delay.
both_thresholds_just_beyondTemperature and speed one test increment beyond their strict boundaries mature the full finding.
speed_threshold_chatter_never_maturesSpeed chatter across the strict loaded threshold repeatedly resets persistence.
setpoint_changes_but_candidate_remains_trueA setpoint change that leaves error above the allowance does not reset the Boolean candidate’s timer.
intentional_low_water_mode_is_host_excludedIntentional free-cooling or mandated low-condenser-water operation raises the raw graph; the host must report NO_EVAL for that mode.
vectors.json
{
  "schema": "cxf-library/vectors/v1",
  "clock": {
    "step_s": 60,
    "horizon_s": 2400
  },
  "scenarios": [
    {
      "name": "on_setpoint_fan_loaded",
      "description": "A loaded fan at the active leaving-water setpoint is not overcooling.",
      "inputs": {
        "tower_leaving_temp": 25.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "naturally_cold_fan_off",
      "description": "Cold water with the fan proven off exposes temperature context but never raises the energy finding.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": false,
        "tower_fan_speed": 0.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "cold_fan_below_minimum",
      "description": "A running fan below the strict diagnostic speed floor is not treated as materially loaded.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 20.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "cold_loaded_at_initialization",
      "description": "A standing violation serves the complete initialization-safe sustained duration.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "temperature_exact_boundary_clear",
      "description": "Exactly 1 K below setpoint is clear because the comparator is strict.",
      "inputs": {
        "tower_leaving_temp": 24.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "temperature_just_beyond_boundary",
      "description": "One test increment beyond the adopted temperature allowance is overcooled.",
      "inputs": {
        "tower_leaving_temp": 23.99,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "speed_exact_boundary_clear",
      "description": "Exactly 30 percent is clear because loaded-fan comparison is strict.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 30.0
      },
      "expect": [
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "speed_just_beyond_boundary",
      "description": "One test increment above the speed floor is materially loaded.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 30.01
      },
      "expect": [
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "violation_ends_one_tick_before_persistence",
      "description": "A 540-second candidate clears one evaluator tick before the 600-second delay and never alarms.",
      "inputs": {
        "tower_leaving_temp": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 540,
            "value": 25.0
          }
        ],
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "mature_alarm_recovers",
      "description": "An already-mature overcooling alarm clears immediately when leaving water returns to setpoint.",
      "inputs": {
        "tower_leaving_temp": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 900,
            "value": 25.0
          }
        ],
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 840,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 900,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "fan_unload_resets_timer",
      "description": "Dropping below the loaded-fan floor resets persistence even while water remains cold.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": [
          {
            "t": 0,
            "value": 50.0
          },
          {
            "t": 300,
            "value": 20.0
          },
          {
            "t": 600,
            "value": 50.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 1140,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 1260,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "setpoint_change_resets_candidate",
      "description": "A temporary active-setpoint increase creates a sub-duration candidate that clears when the setpoint returns.",
      "inputs": {
        "tower_leaving_temp": 25.0,
        "tower_leaving_temp_sp": [
          {
            "t": 0,
            "value": 25.0
          },
          {
            "t": 300,
            "value": 27.0
          },
          {
            "t": 840,
            "value": 25.0
          }
        ],
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "status_false_blocks_high_speed",
      "description": "A high speed value without independent run proof cannot satisfy the loaded-fan gate.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": false,
        "tower_fan_speed": 100.0
      },
      "expect": [
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "two_short_episodes_do_not_accumulate",
      "description": "Separated overcooling episodes each need a complete sustained duration.",
      "inputs": {
        "tower_leaving_temp": [
          {
            "t": 0,
            "value": 23.0
          },
          {
            "t": 300,
            "value": 25.0
          },
          {
            "t": 600,
            "value": 23.0
          },
          {
            "t": 900,
            "value": 25.0
          }
        ],
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "threshold_chatter_never_matures",
      "description": "Temperature chatter across the strict allowance repeatedly resets persistence.",
      "inputs": {
        "tower_leaving_temp": [
          {
            "t": 0,
            "value": 23.99
          },
          {
            "t": 120,
            "value": 24.01
          },
          {
            "t": 240,
            "value": 23.99
          },
          {
            "t": 360,
            "value": 24.01
          },
          {
            "t": 480,
            "value": 23.99
          },
          {
            "t": 600,
            "value": 24.01
          }
        ],
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "startup_violation_uses_full_delay",
      "description": "A candidate beginning after healthy initialization receives the same complete delay.",
      "inputs": {
        "tower_leaving_temp": [
          {
            "t": 0,
            "value": 25.0
          },
          {
            "t": 300,
            "value": 23.0
          }
        ],
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 840,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 960,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "both_thresholds_just_beyond",
      "description": "Temperature and speed one test increment beyond their strict boundaries mature the full finding.",
      "inputs": {
        "tower_leaving_temp": 23.99,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 30.01
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFanLoaded",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 540,
          "equals": false
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "speed_threshold_chatter_never_matures",
      "description": "Speed chatter across the strict loaded threshold repeatedly resets persistence.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": [
          {
            "t": 0,
            "value": 30.01
          },
          {
            "t": 120,
            "value": 30.0
          },
          {
            "t": 240,
            "value": 30.01
          },
          {
            "t": 360,
            "value": 30.0
          },
          {
            "t": 480,
            "value": 30.01
          },
          {
            "t": 600,
            "value": 30.0
          }
        ]
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 0,
          "to_s": 2400,
          "equals": false
        }
      ]
    },
    {
      "name": "setpoint_changes_but_candidate_remains_true",
      "description": "A setpoint change that leaves error above the allowance does not reset the Boolean candidate's timer.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": [
          {
            "t": 0,
            "value": 25.0
          },
          {
            "t": 300,
            "value": 26.0
          }
        ],
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yOvercooled",
          "from_s": 0,
          "to_s": 2400,
          "equals": true
        },
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    },
    {
      "name": "intentional_low_water_mode_is_host_excluded",
      "description": "Intentional free-cooling or mandated low-condenser-water operation raises the raw graph; the host must report NO_EVAL for that mode.",
      "inputs": {
        "tower_leaving_temp": 23.0,
        "tower_leaving_temp_sp": 25.0,
        "tower_fan_status": true,
        "tower_fan_speed": 50.0
      },
      "expect": [
        {
          "output": "yFault",
          "from_s": 660,
          "to_s": 2400,
          "equals": true
        }
      ]
    }
  ]
}