Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b6bd44c
Update constants, instructions for nrel-pysam 7
dzalkind Mar 4, 2026
e34ebef
Update toml
dzalkind Mar 4, 2026
a16621d
Merge remote-tracking branch 'upstream/develop' into pysam_7
dzalkind Mar 4, 2026
bb51e82
First pass run through for multiunit thermal plant
misi9170 Mar 4, 2026
71f3c90
Merge branch 'pysam_7' into feature/mm-thermal
dzalkind Mar 4, 2026
0176b6f
Add logging for individual units within thermal plant
misi9170 Mar 5, 2026
f50a34a
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind Mar 5, 2026
5a71cc2
More formatting
misi9170 Mar 5, 2026
0feae7b
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind Mar 5, 2026
84cf038
Add steam turbine module
Mar 5, 2026
c9b8940
Improvement on loading units into thermal plant
Mar 5, 2026
6f9991d
Bug fix for when there are multiple of the same units in a plant
Mar 5, 2026
65c3955
Track number of starts
dzalkind Mar 5, 2026
b37678c
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind Mar 5, 2026
b458dbc
Make array of units flexible, hacky
dzalkind Mar 5, 2026
9f1d377
Revert "Merge branch 'pysam_7' into feature/mm-thermal"
dzalkind Mar 5, 2026
7fe5752
Track starts in h_dict for outputs
dzalkind Mar 5, 2026
8156bb0
Merge develop
misi9170 Mar 6, 2026
ee48564
Formatting
misi9170 Mar 6, 2026
056695d
Clean up dostrings
misi9170 Mar 6, 2026
aaefd88
Add note that initial conditions are correctly set on h_dict
misi9170 Mar 6, 2026
390cfa9
Docs page for thermal plant
misi9170 Mar 6, 2026
d3ea4c1
Check that units are all thermal
misi9170 Mar 6, 2026
493e661
Add tests for ThermalPlant
misi9170 Mar 6, 2026
810a7b6
Remove SteamTurbine from this branch
misi9170 Mar 9, 2026
4f7b72c
Remove steam turbine example
misi9170 Mar 9, 2026
b1c7905
Clean up example
misi9170 Mar 9, 2026
a4fe752
Add example doc
misi9170 Mar 9, 2026
4c94aef
Minor cleanup
misi9170 Mar 9, 2026
275a9b8
Clean up comment
misi9170 Mar 10, 2026
5a204c0
X axis label
misi9170 Mar 11, 2026
031b610
Update Example 09 folder and documentation
genevievestarke Mar 26, 2026
e543379
Update thermal plant docs page
genevievestarke Mar 26, 2026
53151ce
Final updates for PR
genevievestarke Mar 26, 2026
2511fba
Remove unused import
genevievestarke Mar 26, 2026
e73740f
Add new file
genevievestarke Mar 26, 2026
c6859ef
Update utilities
genevievestarke Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/adding_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ While only generator power is included in `locally_generated_power`, all categor

## Step 2: Register the Component

Add the component to `COMPONENT_REGISTRY` in `hercules/hybrid_plant.py` (see [Hybrid Plant Components](hybrid_plant.md)).
Add the component to `COMPONENT_REGISTRY` in `hercules/component_registry.py` (see [Hybrid Plant Components](hybrid_plant.md)).


The key string (e.g., `"MyComponent"`) is the `component_type` value users will specify in their YAML input files.
Expand Down Expand Up @@ -142,9 +142,8 @@ pytest tests/my_component_test.py -v
- [ ] Inherit from `ComponentBase`
- [ ] Define `component_category` class attribute
- [ ] Implement `__init__`, `step`, `get_initial_conditions_and_meta_data`
- [ ] Import and add to `COMPONENT_REGISTRY` in `hercules/hybrid_plant.py`
- [ ] Import and add to `COMPONENT_REGISTRY` in `hercules/component_registry.py`
- [ ] Create tests in `tests/my_component_test.py`
- [ ] Create `docs/my_component.md`
- [ ] Add to `docs/_toc.yml`
- [ ] Update reference tables in `hybrid_plant.md` and `component_types.md`

27 changes: 27 additions & 0 deletions docs/examples/09_multiunit_thermal_plant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Example 09: Multi-unit Thermal Plant
Comment thread
genevievestarke marked this conversation as resolved.

