import sys
import os
import argparse
import math
# The script must use sys.path.append(...) for the bog_builder import.
# This block attempts to add the parent directory (assuming 'src' is a sibling)
# and then the 'src' directory itself to sys.path to locate bog_builder.
# This setup prioritizes being self-contained while also supporting common project structures.
try:
from bog_builder import BogFolderBuilder
except ImportError:
current_dir = os.path.dirname(os.path.abspath(__file__))
# Assuming bog_builder is in a 'src' directory sibling to the script's directory,
# or directly in the current directory if running from a flat structure.
project_root = os.path.abspath(os.path.join(current_dir, ".."))
sys.path.append(project_root)
sys.path.append(os.path.join(project_root, "src"))
try:
from bog_builder import BogFolderBuilder
except ImportError:
print("Error: bog_builder could not be imported.")
print("Please ensure 'bog_builder.py' is in the current directory,")
print("or in a 'src' directory one level up from this script.")
sys.exit(1)
def main():
"""
Generates a Niagara .bog file for an AHU economizer sequence.
The logic includes:
- One heating valve, one cooling valve, one economizer damper.
- Interlocks to prevent heating and economizing simultaneously.
- Logic for free cooling (economizer) when OAT is suitable and cooling is required.
- Logic for mechanical cooling when OAT is too high for free cooling, or if
free cooling is insufficient (implicitly handled by priority here).
- PID control for Supply Air Temperature (SAT) for all three outputs.
"""
# Requirements: Saves exactly one .bog file named 'economizer.bog'
# TO THE CURRENT WORKING DIRECTORY.
# Requirements: MUST NOT accept any command-line arguments for the output path.
output_filename = "economizer.bog"
output_path = os.path.abspath(output_filename) # Ensures it's in the current working directory
builder = BogFolderBuilder("AHU_Economizer_Sequence", debug=True)
print("--- Creating Top-Level Inputs & Outputs ---")
# --- INPUTS ---
builder.add_numeric_writable("OAT_Sensor_F", default_value=75.0, precision=1)
builder.add_numeric_writable("SAT_Sensor_F", default_value=70.0, precision=1)
builder.add_numeric_writable("SAT_Setpoint_F", default_value=70.0, precision=1)
builder.add_boolean_writable("AHU_Fan_Status", default_value=False) # Master AHU Enable
# --- OUTPUTS ---
builder.add_numeric_writable("Heating_Valve_Cmd", default_value=0.0, precision=1)
builder.add_numeric_writable("Cooling_Valve_Cmd", default_value=0.0, precision=1)
builder.add_numeric_writable("Economizer_Damper_Cmd", default_value=0.0, precision=1)
print("\n--- Creating Setpoints and Constants ---")
builder.start_sub_folder("Setpoints_And_Tuning")
builder.add_numeric_writable("Cooling_PID_PB", default_value=10.0, precision=1)
builder.add_numeric_writable("Cooling_PID_I", default_value=300.0, precision=1)
builder.add_numeric_writable("Heating_PID_PB", default_value=10.0, precision=1)
builder.add_numeric_writable("Heating_PID_I", default_value=300.0, precision=1)
# OAT conditions for economizer and heating lockout
builder.add_numeric_writable("Econ_OAT_Limit_F", default_value=60.0, precision=1) # OAT below this is good for free cooling
builder.add_numeric_writable("Heating_Lockout_OAT_F", default_value=55.0, precision=1) # OAT above this locks out heating
# Deadband for SAT control to prevent rapid cycling
builder.add_numeric_writable("Deadband_Half_F", default_value=1.0, precision=1)
# Common constants
builder.add_numeric_const("Const_Neg_1_For_Multiply", value=-1.0) # Used to invert values
builder.add_numeric_const("Const_PID_Output_Min", value=0.0)
builder.add_numeric_const("Const_PID_Output_Max", value=100.0)
# Boolean constants for PID loopAction (True for Direct, False for Reverse)
builder.add_boolean_const("PID_Action_Direct", value=True)
builder.add_boolean_const("PID_Action_Reverse", value=False)
builder.end_sub_folder()
print("\n--- Creating Demand Calculation Logic ---")
builder.start_sub_folder("Demand_Calculation")
builder.add_subtract("SAT_Error_F") # SAT_Sensor - SAT_Setpoint
# Check if cooling is required (SAT > SP + Deadband_Half)
builder.add_greater_than("Is_Cooling_Required_Bool")
# Check if heating is required (SAT < SP - Deadband_Half)
# This requires (SAT - SP) < (-1 * Deadband_Half)
builder.add_multiply("Neg_Deadband_Half_F_Calc") # For heating requirement comparison
builder.add_less_than("Is_Heating_Required_Bool")
builder.end_sub_folder()
print("\n--- Creating Permissive Conditions ---")
builder.start_sub_folder("Permissive_Conditions")
# Is OAT low enough for free cooling? (OAT < Econ_OAT_Limit)
builder.add_less_than("Is_OAT_Good_For_FreeCooling_Bool")
builder.add_not("Not_OAT_Good_For_FreeCooling_Bool") # True if OAT is NOT good for free cooling
# Is OAT too high for heating? (OAT > Heating_Lockout_OAT)
builder.add_greater_than("Is_OAT_Too_High_For_Heating_Bool")
builder.add_not("Not_OAT_Too_High_For_Heating_Bool") # True if OAT is NOT too high for heating
builder.end_sub_folder()
print("\n--- Creating Master Control Logic ---")
builder.start_sub_folder("Master_Control_Gates")
# Heating Enable Logic:
# AHU_Fan_Status AND Is_Heating_Required AND NOT Is_OAT_Too_High_For_Heating AND NOT Is_OAT_Good_For_FreeCooling
# (Ensures "heat and economize should never happen")
builder.add_and("Heating_Permissive_Gate_1")
builder.add_and("Heating_Permissive_Gate_2")
builder.add_and("Enable_Heating_Cmd")
# Economizer (Free Cooling) Enable Logic:
# AHU_Fan_Status AND Is_Cooling_Required AND Is_OAT_Good_For_FreeCooling
builder.add_and("Econ_Permissive_Gate_1")
builder.add_and("Enable_Economizer_Cmd")
# Mechanical Cooling Enable Logic:
# AHU_Fan_Status AND Is_Cooling_Required AND NOT Is_OAT_Good_For_FreeCooling
# ("free cooling and mechanical cooling okay if it is not too hot outside" implies if OAT is not good for free cooling, use mech)
builder.add_and("MechCool_Permissive_Gate_1")
builder.add_and("Enable_Mechanical_Cooling_Cmd")
builder.end_sub_folder()
print("\n--- Creating PID Control Loops ---")
builder.start_sub_folder("PID_Control_Loops")
# Heating PID (Reverse Acting: Output increases as PV (SAT) falls below SP)
heating_pid_props = {
"loopEnable": {"value": False}, "controlledVariable": {"value": 70.0},
"setpoint": {"value": 70.0}, "proportionalConstant": {"value": 10.0},
"integralConstant": {"value": 300.0}, "outputMin": {"value": 0.0},
"outputMax": {"value": 100.0},
}
builder.add_loop_point("Heating_PID", properties=heating_pid_props)
# Economizer PID (Direct Acting: Output increases as PV (SAT) rises above SP)
econ_pid_props = {
"loopEnable": {"value": False}, "controlledVariable": {"value": 70.0},
"setpoint": {"value": 70.0}, "proportionalConstant": {"value": 10.0},
"integralConstant": {"value": 300.0}, "outputMin": {"value": 0.0},
"outputMax": {"value": 100.0},
}
builder.add_loop_point("Economizer_PID", properties=econ_pid_props)
# Cooling Valve PID (Direct Acting: Output increases as PV (SAT) rises above SP)
cool_pid_props = {
"loopEnable": {"value": False}, "controlledVariable": {"value": 70.0},
"setpoint": {"value": 70.0}, "proportionalConstant": {"value": 10.0},
"integralConstant": {"value": 300.0}, "outputMin": {"value": 0.0},
"outputMax": {"value": 100.0},
}
builder.add_loop_point("Cooling_Valve_PID", properties=cool_pid_props)
builder.end_sub_folder()
print("\n--- Wiring Components ---")
# --- Demand Calculation Wiring ---
builder.add_link("SAT_Sensor_F", "out", "SAT_Error_F", "inA")
builder.add_link("SAT_Setpoint_F", "out", "SAT_Error_F", "inB")
# Is Cooling Required? (SAT > SP + Deadband)
builder.add_link("SAT_Error_F", "out", "Is_Cooling_Required_Bool", "inA")
builder.add_link("Deadband_Half_F", "out", "Is_Cooling_Required_Bool", "inB")
# Is Heating Required? (SAT < SP - Deadband)
builder.add_link("Deadband_Half_F", "out", "Neg_Deadband_Half_F_Calc", "inA")
builder.add_link("Const_Neg_1_For_Multiply", "out", "Neg_Deadband_Half_F_Calc", "inB")
builder.add_link("SAT_Error_F", "out", "Is_Heating_Required_Bool", "inA")
builder.add_link("Neg_Deadband_Half_F_Calc", "out", "Is_Heating_Required_Bool", "inB")
# --- Permissive Conditions Wiring ---
builder.add_link("OAT_Sensor_F", "out", "Is_OAT_Good_For_FreeCooling_Bool", "inA")
builder.add_link("Econ_OAT_Limit_F", "out", "Is_OAT_Good_For_FreeCooling_Bool", "inB")
builder.add_link("Is_OAT_Good_For_FreeCooling_Bool", "out", "Not_OAT_Good_For_FreeCooling_Bool", "in")
builder.add_link("OAT_Sensor_F", "out", "Is_OAT_Too_High_For_Heating_Bool", "inA")
builder.add_link("Heating_Lockout_OAT_F", "out", "Is_OAT_Too_High_For_Heating_Bool", "inB")
builder.add_link("Is_OAT_Too_High_For_Heating_Bool", "out", "Not_OAT_Too_High_For_Heating_Bool", "in")
# --- Master Control Logic Wiring ---
# Heating Enable Logic
builder.add_link("AHU_Fan_Status", "out", "Heating_Permissive_Gate_1", "inA")
builder.add_link("Is_Heating_Required_Bool", "out", "Heating_Permissive_Gate_1", "inB")
builder.add_link("Not_OAT_Too_High_For_Heating_Bool", "out", "Heating_Permissive_Gate_2", "inA")
builder.add_link("Not_OAT_Good_For_FreeCooling_Bool", "out", "Heating_Permissive_Gate_2", "inB") # "heat and economize should never happen"
builder.add_link("Heating_Permissive_Gate_1", "out", "Enable_Heating_Cmd", "inA")
builder.add_link("Heating_Permissive_Gate_2", "out", "Enable_Heating_Cmd", "inB")
# Economizer Enable Logic
builder.add_link("AHU_Fan_Status", "out", "Econ_Permissive_Gate_1", "inA")
builder.add_link("Is_Cooling_Required_Bool", "out", "Econ_Permissive_Gate_1", "inB")
builder.add_link("Econ_Permissive_Gate_1", "out", "Enable_Economizer_Cmd", "inA")
builder.add_link("Is_OAT_Good_For_FreeCooling_Bool", "out", "Enable_Economizer_Cmd", "inB")
# Mechanical Cooling Enable Logic
builder.add_link("AHU_Fan_Status", "out", "MechCool_Permissive_Gate_1", "inA")
builder.add_link("Is_Cooling_Required_Bool", "out", "MechCool_Permissive_Gate_1", "inB")
builder.add_link("MechCool_Permissive_Gate_1", "out", "Enable_Mechanical_Cooling_Cmd", "inA")
builder.add_link("Not_OAT_Good_For_FreeCooling_Bool", "out", "Enable_Mechanical_Cooling_Cmd", "inB")
# --- PID Control Loops Wiring ---
# Heating PID Wiring
builder.add_link("SAT_Sensor_F", "out", "Heating_PID", "controlledVariable")
builder.add_link("SAT_Setpoint_F", "out", "Heating_PID", "setpoint")
builder.add_link("Enable_Heating_Cmd", "out", "Heating_PID", "loopEnable")
builder.add_link("Heating_PID_PB", "out", "Heating_PID", "proportionalConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Heating_PID_I", "out", "Heating_PID", "integralConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Min", "out", "Heating_PID", "outputMin", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Max", "out", "Heating_PID", "outputMax", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("PID_Action_Reverse", "out", "Heating_PID", "loopAction", link_type="b:ConversionLink", converter_type="conv:StatusBooleanToFrozenEnum")
builder.add_link("Heating_PID", "out", "Heating_Valve_Cmd", "in16")
# Economizer PID Wiring
builder.add_link("SAT_Sensor_F", "out", "Economizer_PID", "controlledVariable")
builder.add_link("SAT_Setpoint_F", "out", "Economizer_PID", "setpoint")
builder.add_link("Enable_Economizer_Cmd", "out", "Economizer_PID", "loopEnable")
builder.add_link("Cooling_PID_PB", "out", "Economizer_PID", "proportionalConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Cooling_PID_I", "out", "Economizer_PID", "integralConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Min", "out", "Economizer_PID", "outputMin", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Max", "out", "Economizer_PID", "outputMax", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("PID_Action_Direct", "out", "Economizer_PID", "loopAction", link_type="b:ConversionLink", converter_type="conv:StatusBooleanToFrozenEnum")
builder.add_link("Economizer_PID", "out", "Economizer_Damper_Cmd", "in16")
# Cooling Valve PID Wiring
builder.add_link("SAT_Sensor_F", "out", "Cooling_Valve_PID", "controlledVariable")
builder.add_link("SAT_Setpoint_F", "out", "Cooling_Valve_PID", "setpoint")
builder.add_link("Enable_Mechanical_Cooling_Cmd", "out", "Cooling_Valve_PID", "loopEnable")
builder.add_link("Cooling_PID_PB", "out", "Cooling_Valve_PID", "proportionalConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Cooling_PID_I", "out", "Cooling_Valve_PID", "integralConstant", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Min", "out", "Cooling_Valve_PID", "outputMin", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("Const_PID_Output_Max", "out", "Cooling_Valve_PID", "outputMax", link_type="b:ConversionLink", converter_type="conv:StatusNumericToNumber")
builder.add_link("PID_Action_Direct", "out", "Cooling_Valve_PID", "loopAction", link_type="b:ConversionLink", converter_type="conv:StatusBooleanToFrozenEnum")
builder.add_link("Cooling_Valve_PID", "out", "Cooling_Valve_Cmd", "in16")
# --- Save the .bog file ---
builder.save(output_path)
print(f"\nSuccessfully created Niagara .bog file at: {output_path}")
if __name__ == "__main__":
main()
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Heating_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Heating_PID.outputMax": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Economizer_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Economizer_PID.outputMax": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Cooling_Valve_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Cooling_Valve_PID.outputMax": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Heating_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Heating_PID.outputMax": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Economizer_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Economizer_PID.outputMax": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31b.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Cooling_Valve_PID.outputMin": Target slot does not exist
SEVERE [09:10:06 26-Sep-25 CDT][sys.engine] Cannot activate link "Indirect: h:b31c.out → slot:/Drivers/AHU_Economizer_Sequence/PID_Control_Loops/Cooling_Valve_PID.outputMax": Target slot does not exist