From 46d14e4c745f822d197601fa70dbaacdd0153bc3 Mon Sep 17 00:00:00 2001 From: Frederik Date: Wed, 6 May 2026 17:43:44 -0600 Subject: [PATCH 1/5] Add `minimum_power` functionality to `HybridSupervisoryControllerGeneric` --- .../hybrid_supervisory_controller.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/hycon/controllers/hybrid_supervisory_controller.py b/hycon/controllers/hybrid_supervisory_controller.py index db51009d..df1adb63 100644 --- a/hycon/controllers/hybrid_supervisory_controller.py +++ b/hycon/controllers/hybrid_supervisory_controller.py @@ -90,6 +90,21 @@ def set_controller_parameters(self, component_controllers=[], curtailment_order= else: self.curtailment_order = curtailment_order + # Check valid minimum_power + if minimum_power is None: + # Default is reverse order of component_controllers + self.minimum_power = np.zeros_like(component_controllers) + elif len(minimum_power) != len(component_controllers): + raise ValueError("minimum_power must be the same length as component_controllers.") + elif not all([isinstance(c, (float, int)) and c >= 0 for c in minimum_power]): + raise ValueError( + "All entries in minimum_power must be non-negative floats or integers corresponding" + " to indices of component_controllers." + ) + else: + self.minimum_power = minimum_power + + def compute_controls(self, measurements_dict): """ Pass necessary information to each component controller, and apply power @@ -149,25 +164,28 @@ def compute_controls(self, measurements_dict): # Loop over curtailment order in reverse to bring in power for each component until we hit # the interconnection limit, then curtail as needed according to the order. + # Take into account the minimum_power for each component, which indicates the minimum power + # that component should be allowed to produce. for cidx in self.curtailment_order[::-1]: cc = self.component_controllers[cidx] if cc.plant_parameters[cc.cname]["component_category"] == "generator": - power_reference_component = power_reference_with_storage - power_export_total + power_reference_component = max( + power_reference_with_storage - power_export_total - ( + sum( self.minimum_power[i] for i in self.curtailment_order if i < cidx) ), + self.minimum_power[cidx] + ) elif cc.plant_parameters[cc.cname]["component_category"] == "storage": if cc.plant_parameters[cc.cname].get("allow_grid_charging", True): - power_reference_component = power_reference_total - power_export_total - measurements_dict[cc.cname]["power_limit_lower"] = -np.inf - measurements_dict[cc.cname]["power_limit_upper"] = power_reference_component + power_reference_component = power_reference_total - power_export_total - ( + sum( self.minimum_power[i] for i in self.curtailment_order if i < cidx) + ) else: power_reference_component = max( - power_reference_total - power_export_total, + power_reference_total - power_export_total - + sum( self.minimum_power[i] for i in self.curtailment_order if i < cidx), -locally_generated_power_total, ) - measurements_dict[cc.cname][ - "power_limit_lower" - ] = -locally_generated_power_total - measurements_dict[cc.cname]["power_limit_upper"] = power_reference_component # Reduce or increase the available power to store locally_generated_power_total += measurements_dict[cc.cname]["power"] @@ -178,4 +196,4 @@ def compute_controls(self, measurements_dict): power_export_total += measurements_dict[cc.cname]["power"] - return controls_dict + return controls_dict \ No newline at end of file From a2cd5ddbbac813fd417fd907c0466fe566bb868f Mon Sep 17 00:00:00 2001 From: Frederik Date: Wed, 6 May 2026 17:48:03 -0600 Subject: [PATCH 2/5] Added `thermal_plant_controller` and implemented `minimum_power` in `hybrid_supervisory_controller` --- hycon/controllers/__init__.py | 1 + .../hybrid_supervisory_controller.py | 7 ++- hycon/controllers/thermal_plant_controller.py | 49 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 hycon/controllers/thermal_plant_controller.py diff --git a/hycon/controllers/__init__.py b/hycon/controllers/__init__.py index a10ac27c..722f7e75 100644 --- a/hycon/controllers/__init__.py +++ b/hycon/controllers/__init__.py @@ -17,3 +17,4 @@ WindFarmPowerDistributingController, WindFarmPowerTrackingController, ) +from hycon.controllers.thermal_plant_controller import ThermalPlantController diff --git a/hycon/controllers/hybrid_supervisory_controller.py b/hycon/controllers/hybrid_supervisory_controller.py index df1adb63..fe055d87 100644 --- a/hycon/controllers/hybrid_supervisory_controller.py +++ b/hycon/controllers/hybrid_supervisory_controller.py @@ -46,7 +46,8 @@ def __init__( "interconnect_limit must be a positive value (or -1, indicating no limit)." ) - def set_controller_parameters(self, component_controllers=[], curtailment_order=None): + def set_controller_parameters(self, component_controllers=[], curtailment_order=None, + minimum_power=None): """ Set controller parameters for HybridSupervisoryControllerGeneric. @@ -56,6 +57,10 @@ def set_controller_parameters(self, component_controllers=[], curtailment_order= components in the simulation. curtailment_order: List of integers corresponding to the order in which to curtail components when the overall power reference exceeds the interconnection limit. + minimum_power: List of floats corresponding to the minimum power that each component + should be allowed to produce, even when curtailing to meet the interconnection + limit. Should be the same length as component_controllers, and ordered + correspondingly. """ # Check valid component_controllers diff --git a/hycon/controllers/thermal_plant_controller.py b/hycon/controllers/thermal_plant_controller.py new file mode 100644 index 00000000..9d9aa489 --- /dev/null +++ b/hycon/controllers/thermal_plant_controller.py @@ -0,0 +1,49 @@ +import numpy as np + +from hycon.controllers.controller_base import ControllerBase + +# Default power setpoint in kW (meant to ensure power maximization) +POWER_SETPOINT_DEFAULT = 1e9 + + +class ThermalPlantController(ControllerBase): + """ + Sets thermal plant power reference between turbines without + feedback on current power generation. + """ + + def __init__(self, interface, cname, controller_parameters={}, verbose=False): + super().__init__(interface, cname, verbose) + self.check_controller_parameters(controller_parameters) + self.set_controller_parameters(**controller_parameters) + + # def compute_controls(self, measurements_dict): + + # ref_in_lower_dict = ( + # "power_reference" in measurements_dict[self.cname] + # and measurements_dict[self.cname]["power_reference"] is not None + # ) + # ref_in_upper_dict = ( + # "power_reference" in measurements_dict + # and measurements_dict["power_reference"] is not None + # ) + # if ref_in_lower_dict and ref_in_upper_dict: + # raise KeyError( + # "Found 'power_reference' in both measurements_dict['" + # + self.cname + # + "'] and measurements_dict." + # ) + # elif ref_in_lower_dict: + # farm_power_reference = measurements_dict[self.cname]["power_reference"] + # elif ref_in_upper_dict: + # farm_power_reference = measurements_dict["power_reference"] + # else: + # farm_power_reference = POWER_SETPOINT_DEFAULT + + # return {"power_setpoint": farm_power_reference} + + def set_controller_parameters(self): + pass + + def compute_controls(self, measurements_dict): + return {self.cname: {"power_setpoint": measurements_dict[self.cname]["power_reference"]}} From e70f90bd7624a34996f7fb3270df83bcec6d34ff Mon Sep 17 00:00:00 2001 From: Frederik Date: Wed, 20 May 2026 10:37:33 -0600 Subject: [PATCH 3/5] Update to supervisory controller --- hycon/controllers/hybrid_supervisory_controller.py | 8 +++++++- hycon/interfaces/hercules_interface.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/hycon/controllers/hybrid_supervisory_controller.py b/hycon/controllers/hybrid_supervisory_controller.py index fe055d87..b7682841 100644 --- a/hycon/controllers/hybrid_supervisory_controller.py +++ b/hycon/controllers/hybrid_supervisory_controller.py @@ -47,7 +47,7 @@ def __init__( ) def set_controller_parameters(self, component_controllers=[], curtailment_order=None, - minimum_power=None): + minimum_power=None, maximum_power=None, forced_state=None): """ Set controller parameters for HybridSupervisoryControllerGeneric. @@ -136,6 +136,9 @@ def compute_controls(self, measurements_dict): cc.cname ]["power_setpoint"], ) + cc.plant_parameters[cc.cname][ + "available_storage_for_charging" + ] = total_available_storage_for_charging # Get overall reference, and remove from measurements_dict to avoid confusion for # component controllers. @@ -194,6 +197,9 @@ def compute_controls(self, measurements_dict): # Reduce or increase the available power to store locally_generated_power_total += measurements_dict[cc.cname]["power"] + power_reference_with_storage -= cc.plant_parameters[cc.cname].get( + "available_storage_for_charging", 0) + # Assign power_reference_component for use by lower level controller measurements_dict[cc.cname]["power_reference"] = power_reference_component diff --git a/hycon/interfaces/hercules_interface.py b/hycon/interfaces/hercules_interface.py index f50997b6..68b02e95 100644 --- a/hycon/interfaces/hercules_interface.py +++ b/hycon/interfaces/hercules_interface.py @@ -21,7 +21,7 @@ hercules_solar_types = ["SolarPySAMPVWatts"] hercules_battery_types = ["BatteryLithiumIon", "BatterySimple"] hercules_hydrogen_types = ["ElectrolyzerPlant"] -hercules_thermal_types = ["HardCoalSteamTurbine", "OpenCycleGasTurbine"] +hercules_thermal_types = ["HardCoalSteamTurbine", "OpenCycleGasTurbine", "ThermalPlant"] class HerculesInterface(InterfaceBase): @@ -75,7 +75,13 @@ def __init__(self, h_dict): elif c_type in hercules_hydrogen_types: self.plant_parameters[c] = {"type": "hydrogen", "component_category": "load"} elif c_type in hercules_thermal_types: - self.plant_parameters[c] = {"type": "thermal", "component_category": "generator"} + self.plant_parameters[c] = { + "type": "thermal", + "component_category": "generator", + "P_min": h_dict[c]["min_stable_load_fraction"] * h_dict[c]["rated_capacity"], + "P_max": h_dict[c]["rated_capacity"], + "ramp_rate": h_dict[c]["ramp_rate_fraction"]*h_dict[c]["rated_capacity"]/60.0, + } else: raise ValueError(f"Component '{c}' has unrecognized type '{c_type}' for Hycon.") From 9731525e5476bfa22992d587f187405c82d5cd4d Mon Sep 17 00:00:00 2001 From: dzalkind Date: Tue, 26 May 2026 16:06:40 -0600 Subject: [PATCH 4/5] Add roundtrip_efficiency to hercules_interface --- hycon/interfaces/hercules_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hycon/interfaces/hercules_interface.py b/hycon/interfaces/hercules_interface.py index 68b02e95..039db776 100644 --- a/hycon/interfaces/hercules_interface.py +++ b/hycon/interfaces/hercules_interface.py @@ -71,6 +71,7 @@ def __init__(self, h_dict): "allow_grid_charging": h_dict[c].get("allow_grid_power_consumption", True), "state_of_charge_max": h_dict[c].get("max_SOC", 1.0), "state_of_charge_min": h_dict[c].get("min_SOC", 0.0), + "roundtrip_efficiency": h_dict[c].get("roundtrip_efficiency", 1.0), } elif c_type in hercules_hydrogen_types: self.plant_parameters[c] = {"type": "hydrogen", "component_category": "load"} From 92e7444b063d5672fef251fca4125caa5b8acc23 Mon Sep 17 00:00:00 2001 From: dzalkind Date: Tue, 26 May 2026 16:06:54 -0600 Subject: [PATCH 5/5] Set power_setpoint to 0 for components without controller --- hycon/interfaces/hercules_interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hycon/interfaces/hercules_interface.py b/hycon/interfaces/hercules_interface.py index 039db776..10640242 100644 --- a/hycon/interfaces/hercules_interface.py +++ b/hycon/interfaces/hercules_interface.py @@ -189,5 +189,8 @@ def send_controls( "power_setpoint" ) h_dict[c] = h_dict[c] | controls_dict[c] + else: + # Set a safe default power_setpoint for components without controllers + h_dict[c].setdefault("power_setpoint", 0.0) return h_dict