## Description

Demonstrates a multi-unit thermal plant with three Open Cycle Gas Turbine (OCGT) units. Each unit has its own state machine and ramp behavior, but they share a common controller that issues power setpoints for all units simultaneously. The example illustrates how the plant responds to changes in setpoints while respecting constraints such as minimum up/down times, ramp rates, and minimum stable load of the individual units. The first two individual units are identical, but their commands and responses are tracked separately in the outputs. The third unit is also an Open Cycle Gas Turbine, but it has half the ramp rate of the first two units. It is given the same power set points as the second unit, demonstrating the effect of the ramp rate parameter. This example demonstrates how to both use the same unit definition for two units (OCGT1 & OCGT2), and how to define a unit with its own definition (OCGT3).

## 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 (for the all units separately):
- Power output over time (demonstrating ramp constraints and minimum stable load in response to setpoint changes for the individual units), as well as total plant power output
- Operating state transitions
- Fuel consumption tracking
- Heat rate variation with load
2 changes: 1 addition & 1 deletion docs/hybrid_plant.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The YAML key for each section is a user-chosen `component_name` and is not requi

## Component Registry

All available component types are defined in `COMPONENT_REGISTRY` at the top of `hercules/hybrid_plant.py`. This dictionary maps `component_type` strings to their Python classes:
All available component types are defined in `COMPONENT_REGISTRY` in `hercules/component_registry.py`. This dictionary maps `component_type` strings to their Python classes:

```python
COMPONENT_REGISTRY = {
Expand Down
93 changes: 93 additions & 0 deletions docs/thermal_plant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Thermal Plant

The `ThermalPlant` class models generic single or multiunit thermal power plants. It expects to be assigned one or more thermal units, for example [`OpenCycleGasTurbine`s](open_cycle_gas_turbine.md). The individual units are established in the YAML configuration file, and may be repeats of the same type of units or heterogeneous units.

In order to use the thermal plant model, set `component_type: ThermalPlant` in the component's YAML section. The section key is a user-chosen `component_name` (e.g. `my_thermal_plant`); see [Component Names, Types, and Categories](component_types.md) for details.

For details on the state machine, startup/shutdown behavior, and base parameters, see {doc}`thermal_component_base`.

## Parameters

The `ThermalPlant` class does not have any default parameters. However, key attributes that must be provided in the YAML configuration file are `units`, which is a list that is used to instantiate the individual thermal units that make up the plant, and `unit_names`, which is a list of unique names for each unit. The number of entries in `units` and `unit_names` must match.

See the [YAML Configuration](#yaml-configuration) section below for examples of how to specify these parameters in the input file.

## YAML configuration

The YAML configuration for the thermal plant includes lists `units` and `unit_names`, that define the configuration for each unit. The `component_type` of each unit must be a valid thermal component type, e.g. `OpenCycleGasTurbine`. See [Component Types](component_types.md) for the full list of available component types.

The units listed under the `units` field are used to index the subdictionaries for each unit, which specify the parameters and initial conditions for each unit. For example, if `units: ["open_cycle_gas_turbine", "open_cycle_gas_turbine"]`, then the YAML file must include a subdictionary with the key `open_cycle_gas_turbine:` that specify the parameters and initial conditions that will be used for both of the two gas turbines. Different subdictionaries can be defined for each, or a subset, of units by adding a subdictionary defining the desired parameters and initial conditions, and adding it to the appropriate place in the `units` list. This is illustrated in the below example, where the first two units use the `large_ocgt` subdictionary and the last unit uses the `small_ocgt` subdictionary. The `unit_names` field is a list of unique names for each unit, which are used to identify the units in the HDF5 output file and in the `h_dict` passed to controllers. For example, if `unit_names: ["OCGT1", "OCGT2"]`, then the two gas turbines will be identified as `OCGT1` and `OCGT2` in the output file and in the `h_dict`.

```yaml
my_thermal_plant:
component_type: ThermalPlant
units: ["large_ocgt", "large_ocgt", "small_ocgt"]
unit_names: ["OCGT1", "OCGT2", "OCGT3"]

