Skip to content
29 changes: 29 additions & 0 deletions examples/07_ngct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Example 07: Natural Gas Combustion Turbine (NGCT)

Demonstrates the `CombustionTurbineSimple` component with a 6-hour simulation (1-minute timesteps).

## Turbine Configuration

- **Capacity**: 100 MW
- **Minimum stable load**: 20%
- **Heat rate**: 10,000 kJ/kWh
- **Ramp rates**: 500 kW/s (up/down)
- **Startup/shutdown time**: 1 hour each (default)

## Control Schedule

| Time (min) | Setpoint |
|------------|----------|
| 0–60 | Off |
| 60–120 | 100% |
| 120–180 | 50% |
| 180–210 | 10% (below min stable load) |
| 210–240 | 100% |
| 240+ | Shutdown |

## Running

```bash
python hercules_runscript.py
python plot_outputs.py
```
43 changes: 43 additions & 0 deletions examples/07_ngct/hercules_input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Input YAML for hercules

# Name
name: example_07

###
# Describe this simulation setup
description: Natural Gas Combustion Turbine (NGCT) 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
verbose: False
log_every_n: 1

plant:
interconnect_limit: 100000 # kW (100 MW)

combustion_turbine:
component_type: CombustionTurbineSimple
rated_capacity: 100000 # kW (100 MW)
min_stable_load_fraction: 0.2 # 20% minimum operating point
heat_rate: 10000 # kJ/kWh at rated load
ramp_rate_up: 500 # kW/s
ramp_rate_down: 500 # kW/s
# Optional parameters use defaults:
# startup_time: 3600.0 s (1 hour)
# shutdown_time: 3600.0 s (1 hour)
# min_up_time: 3600.0 s (1 hour)
# min_down_time: 3600.0 s (1 hour)
# part_load_factor: 1.0
log_channels:
- power
- fuel_consumption
- state_num
- heat_rate
- power_setpoint
initial_conditions:
power: 0
state_num: 0 # 0 = off

controller:

86 changes: 86 additions & 0 deletions examples/07_ngct/hercules_runscript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Example 07: Natural Gas Combustion Turbine (NGCT) simulation.

This example demonstrates a simple combustion turbine (natural gas peaker) that:
- Starts off (state=0, power=0)
- At 60 minutes, receives a turn-on command with a setpoint of 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 ControllerNGCT:
"""Controller implementing the NGCT schedule described in the module docstring.

The turbine starts off, then:
- At 60 minutes, it is commanded to 100% of rated capacity.
- At 120 minutes, it is reduced to 50% of rated capacity.
- At 180 minutes, it is reduced to 10% of rated capacity.
- At 210 minutes, it is increased back to 100% of rated capacity.
- At 240 minutes, it is commanded off.
"""

def __init__(self, h_dict):
"""Initialize the controller.

Args:
h_dict (dict): The hercules input dictionary.

"""
self.rated_capacity = h_dict["combustion_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 < 60 * 60: # 60 minutes in seconds
# Before 60 minutes: keep turbine off
power_setpoint = 0.0
elif current_time < 120 * 60: # 120 minutes in seconds
# Between 60 and 120 minutes: 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
else:
# After 240 minutes: shut down
power_setpoint = 0.0

h_dict["combustion_turbine"]["power_setpoint"] = power_setpoint

return h_dict


# Instantiate the controller and assign to the Hercules model
hmodel.assign_controller(ControllerNGCT(hmodel.h_dict))

# Run the simulation
hmodel.run()

hmodel.logger.info("Process completed successfully")
69 changes: 69 additions & 0 deletions examples/07_ngct/plot_outputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Plot the outputs of the simulation for the NGCT 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(3, 1, sharex=True, figsize=(10, 10))

# Plot the power output and setpoint
ax = axarr[0]
ax.plot(time_minutes, df["combustion_turbine.power"] / 1000, label="Power Output", color="b")
ax.plot(
time_minutes,
df["combustion_turbine.power_setpoint"] / 1000,
label="Power Setpoint",
color="r",
linestyle="--",
)
ax.axhline(
h_dict["combustion_turbine"]["rated_capacity"] / 1000,
color="gray",
linestyle=":",
label="Rated Capacity",
)
ax.set_ylabel("Power [MW]")
ax.set_title("Combustion Turbine Power Output")
ax.legend()
ax.grid(True)

# Plot the state
ax = axarr[1]
ax.plot(time_minutes, df["combustion_turbine.state_num"], label="State Number", color="k")
ax.set_ylabel("State")
ax.set_yticks([0, 1, 2, 3])
ax.set_yticklabels(["Off", "Starting", "On", "Stopping"])
ax.set_title("Turbine State (0=Off, 1=Starting, 2=On, 3=Stopping)")
ax.grid(True)

# Plot the fuel consumption
ax = axarr[2]
ax.plot(
time_minutes,
df["combustion_turbine.fuel_consumption"] / 1000,
label="Fuel Consumption",
color="orange",
)
ax.set_ylabel("Fuel [MJ/timestep]")
ax.set_title("Fuel Consumption per Timestep")
ax.grid(True)


plt.tight_layout()
plt.show()
4 changes: 4 additions & 0 deletions hercules/hybrid_plant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.combustion_turbine_simple import CombustionTurbineSimple
from hercules.plant_components.electrolyzer_plant import ElectrolyzerPlant
from hercules.plant_components.solar_pysam_pvwatts import SolarPySAMPVWatts
from hercules.plant_components.wind_meso_to_power import Wind_MesoToPower
Expand Down Expand Up @@ -113,6 +114,9 @@ def get_plant_component(self, component_name, h_dict):
if h_dict[component_name]["component_type"] == "ElectrolyzerPlant":
return ElectrolyzerPlant(h_dict)

if h_dict[component_name]["component_type"] == "CombustionTurbineSimple":
return CombustionTurbineSimple(h_dict)

raise Exception("Unknown component_type: ", h_dict[component_name]["component_type"])

def step(self, h_dict):
Expand Down
Loading