-
Notifications
You must be signed in to change notification settings - Fork 21
Multiunit thermal plants #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
genevievestarke
merged 37 commits into
NatLabRockies:develop
from
misi9170:feature/mm-thermal
Mar 30, 2026
Merged
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 e34ebef
Update toml
dzalkind a16621d
Merge remote-tracking branch 'upstream/develop' into pysam_7
dzalkind bb51e82
First pass run through for multiunit thermal plant
misi9170 71f3c90
Merge branch 'pysam_7' into feature/mm-thermal
dzalkind 0176b6f
Add logging for individual units within thermal plant
misi9170 f50a34a
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind 5a71cc2
More formatting
misi9170 0feae7b
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind 84cf038
Add steam turbine module
c9b8940
Improvement on loading units into thermal plant
6f9991d
Bug fix for when there are multiple of the same units in a plant
65c3955
Track number of starts
dzalkind b37678c
Merge remote-tracking branch 'misi/feature/mm-thermal' into feature/m…
dzalkind b458dbc
Make array of units flexible, hacky
dzalkind 9f1d377
Revert "Merge branch 'pysam_7' into feature/mm-thermal"
dzalkind 7fe5752
Track starts in h_dict for outputs
dzalkind 8156bb0
Merge develop
misi9170 ee48564
Formatting
misi9170 056695d
Clean up dostrings
misi9170 aaefd88
Add note that initial conditions are correctly set on h_dict
misi9170 390cfa9
Docs page for thermal plant
misi9170 d3ea4c1
Check that units are all thermal
misi9170 493e661
Add tests for ThermalPlant
misi9170 810a7b6
Remove SteamTurbine from this branch
misi9170 4f7b72c
Remove steam turbine example
misi9170 b1c7905
Clean up example
misi9170 a4fe752
Add example doc
misi9170 4c94aef
Minor cleanup
misi9170 275a9b8
Clean up comment
misi9170 5a204c0
X axis label
misi9170 031b610
Update Example 09 folder and documentation
genevievestarke e543379
Update thermal plant docs page
genevievestarke 53151ce
Final updates for PR
genevievestarke 2511fba
Remove unused import
genevievestarke e73740f
Add new file
genevievestarke c6859ef
Update utilities
genevievestarke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Example 09: Multi-unit Thermal Plant | ||
|
|
||
| ## 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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: | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.