Skip to content
2 changes: 2 additions & 0 deletions docs/thermal_component_base.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ All parameters below are defined in the Hercules input YAML file. The base class
### Optional Parameters
| Parameter | Units | Description |
|-----------|-------|-------------|
| `startup_fuel_fraction` | fraction (0-1) | Optional, fuel consumption during startup, as a fraction of rated fuel consumption. Defaults to 0 |
| `shutdown_fuel_fraction` | fraction (0-1) | Optional, fuel consumption during shutdown, as a fraction of rated fuel consumption. Defaults to 0 |
| `initial_conditions.time_in_shutdown` | s | An optional parameter to set the `time_in_state` variable if the component is OFF. This can be used to adjust the behavior of a component, e.g. force a warm start at the beginning of the simulation |

### Derived Parameters
Expand Down
6 changes: 5 additions & 1 deletion examples/07_open_cycle_gas_turbine/hercules_input.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: example_07
# Describe this simulation setup
description: Open Cycle Gas Turbine (OCGT) Example

dt: 60.0 # 1 minute time step
dt: 10.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
Expand Down Expand Up @@ -38,6 +38,10 @@ open_cycle_gas_turbine:
# 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]
# Optional startup/shutdown fuel fractions (as fractions of rated fuel flow)
# Approximated using case SC1A described in Exhibits 3-70, 3-71, and 3-173 of [5]
startup_fuel_fraction: 0.35 # 35% of rated fuel flow for startup
shutdown_fuel_fraction: 0.30 # 30% of rated fuel flow for shutdown
efficiency_table:
power_fraction:
- 1.0
Expand Down
59 changes: 48 additions & 11 deletions hercules/plant_components/thermal_component_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ def __init__(self, h_dict, component_name):
self.min_up_time = component_dict["min_up_time"] # s
self.min_down_time = component_dict["min_down_time"] # s

# Extract optional parameters for startup and shutdown fuel fractions
self.startup_fuel_fraction = component_dict.get("startup_fuel_fraction", None)
self.shutdown_fuel_fraction = component_dict.get("shutdown_fuel_fraction", None)

# Check all required parameters are numbers
if not isinstance(self.rated_capacity, (int, float, hercules_float_type)):
raise ValueError("rated_capacity must be a number")
Expand Down Expand Up @@ -593,7 +597,23 @@ def _apply_on_constraints(self, power_setpoint):
return P_constrained

def calculate_efficiency(self, power_output):
"""Calculate HHV net efficiency based on current power output.
"""Calculate HHV net efficiency based on current power output and state.

Args:
power_output (float): Current power output in kW.

Returns:
float: HHV net efficiency as a fraction (0-1).
"""
fuel_consumption_rate = self.calculate_fuel_volume_rate(power_output) # m³/s

if fuel_consumption_rate == 0:
return np.nan # Efficiency is undefined when fuel consumption is zero

return (power_output * 1000.0) / (fuel_consumption_rate * self.hhv)

def interpolate_efficiency(self, power_output):
"""Interpolate HHV net efficiency based on current power output.

Uses linear interpolation from the efficiency table. Values outside the
table range are clamped to the nearest endpoint.
Expand All @@ -604,10 +624,6 @@ def calculate_efficiency(self, power_output):
Returns:
float: HHV net efficiency as a fraction (0-1).
"""
if power_output <= 0:
# Return efficiency at lowest power fraction when off
return self.efficiency_values[0]

# Calculate power fraction
power_fraction = power_output / self.rated_capacity

Expand All @@ -627,15 +643,36 @@ def calculate_fuel_volume_rate(self, power_output):
Returns:
float: Fuel volume flow rate in m³/s.
"""
if power_output <= 0:
rated_fuel_consumption_rate = (self.rated_capacity * 1000.0) / (
self.hhv * self.interpolate_efficiency(self.rated_capacity)
) # m³/s at rated capacity

if self.state == self.STATES.OFF:
# When off, fuel flow is zero
return 0.0
elif self.state == self.STATES.STOPPING and self.shutdown_fuel_fraction is not None:
# When stopping, use shutdown fuel fraction if provided
return max(
self.shutdown_fuel_fraction * rated_fuel_consumption_rate,
power_output * 1000.0 / (self.hhv * self.interpolate_efficiency(power_output)),
)

# Calculate current HHV net efficiency
efficiency = self.calculate_efficiency(power_output)
elif (
self.state
in [
self.STATES.HOT_STARTING,
self.STATES.WARM_STARTING,
self.STATES.COLD_STARTING,
]
and self.startup_fuel_fraction is not None
):
# During startup (HOT_STARTING, WARM_STARTING, COLD_STARTING), use startup fuel fraction
return self.startup_fuel_fraction * rated_fuel_consumption_rate

# When on, calculate fuel rate based on current HHV net efficiency
efficiency = self.interpolate_efficiency(power_output)

# Calculate fuel volume rate using HHV net efficiency
# fuel_volume_rate (m³/s) = power (W) / (efficiency * hhv (J/m³))
# Convert power from kW to W (multiply by 1000)
fuel_m3_per_s = (power_output * 1000.0) / (efficiency * self.hhv)

return fuel_m3_per_s
return (power_output * 1000.0) / (efficiency * self.hhv)
3 changes: 2 additions & 1 deletion tests/thermal_component_base_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy

import numpy as np
import pytest
from hercules.plant_components.thermal_component_base import ThermalComponentBase

Expand Down Expand Up @@ -435,7 +436,7 @@ def test_efficiency_clamping():

# Test at zero power (should return first efficiency value)
eff_0 = tcb.calculate_efficiency(0)
assert eff_0 == pytest.approx(0.30)
assert np.isnan(eff_0)


def test_efficiency_interpolation():
Expand Down
Loading