diff --git a/docs/thermal_component_base.md b/docs/thermal_component_base.md index a580c0b4..0bd5c396 100644 --- a/docs/thermal_component_base.md +++ b/docs/thermal_component_base.md @@ -86,6 +86,10 @@ All parameters below are defined in the Hercules input YAML file. The base class | `fuel_density` | kg/m³ | Fuel density for mass calculations | | `efficiency_table` | dict | Dictionary containing `power_fraction` and `efficiency` arrays (see below). Efficiency values must be HHV net plant efficiencies. | +### Optional Parameters +| `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 | + ### Optional Parameters | Parameter | Units | Description | |-----------|-------|-------------| diff --git a/examples/07_open_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input.yaml index 616ae5af..f90451b6 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input.yaml @@ -32,6 +32,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 diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index bf474651..eb9a7be8 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -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") @@ -573,7 +577,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. @@ -584,10 +604,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 @@ -607,15 +623,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) diff --git a/tests/thermal_component_base_test.py b/tests/thermal_component_base_test.py index 1ff3d180..0bc94690 100644 --- a/tests/thermal_component_base_test.py +++ b/tests/thermal_component_base_test.py @@ -1,5 +1,6 @@ import copy +import numpy as np import pytest from hercules.plant_components.thermal_component_base import ThermalComponentBase @@ -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():