large_ocgt:
component_type: OpenCycleGasTurbine
rated_capacity: 100000 # kW (100 MW)
min_stable_load_fraction: 0.4 # 40% minimum operating point
ramp_rate_fraction: 0.1 # 10%/min ramp rate
run_up_rate_fraction: 0.05 # 5%/min run up rate
hot_startup_time: 420.0 # 7 minutes
warm_startup_time: 480.0 # 8 minutes
cold_startup_time: 480.0 # 8 minutes
min_up_time: 1800 # 30 minutes
min_down_time: 3600 # 1 hour
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³)
fuel_density: 0.768 # kg/m³ for natural gas
efficiency_table:
power_fraction:
- 1.0
- 0.75
- 0.50
- 0.25
efficiency:
- 0.39
- 0.37
- 0.325
- 0.245
log_channels:
- power
- fuel_volume_rate
- fuel_mass_rate
- state
- efficiency
- power_setpoint
initial_conditions:
power: 0

small_ocgt:
component_type: OpenCycleGasTurbine
rated_capacity: 50000 # kW (50 MW)
min_stable_load_fraction: 0.4 # 40% minimum operating point
ramp_rate_fraction: 0.15 # 15%/min ramp rate
run_up_rate_fraction: 0.1 # 10%/min run up rate
hot_startup_time: 300.0 # 5 minutes
warm_startup_time: 360.0 # 6 minutes
cold_startup_time: 420.0 # 7 minutes
min_up_time: 1200 # 20 minutes
min_down_time: 2400 # 40 minutes
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6]
fuel_density: 0.768 # kg/m
efficiency_table:
power_fraction:
- 1.0
- 0.75
- 0.50
- 0.25
efficiency:
- 0.38
- 0.36
- 0.32
- 0.22
log_channels:
- power
initial_conditions:
power: 0
```

## Logging configuration

The `log_channels` parameter controls which outputs are written to the HDF5 output file. Logging is configured separately for each unit, so the `log_channels` field is specified within each unit's subdictionary. For example, if `unit_names: ["OCGT1", "OCGT1"]`, then the log will have columns `my_thermal_plant.OCGT1.power`, `my_thermal_plant.OCGT1.fuel_volume_rate`, etc. for the first unit, and `my_thermal_plant.OCGT2.power`, `my_thermal_plant.OCGT2.fuel_volume_rate`, etc. for the second unit, assuming those channels are included in the `log_channels` list for each unit. The total power for the thermal plant is always logged to `my_thermal_plant.power`, which is the sum of the power outputs of each unit.
97 changes: 97 additions & 0 deletions examples/09_multiunit_thermal_plant/hercules_input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Input YAML for hercules
# Explicitly specify the parameters for demonstration purposes

# Name
name: example_07

###
# Describe this simulation setup
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
verbose: False
log_every_n: 1

plant:
interconnect_limit: 100000 # kW (100 MW)

thermal_power_plant:
component_type: ThermalPlant
Comment thread
genevievestarke marked this conversation as resolved.
units: ["open_cycle_gas_turbine1", "open_cycle_gas_turbine1", "open_cycle_gas_turbine3"]
unit_names: ["OCGT1", "OCGT2", "OCGT3"]

open_cycle_gas_turbine1:
component_type: OpenCycleGasTurbine
rated_capacity: 100000 # kW (100 MW)
min_stable_load_fraction: 0.2 # 20% minimum operating point
ramp_rate_fraction: 0.1 # 10%/min ramp rate
run_up_rate_fraction: 0.05 # 5%/min run up rate
hot_startup_time: 420.0 # 7 minutes
warm_startup_time: 480.0 # 8 minutes
cold_startup_time: 480.0 # 8 minutes
min_up_time: 3600 # 1 hour
min_down_time: 3600 # 1 hour
# 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.75
- 0.50
- 0.25
efficiency: # HHV net plant efficiency, fractions (0-1), from SC1A in Exhibit ES-4 of [5]
- 0.39
- 0.37
- 0.325
- 0.245
log_channels:
- power
- fuel_volume_rate
- fuel_mass_rate
- state
- efficiency
- power_setpoint
initial_conditions:
power: 100000 # Start ON at rated capacity (100 MW)

open_cycle_gas_turbine3:
component_type: OpenCycleGasTurbine
rated_capacity: 100000 # kW (100 MW)
min_stable_load_fraction: 0.2 # 20% minimum operating point
ramp_rate_fraction: 0.05 # 10%/min ramp rate
run_up_rate_fraction: 0.05 # 5%/min run up rate
hot_startup_time: 420.0 # 7 minutes
warm_startup_time: 480.0 # 8 minutes
cold_startup_time: 480.0 # 8 minutes
min_up_time: 3600 # 1 hour
min_down_time: 3600 # 1 hour
# 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.75
- 0.50
- 0.25
efficiency: # HHV net plant efficiency, fractions (0-1), from SC1A in Exhibit ES-4 of [5]
- 0.39
- 0.37
- 0.325
- 0.245
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:
84 changes: 84 additions & 0 deletions examples/09_multiunit_thermal_plant/hercules_runscript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Example 09: Multiunit Thermal Plant

This example demonstrates a thermal power plant constructed from two 50 MW OCGT units.
The power setpoints are split unequally between the two units to demonstrate the ability of the
model to specify setpoints of individual units.
"""

from hercules.hercules_model import HerculesModel
from hercules.utilities_examples import prepare_output_directory

prepare_output_directory()


# Declare the open loop control setpoint sequence used for demonstration.
class OpenLoopController:
"""Controller implementing the unit power setpoints in open loop."""

def __init__(self, h_dict):
# Access total rated capacity from h_dict, as well as capacities of individual units
self.rated_capacity = h_dict["thermal_power_plant"]["rated_capacity"]
self.unit_1_capacity = h_dict["thermal_power_plant"]["OCGT1"]["rated_capacity"]
self.unit_2_capacity = h_dict["thermal_power_plant"]["OCGT2"]["rated_capacity"]
self.unit_3_capacity = h_dict["thermal_power_plant"]["OCGT3"]["rated_capacity"]

def step(self, h_dict):
current_time = h_dict["time"]

# Determine power setpoint based on time
if current_time < 10 * 60: # 10 minutes in seconds
# Before 10 minutes: run all three units at full capacity
self.power_setpoint_1 = self.unit_1_capacity
self.power_setpoint_2 = self.unit_2_capacity
self.power_setpoint_3 = self.unit_3_capacity
elif current_time < 20 * 60: # 20 minutes in seconds
# Between 10 and 20 minutes: shut down unit 1, leave units 2 & 3
self.power_setpoint_1 = 0.0
elif current_time < 40 * 60: # 40 minutes in seconds
# Shut down units 2 & 3
self.power_setpoint_2 = 0.0
self.power_setpoint_3 = 0.0
elif current_time < 120 * 60: # 120 minutes in seconds
# Between 40 and 120 minutes: signal to run at full capacity
self.power_setpoint_1 = self.unit_1_capacity
self.power_setpoint_2 = self.unit_2_capacity
self.power_setpoint_3 = self.unit_3_capacity
elif current_time < 180 * 60: # 180 minutes in seconds
# Between 120 and 180 minutes: reduce power of unit 1 to 50% of rated capacity
self.power_setpoint_1 = 0.5 * self.unit_1_capacity
elif current_time < 210 * 60: # 210 minutes in seconds
# Between 180 and 210 minutes: reduce power of unit 1 to 10% of rated capacity
self.power_setpoint_1 = 0.1 * self.unit_1_capacity
elif current_time < 240 * 60: # 240 minutes in seconds
# Between 210 and 240 minutes: move both units to 50% of rated capacity
self.power_setpoint_1 = 0.5 * self.unit_1_capacity
self.power_setpoint_2 = 0.5 * self.unit_2_capacity
self.power_setpoint_3 = 0.5 * self.unit_3_capacity
else:
# After 240 minutes: shut down
self.power_setpoint_1 = 0.0
self.power_setpoint_2 = 0.0
self.power_setpoint_3 = 0.0

# Update the h_dict with the power setpoints for each unit and return
h_dict["thermal_power_plant"]["power_setpoints"] = [
self.power_setpoint_1,
self.power_setpoint_2,
self.power_setpoint_3,
]

return h_dict


# Runscript
if __name__ == "__main__":
# Initialize the Hercules model
hmodel = HerculesModel("hercules_input.yaml")

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

# Run the simulation
hmodel.run()

hmodel.logger.info("Process completed successfully")
Loading
Loading