From 2e040a050382b2d75b4322da4d71a903d5389d23 Mon Sep 17 00:00:00 2001 From: Frederik Date: Thu, 19 Mar 2026 16:58:53 -0600 Subject: [PATCH 1/9] First stab at including fuel rates when starting up / shutting down --- .../hercules_input.yaml | 2 ++ .../thermal_component_base.py | 36 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input.yaml index a52ea403..ab0c2cac 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input.yaml @@ -32,6 +32,8 @@ 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] + startup_fuel_fraction: 0.6 # 60% of rated fuel flow for startup + shutdown_fuel_fraction: 0.04 # 4% 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 aa7ab2aa..689aad7b 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -607,12 +607,30 @@ def calculate_fuel_volume_rate(self, power_output): if power_output <= 0: return 0.0 - # Calculate current HHV net efficiency - efficiency = self.calculate_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 + if self.state == self.STATES.ON: + # When on, calculate fuel rate based on current HHV net efficiency + efficiency = self.calculate_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) + return (power_output * 1000.0) / (efficiency * self.hhv) + elif self.state == self.STATES.OFF: + # When off, fuel flow is zero + return 0.0 + elif self.state == self.STATES.STOPPING: + if hasattr(self, "shutdown_fuel_fraction"): + # When stopping, use shutdown fuel fraction if provided + return self.shutdown_fuel_fraction * ( + self.rated_capacity * 1000.0 / (self.hhv) * self.calculate_efficiency(self.rated_capacity) + ) + else: + return 0.0 + else: + # During startup (HOT_STARTING, WARM_STARTING, COLD_STARTING), use startup fuel fraction + if hasattr(self, "startup_fuel_fraction"): + return self.startup_fuel_fraction * ( + self.rated_capacity * 1000.0 / (self.hhv) * self.calculate_efficiency(self.rated_capacity) + ) + else: + return 0.0 \ No newline at end of file From dafbfdd4245ce2c203a130360741b397102f1510 Mon Sep 17 00:00:00 2001 From: Frederik Date: Fri, 20 Mar 2026 15:47:37 -0600 Subject: [PATCH 2/9] Fully implemented startup and shutdown fuel rate, updated OCGT example --- .../hercules_input.yaml | 6 ++-- .../07_open_cycle_gas_turbine/plot_outputs.py | 2 +- .../thermal_component_base.py | 28 +++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/hercules_input.yaml b/examples/07_open_cycle_gas_turbine/hercules_input.yaml index ab0c2cac..261687ba 100644 --- a/examples/07_open_cycle_gas_turbine/hercules_input.yaml +++ b/examples/07_open_cycle_gas_turbine/hercules_input.yaml @@ -32,8 +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] - startup_fuel_fraction: 0.6 # 60% of rated fuel flow for startup - shutdown_fuel_fraction: 0.04 # 4% of rated fuel flow for shutdown + # 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/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 292ec11c..35ec8405 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -70,7 +70,7 @@ label="Efficiency", color="g", ) -ax.set_ylabel("Efficiency [%]") +ax.set_ylabel("Efficiency [%]") ax.set_title("Thermal Efficiency") ax.grid(True) diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 689aad7b..25c13f49 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", 0) + self.shutdown_fuel_fraction = component_dict.get("shutdown_fuel_fraction", 0) + # 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") @@ -607,6 +611,10 @@ def calculate_fuel_volume_rate(self, power_output): if power_output <= 0: return 0.0 + rated_fuel_consumption_rate = (self.rated_capacity * 1000.0) / ( + self.hhv * self.calculate_efficiency(self.rated_capacity) + ) # m³/s at rated capacity + if self.state == self.STATES.ON: # When on, calculate fuel rate based on current HHV net efficiency efficiency = self.calculate_efficiency(power_output) @@ -614,23 +622,15 @@ def calculate_fuel_volume_rate(self, 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) - return (power_output * 1000.0) / (efficiency * self.hhv) + # Ensure fuel rate is at least the startup fuel fraction when on + return max( (power_output * 1000.0) / (efficiency * self.hhv), + rated_fuel_consumption_rate * self.startup_fuel_fraction) elif self.state == self.STATES.OFF: # When off, fuel flow is zero return 0.0 elif self.state == self.STATES.STOPPING: - if hasattr(self, "shutdown_fuel_fraction"): - # When stopping, use shutdown fuel fraction if provided - return self.shutdown_fuel_fraction * ( - self.rated_capacity * 1000.0 / (self.hhv) * self.calculate_efficiency(self.rated_capacity) - ) - else: - return 0.0 + # When stopping, use shutdown fuel fraction if provided + return self.shutdown_fuel_fraction * rated_fuel_consumption_rate else: # During startup (HOT_STARTING, WARM_STARTING, COLD_STARTING), use startup fuel fraction - if hasattr(self, "startup_fuel_fraction"): - return self.startup_fuel_fraction * ( - self.rated_capacity * 1000.0 / (self.hhv) * self.calculate_efficiency(self.rated_capacity) - ) - else: - return 0.0 \ No newline at end of file + return self.startup_fuel_fraction * rated_fuel_consumption_rate \ No newline at end of file From e73524f95e7d7dcf73059a7ff684e8dc8354b872 Mon Sep 17 00:00:00 2001 From: Frederik Date: Fri, 20 Mar 2026 15:57:55 -0600 Subject: [PATCH 3/9] Ruff --- examples/07_open_cycle_gas_turbine/plot_outputs.py | 2 +- hercules/plant_components/thermal_component_base.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/07_open_cycle_gas_turbine/plot_outputs.py b/examples/07_open_cycle_gas_turbine/plot_outputs.py index 35ec8405..292ec11c 100644 --- a/examples/07_open_cycle_gas_turbine/plot_outputs.py +++ b/examples/07_open_cycle_gas_turbine/plot_outputs.py @@ -70,7 +70,7 @@ label="Efficiency", color="g", ) -ax.set_ylabel("Efficiency [%]") +ax.set_ylabel("Efficiency [%]") ax.set_title("Thermal Efficiency") ax.grid(True) diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 25c13f49..961be1bf 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -623,8 +623,10 @@ def calculate_fuel_volume_rate(self, power_output): # fuel_volume_rate (m³/s) = power (W) / (efficiency * hhv (J/m³)) # Convert power from kW to W (multiply by 1000) # Ensure fuel rate is at least the startup fuel fraction when on - return max( (power_output * 1000.0) / (efficiency * self.hhv), - rated_fuel_consumption_rate * self.startup_fuel_fraction) + return max( + (power_output * 1000.0) / (efficiency * self.hhv), + rated_fuel_consumption_rate * self.startup_fuel_fraction, + ) elif self.state == self.STATES.OFF: # When off, fuel flow is zero return 0.0 @@ -633,4 +635,4 @@ def calculate_fuel_volume_rate(self, power_output): return self.shutdown_fuel_fraction * rated_fuel_consumption_rate else: # During startup (HOT_STARTING, WARM_STARTING, COLD_STARTING), use startup fuel fraction - return self.startup_fuel_fraction * rated_fuel_consumption_rate \ No newline at end of file + return self.startup_fuel_fraction * rated_fuel_consumption_rate From 85078bc0a1b11dadab0546ff106f2dc36e6d6dec Mon Sep 17 00:00:00 2001 From: Frederik Date: Fri, 20 Mar 2026 16:21:33 -0600 Subject: [PATCH 4/9] Updated docs --- docs/thermal_component_base.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/thermal_component_base.md b/docs/thermal_component_base.md index d93c14e1..836e8bf7 100644 --- a/docs/thermal_component_base.md +++ b/docs/thermal_component_base.md @@ -85,6 +85,8 @@ All parameters below are defined in the Hercules input YAML file. The base class | `hhv` | J/m³ | Higher heating value of fuel | | `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. | +| `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 | ### Derived Parameters From 2e59d1d9a0ac54cc2629e29fcbe68cf70628295a Mon Sep 17 00:00:00 2001 From: Frederik Date: Tue, 24 Mar 2026 14:43:03 -0600 Subject: [PATCH 5/9] Addressing comments by Gen --- docs/thermal_component_base.md | 2 ++ .../thermal_component_base.py | 33 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/thermal_component_base.md b/docs/thermal_component_base.md index 2b75af79..0bd5c396 100644 --- a/docs/thermal_component_base.md +++ b/docs/thermal_component_base.md @@ -85,6 +85,8 @@ All parameters below are defined in the Hercules input YAML file. The base class | `hhv` | J/m³ | Higher heating value of fuel | | `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 | diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index b1fdd5a0..6443c067 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -611,31 +611,30 @@ def calculate_fuel_volume_rate(self, power_output): Returns: float: Fuel volume flow rate in m³/s. """ - if power_output <= 0: - return 0.0 - rated_fuel_consumption_rate = (self.rated_capacity * 1000.0) / ( self.hhv * self.calculate_efficiency(self.rated_capacity) ) # m³/s at rated capacity - if self.state == self.STATES.ON: - # When on, calculate fuel rate based on current HHV net efficiency - efficiency = self.calculate_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) - # Ensure fuel rate is at least the startup fuel fraction when on - return max( - (power_output * 1000.0) / (efficiency * self.hhv), - rated_fuel_consumption_rate * self.startup_fuel_fraction, - ) - elif self.state == self.STATES.OFF: + if self.state == self.STATES.OFF: # When off, fuel flow is zero return 0.0 elif self.state == self.STATES.STOPPING: # When stopping, use shutdown fuel fraction if provided return self.shutdown_fuel_fraction * rated_fuel_consumption_rate - else: + elif self.state in [ + self.STATES.HOT_STARTING, self.STATES.WARM_STARTING, self.STATES.COLD_STARTING + ]: # 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.calculate_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) + # Ensure fuel rate is at least the startup fuel fraction when on + return max( + (power_output * 1000.0) / (efficiency * self.hhv), + rated_fuel_consumption_rate * self.startup_fuel_fraction, + ) From 93df2f78578f987302c6bd83e6f6c375fc210e8f Mon Sep 17 00:00:00 2001 From: Frederik Date: Tue, 24 Mar 2026 14:47:55 -0600 Subject: [PATCH 6/9] Ruff --- hercules/plant_components/thermal_component_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 6443c067..9d3c5a83 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -622,11 +622,13 @@ def calculate_fuel_volume_rate(self, power_output): # When stopping, use shutdown fuel fraction if provided return self.shutdown_fuel_fraction * rated_fuel_consumption_rate elif self.state in [ - self.STATES.HOT_STARTING, self.STATES.WARM_STARTING, self.STATES.COLD_STARTING - ]: + self.STATES.HOT_STARTING, + self.STATES.WARM_STARTING, + self.STATES.COLD_STARTING, + ]: # 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.calculate_efficiency(power_output) From 06d62751c11039d1b4bf4426cc3bd69054b0d7a6 Mon Sep 17 00:00:00 2001 From: Frederik Date: Tue, 24 Mar 2026 17:43:13 -0600 Subject: [PATCH 7/9] Addressing latest comments by Gen --- .../thermal_component_base.py | 52 ++++++++++++------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 9d3c5a83..93e3298f 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -125,8 +125,8 @@ def __init__(self, h_dict, component_name): 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", 0) - self.shutdown_fuel_fraction = component_dict.get("shutdown_fuel_fraction", 0) + 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)): @@ -577,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. @@ -588,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 @@ -612,31 +624,31 @@ def calculate_fuel_volume_rate(self, power_output): float: Fuel volume flow rate in m³/s. """ rated_fuel_consumption_rate = (self.rated_capacity * 1000.0) / ( - self.hhv * self.calculate_efficiency(self.rated_capacity) + 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: + elif self.state == self.STATES.STOPPING and self.shutdown_fuel_fraction is not None: # When stopping, use shutdown fuel fraction if provided return self.shutdown_fuel_fraction * rated_fuel_consumption_rate - elif self.state in [ - self.STATES.HOT_STARTING, - self.STATES.WARM_STARTING, - self.STATES.COLD_STARTING, - ]: + 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.calculate_efficiency(power_output) + 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) - # Ensure fuel rate is at least the startup fuel fraction when on - return max( - (power_output * 1000.0) / (efficiency * self.hhv), - rated_fuel_consumption_rate * self.startup_fuel_fraction, - ) + return (power_output * 1000.0) / (efficiency * self.hhv) From df35bec3387596fa11d2d0d22065953f6f876e67 Mon Sep 17 00:00:00 2001 From: Frederik Date: Wed, 25 Mar 2026 10:21:36 -0600 Subject: [PATCH 8/9] Updated tests --- tests/thermal_component_base_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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(): From 472a5943b669af0b28cd2d4c44f43c39aa219572 Mon Sep 17 00:00:00 2001 From: Frederik Date: Thu, 26 Mar 2026 14:57:42 -0600 Subject: [PATCH 9/9] Final tweaks --- hercules/plant_components/thermal_component_base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hercules/plant_components/thermal_component_base.py b/hercules/plant_components/thermal_component_base.py index 93e3298f..eb9a7be8 100644 --- a/hercules/plant_components/thermal_component_base.py +++ b/hercules/plant_components/thermal_component_base.py @@ -632,7 +632,11 @@ def calculate_fuel_volume_rate(self, power_output): 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 self.shutdown_fuel_fraction * rated_fuel_consumption_rate + return max( + self.shutdown_fuel_fraction * rated_fuel_consumption_rate, + power_output * 1000.0 / (self.hhv * self.interpolate_efficiency(power_output)), + ) + elif ( self.state in [