-
Notifications
You must be signed in to change notification settings - Fork 19
Feature/add ct #189
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
base: develop
Are you sure you want to change the base?
Feature/add ct #189
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a new natural gas combustion turbine model (CombustionTurbineSimple) to Hercules with basic state management, ramp rate constraints, minimum stable load enforcement, and fuel consumption tracking. The implementation includes integration with the plant component system, a basic test, and a comprehensive example demonstrating turbine operation through various power setpoints and state transitions.
Key Changes:
- Adds
CombustionTurbineSimplecomponent with 4-state machine (off, starting, on, stopping) and configurable operational constraints - Integrates combustion turbine as a generator component in the utilities and hybrid plant infrastructure
- Provides Example 07 demonstrating a 6-hour simulation with complex control schedule
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
hercules/plant_components/combustion_turbine_simple.py |
New combustion turbine model with state machine, ramp constraints, fuel consumption, and part-load efficiency calculations |
hercules/hybrid_plant.py |
Adds component instantiation logic for CombustionTurbineSimple |
hercules/utilities.py |
Registers combustion_turbine as available component and generator type |
tests/combustion_turbine_simple_test.py |
Basic initialization test (requires missing test fixture to run) |
examples/07_ngct/hercules_input.yaml |
Configuration for 100 MW turbine with 20% minimum load |
examples/07_ngct/hercules_runscript.py |
Controller implementing multi-stage power schedule |
examples/07_ngct/plot_outputs.py |
Visualization script for power, state, and fuel consumption |
examples/07_ngct/README.md |
Documentation of example configuration and control schedule |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.time_in_state = 0.0 | ||
| return 0.0 | ||
|
|
||
| return shutdown_power |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _control method uses if-elif statements for all state checks but doesn't have a final else clause or explicit return at the end of the function. If self.state_num has an invalid value (though validated in init), the function will implicitly return None. For robustness and clarity, add an else clause that raises an exception for unexpected state values.
| return shutdown_power | |
| return shutdown_power | |
| else: | |
| raise ValueError(f"Unexpected state_num in _control: {self.state_num}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
| if self.rated_capacity <= 0: | ||
| raise ValueError("rated_capacity must be greater than 0") | ||
| if self.min_stable_load_fraction < 0 or self.min_stable_load_fraction > 1: | ||
| raise ValueError("min_stable_load_fraction must be between 0 and 1 (inclusive)") |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message references 'min_stable_load_fraction' but the parameter in the h_dict and documentation is called 'min_stable_load'. This inconsistency between the internal variable name and the user-facing parameter name could confuse users. Consider using 'min_stable_load' in the error message to match the documented parameter name.
| raise ValueError("min_stable_load_fraction must be between 0 and 1 (inclusive)") | |
| raise ValueError("min_stable_load must be between 0 and 1 (inclusive)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think min_stable_load_fraction is more informative, moving everything to that
| dict: Updated h_dict with combustion turbine outputs: | ||
| - power: Actual power output [kW] | ||
| - state_num: Operating state number (0=off, 1=starting, 2=on, 3=stopping) | ||
| - state_name: Operating state string ("off", "starting", "on", "stopping") |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The step method documentation lists 'state_name' as an output (line 189), but the actual code has this output commented out (line 208). This creates a discrepancy between documentation and implementation. Either uncomment the output if it should be included, or remove it from the documentation if it's intentionally excluded.
| - state_name: Operating state string ("off", "starting", "on", "stopping") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This output was removed because it created problems to have a string in the h_dict output, (and anyway the info is in statenum). Removing from docstring
| import copy | ||
|
|
||
| from hercules.plant_components.combustion_turbine_simple import CombustionTurbineSimple | ||
|
|
||
| from .test_inputs.h_dict import ( | ||
| h_dict_combustion_turbine, | ||
| ) | ||
|
|
||
|
|
||
| def test_init_from_dict(): | ||
| """Test that CombustionTurbineSimple can be initialized from a dictionary.""" | ||
| ct = CombustionTurbineSimple(copy.deepcopy(h_dict_combustion_turbine)) | ||
| assert ct is not None |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test suite lacks coverage for core combustion turbine functionality. Consider adding tests for: 1) state transitions (OFF→STARTING→ON→STOPPING→OFF), 2) minimum stable load enforcement, 3) ramp rate constraints, 4) min_up_time and min_down_time constraints, 5) fuel consumption calculations, 6) part load efficiency degradation, and 7) startup/shutdown time behavior. These are critical features that should have test coverage given that other components in the codebase have comprehensive tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is in TODO
|
|
||
| # Check that initial conditions are valid | ||
| if self.power_output < 0 or self.power_output > self.rated_capacity: | ||
| raise ValueError("power_output must be between 0 and rated_capacity (inclusive)") |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message references 'power_output' but this is not a user-provided parameter - it comes from initial_conditions["power"]. The error message should reference 'initial power' or 'initial_conditions.power' to help users identify which configuration value is invalid.
| raise ValueError("power_output must be between 0 and rated_capacity (inclusive)") | |
| raise ValueError( | |
| "initial_conditions['power'] (initial power) must be between 0 and rated_capacity (inclusive)" | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| raise ValueError( | ||
| "state_num must be one of the following: " + str(self.STATE_NAMES.values()) | ||
| ) |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message on line 120 calls str(self.STATE_NAMES.values()) which will produce an unhelpful output like "dict_values(['off', 'starting', 'on', 'stopping'])". Consider using a clearer format such as "state_num must be 0 (off), 1 (starting), 2 (on), or 3 (stopping)" or listing the valid integer values explicitly.
| raise ValueError( | |
| "state_num must be one of the following: " + str(self.STATE_NAMES.values()) | |
| ) | |
| valid_states = ", ".join( | |
| f"{state} ({name})" for state, name in self.STATE_NAMES.items() | |
| ) | |
| raise ValueError(f"state_num must be one of the following: {valid_states}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion, adopting it
| """Simple controller for combustion turbine on/off scheduling. | ||
|
|
||
| This controller turns the combustion turbine on at 60 minutes, | ||
| runs at full power for 3 hours, then shuts down at 240 minutes. |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class docstring description is inconsistent with the actual controller behavior. The docstring says "runs at full power for 3 hours" but the implementation has a more complex schedule with multiple power level changes (100% → 50% → 10% → 100% → off) as documented in lines 5-9 of the module docstring. Consider updating the class docstring to accurately reflect the control logic or simply reference the module docstring.
| """Simple controller for combustion turbine on/off scheduling. | |
| This controller turns the combustion turbine on at 60 minutes, | |
| runs at full power for 3 hours, then shuts down at 240 minutes. | |
| """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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| from .test_inputs.h_dict import ( | ||
| h_dict_combustion_turbine, | ||
| ) | ||
|
|
||
|
|
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The imported h_dict_combustion_turbine is not defined in the tests/test_inputs/h_dict.py file. This will cause an ImportError when the test is run. You need to add a combustion turbine h_dict fixture to tests/test_inputs/h_dict.py following the pattern of other components like h_dict_electrolyzer or h_dict_battery.
| from .test_inputs.h_dict import ( | |
| h_dict_combustion_turbine, | |
| ) | |
| # Local combustion turbine h_dict fixture used for initializing CombustionTurbineSimple | |
| h_dict_combustion_turbine = { | |
| "name": "simple_combustion_turbine", | |
| } |
| def test_init_from_dict(): | ||
| """Test that CombustionTurbineSimple can be initialized from a dictionary.""" | ||
| ct = CombustionTurbineSimple(copy.deepcopy(h_dict_combustion_turbine)) | ||
| assert ct is not None |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only verifies initialization succeeds but doesn't test any actual behavior or state of the combustion turbine. Consider adding assertions to verify key properties like rated_capacity, min_stable_load_fraction, state_num, and power_output match the expected values from the h_dict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a TODO item
| if self.state_num not in self.STATE_NAMES: | ||
| raise ValueError( | ||
| "state_num must be one of the following: " + str(self.STATE_NAMES.values()) | ||
| ) |
Copilot
AI
Dec 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state_num validation is redundant. Lines 116-117 already check if state_num is between 0 and 3 (inclusive), which is identical to checking if it's in STATE_NAMES (which contains keys 0, 1, 2, 3). The check on line 118 will always pass if line 116 passes. Consider removing lines 118-121 to eliminate redundancy.
| if self.state_num not in self.STATE_NAMES: | |
| raise ValueError( | |
| "state_num must be one of the following: " + str(self.STATE_NAMES.values()) | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
|
@misi9170 and @genevievestarke , update today is I merged back in develop to this PR and then went through the review comments from co-pilot (most of which were helpful!) |
This PR adds a first NG Combustion Turbine model:
CombustionTurbineSimpleto Hercules. I'll update this PR as we go but the model is in now and a first example but still to go are:Note, some of the parameterization (minimum stable load, ramp rates, 1-hour start-up/shut down adapted a bit from: https://docs.nrel.gov/docs/fy24osti/87554.pdf, but only loosely, thinking mainly of Figure 2 and Table 1 and only taking the more physical parameters and not things like repair times etc.