Skip to content

Commit a54f793

Browse files
Merge branch 'develop' of https://github.com/NatLabRockies/hercules into develop
2 parents c97acc8 + 3abe70d commit a54f793

16 files changed

Lines changed: 759 additions & 31 deletions

docs/adding_components.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ While only generator power is included in `locally_generated_power`, all categor
9797

9898
## Step 2: Register the Component
9999

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

102102

103103
The key string (e.g., `"MyComponent"`) is the `component_type` value users will specify in their YAML input files.
@@ -142,9 +142,8 @@ pytest tests/my_component_test.py -v
142142
- [ ] Inherit from `ComponentBase`
143143
- [ ] Define `component_category` class attribute
144144
- [ ] Implement `__init__`, `step`, `get_initial_conditions_and_meta_data`
145-
- [ ] Import and add to `COMPONENT_REGISTRY` in `hercules/hybrid_plant.py`
145+
- [ ] Import and add to `COMPONENT_REGISTRY` in `hercules/component_registry.py`
146146
- [ ] Create tests in `tests/my_component_test.py`
147147
- [ ] Create `docs/my_component.md`
148148
- [ ] Add to `docs/_toc.yml`
149149
- [ ] Update reference tables in `hybrid_plant.md` and `component_types.md`
150-
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Example 09: Multi-unit Thermal Plant
2+
3+
## Description
4+
5+
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).
6+
7+
## Running
8+
9+
To run the example, execute the following command in the terminal:
10+
11+
```bash
12+
python hercules_runscript.py
13+
```
14+
15+
## Outputs
16+
17+
To plot the outputs, run:
18+
19+
```bash
20+
python plot_outputs.py
21+
```
22+
23+
The plot shows (for the all units separately):
24+
- 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
25+
- Operating state transitions
26+
- Fuel consumption tracking
27+
- Heat rate variation with load

docs/hybrid_plant.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The YAML key for each section is a user-chosen `component_name` and is not requi
2828

2929
## Component Registry
3030

31-
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:
31+
All available component types are defined in `COMPONENT_REGISTRY` in `hercules/component_registry.py`. This dictionary maps `component_type` strings to their Python classes:
3232

