Skip to content

Feature/add ct#189

Open
paulf81 wants to merge 44 commits intoNatLabRockies:developfrom
paulf81:feature/add_ct
Open

Feature/add ct#189
paulf81 wants to merge 44 commits intoNatLabRockies:developfrom
paulf81:feature/add_ct

Conversation

@paulf81
Copy link
Collaborator

@paulf81 paulf81 commented Dec 18, 2025

This PR adds a base class for thermal components, including both gas and coal plants. The modeling is based largely on 3 papers:

[1] Agora Energiewende (2017): Flexibility in thermal power plants
With a focus on existing coal-fired power plants.
[2] "Impact of Detailed Parameter Modeling of Open-Cycle Gas Turbines on
Production Cost Simulation", NREL/CP-6A40-87554, National Renewable
Energy Laboratory, 2024.
[3] Deane, J.P., G. Drayton, and B.P. Ó Gallachóir. “The Impact of Sub-Hourly
Modelling in Power Systems with Significant Levels of Renewable Generation.”
Applied Energy 113 (January 2014): 152–58.
https://doi.org/10.1016/j.apenergy.2013.07.027.

In this PR, in addition to the base class: ThermalComponentBase a first subclass is added: OpenCycleGasTurbine, implementing an open-cycle (CT, peaker) NG component. The PR includes an example demonstrating OpenCycleGasTurbine, docs for the new code and tests for both.

Still todo:

  • @genevievestarke could you review this model and compare with what is in HOPP/H2I?
  • also @genevievestarke or @misi9170 , I'd like second pair of eyes on the heat-rate and fuel consumption code. Could you review those parts and also maybe add a small first principles test to tests/open_cycle_gas_turbine_test.py (note there is TODO comment at the relavent location in the file)

Copy link
Contributor

Copilot AI left a 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 CombustionTurbineSimple component 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.

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")
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
- state_name: Operating state string ("off", "starting", "on", "stopping")

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

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


# 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)")
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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)"
)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 23 to 26
"""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.
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
"""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.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 118 to 121
if self.state_num not in self.STATE_NAMES:
raise ValueError(
"state_num must be one of the following: " + str(self.STATE_NAMES.values())
)
Copy link

Copilot AI Dec 18, 2025

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.

Suggested change
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 uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

@paulf81
Copy link
Collaborator Author

paulf81 commented Dec 22, 2025

@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!)

float: Fuel consumed this timestep in kJ.
"""

# TODO: Is this correct? Should we be getting to m^3 of natural gas?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@genevievestarke check this one?

@paulf81 paulf81 marked this pull request as ready for review February 2, 2026 16:49
@paulf81
Copy link
Collaborator Author

paulf81 commented Feb 2, 2026

ok @genevievestarke and @misi9170 , I now wrote a set of tests and updated the docs so this one should be ready for review.

@paulf81
Copy link
Collaborator Author

paulf81 commented Feb 4, 2026

ok @genevievestarke and @misi9170 , this PR is now fully reworked following our discussion. It most closely follows the model descriptions in:

[1] Agora Energiewende (2017): Flexibility in thermal power plants
With a focus on existing coal-fired power plants.

I redid the top comment box on this PR and added 2 todo items if you don't mind to have a look, or can just talk with me about it.

@brookeslawski I added you as a reviewer in case you have a few minutes would value your feedback, but understand if you don't!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +333 to +342
# Ramp up using run_up_rate
startup_power = (self.time_in_state - self.hot_readying_time) * self.run_up_rate

# Check if ramping is complete
if startup_power >= self.P_min:
self.state_num = self.STATE_ON
self.time_in_state = 0.0
return startup_power

return startup_power
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When startup ramping reaches/exceeds P_min, _control transitions to STATE_ON but returns startup_power without any cap. With larger dt or high run_up_rate, this can exceed rated_capacity and propagate unrealistic power (and downstream fuel burn). Clamp the returned power at least to [0, P_max] (and typically return exactly P_min on the transition).

Copilot uses AI. Check for mistakes.
# Store the type of this component
self.component_type = "OpenCycleGasTurbine"

# Apply fixeddefault parameters based on [1], [2] and [3]
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: “fixeddefault” should be “fixed default”.

Suggested change
# Apply fixeddefault parameters based on [1], [2] and [3]
# Apply fixed default parameters based on [1], [2] and [3]

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +78
# Test cold_startup_time must be a number greater than the ramp_time
# determined by the ramp_rate_fraction
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says the cold startup constraint is “determined by the ramp_rate_fraction”, but the implementation enforces cold_startup_time >= hot_startup_time (and ramp time is based on run_up_rate_fraction). Consider updating the comment to reflect the actual validation being exercised.

Suggested change
# Test cold_startup_time must be a number greater than the ramp_time
# determined by the ramp_rate_fraction
# Test cold_startup_time must be a number greater than or equal to the
# hot_startup_time (which in this setup equals the ramp_time determined
# by the run_up_rate_fraction)

Copilot uses AI. Check for mistakes.
# Transition to shutdown sequence
self.state_num = self.STATE_STOPPING
self.time_in_state = 0.0

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the STATE_ON branch, you set state_num = STATE_STOPPING but still return _apply_on_constraints(...) (ON-state logic). This means the first timestep of STOPPING still enforces P_min/ON constraints, which can delay or distort shutdown behavior (especially for large dt). After transitioning to STATE_STOPPING, return stopping-state ramp-down power instead (or early-return and let the STOPPING branch handle output).

Suggested change
# Immediately apply stopping-state ramp-down behavior
shutdown_power = self.power_output - self.ramp_rate * self.dt
# Check if shutdown is complete in this timestep
if shutdown_power <= 0:
self.state_num = self.STATE_OFF
self.time_in_state = 0.0
return 0.0
return shutdown_power

Copilot uses AI. Check for mistakes.


def test_init_from_dict():
"""Test that ThermalComponentBase can be initialized from a dictionary."""
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test docstring references ThermalComponentBase, but this test initializes OpenCycleGasTurbine. Update the docstring to match what’s being tested.

Suggested change
"""Test that ThermalComponentBase can be initialized from a dictionary."""
"""Test that OpenCycleGasTurbine can be initialized from a dictionary."""

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +93
# TODO: Someone familiar with heat rate and fuel consumption please add tests based
# on first principles for the heat rate and fuel consumption.
def test_post_process():
"""Test that OpenCycleGasTurbine post-processes correctly."""
h_dict = copy.deepcopy(h_dict_open_cycle_gas_turbine)
ocgt = OpenCycleGasTurbine(h_dict)
h_dict = ocgt._post_process(h_dict)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_post_process doesn’t assert anything about the post-processed outputs, so it will pass even if _post_process is broken. Add assertions that fuel_consumption and heat_rate keys are written to h_dict and that values match expected first-principles calculations for a known power_output/dt (including an off/zero-power case).

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,84 @@
# Plot the outputs of the simulation for the NGCT example
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header comment references an “NGCT example”, but this example is for an open-cycle gas turbine (OCGT). Consider renaming references (and possibly ControllerNGCT) to avoid confusion.

Suggested change
# Plot the outputs of the simulation for the NGCT example
# Plot the outputs of the simulation for the open-cycle gas turbine (OCGT) example

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines +19 to +20
h_dict = copy.deepcopy(h_dict_thermal_component)

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to 'h_dict' is unnecessary as it is redefined before this value is used.

Suggested change
h_dict = copy.deepcopy(h_dict_thermal_component)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant