Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hycon/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
WindFarmPowerDistributingController,
WindFarmPowerTrackingController,
)
from hycon.controllers.thermal_plant_controller import ThermalPlantController
51 changes: 40 additions & 11 deletions hycon/controllers/hybrid_supervisory_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, maximum_power=None, forced_state=None):
"""
Set controller parameters for HybridSupervisoryControllerGeneric.

Expand All @@ -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
Expand Down Expand Up @@ -90,6 +95,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
Expand All @@ -116,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.
Expand Down Expand Up @@ -149,33 +172,39 @@ 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"]

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

controls_dict.update(cc.compute_controls(measurements_dict))

power_export_total += measurements_dict[cc.cname]["power"]

return controls_dict
return controls_dict
49 changes: 49 additions & 0 deletions hycon/controllers/thermal_plant_controller.py
Original file line number Diff line number Diff line change
@@ -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"]}}
14 changes: 12 additions & 2 deletions hycon/interfaces/hercules_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -71,11 +71,18 @@ 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"}
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.")

Expand Down Expand Up @@ -182,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
Loading