Skip to content

PID.outputMax link bug #25

@bbartling

Description

@bbartling
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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions