diff --git a/docs/thermal_component_base.md b/docs/thermal_component_base.md index a580c0b4..d31f15ac 100644 --- a/docs/thermal_component_base.md +++ b/docs/thermal_component_base.md @@ -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 diff --git a/examples/07_open_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input.yaml index 5700ca37..92e8a913 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input.yaml @@ -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 @@ -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 diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 0262604b..37d9ff25 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") @@ -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. @@ -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 @@ -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) 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():