From cc70f4e576ead08f160fd5097d890b95c07e97b6 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 23 Feb 2026 14:32:11 -0500 Subject: [PATCH 01/14] CCGT class --- .../combined_cycle_gas_turbine.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 hercules/plant_components/combined_cycle_gas_turbine.py diff --git a/hercules/plant_components/combined_cycle_gas_turbine.py b/hercules/plant_components/combined_cycle_gas_turbine.py new file mode 100644 index 00000000..265d1f9a --- /dev/null +++ b/hercules/plant_components/combined_cycle_gas_turbine.py @@ -0,0 +1,137 @@ +""" +Combined Cycle Gas Turbine Class. + +Combined cycle gas turbine (CCGT) model is a subclass of the ThermalComponentBase class. +It implements the model as presented in [1], [2], [3], [4], [5] and [6]. + +Like other subclasses of ThermalComponentBase, it inherits the main control functions, +and adds defaults for many variables based on [1], [2], [3], [4], [5] and [6]. + +Note: All efficiency values are HHV (Higher Heating Value) net plant efficiencies. +The default efficiency table is based on the SC1A curve from Exhibit ES-4 of [5]. + +Note: This class is based on aeroderivative combined cycle gas turbines, +which are commonly used for flexible power generation. + +References: + +[1] Agora Energiewende (2017): Flexibility in thermal power plants + With a focus on existing coal-fired power plants. +[2] "Impact of Detailed Parameter Modeling of Open-Cycle Gas Turbines on + Production Cost Simulation", NREL/CP-6A40-87554, National Renewable + Energy Laboratory, 2024. +[3] Deane, J.P., G. Drayton, and B.P. Ó Gallachóir. "The Impact of Sub-Hourly + Modelling in Power Systems with Significant Levels of Renewable Generation." + Applied Energy 113 (January 2014): 152–58. + https://doi.org/10.1016/j.apenergy.2013.07.027. +[4] IRENA (2019), Innovation landscape brief: Flexibility in conventional power plants, + International Renewable Energy Agency, Abu Dhabi. +[5] M. Oakes, M. Turner, " Cost and Performance Baseline for Fossil Energy Plants, Volume 5: + Natural Gas Electricity Generating Units for Flexible Operation," National Energy + Technology Laboratory, Pittsburgh, May 5, 2023. +[6] I. Staffell, "The Energy and Fuel Data Sheet," University of Birmingham, March 2011. + https://claverton-energy.com/cms4/wp-content/uploads/2012/08/the_energy_and_fuel_data_sheet.pdf +""" + +from hercules.plant_components.thermal_component_base import ThermalComponentBase + + +class CombinedCycleGasTurbine(ThermalComponentBase): + """Combined cycle gas turbine model. + + This model represents a combined cycle gas turbine with state + management, ramp rate constraints, minimum stable load, and fuel consumption + tracking. Note it is a subclass of the ThermalComponentBase class. + + All efficiency values are HHV (Higher Heating Value) net plant efficiencies. + """ + + component_name = "combined_cycle_gas_turbine" + component_type = "CombinedCycleGasTurbine" + + def __init__(self, h_dict): + """Initialize the CombinedCycleGasTurbine class. + + Args: + h_dict (dict): Dictionary containing simulation parameters including: + - rated_capacity: Maximum power output in kW + - min_stable_load_fraction: Optional, minimum operating point as fraction (0-1). + Default: 0.40 (40%) [4] + - ramp_rate_fraction: Optional, maximum rate of power increase/decrease + as fraction of rated capacity per minute. Default: 0.03 (3%) + - run_up_rate_fraction: Optional, maximum rate of power increase during startup + as fraction of rated capacity per minute. Default: ramp_rate_fraction + - hot_startup_time: Optional, time to reach min_stable_load_fraction from off + in s. Includes both readying time and ramping time. + Default: 4500.0 s (75 minutes) [1, 5] + - warm_startup_time: Optional, time to reach min_stable_load_fraction from off + in s. Includes both readying time and ramping time. + Default: 7200.0 s (120 minutes/2 hours) [1, 5] + - cold_startup_time: Optional, time to reach min_stable_load_fraction from off + in s. Includes both readying time and ramping time. + Default: 10800.0 s (180 minutes/3 hours) [1, 5] + - min_up_time: Optional, minimum time unit must remain on in s. + Default: 14400.0 s (4 hours) [4] + - min_down_time: Optional, minimum time unit must remain off in s. + Default: 7200.0 s (2 hours) [4] + - initial_conditions: Dictionary with initial power (state is + derived automatically: power > 0 means ON, power == 0 means OFF) + - hhv: Optional, higher heating value of natural gas in J/m³. + Default: 39050000 J/m³ (39.05 MJ/m³) [6] + - fuel_density: Optional, fuel density in kg/m³. + Default: 0.768 kg/m³ [6] + - efficiency_table: Optional, dictionary with power_fraction and + efficiency arrays (both as fractions 0-1). Efficiency values must + be HHV net plant efficiencies. Default values are approximate + readings from the CC1A-F curve in Exhibit ES-4 of [5]: + power_fraction = [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, + 0.6, 0.55, 0.50, 0.4], + efficiency = [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, + 0.505, 0.5, 0.49, 0.47]. + F-class are typically smaller, older, less efficient: 250 MW + H-class are typically larger, newer, more efficient: 500 MW + """ + + # Apply fixed default parameters based on [1], [2] and [3] + # back into the h_dict if they are not provided + if "min_stable_load_fraction" not in h_dict[self.component_name]: + h_dict[self.component_name]["min_stable_load_fraction"] = 0.40 + if "ramp_rate_fraction" not in h_dict[self.component_name]: + h_dict[self.component_name]["ramp_rate_fraction"] = 0.03 + if "hot_startup_time" not in h_dict[self.component_name]: + h_dict[self.component_name]["hot_startup_time"] = 4500.0 + if "warm_startup_time" not in h_dict[self.component_name]: + h_dict[self.component_name]["warm_startup_time"] = 7200.0 + if "cold_startup_time" not in h_dict[self.component_name]: + h_dict[self.component_name]["cold_startup_time"] = 10800.0 + if "min_up_time" not in h_dict[self.component_name]: + h_dict[self.component_name]["min_up_time"] = 14400.0 + if "min_down_time" not in h_dict[self.component_name]: + h_dict[self.component_name]["min_down_time"] = 7200.0 + + # If the run_up_rate_fraction is not provided, it defaults to the ramp_rate_fraction + if "run_up_rate_fraction" not in h_dict[self.component_name]: + h_dict[self.component_name]["run_up_rate_fraction"] = h_dict[self.component_name][ + "ramp_rate_fraction" + ] + + # Default HHV for natural gas (39.05 MJ/m³) from [6] + if "hhv" not in h_dict[self.component_name]: + h_dict[self.component_name]["hhv"] = 39050000 # J/m³ (39.05 MJ/m³) + + # Default fuel density for natural gas (0.768 kg/m³) from [6] + if "fuel_density" not in h_dict[self.component_name]: + h_dict[self.component_name]["fuel_density"] = 0.768 # kg/m³ + + # Default HHV net plant efficiency table based on approximate readings from + # the CC1A-F curve in Exhibit ES-4 of [5] + if "efficiency_table" not in h_dict[self.component_name]: + h_dict[self.component_name]["efficiency_table"] = { + "power_fraction": [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, + 0.6, 0.55, 0.50, 0.4], + "efficiency": [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, + 0.505, 0.5, 0.49, 0.47], + } + + # Call the base class init + super().__init__(h_dict) From 6531ddd173796ebf27c7cd9a17c0eb12215d8a4a Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 23 Feb 2026 18:02:53 -0500 Subject: [PATCH 02/14] Update doc strings --- .../combined_cycle_gas_turbine.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/hercules/plant_components/combined_cycle_gas_turbine.py b/hercules/plant_components/combined_cycle_gas_turbine.py index 265d1f9a..5c47e975 100644 --- a/hercules/plant_components/combined_cycle_gas_turbine.py +++ b/hercules/plant_components/combined_cycle_gas_turbine.py @@ -8,10 +8,7 @@ and adds defaults for many variables based on [1], [2], [3], [4], [5] and [6]. Note: All efficiency values are HHV (Higher Heating Value) net plant efficiencies. -The default efficiency table is based on the SC1A curve from Exhibit ES-4 of [5]. - -Note: This class is based on aeroderivative combined cycle gas turbines, -which are commonly used for flexible power generation. +The default efficiency table is based on the CC1A-F curve from Exhibit ES-4 of [5]. References: @@ -84,9 +81,9 @@ def __init__(self, h_dict): efficiency arrays (both as fractions 0-1). Efficiency values must be HHV net plant efficiencies. Default values are approximate readings from the CC1A-F curve in Exhibit ES-4 of [5]: - power_fraction = [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, + power_fraction = [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, 0.6, 0.55, 0.50, 0.4], - efficiency = [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, + efficiency = [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.505, 0.5, 0.49, 0.47]. F-class are typically smaller, older, less efficient: 250 MW H-class are typically larger, newer, more efficient: 500 MW @@ -127,10 +124,34 @@ def __init__(self, h_dict): # the CC1A-F curve in Exhibit ES-4 of [5] if "efficiency_table" not in h_dict[self.component_name]: h_dict[self.component_name]["efficiency_table"] = { - "power_fraction": [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, - 0.6, 0.55, 0.50, 0.4], - "efficiency": [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, - 0.505, 0.5, 0.49, 0.47], + "power_fraction": [ + 1.0, + 0.95, + 0.90, + 0.85, + 0.80, + 0.75, + 0.7, + 0.65, + 0.6, + 0.55, + 0.50, + 0.4, + ], + "efficiency": [ + 0.53, + 0.515, + 0.52, + 0.52, + 0.52, + 0.52, + 0.52, + 0.515, + 0.505, + 0.5, + 0.49, + 0.47, + ], } # Call the base class init From 1b968d0f29141fa8c4834115e687281877f27b88 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 26 Feb 2026 19:38:58 -0500 Subject: [PATCH 03/14] Add tests for combined cycle model --- tests/combined_cycle_gas_turbine_test.py | 109 +++++++++++++++++++++++ tests/test_inputs/h_dict.py | 42 +++++++++ 2 files changed, 151 insertions(+) create mode 100644 tests/combined_cycle_gas_turbine_test.py diff --git a/tests/combined_cycle_gas_turbine_test.py b/tests/combined_cycle_gas_turbine_test.py new file mode 100644 index 00000000..a96f2af7 --- /dev/null +++ b/tests/combined_cycle_gas_turbine_test.py @@ -0,0 +1,109 @@ +import copy + +from hercules.plant_components.combined_cycle_gas_turbine import CombinedCycleGasTurbine + +from .test_inputs.h_dict import ( + h_dict_combined_cycle_gas_turbine, +) + + +def test_init_from_dict(): + """Test that CombinedCycleGasTurbine can be initialized from a dictionary.""" + ccgt = CombinedCycleGasTurbine(copy.deepcopy(h_dict_combined_cycle_gas_turbine)) + assert ccgt is not None + + +def test_default_inputs(): + """Test that CombinedCycleGasTurbine uses default inputs when not provided.""" + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + + # Test that the ramp_rate_fraction is 0.1 (from test fixture) + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.ramp_rate_fraction == 0.1 + + # Test that the run_up_rate_fraction is 0.05 (from test fixture) + assert ccgt.run_up_rate_fraction == 0.05 + + # Test that if the run_up_rate_fraction is not provided, + # it defaults to the ramp_rate_fraction + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["run_up_rate_fraction"] + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.run_up_rate_fraction == ccgt.ramp_rate_fraction + + # Now test that the default value of the ramp_rate_fraction is + # applied to both the ramp_rate_fraction and the run_up_rate_fraction + # if they are both not provided + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["ramp_rate_fraction"] + del h_dict["combined_cycle_gas_turbine"]["run_up_rate_fraction"] + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.ramp_rate_fraction == 0.03 + assert ccgt.run_up_rate_fraction == 0.03 + + # Test the remaining default values + # Delete startup times first, since changing min_stable_load_fraction and + # ramp rates affects ramp_time validation against startup times + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["ramp_rate_fraction"] + del h_dict["combined_cycle_gas_turbine"]["run_up_rate_fraction"] + del h_dict["combined_cycle_gas_turbine"]["cold_startup_time"] + del h_dict["combined_cycle_gas_turbine"]["warm_startup_time"] + del h_dict["combined_cycle_gas_turbine"]["hot_startup_time"] + del h_dict["combined_cycle_gas_turbine"]["min_stable_load_fraction"] + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.min_stable_load_fraction == 0.40 + assert ccgt.hot_startup_time == 75 * 60.0 + assert ccgt.warm_startup_time == 120 * 60.0 + assert ccgt.cold_startup_time == 180 * 60.0 + + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["min_up_time"] + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.min_up_time == 4 * 60 * 60.0 + + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["min_down_time"] + ccgt = CombinedCycleGasTurbine(h_dict) + assert ccgt.min_down_time == 2 * 60 * 60.0 + + +def test_default_hhv(): + """Test that CombinedCycleGasTurbine provides default HHV for natural gas from [6].""" + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["hhv"] + ccgt = CombinedCycleGasTurbine(h_dict) + # Default HHV for natural gas is 39.05 MJ/m³ = 39,050,000 J/m³ from [6] + assert ccgt.hhv == 39050000 + + +def test_default_fuel_density(): + """Test that CombinedCycleGasTurbine provides default fuel density from [6].""" + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + if "fuel_density" in h_dict["combined_cycle_gas_turbine"]: + del h_dict["combined_cycle_gas_turbine"]["fuel_density"] + ccgt = CombinedCycleGasTurbine(h_dict) + # Default fuel density for natural gas is 0.768 kg/m³ from [6] + assert ccgt.fuel_density == 0.768 + + +def test_default_efficiency_table(): + """Test that CombinedCycleGasTurbine provides default HHV net efficiency table from [5]. + + Default values are approximate readings from the CC1A-F curve in + Exhibit ES-4 of [5]. + """ + import numpy as np + + h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) + del h_dict["combined_cycle_gas_turbine"]["efficiency_table"] + ccgt = CombinedCycleGasTurbine(h_dict) + # Default HHV net plant efficiency from CC1A-F curve in Exhibit ES-4 of [5] + np.testing.assert_array_equal( + ccgt.efficiency_power_fraction, + np.array([0.4, 0.50, 0.55, 0.6, 0.65, 0.70, 0.75, 0.80, 0.85, 0.9, 0.95, 1.0]), + ) + np.testing.assert_array_equal( + ccgt.efficiency_values, + np.array([0.47, 0.49, 0.5, 0.505, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.53]), + ) diff --git a/tests/test_inputs/h_dict.py b/tests/test_inputs/h_dict.py index 6b614fba..0bf59ea7 100644 --- a/tests/test_inputs/h_dict.py +++ b/tests/test_inputs/h_dict.py @@ -125,6 +125,7 @@ }, } + open_cycle_gas_turbine = { "component_type": "OpenCycleGasTurbine", "rated_capacity": 1000, # kW (1 MW) @@ -153,6 +154,34 @@ } +combined_cycle_gas_turbine = { + "component_type": "CombinedCycleGasTurbine", + "rated_capacity": 1000, # kW (1 MW) + "min_stable_load_fraction": 0.40, # 40% minimum operating point + "ramp_rate_fraction": 0.10, # 10% of rated capacity per minute + "run_up_rate_fraction": 0.05, # 5% of rated capacity per minute + "hot_startup_time": 1200.0, # s (must be >= run_up_rate_fraction of 60s) + "warm_startup_time": 1200.0, # s (must be >= ramp_time of 60s) + "cold_startup_time": 1200.0, # s (must be >= ramp_time of 60s) + "min_up_time": 10.0, # s + "min_down_time": 10.0, # s + "log_channels": [ + "power", + "state", + "efficiency", + "fuel_volume_rate", + "fuel_mass_rate", + ], + "initial_conditions": {"power": 1000}, # power > 0 implies ON state + "hhv": 39050000, # J/m³ (natural gas HHV from [6]) + # HHV net plant efficiency from SC1A curve in Exhibit ES-4 of [5] + "efficiency_table": { + "power_fraction": [1.0, 0.95, 0.90, 0.85, 0.80, 0.75, 0.7, 0.65, 0.6, 0.55, 0.50, 0.4], + "efficiency": [0.53, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.505, 0.5, 0.49, 0.47], + }, +} + + electrolyzer = { "component_type": "ElectrolyzerPlant", "initial_conditions": { @@ -380,3 +409,16 @@ "plant": plant, "open_cycle_gas_turbine": open_cycle_gas_turbine, } + +h_dict_combined_cycle_gas_turbine = { + "dt": 1.0, + "starttime": 0.0, + "endtime": 10.0, + "starttime_utc": pd.to_datetime("2018-05-10 12:31:00", utc=True), + "endtime_utc": pd.to_datetime("2018-05-10 12:31:10", utc=True), + "verbose": False, + "step": 0, + "time": 0.0, + "plant": plant, + "combined_cycle_gas_turbine": combined_cycle_gas_turbine, +} From d4b40fd17f9f4fc61a9b6de7fd49f95680d3aeb5 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 26 Feb 2026 20:05:17 -0500 Subject: [PATCH 04/14] Add to hercules simulator and example --- .../hercules_input.yaml | 71 ++++++++++++++ .../hercules_runscript.py | 85 +++++++++++++++++ .../plot_outputs.py | 94 +++++++++++++++++++ hercules/hybrid_plant.py | 4 + hercules/utilities.py | 7 +- tests/combined_cycle_gas_turbine_test.py | 5 +- 6 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 examples/08_combined_cycle_gas_turbine/hercules_input.yaml create mode 100644 examples/08_combined_cycle_gas_turbine/hercules_runscript.py create mode 100644 examples/08_combined_cycle_gas_turbine/plot_outputs.py diff --git a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml new file mode 100644 index 00000000..45f87003 --- /dev/null +++ b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml @@ -0,0 +1,71 @@ +# Input YAML for hercules +# Explicitly specify the parameters for demonstration purposes + +# Name +name: example_08 + +### +# Describe this simulation setup +description: Combined Cycle Gas Turbine (OCGT) Example + +dt: 60.0 # 1 minute time step +starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC +endtime_utc: "2020-01-01T12:00:00Z" # 12 hours later +verbose: False +log_every_n: 1 + +plant: + interconnect_limit: 100000 # kW (100 MW) + +combined_cycle_gas_turbine: + component_type: CombinedCycleGasTurbine + rated_capacity: 100000 # kW (100 MW) + min_stable_load_fraction: 0.4 # 40% minimum operating point + ramp_rate_fraction: 0.03 # 3%/min ramp rate + hot_startup_time: 4500.0 # 75 minutes + warm_startup_time: 7200.0 # 120 minutes + cold_startup_time: 10800.0 # 180 minutes + min_up_time: 14400 # 4 hours + min_down_time: 7200 # 2 hours + # Natural gas properties from [6] Staffell, "The Energy and Fuel Data Sheet", 2011 + # HHV: 39.05 MJ/m³, Density: 0.768 kg/m³ + hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6] + fuel_density: 0.768 # kg/m³ for natural gas [6] + efficiency_table: + power_fraction: + - 1.0 + - 0.95 + - 0.90 + - 0.85 + - 0.80 + - 0.75 + - 0.7 + - 0.65 + - 0.6 + - 0.55 + - 0.50 + - 0.4 + efficiency: # HHV net plant efficiency, fractions (0-1), from CC1A-F curve in Exhibit ES-4 of [5] + - 0.53 + - 0.515 + - 0.52 + - 0.52 + - 0.52 + - 0.52 + - 0.52 + - 0.515 + - 0.505 + - 0.5 + - 0.49 + - 0.47 + log_channels: + - power + - fuel_volume_rate + - fuel_mass_rate + - state + - efficiency + - power_setpoint + initial_conditions: + power: 100000 # Start ON at rated capacity (100 MW) + +controller: diff --git a/examples/08_combined_cycle_gas_turbine/hercules_runscript.py b/examples/08_combined_cycle_gas_turbine/hercules_runscript.py new file mode 100644 index 00000000..412c50cc --- /dev/null +++ b/examples/08_combined_cycle_gas_turbine/hercules_runscript.py @@ -0,0 +1,85 @@ +"""Example 07: Open Cycle Gas Turbine (OCGT) simulation. + +This example demonstrates a simple open cycle gas turbine (OCGT) that: +- Starts on at rated capacity (100 MW) +- At 10 minutes, receives a shutdown command and begins ramping down +- At ~30 minutes, reaches 0 MW and transitions to off +- At 60 minutes, receives a turn-on command with a setpoint of 100% of rated capacity +- At ~80 minutes, 1 hour down-time minimum is reached and the turbine begins hot starting +- At ~87 minutes, hot start completes, continues ramping up to 100% of rated capacity +- At 120 minutes, receives a command to reduce power to 50% of rated capacity +- At 180 minutes, receives a command to reduce power to 10% of rated capacity + (note this is below the minimum stable load) +- At 210 minutes, receives a command to increase power to 100% of rated capacity +- At 240 minutes (4 hours), receives a shutdown command +- Simulation runs for 6 hours total with 1 minute time steps +""" + +from hercules.hercules_model import HerculesModel +from hercules.utilities_examples import prepare_output_directory + +prepare_output_directory() + +# Initialize the Hercules model +hmodel = HerculesModel("hercules_input.yaml") + + +class ControllerCCGT: + """Controller implementing the CCGT schedule described in the module docstring.""" + + def __init__(self, h_dict): + """Initialize the controller. + + Args: + h_dict (dict): The hercules input dictionary. + + """ + self.rated_capacity = h_dict["combined_cycle_gas_turbine"]["rated_capacity"] + + def step(self, h_dict): + """Execute one control step. + + Args: + h_dict (dict): The hercules input dictionary. + + Returns: + dict: The updated hercules input dictionary. + + """ + current_time = h_dict["time"] + + # Determine power setpoint based on time + if current_time < 10 * 60: # 10 minutes in seconds + # Before 10 minutes: run at full capacity + power_setpoint = self.rated_capacity + elif current_time < 60 * 60: # 60 minutes in seconds + # Between 10 and 60 minutes: shut down + power_setpoint = 0.0 + elif current_time < 240 * 60: # 240 minutes in seconds + # Between 60 and 240 minutes: signal to run at full capacity + power_setpoint = self.rated_capacity + elif current_time < 360 * 60: # 360 minutes in seconds + # Between 240 and 360 minutes: reduce power to 50% of rated capacity + power_setpoint = 0.5 * self.rated_capacity + elif current_time < 480 * 60: # 480 minutes in seconds + # Between 360 and 480 minutes: reduce power to 10% of rated capacity + power_setpoint = 0.1 * self.rated_capacity + elif current_time < 540 * 60: # 540 minutes in seconds + # Between 480 and 540 minutes: increase power to 100% of rated capacity + power_setpoint = self.rated_capacity + else: + # After 540 minutes: shut down + power_setpoint = 0.0 + + h_dict["combined_cycle_gas_turbine"]["power_setpoint"] = power_setpoint + + return h_dict + + +# Instantiate the controller and assign to the Hercules model +hmodel.assign_controller(ControllerCCGT(hmodel.h_dict)) + +# Run the simulation +hmodel.run() + +hmodel.logger.info("Process completed successfully") diff --git a/examples/08_combined_cycle_gas_turbine/plot_outputs.py b/examples/08_combined_cycle_gas_turbine/plot_outputs.py new file mode 100644 index 00000000..5d26c63e --- /dev/null +++ b/examples/08_combined_cycle_gas_turbine/plot_outputs.py @@ -0,0 +1,94 @@ +# Plot the outputs of the simulation for the OCGT example + +import matplotlib.pyplot as plt +from hercules import HerculesOutput + +# Read the Hercules output file using HerculesOutput +ho = HerculesOutput("outputs/hercules_output.h5") + +# Print metadata information +print("Simulation Metadata:") +ho.print_metadata() +print() + +# Create a shortcut to the dataframe +df = ho.df + +# Get the h_dict from metadata +h_dict = ho.h_dict + +# Convert time to minutes for easier reading +time_minutes = df["time"] / 60 + +fig, axarr = plt.subplots(4, 1, sharex=True, figsize=(10, 10)) + +# Plot the power output and setpoint +ax = axarr[0] +ax.plot( + time_minutes, df["combined_cycle_gas_turbine.power"] / 1000, label="Power Output", color="b" +) +ax.plot( + time_minutes, + df["combined_cycle_gas_turbine.power_setpoint"] / 1000, + label="Power Setpoint", + color="r", + linestyle="--", +) +ax.axhline( + h_dict["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, + color="gray", + linestyle=":", + label="Rated Capacity", +) +ax.axhline( + h_dict["combined_cycle_gas_turbine"]["min_stable_load_fraction"] + * h_dict["combined_cycle_gas_turbine"]["rated_capacity"] + / 1000, + color="gray", + linestyle="--", + label="Minimum Stable Load", +) +ax.set_ylabel("Power [MW]") +ax.set_title("Combined Cycle Gas Turbine Power Output") +ax.legend() +ax.grid(True) + +# Plot the state +ax = axarr[1] +ax.plot(time_minutes, df["combined_cycle_gas_turbine.state"], label="State", color="k") +ax.set_ylabel("State") +ax.set_yticks([0, 1, 2, 3, 4, 5]) +ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) +ax.set_title( + "Turbine State (0=Off, 1=Hot Starting, 2=Warm Starting, 3=Cold Starting, 4=On, 5=Stopping)" +) +ax.grid(True) + +# Plot the efficiency +ax = axarr[2] +ax.plot( + time_minutes, + df["combined_cycle_gas_turbine.efficiency"] * 100, + label="Efficiency", + color="g", +) +ax.set_ylabel("Efficiency [%]") +ax.set_title("Thermal Efficiency") +ax.grid(True) + +# Plot the fuel consumption +ax = axarr[3] +ax.plot( + time_minutes, + df["combined_cycle_gas_turbine.fuel_volume_rate"], + label="Fuel Volume Rate", + color="orange", +) +ax.set_ylabel("Fuel [m³/s]") +ax.set_title("Fuel Volume Rate") +ax.grid(True) + +ax.set_xlabel("Time [minutes]") + +plt.tight_layout() +plt.show() diff --git a/hercules/hybrid_plant.py b/hercules/hybrid_plant.py index 439f85c4..dfc995d3 100644 --- a/hercules/hybrid_plant.py +++ b/hercules/hybrid_plant.py @@ -2,6 +2,7 @@ from hercules.plant_components.battery_lithium_ion import BatteryLithiumIon from hercules.plant_components.battery_simple import BatterySimple +from hercules.plant_components.combined_cycle_gas_turbine import CombinedCycleGasTurbine from hercules.plant_components.electrolyzer_plant import ElectrolyzerPlant from hercules.plant_components.open_cycle_gas_turbine import OpenCycleGasTurbine from hercules.plant_components.solar_pysam_pvwatts import SolarPySAMPVWatts @@ -120,6 +121,9 @@ def get_plant_component(self, component_name, h_dict): if component_type == "OpenCycleGasTurbine": return OpenCycleGasTurbine(h_dict) + if component_type == "CombinedCycleGasTurbine": + return CombinedCycleGasTurbine(h_dict) + raise Exception( f"Unknown component_type '{component_type}' for component '{component_name}'" ) diff --git a/hercules/utilities.py b/hercules/utilities.py index e6a340f0..f34ca880 100644 --- a/hercules/utilities.py +++ b/hercules/utilities.py @@ -28,14 +28,15 @@ def get_available_component_names(): "battery", "electrolyzer", "open_cycle_gas_turbine", + "combined_cycle_gas_turbine", ] def get_available_generator_names(): """Return available generator component names. - Returns power generators (wind_farm, solar_farm, open_cycle_gas_turbine), excluding - storage and conversion components. + Returns power generators (wind_farm, solar_farm, open_cycle_gas_turbine, + combined_cycle_gas_turbine), excluding storage and conversion components. Returns: list: Available generator component names. @@ -44,6 +45,7 @@ def get_available_generator_names(): "wind_farm", "solar_farm", "open_cycle_gas_turbine", + "combined_cycle_gas_turbine", ] @@ -62,6 +64,7 @@ def get_available_component_types(): "battery": ["BatterySimple", "BatteryLithiumIon"], "electrolyzer": ["ElectrolyzerPlant"], "open_cycle_gas_turbine": ["OpenCycleGasTurbine"], + "combined_cycle_gas_turbine": ["CombinedCycleGasTurbine"], } diff --git a/tests/combined_cycle_gas_turbine_test.py b/tests/combined_cycle_gas_turbine_test.py index a96f2af7..dac2a0b4 100644 --- a/tests/combined_cycle_gas_turbine_test.py +++ b/tests/combined_cycle_gas_turbine_test.py @@ -1,6 +1,8 @@ import copy +import numpy as np from hercules.plant_components.combined_cycle_gas_turbine import CombinedCycleGasTurbine +from hercules.utilities import hercules_float_type from .test_inputs.h_dict import ( h_dict_combined_cycle_gas_turbine, @@ -93,7 +95,6 @@ def test_default_efficiency_table(): Default values are approximate readings from the CC1A-F curve in Exhibit ES-4 of [5]. """ - import numpy as np h_dict = copy.deepcopy(h_dict_combined_cycle_gas_turbine) del h_dict["combined_cycle_gas_turbine"]["efficiency_table"] @@ -102,8 +103,10 @@ def test_default_efficiency_table(): np.testing.assert_array_equal( ccgt.efficiency_power_fraction, np.array([0.4, 0.50, 0.55, 0.6, 0.65, 0.70, 0.75, 0.80, 0.85, 0.9, 0.95, 1.0]), + dtype=hercules_float_type, ) np.testing.assert_array_equal( ccgt.efficiency_values, np.array([0.47, 0.49, 0.5, 0.505, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.53]), + dtype=hercules_float_type, ) From 5d4ca21464e27a368b1aa4538ab3e900e3388c1b Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 26 Feb 2026 20:27:25 -0500 Subject: [PATCH 05/14] Update test format --- .../hercules_input.yaml | 2 +- hercules/plant_components/thermal_component_base.py | 12 ++++++++++++ tests/combined_cycle_gas_turbine_test.py | 12 ++++++++---- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml index 45f87003..178a5be9 100644 --- a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml +++ b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml @@ -10,7 +10,7 @@ description: Combined Cycle Gas Turbine (OCGT) Example dt: 60.0 # 1 minute time step starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC -endtime_utc: "2020-01-01T12:00:00Z" # 12 hours later +endtime_utc: "2020-01-01T5:00:00Z" # 12 hours later verbose: False log_every_n: 1 diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 3553b03c..fd2af8d2 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -113,6 +113,8 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) + self.time_counter = 0.0 + # Extract parameters from the h_dict component_dict = h_dict[self.component_name] self.rated_capacity = component_dict["rated_capacity"] # kW @@ -346,6 +348,15 @@ def step(self, h_dict): h_dict[self.component_name]["fuel_volume_rate"] = self.fuel_volume_rate h_dict[self.component_name]["fuel_mass_rate"] = self.fuel_mass_rate + self.time_counter += self.dt + print("##############################################################") + print("Time step:", self.time_counter) + print(f"Power setpoint: {self.power_output:.2f} kW") + print("Min up time:", self.min_up_time) + print("Min down time:", self.min_down_time) + print("Time in state:", self.time_in_state) + print("Current state:", self.STATES(self.state).name) + return h_dict def _control(self, power_setpoint): @@ -511,6 +522,7 @@ def _control(self, power_setpoint): elif self.state == self.STATES.ON: # Check if we can shut down (min_up_time satisfied) can_shutdown = self.time_in_state >= self.min_up_time + print("Can shutdown:", can_shutdown) if power_setpoint <= 0 and can_shutdown: # Transition to shutdown sequence diff --git a/tests/combined_cycle_gas_turbine_test.py b/tests/combined_cycle_gas_turbine_test.py index dac2a0b4..4daee652 100644 --- a/tests/combined_cycle_gas_turbine_test.py +++ b/tests/combined_cycle_gas_turbine_test.py @@ -102,11 +102,15 @@ def test_default_efficiency_table(): # Default HHV net plant efficiency from CC1A-F curve in Exhibit ES-4 of [5] np.testing.assert_array_equal( ccgt.efficiency_power_fraction, - np.array([0.4, 0.50, 0.55, 0.6, 0.65, 0.70, 0.75, 0.80, 0.85, 0.9, 0.95, 1.0]), - dtype=hercules_float_type, + np.array( + [0.4, 0.50, 0.55, 0.6, 0.65, 0.70, 0.75, 0.80, 0.85, 0.9, 0.95, 1.0], + dtype=hercules_float_type, + ), ) np.testing.assert_array_equal( ccgt.efficiency_values, - np.array([0.47, 0.49, 0.5, 0.505, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.53]), - dtype=hercules_float_type, + np.array( + [0.47, 0.49, 0.5, 0.505, 0.515, 0.52, 0.52, 0.52, 0.52, 0.52, 0.515, 0.53], + dtype=hercules_float_type, + ), ) From 187b69df214bba55772fd0244b5f7ab9391ccc16 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 27 Feb 2026 18:50:50 -0500 Subject: [PATCH 06/14] Update examples and docs --- docs/examples/07_open_cycle_gas_turbine.md | 4 +- .../examples/08_combined_cycle_gas_turbine.md | 68 +++++++++++++++++++ .../hercules_input.yaml | 2 +- .../hercules_runscript.py | 4 +- .../plot_outputs.py | 16 ++--- .../thermal_component_base.py | 12 ---- 6 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 docs/examples/08_combined_cycle_gas_turbine.md diff --git a/docs/examples/07_open_cycle_gas_turbine.md b/docs/examples/07_open_cycle_gas_turbine.md index eb342482..30d96d5b 100644 --- a/docs/examples/07_open_cycle_gas_turbine.md +++ b/docs/examples/07_open_cycle_gas_turbine.md @@ -14,7 +14,9 @@ The simulation runs for 6 hours with 1-minute time steps. A controller commands | Time (min) | Event Type | Setpoint | State | Description | |------------|------------|----------|-------|-------------| -| 0 | Initial | 0 | OFF (0) | Turbine starts off, `time_in_state` begins counting | +| 0 | Initial | 100 MW | ON (4) | Turbine starts at rated power | +| 10 | Command + State | → 0 | → STOPPING (5) | Shutdown command; `min_up_time` satisfied (initialized to be dispatchable), begins stopping sequence | +| ~20 | State | 0 | → OFF (0) | Power reaches 0 (ramped down at 10 MW/min), turbine off | | 40 | Command | → 100 MW | OFF (0) | Setpoint changes to full power, but `min_down_time` (60 min) not yet satisfied—turbine remains off | | 60 | State | 100 MW | → HOT STARTING (1) | `min_down_time` satisfied, turbine begins hot starting sequence | | ~64 | State | 100 MW | HOT STARTING (1) | `hot_readying_time` (~4.2 min) complete, run-up ramp begins | diff --git a/docs/examples/08_combined_cycle_gas_turbine.md b/docs/examples/08_combined_cycle_gas_turbine.md new file mode 100644 index 00000000..abcbe445 --- /dev/null +++ b/docs/examples/08_combined_cycle_gas_turbine.md @@ -0,0 +1,68 @@ +# Example 08: Combined Cycle Gas Turbine (CCGT) + +## Description + +This example demonstrates a standalone combined-cycle gas turbine (CCGT) simulation. The example showcases the turbine's state machine behavior including startup sequences, power ramping, minimum stable load constraints, and shutdown sequences. + +For details on CCGT parameters and configuration, see {doc}`../combined_cycle_gas_turbine`. For details on the underlying state machine and ramp behavior, see {doc}`../thermal_component_base`. + +## Scenario + +The simulation runs for 10 hours with 1-minute time steps. A controller commands the turbine through several operating phases. The table below shows both **control commands** (setpoint changes) and **state transitions** (responses to commands based on constraints). + +### Timeline + +| Time (min) | Event Type | Setpoint | State | Description | +|------------|------------|----------|-------|-------------| +| 0 | Initial | 100 MW | ON (4) | Turbine starts on at rated power, `time_in_state` begins counting | +| 10 | Command + State | → 0 | → STOPPING (5) | Shutdown command; `min_up_time` satisfied (initialized to be dispatchable), begins stopping sequence | +| ~45 | State | 0 | → OFF (0) | Power reaches 0 (ramped down at 3 MW/min), turbine off | +| 60 | Command | → 100 MW | OFF (0) | Setpoint changes to full power, but `min_down_time` (120 min) not yet satisfied—turbine remains off | +| ~160 | State | 100 MW | → HOT STARTING (1) | `min_down_time` satisfied, turbine begins hot starting sequence | +| ~225 | State | 100 MW | HOT STARTING (1) | `hot_readying_time` (~65 min) complete, run-up ramp begins | +| ~235 | State | 100 MW | → ON (4) | Power reaches P_min (40 MW) after `hot_startup_time` (~75 min), turbine now operational | +| ~260 | Ramp | 100 MW | ON (4) | Power reaches 100 MW (ramped at 3 MW/min from P_min) | +| 260 | Command | → 50 MW | ON (4) | Setpoint reduced to 50% capacity | +| ~276 | Ramp | 50 MW | ON (4) | Power reaches 50 MW (ramped down at 3 MW/min) | +| 360 | Command | → 10 MW | ON (4) | Setpoint reduced to 10% (below P_min), power clamped to P_min (40 MW) | +| ~365 | Ramp | 10 MW | ON (4) | Power reaches P_min (40 MW), cannot go lower | +| 480 | Command | → 100 MW | ON (4) | Setpoint increased to full power | +| ~500 | Ramp | 100 MW | ON (4) | Power reaches 100 MW | +| 540 | Command + State | → 0 | → STOPPING (5) | Shutdown command; `min_up_time` satisfied (~305 min on), begins stopping sequence | +| ~570 | State | 0 | → OFF (0) | Power reaches 0 (ramped down at 3 MW/min), turbine off | +| 600 | End | 0 | OFF (0) | Simulation ends | + +### Key Behaviors Demonstrated + +- **Minimum down time**: The turbine cannot start until `min_down_time` (120 min) is satisfied, even though the command is issued at 60 min +- **Hot startup sequence**: After `min_down_time`, the turbine enters HOT STARTING, waits through `hot_readying_time`, then ramps to P_min using `run_up_rate` +- **Ramp rate constraints**: All power changes in ON state are limited by `ramp_rate` (3 MW/min) +- **Minimum stable load**: When commanded to 10 MW (below P_min = 40 MW), power is clamped to P_min +- **Minimum up time**: Shutdown is allowed immediately at 570 min because `min_up_time` (240 min) was satisfied long ago +- **Stopping sequence**: The turbine ramps down to zero at `ramp_rate` before transitioning to OFF + +## Setup + +No manual setup is required. The example uses only the CCGT component which requires no external data files. + +## Running + +To run the example, execute the following command in the terminal: + +```bash +python hercules_runscript.py +``` + +## Outputs + +To plot the outputs, run: + +```bash +python plot_outputs.py +``` + +The plot shows: +- Power output over time (demonstrating ramp constraints and minimum stable load) +- Operating state transitions +- Fuel consumption tracking +- Heat rate variation with load diff --git a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml index 178a5be9..6c196ca6 100644 --- a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml +++ b/examples/08_combined_cycle_gas_turbine/hercules_input.yaml @@ -10,7 +10,7 @@ description: Combined Cycle Gas Turbine (OCGT) Example dt: 60.0 # 1 minute time step starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC -endtime_utc: "2020-01-01T5:00:00Z" # 12 hours later +endtime_utc: "2020-01-01T10:00:00Z" # 12 hours later verbose: False log_every_n: 1 diff --git a/examples/08_combined_cycle_gas_turbine/hercules_runscript.py b/examples/08_combined_cycle_gas_turbine/hercules_runscript.py index 412c50cc..3b82e729 100644 --- a/examples/08_combined_cycle_gas_turbine/hercules_runscript.py +++ b/examples/08_combined_cycle_gas_turbine/hercules_runscript.py @@ -55,8 +55,8 @@ def step(self, h_dict): elif current_time < 60 * 60: # 60 minutes in seconds # Between 10 and 60 minutes: shut down power_setpoint = 0.0 - elif current_time < 240 * 60: # 240 minutes in seconds - # Between 60 and 240 minutes: signal to run at full capacity + elif current_time < 260 * 60: # 260 minutes in seconds + # Between 60 and 260 minutes: signal to run at full capacity power_setpoint = self.rated_capacity elif current_time < 360 * 60: # 360 minutes in seconds # Between 240 and 360 minutes: reduce power to 50% of rated capacity diff --git a/examples/08_combined_cycle_gas_turbine/plot_outputs.py b/examples/08_combined_cycle_gas_turbine/plot_outputs.py index 5d26c63e..fa4aeb7c 100644 --- a/examples/08_combined_cycle_gas_turbine/plot_outputs.py +++ b/examples/08_combined_cycle_gas_turbine/plot_outputs.py @@ -17,18 +17,16 @@ # Get the h_dict from metadata h_dict = ho.h_dict -# Convert time to minutes for easier reading -time_minutes = df["time"] / 60 +# Convert time to hours for easier reading +time_hours = df["time"] / 60 / 60 fig, axarr = plt.subplots(4, 1, sharex=True, figsize=(10, 10)) # Plot the power output and setpoint ax = axarr[0] +ax.plot(time_hours, df["combined_cycle_gas_turbine.power"] / 1000, label="Power Output", color="b") ax.plot( - time_minutes, df["combined_cycle_gas_turbine.power"] / 1000, label="Power Output", color="b" -) -ax.plot( - time_minutes, + time_hours, df["combined_cycle_gas_turbine.power_setpoint"] / 1000, label="Power Setpoint", color="r", @@ -55,7 +53,7 @@ # Plot the state ax = axarr[1] -ax.plot(time_minutes, df["combined_cycle_gas_turbine.state"], label="State", color="k") +ax.plot(time_hours, df["combined_cycle_gas_turbine.state"], label="State", color="k") ax.set_ylabel("State") ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) @@ -67,7 +65,7 @@ # Plot the efficiency ax = axarr[2] ax.plot( - time_minutes, + time_hours, df["combined_cycle_gas_turbine.efficiency"] * 100, label="Efficiency", color="g", @@ -79,7 +77,7 @@ # Plot the fuel consumption ax = axarr[3] ax.plot( - time_minutes, + time_hours, df["combined_cycle_gas_turbine.fuel_volume_rate"], label="Fuel Volume Rate", color="orange", diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index fd2af8d2..3553b03c 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -113,8 +113,6 @@ def __init__(self, h_dict): # Call the base class init super().__init__(h_dict, self.component_name) - self.time_counter = 0.0 - # Extract parameters from the h_dict component_dict = h_dict[self.component_name] self.rated_capacity = component_dict["rated_capacity"] # kW @@ -348,15 +346,6 @@ def step(self, h_dict): h_dict[self.component_name]["fuel_volume_rate"] = self.fuel_volume_rate h_dict[self.component_name]["fuel_mass_rate"] = self.fuel_mass_rate - self.time_counter += self.dt - print("##############################################################") - print("Time step:", self.time_counter) - print(f"Power setpoint: {self.power_output:.2f} kW") - print("Min up time:", self.min_up_time) - print("Min down time:", self.min_down_time) - print("Time in state:", self.time_in_state) - print("Current state:", self.STATES(self.state).name) - return h_dict def _control(self, power_setpoint): @@ -522,7 +511,6 @@ def _control(self, power_setpoint): elif self.state == self.STATES.ON: # Check if we can shut down (min_up_time satisfied) can_shutdown = self.time_in_state >= self.min_up_time - print("Can shutdown:", can_shutdown) if power_setpoint <= 0 and can_shutdown: # Transition to shutdown sequence From 10c1059d71f65f84091358ab3e6d562f966488eb Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 27 Feb 2026 19:15:53 -0500 Subject: [PATCH 07/14] Update docs for combined cycle turbine --- docs/combined_cycle_gas_turbine.md | 189 +++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 docs/combined_cycle_gas_turbine.md diff --git a/docs/combined_cycle_gas_turbine.md b/docs/combined_cycle_gas_turbine.md new file mode 100644 index 00000000..9334a2cc --- /dev/null +++ b/docs/combined_cycle_gas_turbine.md @@ -0,0 +1,189 @@ +# Combined Cycle Gas Turbine + +The `CombinedCycleGasTurbine` class models a combined-cycle gas turbine (CCGT). It is a subclass of {doc}`ThermalComponentBase ` and inherits all state machine behavior, ramp constraints, and operational logic from the base class. This class represents the combined gas turbine and steam turbine operation as an improvement in the fuel efficiency of the unit and does not model the individual generators. + +For details on the state machine, startup/shutdown behavior, and base parameters, see {doc}`thermal_component_base`. + +## CCGT-Specific Parameters + +The CCGT class provides default values for natural gas properties from [6]: + +| Parameter | Units | Default | Description | +|-----------|-------|---------|-------------| +| `hhv` | J/m³ | 39050000 | Higher heating value of natural gas (39.05 MJ/m³) [6] | +| `fuel_density` | kg/m³ | 0.768 | Fuel density for mass calculations [6] | + +The `efficiency_table` parameter is **optional**. If not provided, default values based on approximate readings from the CC1A-F curve in Exhibit ES-4 of [5] are used. All efficiency values are **HHV (Higher Heating Value) net plant efficiencies**. See {doc}`thermal_component_base` for details on the efficiency table format. + +## Default Parameter Values + +The `CombinedCycleGasTurbine` class provides default values for base class parameters based on References [1-5]. Only `rated_capacity` and `initial_conditions` are required in the YAML configuration. + +| Parameter | Default Value | Source | +|-----------|---------------|--------| +| `min_stable_load_fraction` | 0.40 (40%) | [4] | +| `ramp_rate_fraction` | 0.03 (3%/min) | [1] | +| `run_up_rate_fraction` | Same as `ramp_rate_fraction` | — | +| `hot_startup_time` | 4500 s (75 minutes, 1.25 hours) | [1], [5] | +| `warm_startup_time` | 7200 s (120 minutes, 2 hours) | [1], [5] | +| `cold_startup_time` | 10800 s (180 minutes, 3 hours) | [1], [5] | +| `min_up_time` | 14400 s (240 minutes, 4 hours) | [4] | +| `min_down_time` | 7200 s (120 minutes, 2 hours) | [4] | +| `hhv` | 39050000 J/m³ (39.05 MJ/m³) | [6] | +| `fuel_density` | 0.768 kg/m³ | [6] | +| `efficiency_table` | SC1A HHV net efficiency (see below) | Exhibit ES-4 of [5] | + +### Default Efficiency Table + +The default HHV net plant efficiency table is based on approximate readings from the CC1A-F curve in Exhibit ES-4 of [5]: + +| Power Fraction | HHV Net Efficiency | +|---------------|-------------------| +| 1.00 | 0.53 (53%) | +| 0.95 | 0.515 (51.5%) | +| 0.90 | 0.52 (52%) | +| 0.85 | 0.52 (52%) | +| 0.80 | 0.52 (52%) | +| 0.75 | 0.52 (52%) | +| 0.70 | 0.52 (52%) | +| 0.65 | 0.515 (51.5%) | +| 0.60 | 0.505 (50.5%) | +| 0.55 | 0.5 (50%) | +| 0.50 | 0.49 (49%) | +| 0.4 | 0.47 (47%) | + +## CCGT Outputs + +The CCGT model provides the following outputs (inherited from base class): + +| Output | Units | Description | +|--------|-------|-------------| +| `power` | kW | Actual power output | +| `state` | integer | Operating state number (0-5), corresponding to the `STATES` enum | +| `efficiency` | fraction (0-1) | Current HHV net plant efficiency | +| `fuel_volume_rate` | m³/s | Fuel volume flow rate | +| `fuel_mass_rate` | kg/s | Fuel mass flow rate (computed using `fuel_density` [6]) | + +### Efficiency and Fuel Rate + +HHV net plant efficiency varies with load based on the `efficiency_table`. The fuel volume rate is calculated as: + +$$ +\text{fuel\_volume\_rate} = \frac{\text{power}}{\text{efficiency} \times \text{hhv}} +$$ + +Where: +- `power` is in W (converted from kW internally) +- `efficiency` is the HHV net efficiency interpolated from the efficiency table +- `hhv` is the higher heating value in J/m³ (default 39.05 MJ/m³ for natural gas [6]) +- Result is fuel volume rate in m³/s + +The fuel mass rate is then computed from the volume rate using the fuel density [6]: + +$$ +\text{fuel\_mass\_rate} = \text{fuel\_volume\_rate} \times \text{fuel\_density} +$$ + +Where: +- `fuel_volume_rate` is in m³/s +- `fuel_density` is in kg/m³ (default 0.768 kg/m³ for natural gas [6]) +- Result is fuel mass rate in kg/s + +## YAML Configuration + +### Minimal Configuration + +Required parameters only (uses defaults for `hhv`, `efficiency_table`, and other parameters): + +```yaml +combined_cycle_gas_turbine: + component_type: CombinedCycleGasTurbine + rated_capacity: 100000 # kW (100 MW) + initial_conditions: + power: 0 # 0 kW means OFF; power > 0 means ON +``` + +### Full Configuration + +All parameters explicitly specified: + +```yaml +combined_cycle_gas_turbine: + component_type: CombinedCycleGasTurbine + rated_capacity: 100000 # kW (100 MW) + min_stable_load_fraction: 0.4 # 40% minimum operating point + ramp_rate_fraction: 0.03 # 3%/min ramp rate + run_up_rate_fraction: 0.02 # 2%/min run up rate + hot_startup_time: 4500.0 # 75 minutes + warm_startup_time: 7200.0 # 120 minutes + cold_startup_time: 10800.0 # 180 minutes + min_up_time: 14400 # 4 hours + min_down_time: 7200 # 2 hours + # Natural gas properties from [6] Staffell, "The Energy and Fuel Data Sheet", 2011 + # HHV: 39.05 MJ/m³, Density: 0.768 kg/m³ + hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6] + fuel_density: 0.768 # kg/m³ for natural gas [6] + efficiency_table: + power_fraction: + - 1.0 + - 0.95 + - 0.90 + - 0.85 + - 0.80 + - 0.75 + - 0.7 + - 0.65 + - 0.6 + - 0.55 + - 0.50 + - 0.4 + efficiency: # HHV net plant efficiency, fractions (0-1), from CC1A-F curve in Exhibit ES-4 of [5] + - 0.53 + - 0.515 + - 0.52 + - 0.52 + - 0.52 + - 0.52 + - 0.52 + - 0.515 + - 0.505 + - 0.5 + - 0.49 + - 0.47 + log_channels: + - power + - fuel_volume_rate + - fuel_mass_rate + - state + - efficiency + - power_setpoint + initial_conditions: + power: 100000 # 0 kW means OFF; power > 0 means ON + +``` + +## Logging Configuration + +The `log_channels` parameter controls which outputs are written to the HDF5 output file. + +**Available Channels:** +- `power`: Actual power output in kW (always logged) +- `state`: Operating state number (0-5), corresponding to the `STATES` enum +- `fuel_volume_rate`: Fuel volume flow rate in m³/s +- `fuel_mass_rate`: Fuel mass flow rate in kg/s (computed using `fuel_density` [6]) +- `efficiency`: Current HHV net plant efficiency (0-1) +- `power_setpoint`: Requested power setpoint in kW + +## References + +1. Agora Energiewende (2017): "Flexibility in thermal power plants - With a focus on existing coal-fired power plants." + +2. "Impact of Detailed Parameter Modeling of Open-Cycle Gas Turbines on Production Cost Simulation", NREL/CP-6A40-87554, National Renewable Energy Laboratory, 2024. + +3. Deane, J.P., G. Drayton, and B.P. Ó Gallachóir. "The Impact of Sub-Hourly Modelling in Power Systems with Significant Levels of Renewable Generation." Applied Energy 113 (January 2014): 152–58. https://doi.org/10.1016/j.apenergy.2013.07.027. + +4. IRENA (2019), Innovation landscape brief: Flexibility in conventional power plants, International Renewable Energy Agency, Abu Dhabi. + +5. M. Oakes, M. Turner, "Cost and Performance Baseline for Fossil Energy Plants, Volume 5: Natural Gas Electricity Generating Units for Flexible Operation," National Energy Technology Laboratory, Pittsburgh, May 5, 2023. + +6. I. Staffell, "The Energy and Fuel Data Sheet," University of Birmingham, March 2011. https://claverton-energy.com/cms4/wp-content/uploads/2012/08/the_energy_and_fuel_data_sheet.pdf From ec2df62ed253f1086535ac1332d799d61d090e50 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 2 Mar 2026 11:53:26 -0500 Subject: [PATCH 08/14] Update Example 07 to include CCGT --- .../hercules_input_ccgt.yaml} | 1 + ...es_input.yaml => hercules_input_ocgt.yaml} | 2 +- .../hercules_runscript.py | 83 ++++++++++++- .../07_open_cycle_gas_turbine/plot_outputs.py | 113 ++++++++++++++---- .../hercules_runscript.py | 85 ------------- .../plot_outputs.py | 92 -------------- 6 files changed, 171 insertions(+), 205 deletions(-) rename examples/{08_combined_cycle_gas_turbine/hercules_input.yaml => 07_open_cycle_gas_turbine/hercules_input_ccgt.yaml} (97%) rename examples/07_open_cycle_gas_turbine/{hercules_input.yaml => hercules_input_ocgt.yaml} (97%) delete mode 100644 examples/08_combined_cycle_gas_turbine/hercules_runscript.py delete mode 100644 examples/08_combined_cycle_gas_turbine/plot_outputs.py diff --git a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input_ccgt.yaml similarity index 97% rename from examples/08_combined_cycle_gas_turbine/hercules_input.yaml rename to examples/07_open_cycle_gas_turbine/hercules_input_ccgt.yaml index 6c196ca6..16580a5d 100644 --- a/examples/08_combined_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input_ccgt.yaml @@ -13,6 +13,7 @@ starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC endtime_utc: "2020-01-01T10:00:00Z" # 12 hours later verbose: False log_every_n: 1 +output_file: "outputs/hercules_output_ccgt" plant: interconnect_limit: 100000 # kW (100 MW) diff --git a/examples/07_open_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml similarity index 97% rename from examples/07_open_cycle_gas_turbine/hercules_input.yaml rename to examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml index a52ea403..189c045f 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml @@ -13,6 +13,7 @@ starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC endtime_utc: "2020-01-01T06:00:00Z" # 6 hours later verbose: False log_every_n: 1 +output_file: "outputs/hercules_output_ocgt" plant: interconnect_limit: 100000 # kW (100 MW) @@ -54,4 +55,3 @@ open_cycle_gas_turbine: power: 100000 # Start ON at rated capacity (100 MW) controller: - diff --git a/examples/07_open_cycle_gas_turbine/hercules_runscript.py b/examples/07_open_cycle_gas_turbine/hercules_runscript.py index e0d00879..c5047889 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_runscript.py +++ b/examples/07_open_cycle_gas_turbine/hercules_runscript.py @@ -21,7 +21,7 @@ prepare_output_directory() # Initialize the Hercules model -hmodel = HerculesModel("hercules_input.yaml") +hmodel = HerculesModel("hercules_input_ocgt.yaml") class ControllerOCGT: @@ -83,3 +83,84 @@ def step(self, h_dict): hmodel.run() hmodel.logger.info("Process completed successfully") + +"""Example 07: Combined Cycle Gas Turbine (OCGT) simulation. + +This example demonstrates a simple combined cycle gas turbine (CCGT) that: +- Starts on at rated capacity (100 MW) +- At 10 minutes, receives a shutdown command and begins ramping down +- At ~30 minutes, reaches 0 MW and transitions to off +- At 60 minutes, receives a turn-on command with a setpoint of 100% of rated capacity +- At ~80 minutes, 1 hour down-time minimum is reached and the turbine begins hot starting +- At ~87 minutes, hot start completes, continues ramping up to 100% of rated capacity +- At 120 minutes, receives a command to reduce power to 50% of rated capacity +- At 180 minutes, receives a command to reduce power to 10% of rated capacity + (note this is below the minimum stable load) +- At 210 minutes, receives a command to increase power to 100% of rated capacity +- At 240 minutes (4 hours), receives a shutdown command +- Simulation runs for 6 hours total with 1 minute time steps +""" + +# Initialize the Hercules model +hmodel = HerculesModel("hercules_input_ccgt.yaml") + + +class ControllerCCGT: + """Controller implementing the CCGT schedule described in the module docstring.""" + + def __init__(self, h_dict): + """Initialize the controller. + + Args: + h_dict (dict): The hercules input dictionary. + + """ + self.rated_capacity = h_dict["combined_cycle_gas_turbine"]["rated_capacity"] + + def step(self, h_dict): + """Execute one control step. + + Args: + h_dict (dict): The hercules input dictionary. + + Returns: + dict: The updated hercules input dictionary. + + """ + current_time = h_dict["time"] + + # Determine power setpoint based on time + if current_time < 10 * 60: # 10 minutes in seconds + # Before 10 minutes: run at full capacity + power_setpoint = self.rated_capacity + elif current_time < 60 * 60: # 60 minutes in seconds + # Between 10 and 60 minutes: shut down + power_setpoint = 0.0 + elif current_time < 260 * 60: # 260 minutes in seconds + # Between 60 and 260 minutes: signal to run at full capacity + power_setpoint = self.rated_capacity + elif current_time < 360 * 60: # 360 minutes in seconds + # Between 240 and 360 minutes: reduce power to 50% of rated capacity + power_setpoint = 0.5 * self.rated_capacity + elif current_time < 480 * 60: # 480 minutes in seconds + # Between 360 and 480 minutes: reduce power to 10% of rated capacity + power_setpoint = 0.1 * self.rated_capacity + elif current_time < 540 * 60: # 540 minutes in seconds + # Between 480 and 540 minutes: increase power to 100% of rated capacity + power_setpoint = self.rated_capacity + else: + # After 540 minutes: shut down + power_setpoint = 0.0 + + h_dict["combined_cycle_gas_turbine"]["power_setpoint"] = power_setpoint + + return h_dict + + +# Instantiate the controller and assign to the Hercules model +hmodel.assign_controller(ControllerCCGT(hmodel.h_dict)) + +# Run the simulation +hmodel.run() + +hmodel.logger.info("Process completed successfully") diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 292ec11c..b1aae87e 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -4,72 +4,125 @@ from hercules import HerculesOutput # Read the Hercules output file using HerculesOutput -ho = HerculesOutput("outputs/hercules_output.h5") +ho_ocgt = HerculesOutput("outputs/hercules_output_ocgt.h5") +ho_ccgt = HerculesOutput("outputs/hercules_output_ccgt.h5") # Print metadata information print("Simulation Metadata:") -ho.print_metadata() +ho_ocgt.print_metadata() print() # Create a shortcut to the dataframe -df = ho.df +df_ocgt = ho_ocgt.df +df_ccgt = ho_ccgt.df # Get the h_dict from metadata -h_dict = ho.h_dict +h_dict_ocgt = ho_ocgt.h_dict +h_dict_ccgt = ho_ccgt.h_dict # Convert time to minutes for easier reading -time_minutes = df["time"] / 60 +time_hours_ocgt = df_ocgt["time"] / 60 / 60 +time_hours_ccgt = df_ccgt["time"] / 60 / 60 fig, axarr = plt.subplots(4, 1, sharex=True, figsize=(10, 10)) # Plot the power output and setpoint ax = axarr[0] -ax.plot(time_minutes, df["open_cycle_gas_turbine.power"] / 1000, label="Power Output", color="b") +ax.axhline( + h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, + color="gray", + linestyle=":", + label="Rated Capacity", +) +ax.plot( + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.power"] / 1000, + label="OCGT Power Output", + color="darkred", +) ax.plot( - time_minutes, - df["open_cycle_gas_turbine.power_setpoint"] / 1000, - label="Power Setpoint", + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.power_setpoint"] / 1000, + label="OCGT Power Setpoint", color="r", linestyle="--", ) ax.axhline( - h_dict["open_cycle_gas_turbine"]["rated_capacity"] / 1000, - color="gray", - linestyle=":", - label="Rated Capacity", + h_dict_ocgt["open_cycle_gas_turbine"]["min_stable_load_fraction"] + * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] + / 1000, + color="rosybrown", + linestyle="--", + label="OCGT Minimum Stable Load", ) +ax.plot( + time_hours_ccgt, + df_ccgt["combined_cycle_gas_turbine.power"] / 1000, + label="CCGT Power Output", + color="darkblue", +) +ax.plot( + time_hours_ccgt, + df_ccgt["combined_cycle_gas_turbine.power_setpoint"] / 1000, + label="CCGT Power Setpoint", + color="dodgerblue", + linestyle="--", +) +# ax.axhline( +# h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, +# color="gray", +# linestyle=":", +# label="Rated Capacity", +# ) ax.axhline( - h_dict["open_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict["open_cycle_gas_turbine"]["rated_capacity"] + h_dict_ccgt["combined_cycle_gas_turbine"]["min_stable_load_fraction"] + * h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, - color="gray", + color="lightsteelblue", linestyle="--", - label="Minimum Stable Load", + label="CCGT Minimum Stable Load", ) ax.set_ylabel("Power [MW]") -ax.set_title("Open Cycle Gas Turbine Power Output") +ax.set_title("Gas Turbine Power Output") ax.legend() ax.grid(True) # Plot the state ax = axarr[1] -ax.plot(time_minutes, df["open_cycle_gas_turbine.state"], label="State", color="k") +ax.plot(time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.state"], label="OCGT State", color="k") +ax.plot( + time_hours_ccgt, + df_ccgt["combined_cycle_gas_turbine.state"], + label="CCGT State", + color="gray", + linestyle="--", +) ax.set_ylabel("State") ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) ax.set_title( "Turbine State (0=Off, 1=Hot Starting, 2=Warm Starting, 3=Cold Starting, 4=On, 5=Stopping)" ) +ax.legend() ax.grid(True) # Plot the efficiency ax = axarr[2] ax.plot( - time_minutes, - df["open_cycle_gas_turbine.efficiency"] * 100, - label="Efficiency", + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.efficiency"] * 100, + label="OCGT Efficiency", color="g", ) +ax.plot( + time_hours_ccgt, + df_ccgt["combined_cycle_gas_turbine.efficiency"] * 100, + label="CCGT Efficiency", + color="darkgreen", + linestyle="--", +) +ax.legend() +ax.set_ylim(0, 100) ax.set_ylabel("Efficiency [%]") ax.set_title("Thermal Efficiency") ax.grid(True) @@ -77,16 +130,24 @@ # Plot the fuel consumption ax = axarr[3] ax.plot( - time_minutes, - df["open_cycle_gas_turbine.fuel_volume_rate"], - label="Fuel Volume Rate", + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.fuel_volume_rate"], + label="OCGT Fuel Volume Rate", color="orange", ) +ax.plot( + time_hours_ccgt, + df_ccgt["combined_cycle_gas_turbine.fuel_volume_rate"], + label="CCGT Fuel Volume Rate", + color="darkorange", + linestyle="--", +) +ax.legend() ax.set_ylabel("Fuel [m³/s]") ax.set_title("Fuel Volume Rate") ax.grid(True) -ax.set_xlabel("Time [minutes]") +ax.set_xlabel("Time [hours]") plt.tight_layout() plt.show() diff --git a/examples/08_combined_cycle_gas_turbine/hercules_runscript.py b/examples/08_combined_cycle_gas_turbine/hercules_runscript.py deleted file mode 100644 index 3b82e729..00000000 --- a/examples/08_combined_cycle_gas_turbine/hercules_runscript.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Example 07: Open Cycle Gas Turbine (OCGT) simulation. - -This example demonstrates a simple open cycle gas turbine (OCGT) that: -- Starts on at rated capacity (100 MW) -- At 10 minutes, receives a shutdown command and begins ramping down -- At ~30 minutes, reaches 0 MW and transitions to off -- At 60 minutes, receives a turn-on command with a setpoint of 100% of rated capacity -- At ~80 minutes, 1 hour down-time minimum is reached and the turbine begins hot starting -- At ~87 minutes, hot start completes, continues ramping up to 100% of rated capacity -- At 120 minutes, receives a command to reduce power to 50% of rated capacity -- At 180 minutes, receives a command to reduce power to 10% of rated capacity - (note this is below the minimum stable load) -- At 210 minutes, receives a command to increase power to 100% of rated capacity -- At 240 minutes (4 hours), receives a shutdown command -- Simulation runs for 6 hours total with 1 minute time steps -""" - -from hercules.hercules_model import HerculesModel -from hercules.utilities_examples import prepare_output_directory - -prepare_output_directory() - -# Initialize the Hercules model -hmodel = HerculesModel("hercules_input.yaml") - - -class ControllerCCGT: - """Controller implementing the CCGT schedule described in the module docstring.""" - - def __init__(self, h_dict): - """Initialize the controller. - - Args: - h_dict (dict): The hercules input dictionary. - - """ - self.rated_capacity = h_dict["combined_cycle_gas_turbine"]["rated_capacity"] - - def step(self, h_dict): - """Execute one control step. - - Args: - h_dict (dict): The hercules input dictionary. - - Returns: - dict: The updated hercules input dictionary. - - """ - current_time = h_dict["time"] - - # Determine power setpoint based on time - if current_time < 10 * 60: # 10 minutes in seconds - # Before 10 minutes: run at full capacity - power_setpoint = self.rated_capacity - elif current_time < 60 * 60: # 60 minutes in seconds - # Between 10 and 60 minutes: shut down - power_setpoint = 0.0 - elif current_time < 260 * 60: # 260 minutes in seconds - # Between 60 and 260 minutes: signal to run at full capacity - power_setpoint = self.rated_capacity - elif current_time < 360 * 60: # 360 minutes in seconds - # Between 240 and 360 minutes: reduce power to 50% of rated capacity - power_setpoint = 0.5 * self.rated_capacity - elif current_time < 480 * 60: # 480 minutes in seconds - # Between 360 and 480 minutes: reduce power to 10% of rated capacity - power_setpoint = 0.1 * self.rated_capacity - elif current_time < 540 * 60: # 540 minutes in seconds - # Between 480 and 540 minutes: increase power to 100% of rated capacity - power_setpoint = self.rated_capacity - else: - # After 540 minutes: shut down - power_setpoint = 0.0 - - h_dict["combined_cycle_gas_turbine"]["power_setpoint"] = power_setpoint - - return h_dict - - -# Instantiate the controller and assign to the Hercules model -hmodel.assign_controller(ControllerCCGT(hmodel.h_dict)) - -# Run the simulation -hmodel.run() - -hmodel.logger.info("Process completed successfully") diff --git a/examples/08_combined_cycle_gas_turbine/plot_outputs.py b/examples/08_combined_cycle_gas_turbine/plot_outputs.py deleted file mode 100644 index fa4aeb7c..00000000 --- a/examples/08_combined_cycle_gas_turbine/plot_outputs.py +++ /dev/null @@ -1,92 +0,0 @@ -# Plot the outputs of the simulation for the OCGT example - -import matplotlib.pyplot as plt -from hercules import HerculesOutput - -# Read the Hercules output file using HerculesOutput -ho = HerculesOutput("outputs/hercules_output.h5") - -# Print metadata information -print("Simulation Metadata:") -ho.print_metadata() -print() - -# Create a shortcut to the dataframe -df = ho.df - -# Get the h_dict from metadata -h_dict = ho.h_dict - -# Convert time to hours for easier reading -time_hours = df["time"] / 60 / 60 - -fig, axarr = plt.subplots(4, 1, sharex=True, figsize=(10, 10)) - -# Plot the power output and setpoint -ax = axarr[0] -ax.plot(time_hours, df["combined_cycle_gas_turbine.power"] / 1000, label="Power Output", color="b") -ax.plot( - time_hours, - df["combined_cycle_gas_turbine.power_setpoint"] / 1000, - label="Power Setpoint", - color="r", - linestyle="--", -) -ax.axhline( - h_dict["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, - color="gray", - linestyle=":", - label="Rated Capacity", -) -ax.axhline( - h_dict["combined_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict["combined_cycle_gas_turbine"]["rated_capacity"] - / 1000, - color="gray", - linestyle="--", - label="Minimum Stable Load", -) -ax.set_ylabel("Power [MW]") -ax.set_title("Combined Cycle Gas Turbine Power Output") -ax.legend() -ax.grid(True) - -# Plot the state -ax = axarr[1] -ax.plot(time_hours, df["combined_cycle_gas_turbine.state"], label="State", color="k") -ax.set_ylabel("State") -ax.set_yticks([0, 1, 2, 3, 4, 5]) -ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) -ax.set_title( - "Turbine State (0=Off, 1=Hot Starting, 2=Warm Starting, 3=Cold Starting, 4=On, 5=Stopping)" -) -ax.grid(True) - -# Plot the efficiency -ax = axarr[2] -ax.plot( - time_hours, - df["combined_cycle_gas_turbine.efficiency"] * 100, - label="Efficiency", - color="g", -) -ax.set_ylabel("Efficiency [%]") -ax.set_title("Thermal Efficiency") -ax.grid(True) - -# Plot the fuel consumption -ax = axarr[3] -ax.plot( - time_hours, - df["combined_cycle_gas_turbine.fuel_volume_rate"], - label="Fuel Volume Rate", - color="orange", -) -ax.set_ylabel("Fuel [m³/s]") -ax.set_title("Fuel Volume Rate") -ax.grid(True) - -ax.set_xlabel("Time [minutes]") - -plt.tight_layout() -plt.show() From 833786e63cdec3f02aef82d28e9c72af9c6027d8 Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 2 Mar 2026 11:07:50 -0700 Subject: [PATCH 09/14] Match references between turbines --- .../hercules_input_ocgt.yaml | 2 +- .../hercules_runscript.py | 113 +++++------------- .../07_open_cycle_gas_turbine/plot_outputs.py | 80 +++++-------- 3 files changed, 58 insertions(+), 137 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml b/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml index 189c045f..fb01ceef 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input_ocgt.yaml @@ -10,7 +10,7 @@ description: Open Cycle Gas Turbine (OCGT) Example dt: 60.0 # 1 minute time step starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC -endtime_utc: "2020-01-01T06:00:00Z" # 6 hours later +endtime_utc: "2020-01-01T10:00:00Z" # 6 hours later verbose: False log_every_n: 1 output_file: "outputs/hercules_output_ocgt" diff --git a/examples/07_open_cycle_gas_turbine/hercules_runscript.py b/examples/07_open_cycle_gas_turbine/hercules_runscript.py index c5047889..99bc329b 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_runscript.py +++ b/examples/07_open_cycle_gas_turbine/hercules_runscript.py @@ -24,7 +24,7 @@ hmodel = HerculesModel("hercules_input_ocgt.yaml") -class ControllerOCGT: +class OpenLoopController: """Controller implementing the OCGT schedule described in the module docstring.""" def __init__(self, h_dict): @@ -33,89 +33,14 @@ def __init__(self, h_dict): Args: h_dict (dict): The hercules input dictionary. - """ - self.rated_capacity = h_dict["open_cycle_gas_turbine"]["rated_capacity"] - - def step(self, h_dict): - """Execute one control step. - - Args: - h_dict (dict): The hercules input dictionary. - - Returns: - dict: The updated hercules input dictionary. - - """ - current_time = h_dict["time"] - - # Determine power setpoint based on time - if current_time < 10 * 60: # 10 minutes in seconds - # Before 10 minutes: run at full capacity - power_setpoint = self.rated_capacity - elif current_time < 40 * 60: # 40 minutes in seconds - # Between 10 and 40 minutes: shut down - power_setpoint = 0.0 - elif current_time < 120 * 60: # 120 minutes in seconds - # Between 40 and 120 minutes: signal to run at full capacity - power_setpoint = self.rated_capacity - elif current_time < 180 * 60: # 180 minutes in seconds - # Between 120 and 180 minutes: reduce power to 50% of rated capacity - power_setpoint = 0.5 * self.rated_capacity - elif current_time < 210 * 60: # 210 minutes in seconds - # Between 180 and 210 minutes: reduce power to 10% of rated capacity - power_setpoint = 0.1 * self.rated_capacity - elif current_time < 240 * 60: # 240 minutes in seconds - # Between 210 and 240 minutes: increase power to 100% of rated capacity - power_setpoint = self.rated_capacity + """ + # TODO: Improve this once component-type reconfigured + if "open_cycle_gas_turbine" in h_dict: + self.rated_capacity = h_dict["open_cycle_gas_turbine"]["rated_capacity"] + elif "combined_cycle_gas_turbine" in h_dict: + self.rated_capacity = h_dict["combined_cycle_gas_turbine"]["rated_capacity"] else: - # After 240 minutes: shut down - power_setpoint = 0.0 - - h_dict["open_cycle_gas_turbine"]["power_setpoint"] = power_setpoint - - return h_dict - - -# Instantiate the controller and assign to the Hercules model -hmodel.assign_controller(ControllerOCGT(hmodel.h_dict)) - -# Run the simulation -hmodel.run() - -hmodel.logger.info("Process completed successfully") - -"""Example 07: Combined Cycle Gas Turbine (OCGT) simulation. - -This example demonstrates a simple combined cycle gas turbine (CCGT) that: -- Starts on at rated capacity (100 MW) -- At 10 minutes, receives a shutdown command and begins ramping down -- At ~30 minutes, reaches 0 MW and transitions to off -- At 60 minutes, receives a turn-on command with a setpoint of 100% of rated capacity -- At ~80 minutes, 1 hour down-time minimum is reached and the turbine begins hot starting -- At ~87 minutes, hot start completes, continues ramping up to 100% of rated capacity -- At 120 minutes, receives a command to reduce power to 50% of rated capacity -- At 180 minutes, receives a command to reduce power to 10% of rated capacity - (note this is below the minimum stable load) -- At 210 minutes, receives a command to increase power to 100% of rated capacity -- At 240 minutes (4 hours), receives a shutdown command -- Simulation runs for 6 hours total with 1 minute time steps -""" - -# Initialize the Hercules model -hmodel = HerculesModel("hercules_input_ccgt.yaml") - - -class ControllerCCGT: - """Controller implementing the CCGT schedule described in the module docstring.""" - - def __init__(self, h_dict): - """Initialize the controller. - - Args: - h_dict (dict): The hercules input dictionary. - - """ - self.rated_capacity = h_dict["combined_cycle_gas_turbine"]["rated_capacity"] + raise ValueError("No gas turbine component found in input dictionary.") def step(self, h_dict): """Execute one control step. @@ -152,15 +77,33 @@ def step(self, h_dict): # After 540 minutes: shut down power_setpoint = 0.0 - h_dict["combined_cycle_gas_turbine"]["power_setpoint"] = power_setpoint + # TODO: Improve this once component-type reconfigured + if "open_cycle_gas_turbine" in h_dict: + h_dict["open_cycle_gas_turbine"]["power_setpoint"] = power_setpoint + elif "combined_cycle_gas_turbine" in h_dict: + h_dict["combined_cycle_gas_turbine"]["power_setpoint"] = power_setpoint return h_dict # Instantiate the controller and assign to the Hercules model -hmodel.assign_controller(ControllerCCGT(hmodel.h_dict)) +hmodel.assign_controller(OpenLoopController(hmodel.h_dict)) + +# Run the simulation +print("Running OCGT simulation...") +hmodel.run() + +hmodel.logger.info("Process completed successfully") + +# Initialize the Hercules model +hmodel = HerculesModel("hercules_input_ccgt.yaml") + + +# Instantiate the controller and assign to the Hercules model +hmodel.assign_controller(OpenLoopController(hmodel.h_dict)) # Run the simulation +print("Running CCGT simulation...") hmodel.run() hmodel.logger.info("Process completed successfully") diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index b1aae87e..045a9f9c 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -16,6 +16,9 @@ df_ocgt = ho_ocgt.df df_ccgt = ho_ccgt.df +col_ocgt = "darkred" +col_ccgt = "darkblue" + # Get the h_dict from metadata h_dict_ocgt = ho_ocgt.h_dict h_dict_ccgt = ho_ccgt.h_dict @@ -32,77 +35,54 @@ h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, color="gray", linestyle=":", - label="Rated Capacity", + label="Rated capacity, min. stable load", ) -ax.plot( - time_hours_ocgt, - df_ocgt["open_cycle_gas_turbine.power"] / 1000, - label="OCGT Power Output", - color="darkred", +ax.axhline( + h_dict_ocgt["open_cycle_gas_turbine"]["min_stable_load_fraction"] + * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, + color="gray", + linestyle=":", ) ax.plot( time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.power_setpoint"] / 1000, - label="OCGT Power Setpoint", - color="r", + label="Power setpoint", + color="k", linestyle="--", ) -ax.axhline( - h_dict_ocgt["open_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] - / 1000, - color="rosybrown", - linestyle="--", - label="OCGT Minimum Stable Load", -) ax.plot( - time_hours_ccgt, - df_ccgt["combined_cycle_gas_turbine.power"] / 1000, - label="CCGT Power Output", - color="darkblue", + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.power"] / 1000, + label="OCGT output", + color=col_ocgt, ) ax.plot( time_hours_ccgt, - df_ccgt["combined_cycle_gas_turbine.power_setpoint"] / 1000, - label="CCGT Power Setpoint", - color="dodgerblue", - linestyle="--", -) -# ax.axhline( -# h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, -# color="gray", -# linestyle=":", -# label="Rated Capacity", -# ) -ax.axhline( - h_dict_ccgt["combined_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] - / 1000, - color="lightsteelblue", - linestyle="--", - label="CCGT Minimum Stable Load", + df_ccgt["combined_cycle_gas_turbine.power"] / 1000, + label="CCGT output", + color=col_ccgt, ) ax.set_ylabel("Power [MW]") -ax.set_title("Gas Turbine Power Output") ax.legend() ax.grid(True) # Plot the state ax = axarr[1] -ax.plot(time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.state"], label="OCGT State", color="k") +ax.plot( + time_hours_ocgt, + df_ocgt["open_cycle_gas_turbine.state"], + label="OCGT State", + color=col_ocgt +) ax.plot( time_hours_ccgt, df_ccgt["combined_cycle_gas_turbine.state"], label="CCGT State", - color="gray", - linestyle="--", + color=col_ccgt, ) ax.set_ylabel("State") ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) -ax.set_title( - "Turbine State (0=Off, 1=Hot Starting, 2=Warm Starting, 3=Cold Starting, 4=On, 5=Stopping)" -) ax.legend() ax.grid(True) @@ -112,14 +92,13 @@ time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.efficiency"] * 100, label="OCGT Efficiency", - color="g", + color=col_ocgt, ) ax.plot( time_hours_ccgt, df_ccgt["combined_cycle_gas_turbine.efficiency"] * 100, label="CCGT Efficiency", - color="darkgreen", - linestyle="--", + color=col_ccgt, ) ax.legend() ax.set_ylim(0, 100) @@ -133,14 +112,13 @@ time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.fuel_volume_rate"], label="OCGT Fuel Volume Rate", - color="orange", + color=col_ocgt, ) ax.plot( time_hours_ccgt, df_ccgt["combined_cycle_gas_turbine.fuel_volume_rate"], label="CCGT Fuel Volume Rate", - color="darkorange", - linestyle="--", + color=col_ccgt, ) ax.legend() ax.set_ylabel("Fuel [m³/s]") From 0f50c75c9b8236c86deeb5bea286752d00eaf693 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 2 Mar 2026 13:31:04 -0500 Subject: [PATCH 10/14] Update for accessability --- examples/07_open_cycle_gas_turbine/plot_outputs.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 045a9f9c..352b1562 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -39,7 +39,8 @@ ) ax.axhline( h_dict_ocgt["open_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, + * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] + / 1000, color="gray", linestyle=":", ) @@ -61,6 +62,7 @@ df_ccgt["combined_cycle_gas_turbine.power"] / 1000, label="CCGT output", color=col_ccgt, + linestyle="-.", ) ax.set_ylabel("Power [MW]") ax.legend() @@ -69,16 +71,14 @@ # Plot the state ax = axarr[1] ax.plot( - time_hours_ocgt, - df_ocgt["open_cycle_gas_turbine.state"], - label="OCGT State", - color=col_ocgt + time_hours_ocgt, df_ocgt["open_cycle_gas_turbine.state"], label="OCGT State", color=col_ocgt ) ax.plot( time_hours_ccgt, df_ccgt["combined_cycle_gas_turbine.state"], label="CCGT State", color=col_ccgt, + linestyle="-.", ) ax.set_ylabel("State") ax.set_yticks([0, 1, 2, 3, 4, 5]) @@ -99,6 +99,7 @@ df_ccgt["combined_cycle_gas_turbine.efficiency"] * 100, label="CCGT Efficiency", color=col_ccgt, + linestyle="-.", ) ax.legend() ax.set_ylim(0, 100) @@ -119,6 +120,7 @@ df_ccgt["combined_cycle_gas_turbine.fuel_volume_rate"], label="CCGT Fuel Volume Rate", color=col_ccgt, + linestyle="-.", ) ax.legend() ax.set_ylabel("Fuel [m³/s]") From 58a15cbe230fb9724deb8980c4eeb10ed0e913df Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 2 Mar 2026 11:34:36 -0700 Subject: [PATCH 11/14] Differentiate minimum setpoints, aesthetics --- .../07_open_cycle_gas_turbine/plot_outputs.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 352b1562..97efffda 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -35,14 +35,22 @@ h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, color="gray", linestyle=":", - label="Rated capacity, min. stable load", + label="Rated capacity (both)", ) ax.axhline( h_dict_ocgt["open_cycle_gas_turbine"]["min_stable_load_fraction"] * h_dict_ocgt["open_cycle_gas_turbine"]["rated_capacity"] / 1000, - color="gray", + color=col_ocgt, + linestyle=":", + label="OCGT min. stable load", +) +ax.axhline( + h_dict_ccgt["combined_cycle_gas_turbine"]["min_stable_load_fraction"] + * h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, + color=col_ccgt, linestyle=":", + label="CCGT min. stable load", ) ax.plot( time_hours_ocgt, @@ -65,7 +73,7 @@ linestyle="-.", ) ax.set_ylabel("Power [MW]") -ax.legend() +ax.legend(loc="upper right") ax.grid(True) # Plot the state @@ -83,7 +91,7 @@ ax.set_ylabel("State") ax.set_yticks([0, 1, 2, 3, 4, 5]) ax.set_yticklabels(["Off", "Hot Starting", "Warm Starting", "Cold Starting", "On", "Stopping"]) -ax.legend() +ax.legend(loc="upper right") ax.grid(True) # Plot the efficiency @@ -101,7 +109,7 @@ color=col_ccgt, linestyle="-.", ) -ax.legend() +ax.legend(loc="upper right") ax.set_ylim(0, 100) ax.set_ylabel("Efficiency [%]") ax.set_title("Thermal Efficiency") @@ -122,12 +130,13 @@ color=col_ccgt, linestyle="-.", ) -ax.legend() +ax.legend(loc="upper right") ax.set_ylabel("Fuel [m³/s]") ax.set_title("Fuel Volume Rate") ax.grid(True) ax.set_xlabel("Time [hours]") +ax.set_xlim(0, 10) plt.tight_layout() plt.show() From 105543148a3c244592059b9a00f289d567e8db2b Mon Sep 17 00:00:00 2001 From: misi9170 Date: Mon, 2 Mar 2026 11:42:51 -0700 Subject: [PATCH 12/14] Ruff formatting --- examples/07_open_cycle_gas_turbine/hercules_runscript.py | 2 +- examples/07_open_cycle_gas_turbine/plot_outputs.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_runscript.py b/examples/07_open_cycle_gas_turbine/hercules_runscript.py index 99bc329b..c0d9549c 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_runscript.py +++ b/examples/07_open_cycle_gas_turbine/hercules_runscript.py @@ -33,7 +33,7 @@ def __init__(self, h_dict): Args: h_dict (dict): The hercules input dictionary. - """ + """ # TODO: Improve this once component-type reconfigured if "open_cycle_gas_turbine" in h_dict: self.rated_capacity = h_dict["open_cycle_gas_turbine"]["rated_capacity"] diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 97efffda..84d1036b 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -47,7 +47,8 @@ ) ax.axhline( h_dict_ccgt["combined_cycle_gas_turbine"]["min_stable_load_fraction"] - * h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] / 1000, + * h_dict_ccgt["combined_cycle_gas_turbine"]["rated_capacity"] + / 1000, color=col_ccgt, linestyle=":", label="CCGT min. stable load", From 2bfaefbd03f63be21b7e54a15780d41700a256a3 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 2 Mar 2026 13:43:33 -0500 Subject: [PATCH 13/14] Update run script doc string --- .../hercules_runscript.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_runscript.py b/examples/07_open_cycle_gas_turbine/hercules_runscript.py index 99bc329b..95c954fb 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_runscript.py +++ b/examples/07_open_cycle_gas_turbine/hercules_runscript.py @@ -1,18 +1,17 @@ """Example 07: Open Cycle Gas Turbine (OCGT) simulation. -This example demonstrates a simple open cycle gas turbine (OCGT) that: +This example demonstrates a simple open cycle gas turbine (OCGT) and a combined + cycle turbine (CCGT) in a simulation that compares their performance using the + following schedule of commands: - Starts on at rated capacity (100 MW) - At 10 minutes, receives a shutdown command and begins ramping down -- At ~20 minutes, reaches 0 MW and transitions to off -- At 40 minutes, receives a turn-on command with a setpoint of 100% of rated capacity -- At ~80 minutes, 1 hour down-time minimum is reached and the turbine begins hot starting -- At ~87 minutes, hot start completes, continues ramping up to 100% of rated capacity -- At 120 minutes, receives a command to reduce power to 50% of rated capacity -- At 180 minutes, receives a command to reduce power to 10% of rated capacity +- At 60 minutes, receives a turn-on command with a setpoint of 100% of rated capacity +- At 260 minutes, receives a command to reduce power to 50% of rated capacity +- At 360 minutes, receives a command to reduce power to 10% of rated capacity (note this is below the minimum stable load) -- At 210 minutes, receives a command to increase power to 100% of rated capacity -- At 240 minutes (4 hours), receives a shutdown command -- Simulation runs for 6 hours total with 1 minute time steps +- At 480 minutes, receives a command to increase power to 100% of rated capacity +- At 540 minutes (9 hours), receives a shutdown command +- Simulation runs for 10 hours total with 1 minute time steps """ from hercules.hercules_model import HerculesModel @@ -33,7 +32,7 @@ def __init__(self, h_dict): Args: h_dict (dict): The hercules input dictionary. - """ + """ # TODO: Improve this once component-type reconfigured if "open_cycle_gas_turbine" in h_dict: self.rated_capacity = h_dict["open_cycle_gas_turbine"]["rated_capacity"] @@ -65,7 +64,7 @@ def step(self, h_dict): # Between 60 and 260 minutes: signal to run at full capacity power_setpoint = self.rated_capacity elif current_time < 360 * 60: # 360 minutes in seconds - # Between 240 and 360 minutes: reduce power to 50% of rated capacity + # Between 260 and 360 minutes: reduce power to 50% of rated capacity power_setpoint = 0.5 * self.rated_capacity elif current_time < 480 * 60: # 480 minutes in seconds # Between 360 and 480 minutes: reduce power to 10% of rated capacity From c94dcbfccdb62347b70f9800c2fdec912f891f55 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Mon, 2 Mar 2026 13:45:43 -0500 Subject: [PATCH 14/14] Formatting --- examples/07_open_cycle_gas_turbine/hercules_runscript.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_runscript.py b/examples/07_open_cycle_gas_turbine/hercules_runscript.py index 95c954fb..d68cfe0e 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_runscript.py +++ b/examples/07_open_cycle_gas_turbine/hercules_runscript.py @@ -1,4 +1,5 @@ -"""Example 07: Open Cycle Gas Turbine (OCGT) simulation. +""" +Example 07: Open Cycle Gas Turbine (OCGT) simulation. This example demonstrates a simple open cycle gas turbine (OCGT) and a combined cycle turbine (CCGT) in a simulation that compares their performance using the