3333
```python
3434
COMPONENT_REGISTRY = {

docs/thermal_plant.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Thermal Plant
2+
3+
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.
4+
5+
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.
6+
7+
For details on the state machine, startup/shutdown behavior, and base parameters, see {doc}`thermal_component_base`.
8+
9+
## Parameters
10+
11+
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.
12+
13+
See the [YAML Configuration](#yaml-configuration) section below for examples of how to specify these parameters in the input file.
14+
15+
## YAML configuration
16+
17+
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.
18+
19+
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`.
20+
21+
```yaml
22+
my_thermal_plant:
23+
component_type: ThermalPlant
24+
units: ["large_ocgt", "large_ocgt", "small_ocgt"]
25+
unit_names: ["OCGT1", "OCGT2", "OCGT3"]
26+
27+
large_ocgt:
28+
component_type: OpenCycleGasTurbine
29+
rated_capacity: 100000 # kW (100 MW)
30+
min_stable_load_fraction: 0.4 # 40% minimum operating point
31+
ramp_rate_fraction: 0.1 # 10%/min ramp rate
32+
run_up_rate_fraction: 0.05 # 5%/min run up rate
33+
hot_startup_time: 420.0 # 7 minutes
34+
warm_startup_time: 480.0 # 8 minutes
35+
cold_startup_time: 480.0 # 8 minutes
36+
min_up_time: 1800 # 30 minutes
37+
min_down_time: 3600 # 1 hour
38+
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³)
39+
fuel_density: 0.768 # kg/m³ for natural gas
40+
efficiency_table:
41+
power_fraction:
42+
- 1.0
43+
- 0.75
44+
- 0.50
45+
- 0.25
46+
efficiency:
47+
- 0.39
48+
- 0.37
49+
- 0.325
50+
- 0.245
51+
log_channels:
52+
- power
53+
- fuel_volume_rate
54+
- fuel_mass_rate
55+
- state
56+
- efficiency
57+
- power_setpoint
58+
initial_conditions:
59+
power: 0
60+
61+
small_ocgt:
62+
component_type: OpenCycleGasTurbine
63+
rated_capacity: 50000 # kW (50 MW)
64+
min_stable_load_fraction: 0.4 # 40% minimum operating point
65+
ramp_rate_fraction: 0.15 # 15%/min ramp rate
66+
run_up_rate_fraction: 0.1 # 10%/min run up rate
67+
hot_startup_time: 300.0 # 5 minutes
68+
warm_startup_time: 360.0 # 6 minutes
69+
cold_startup_time: 420.0 # 7 minutes
70+
min_up_time: 1200 # 20 minutes
71+
min_down_time: 2400 # 40 minutes
72+
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6]
73+
fuel_density: 0.768 # kg/m
74+
efficiency_table:
75+
power_fraction:
76+
- 1.0
77+
- 0.75
78+
- 0.50
79+
- 0.25
80+
efficiency:
81+
- 0.38
82+
- 0.36
83+
- 0.32
84+
- 0.22
85+
log_channels:
86+
- power
87+
initial_conditions:
88+
power: 0
89+
```
90+
91+
## Logging configuration
92+
93+
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.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Input YAML for hercules
2+
# Explicitly specify the parameters for demonstration purposes
3+
4+
# Name
5+
name: example_07
6+
7+
###
8+
# Describe this simulation setup
9+
description: Open Cycle Gas Turbine (OCGT) Example
10+
11+
dt: 60.0 # 1 minute time step
12+
starttime_utc: "2020-01-01T00:00:00Z" # Midnight Jan 1, 2020 UTC
13+
endtime_utc: "2020-01-01T06:00:00Z" # 6 hours later
14+
verbose: False
15+
log_every_n: 1
16+
17+
plant:
18+
interconnect_limit: 100000 # kW (100 MW)
19+
20+
thermal_power_plant:
21+
component_type: ThermalPlant
22+
units: ["open_cycle_gas_turbine1", "open_cycle_gas_turbine1", "open_cycle_gas_turbine3"]
23+
unit_names: ["OCGT1", "OCGT2", "OCGT3"]
24+
25+
open_cycle_gas_turbine1:
26+
component_type: OpenCycleGasTurbine
27+
rated_capacity: 100000 # kW (100 MW)
28+
min_stable_load_fraction: 0.2 # 20% minimum operating point
29+
ramp_rate_fraction: 0.1 # 10%/min ramp rate
30+
run_up_rate_fraction: 0.05 # 5%/min run up rate
31+
hot_startup_time: 420.0 # 7 minutes
32+
warm_startup_time: 480.0 # 8 minutes
33+
cold_startup_time: 480.0 # 8 minutes
34+
min_up_time: 3600 # 1 hour
35+
min_down_time: 3600 # 1 hour
36+
# Natural gas properties from [6] Staffell, "The Energy and Fuel Data Sheet", 2011
37+
# HHV: 39.05 MJ/m³, Density: 0.768 kg/m³
38+
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6]
39+
fuel_density: 0.768 # kg/m³ for natural gas [6]
40+
efficiency_table:
41+
power_fraction:
42+
- 1.0
43+
- 0.75
44+
- 0.50
45+
- 0.25
46+
efficiency: # HHV net plant efficiency, fractions (0-1), from SC1A in Exhibit ES-4 of [5]
47+
- 0.39
48+
- 0.37
49+
- 0.325
50+
- 0.245
51+
log_channels:
52+
- power
53+
- fuel_volume_rate
54+
- fuel_mass_rate
55+
- state
56+
- efficiency
57+
- power_setpoint
58+
initial_conditions:
59+
power: 100000 # Start ON at rated capacity (100 MW)
60+
61+
open_cycle_gas_turbine3:
62+
component_type: OpenCycleGasTurbine
63+
rated_capacity: 100000 # kW (100 MW)
64+
min_stable_load_fraction: 0.2 # 20% minimum operating point
65+
ramp_rate_fraction: 0.05 # 10%/min ramp rate
66+
run_up_rate_fraction: 0.05 # 5%/min run up rate
67+
hot_startup_time: 420.0 # 7 minutes
68+
warm_startup_time: 480.0 # 8 minutes
69+
cold_startup_time: 480.0 # 8 minutes
70+
min_up_time: 3600 # 1 hour
71+
min_down_time: 3600 # 1 hour
72+
# Natural gas properties from [6] Staffell, "The Energy and Fuel Data Sheet", 2011
73+
# HHV: 39.05 MJ/m³, Density: 0.768 kg/m³
74+
hhv: 39050000 # J/m³ for natural gas (39.05 MJ/m³) [6]
75+
fuel_density: 0.768 # kg/m³ for natural gas [6]
76+
efficiency_table:
77+
power_fraction:
78+
- 1.0
79+
- 0.75
80+
- 0.50
81+
- 0.25
82+
efficiency: # HHV net plant efficiency, fractions (0-1), from SC1A in Exhibit ES-4 of [5]
83+
- 0.39
84+
- 0.37
85+
- 0.325
86+
- 0.245
87+
log_channels:
88+
- power
89+
- fuel_volume_rate
90+
- fuel_mass_rate
91+
- state
92+
- efficiency
93+
- power_setpoint
94+
initial_conditions:
95+
power: 100000 # Start ON at rated capacity (100 MW)
96+
97+
controller:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Example 09: Multiunit Thermal Plant
2+
3+
This example demonstrates a thermal power plant constructed from two 50 MW OCGT units.
4+
The power setpoints are split unequally between the two units to demonstrate the ability of the
5+
model to specify setpoints of individual units.
6+
"""
7+
8+
from hercules.hercules_model import HerculesModel
9+
from hercules.utilities_examples import prepare_output_directory
10+
11+
prepare_output_directory()
12+
13+
14+
# Declare the open loop control setpoint sequence used for demonstration.
15+
class OpenLoopController:
16+
"""Controller implementing the unit power setpoints in open loop."""
17+
18+
def __init__(self, h_dict):
19+
# Access total rated capacity from h_dict, as well as capacities of individual units
20+
self.rated_capacity = h_dict["thermal_power_plant"]["rated_capacity"]
21+
self.unit_1_capacity = h_dict["thermal_power_plant"]["OCGT1"]["rated_capacity"]
22+
self.unit_2_capacity = h_dict["thermal_power_plant"]["OCGT2"]["rated_capacity"]
23+
self.unit_3_capacity = h_dict["thermal_power_plant"]["OCGT3"]["rated_capacity"]
24+
25+
def step(self, h_dict):
26+
current_time = h_dict["time"]
27+
28+
# Determine power setpoint based on time
29+
if current_time < 10 * 60: # 10 minutes in seconds
30+
# Before 10 minutes: run all three units at full capacity
31+
self.power_setpoint_1 = self.unit_1_capacity
32+
self.power_setpoint_2 = self.unit_2_capacity
33+
self.power_setpoint_3 = self.unit_3_capacity
34+
elif current_time < 20 * 60: # 20 minutes in seconds
35+
# Between 10 and 20 minutes: shut down unit 1, leave units 2 & 3
36+
self.power_setpoint_1 = 0.0
37+
elif current_time < 40 * 60: # 40 minutes in seconds
38+
# Shut down units 2 & 3
39+
self.power_setpoint_2 = 0.0
40+
self.power_setpoint_3 = 0.0
41+
elif current_time < 120 * 60: # 120 minutes in seconds
42+
# Between 40 and 120 minutes: signal to run at full capacity
43+
self.power_setpoint_1 = self.unit_1_capacity
44+
self.power_setpoint_2 = self.unit_2_capacity
45+
self.power_setpoint_3 = self.unit_3_capacity
46+
elif current_time < 180 * 60: # 180 minutes in seconds
47+
# Between 120 and 180 minutes: reduce power of unit 1 to 50% of rated capacity
48+
self.power_setpoint_1 = 0.5 * self.unit_1_capacity
49+
elif current_time < 210 * 60: # 210 minutes in seconds
50+
# Between 180 and 210 minutes: reduce power of unit 1 to 10% of rated capacity
51+
self.power_setpoint_1 = 0.1 * self.unit_1_capacity
52+
elif current_time < 240 * 60: # 240 minutes in seconds
53+
# Between 210 and 240 minutes: move both units to 50% of rated capacity
54+
self.power_setpoint_1 = 0.5 * self.unit_1_capacity
55+
self.power_setpoint_2 = 0.5 * self.unit_2_capacity
56+
self.power_setpoint_3 = 0.5 * self.unit_3_capacity
57+
else:
58+
# After 240 minutes: shut down
59+
self.power_setpoint_1 = 0.0
60+
self.power_setpoint_2 = 0.0
61+
self.power_setpoint_3 = 0.0
62+
63+
# Update the h_dict with the power setpoints for each unit and return
64+
h_dict["thermal_power_plant"]["power_setpoints"] = [
65+
self.power_setpoint_1,
66+
self.power_setpoint_2,
67+
self.power_setpoint_3,
68+
]
69+
70+
return h_dict
71+
72+
73+
# Runscript
74+
if __name__ == "__main__":
75+
# Initialize the Hercules model
76+
hmodel = HerculesModel("hercules_input.yaml")
77+
78+
# Instantiate the controller and assign to the Hercules model
79+
hmodel.assign_controller(OpenLoopController(hmodel.h_dict))
80+
81+
# Run the simulation
82+
hmodel.run()
83+
84+
hmodel.logger.info("Process completed successfully")

0 commit comments

Comments
 (0)