From e52314a2c6dcd6d89f4008546fde9c3db58a3c0e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:52:56 -0700 Subject: [PATCH 01/40] added in-progress refactored iron winning models --- .../converters/iron/rosner_iron_cost_model.py | 161 +++++++++++ .../converters/iron/rosner_iron_perf_model.py | 260 ++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 h2integrate/converters/iron/rosner_iron_cost_model.py create mode 100644 h2integrate/converters/iron/rosner_iron_perf_model.py diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py new file mode 100644 index 000000000..d0a78bb40 --- /dev/null +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -0,0 +1,161 @@ +import pandas as pd +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.validators import contains +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi + + +@define +class RosnerIronPlantCostConfig(BaseConfig): + """Configuration class for RosnerIronPlantCostComponent. + + Attributes: + taconite_pellet_type (str): type of taconite pellets, options are "std" or "drg". + mine (str): name of ore mine. Must be "Hibbing", "Northshore", "United", + "Minorca" or "Tilden" + max_ore_production_rate_tonnes_per_hr (float): capacity of the pellet plant + in units of metric tonnes of pellets produced per hour. + cost_year (int): target dollar year to convert costs to + """ + + max_ore_production_rate_tonnes_per_hr: float = field() + + taconite_pellet_type: str = field( + converter=(str.lower, str.strip), validator=contains(["std", "drg"]) + ) + + mine: str = field(validator=contains(["Hibbing", "Northshore", "United", "Minorca", "Tilden"])) + cost_year: int = field(converter=int) + + +class RosnerIronPlantCostComponent(CostModelBaseClass): + def setup(self): + self.target_dollar_year = self.options["plant_config"]["finance_parameters"][ + "cost_adjustment_parameters" + ]["target_dollar_year"] + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") + config_dict.update({"cost_year": self.target_dollar_year}) + + self.config = RosnerIronPlantCostConfig.from_dict(config_dict, strict=False) + + super().setup() + + self.add_input( + "system_capacity", + val=self.config.max_ore_production_rate_tonnes_per_hr, + # shape=n_timesteps, + units="t/h", + desc="Annual ore production capacity", + ) + self.add_input( + "iron_ore_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Iron ore pellets produced", + ) + + coeff_fpath = ( + ROOT_DIR / "simulation" / "technologies" / "iron" / "martin_ore" / "cost_coeffs.csv" + ) + # martin ore performance model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) + + def format_coeff_df(self, coeff_df, mine): + """Update the coefficient dataframe such that values are adjusted to standard units + and units are compatible with OpenMDAO units. Also filter the dataframe to include + only the data necessary for a given mine and pellet type. + + Args: + coeff_df (pd.DataFrame): cost coefficient dataframe. + mine (str): name of mine that ore is extracted from. + + Returns: + pd.DataFrame: cost coefficient dataframe + """ + + # only include data for the given product + coeff_df = coeff_df[ + coeff_df["Product"] == f"{self.config.taconite_pellet_type}_taconite_pellets" + ] + data_cols = ["Name", "Type", "Coeff", "Unit", mine] + coeff_df = coeff_df[data_cols] + coeff_df = coeff_df.rename(columns={mine: "Value"}) + + # convert wet to dry + moisture_percent = 2.0 + dry_fraction = (100 - moisture_percent) / 100 + + # convert wet long tons per year to dry long tons per year + i_wlt = coeff_df[coeff_df["Unit"] == "wltpy"].index.to_list() + coeff_df.loc[i_wlt, "Value"] = coeff_df.loc[i_wlt, "Value"] * dry_fraction + coeff_df.loc[i_wlt, "Unit"] = "lt/yr" + + # convert kWh/wet long ton to kWh/dry long ton + i_per_wlt = coeff_df[coeff_df["Unit"] == "2021 $ per wlt pellet"].index.to_list() + coeff_df.loc[i_per_wlt, "Value"] = coeff_df.loc[i_per_wlt, "Value"] / dry_fraction + coeff_df.loc[i_per_wlt, "Unit"] = "USD/lt" + coeff_df.loc[i_per_wlt, "Type"] = "variable opex/pellet" + + # convert units to standardized units + unit_rename_mapper = {} + old_units = list(set(coeff_df["Unit"].to_list())) + for ii, old_unit in enumerate(old_units): + if "2021 $" in old_unit: + old_unit = old_unit.replace("2021 $", "USD") + if "mlt" in old_unit: # millon long tons + old_unit = old_unit.replace("mlt", "(2240*Mlb)") + if "lt" in old_unit: # dry long tons + old_unit = old_unit.replace("lt", "(2240*lb)") + if "mt" in old_unit: # metric tonne + old_unit = old_unit.replace("mt", "t") + if "wt %" in old_unit: + old_unit = old_unit.replace("wt %", "unitless") + if "deg N" in old_unit or "deg E" in old_unit: + old_unit = "deg" + unit_rename_mapper.update({old_units[ii]: old_unit}) + coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) + + convert_units_dict = { + "USD/(2240*lb)": "USD/t", + "(2240*Mlb)": "t", + "(2240*lb)/yr": "t/yr", + } + for i in coeff_df.index.to_list(): + if coeff_df.loc[i, "Unit"] in convert_units_dict: + current_units = coeff_df.loc[i, "Unit"] + desired_units = convert_units_dict[current_units] + coeff_df.loc[i, "Value"] = units.convert_units( + coeff_df.loc[i, "Value"], current_units, desired_units + ) + coeff_df.loc[i, "Unit"] = desired_units + + return coeff_df + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + ref_Oreproduced = self.coeff_df[self.coeff_df["Name"] == "Ore pellets produced"][ + "Value" + ].values + + # get the capital cost for the reference design + ref_tot_capex = self.coeff_df[self.coeff_df["Type"] == "capital"]["Value"].sum() + ref_capex_per_anual_processed_ore = ref_tot_capex / ref_Oreproduced # USD/t/yr + ref_capex_per_processed_ore = ref_capex_per_anual_processed_ore * 8760 # USD/t/hr + tot_capex_2021USD = inputs["system_capacity"] * ref_capex_per_processed_ore # USD + + # get the variable om cost based on the total pellet production + total_pellets_produced = sum(inputs["iron_ore_out"]) + var_om_2021USD = ( + self.coeff_df[self.coeff_df["Type"] == "variable opex/pellet"]["Value"] + * total_pellets_produced + ).sum() + + # adjust costs to cost year + outputs["CapEx"] = inflate_cpi(tot_capex_2021USD, 2021, self.config.cost_year) + outputs["VarOpEx"] = inflate_cpi(var_om_2021USD, 2021, self.config.cost_year) diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py new file mode 100644 index 000000000..f8ba80050 --- /dev/null +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -0,0 +1,260 @@ +import numpy as np +import pandas as pd +import openmdao.api as om +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.validators import contains + + +@define +class RosnerIronPlantPerformanceConfig(BaseConfig): + """Configuration class for MartinIronMinePerformanceComponent. + + Attributes: + taconite_pellet_type (str): type of taconite pellets, options are "std" or "drg". + mine (str): name of ore mine. Must be "Hibbing", "Northshore", "United", + "Minorca" or "Tilden" + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + """ + + pig_iron_production_rate_tonnes_per_hr: float = field() + + reduction_chemical: str = field( + converter=(str.lower, str.strip), validator=contains(["ng", "h2"]) + ) + + mine: str = field(validator=contains(["Hibbing", "Northshore", "United", "Minorca", "Tilden"])) + + +class NaturalGasIronReudctionPlantPerformanceComponent(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + self.config = RosnerIronPlantPerformanceConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, + ) + + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + units="t/h", + desc="Annual pig iron production capacity", + ) + + feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "gal/h", + "iron_ore": "t/h", + "electricity": "kW", + "reformer_catalyst": "(m**3)/h", + } + # Add feedstock inputs and outputs, default to 0 --> set using feedstock component + for feedstock, feedstock_units in feedstocks_to_units.items(): + self.add_input( + f"{feedstock}_in", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} available for iron reduction", + ) + self.add_input( + f"{feedstock}_in", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} consumed for iron reduction", + ) + + # Add iron ore input, default to 0 --> set using feedstock component + + # Default the reduced iron demand input as the rated capacity + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + # shape=n_timesteps, + units="t/h", + desc="Annual ore production capacity", + ) + + self.add_input( + "pig_iron_demand", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + shape=n_timesteps, + units="t/h", + desc="Pig iron demand for iron plant", + ) + + self.add_output( + "pig_iron_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Pig iron produced", + ) + + coeff_fpath = ( + ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "perf_coeffs.csv" + ) + # martin ore performance model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that values are adjusted to standard units + and units are compatible with OpenMDAO units. Also filter the dataframe to include + only the data necessary for a given mine and pellet type. + + Args: + coeff_df (pd.DataFrame): cost coefficient dataframe. + mine (str): name of mine that ore is extracted from. + + Returns: + pd.DataFrame: cost coefficient dataframe + """ + # only include data for the given product + coeff_df = coeff_df[coeff_df["Product"] == "ng_dri"] + data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] + coeff_df = coeff_df[data_cols] + coeff_df = coeff_df.rename(columns={"Model": "Value"}) + coeff_df = coeff_df[coeff_df["Value"] > 0] + coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data + + # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst + # h2_dri needs natural gas, water, hydrogen, iron ore. + # hm.loc["ng_dri","feed"][hm.loc["ng_dri","feed"]["Model"]>0] + steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"] + iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"] + + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} + + # units are mtpy, %, mt ore/mt steel, m3 catalyst/mt steel, GJ-LHV NG/mt steel, + # mt H2O/mt steel, mt CO2/mt steel, mt H20 discharged/mt H2O withdrawn, kW/mtpy steel + # convert wet long tons per year to dry long tons per year + + # convert units to standardized units + old_units = list(set(coeff_df["Unit"].to_list())) + for ii, old_unit in enumerate(old_units): + if old_unit in unit_rename_mapper: + continue + if "steel" in old_unit: + feedstock_unit, capacity_unit = old_unit.split("/") + + i_update = coeff_df[coeff_df["Unit"] == old_unit].index + # convert from feedstock per unit steel to feedstock per unit pig iron + coeff_df.loc[i_update]["Model"] = ( + coeff_df.loc[i_update]["Model"] * steel_to_iron_ratio + ) + # TODO: update name + + # if 'mtpy' in capacity_unit: + # # convert from amount/annual steel to amount/unit pig iron + # total_feedstock_usage = coeff_df.loc[i_update]["Model"]*steel_plant_capacity + # feedstock_usage_ratio = total_feedstock_usage/iron_plant_capacity + # if 'mt ' in capacity_unit: + + if "MWh" in old_unit: + old_unit = old_unit.replace("MWh", "(MW*h)") + if "mlt" in old_unit: # millon long tons + old_unit = old_unit.replace("mlt", "(2240*Mlb)") + if "lt" in old_unit: # dry long tons + old_unit = old_unit.replace("lt", "(2240*lb)") + if "mt" in old_unit: # metric tonne + old_unit = old_unit.replace("mt", "t") + if "wt %" in old_unit: + old_unit = old_unit.replace("wt %", "unitless") + if "deg N" in old_unit or "deg E" in old_unit: + old_unit = "deg" + unit_rename_mapper.update({old_units[ii]: old_unit}) + coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) + + convert_units_dict = { + "(kW*h)/(2240*lb)": "(kW*h)/t", + "(2240*Mlb)": "t", + "(2240*lb)/yr": "t/yr", + } + for i in coeff_df.index.to_list(): + if coeff_df.loc[i, "Unit"] in convert_units_dict: + current_units = coeff_df.loc[i, "Unit"] + desired_units = convert_units_dict[current_units] + coeff_df.loc[i, "Value"] = units.convert_units( + coeff_df.loc[i, "Value"], current_units, desired_units + ) + coeff_df.loc[i, "Unit"] = desired_units + + return coeff_df + + def compute(self, inputs, outputs): + # calculate crude ore required per amount of ore processed + ref_Orefeedstock = self.coeff_df[self.coeff_df["Name"] == "Crude ore processed"][ + "Value" + ].values + ref_Oreproduced = self.coeff_df[self.coeff_df["Name"] == "Ore pellets produced"][ + "Value" + ].values + crude_ore_usage_per_processed_ore = ref_Orefeedstock / ref_Oreproduced + + # energy consumption based on ore production + energy_usage_per_processed_ore = self.coeff_df[ + self.coeff_df["Type"] == "energy use/pellet" + ]["Value"].sum() + # check that units work out + energy_usage_unit = self.coeff_df[self.coeff_df["Type"] == "energy use/pellet"][ + "Unit" + ].values[0] + energy_usage_per_processed_ore = units.convert_units( + energy_usage_per_processed_ore, f"(t/h)*({energy_usage_unit})", "(kW*h)/h" + ) + + # calculate max inputs/outputs based on rated capacity + max_crude_ore_consumption = inputs["system_capacity"] * crude_ore_usage_per_processed_ore + max_energy_consumption = inputs["system_capacity"] * energy_usage_per_processed_ore + + # iron ore demand, saturated at maximum rated system capacity + processed_ore_demand = np.where( + inputs["iron_ore_demand"] > inputs["system_capacity"], + inputs["system_capacity"], + inputs["iron_ore_demand"], + ) + + # available feedstocks, saturated at maximum system feedstock consumption + crude_ore_available = np.where( + inputs["crude_ore_in"] > max_crude_ore_consumption, + max_crude_ore_consumption, + inputs["crude_ore_in"], + ) + + energy_available = np.where( + inputs["electricity_in"] > max_energy_consumption, + max_energy_consumption, + inputs["electricity_in"], + ) + + # how much output can be produced from each of the feedstocks + processed_ore_from_electricity = energy_available / energy_usage_per_processed_ore + processed_ore_from_crude_ore = crude_ore_available / crude_ore_usage_per_processed_ore + + # output is minimum between available feedstocks and output demand + processed_ore_production = np.minimum.reduce( + [processed_ore_from_crude_ore, processed_ore_from_electricity, processed_ore_demand] + ) + + # energy consumption + energy_consumed = processed_ore_production * energy_usage_per_processed_ore + + # crude ore consumption + crude_ore_consumption = processed_ore_production * crude_ore_usage_per_processed_ore + + outputs["iron_ore_out"] = processed_ore_production + outputs["electricity_consumed"] = energy_consumed + outputs["crude_ore_consumed"] = crude_ore_consumption From 4d86b83e40ba689739f6fa7db7ec0d490d35d901 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 10 Dec 2025 16:24:07 -0700 Subject: [PATCH 02/40] updated natural gas dri performance model --- .../converters/iron/rosner_iron_cost_model.py | 9 +- .../converters/iron/rosner_iron_perf_model.py | 201 +++++++++--------- 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index d0a78bb40..afca55fd5 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -67,14 +67,13 @@ def setup(self): coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) - def format_coeff_df(self, coeff_df, mine): + def format_coeff_df(self, coeff_df): """Update the coefficient dataframe such that values are adjusted to standard units and units are compatible with OpenMDAO units. Also filter the dataframe to include - only the data necessary for a given mine and pellet type. + only the data necessary for a given dri type. Args: coeff_df (pd.DataFrame): cost coefficient dataframe. - mine (str): name of mine that ore is extracted from. Returns: pd.DataFrame: cost coefficient dataframe @@ -84,9 +83,9 @@ def format_coeff_df(self, coeff_df, mine): coeff_df = coeff_df[ coeff_df["Product"] == f"{self.config.taconite_pellet_type}_taconite_pellets" ] - data_cols = ["Name", "Type", "Coeff", "Unit", mine] + data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] coeff_df = coeff_df[data_cols] - coeff_df = coeff_df.rename(columns={mine: "Value"}) + coeff_df = coeff_df.rename(columns={"Model": "Value"}) # convert wet to dry moisture_percent = 2.0 diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index f8ba80050..96461f381 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -6,7 +6,6 @@ from h2integrate import ROOT_DIR from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.validators import contains @define @@ -23,11 +22,11 @@ class RosnerIronPlantPerformanceConfig(BaseConfig): pig_iron_production_rate_tonnes_per_hr: float = field() - reduction_chemical: str = field( - converter=(str.lower, str.strip), validator=contains(["ng", "h2"]) - ) + # reduction_chemical: str = field( + # converter=(str.lower, str.strip), validator=contains(["ng", "h2"]) + # ) - mine: str = field(validator=contains(["Hibbing", "Northshore", "United", "Minorca", "Tilden"])) + water_density: float = field(default=1000) # kg/m3 class NaturalGasIronReudctionPlantPerformanceComponent(om.ExplicitComponent): @@ -53,7 +52,7 @@ def setup(self): feedstocks_to_units = { "natural_gas": "MMBtu", - "water": "gal/h", + "water": "galUS/h", "iron_ore": "t/h", "electricity": "kW", "reformer_catalyst": "(m**3)/h", @@ -67,8 +66,8 @@ def setup(self): units=feedstock_units, desc=f"{feedstock} available for iron reduction", ) - self.add_input( - f"{feedstock}_in", + self.add_output( + f"{feedstock}_consumed", val=0.0, shape=n_timesteps, units=feedstock_units, @@ -78,13 +77,6 @@ def setup(self): # Add iron ore input, default to 0 --> set using feedstock component # Default the reduced iron demand input as the rated capacity - self.add_input( - "system_capacity", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - # shape=n_timesteps, - units="t/h", - desc="Annual ore production capacity", - ) self.add_input( "pig_iron_demand", @@ -107,7 +99,8 @@ def setup(self): ) # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) + self.coeff_df = self.format_coeff_df(coeff_df) + self.feedstock_to_units = feedstocks_to_units def format_coeff_df(self, coeff_df): """Update the coefficient dataframe such that values are adjusted to standard units @@ -132,8 +125,8 @@ def format_coeff_df(self, coeff_df): # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst # h2_dri needs natural gas, water, hydrogen, iron ore. # hm.loc["ng_dri","feed"][hm.loc["ng_dri","feed"]["Model"]>0] - steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"] - iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"] + steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] + iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} @@ -151,37 +144,50 @@ def format_coeff_df(self, coeff_df): feedstock_unit, capacity_unit = old_unit.split("/") i_update = coeff_df[coeff_df["Unit"] == old_unit].index + # convert from feedstock per unit steel to feedstock per unit pig iron - coeff_df.loc[i_update]["Model"] = ( - coeff_df.loc[i_update]["Model"] * steel_to_iron_ratio + coeff_df.loc[i_update]["Value"] = ( + coeff_df.loc[i_update]["Value"] * steel_to_iron_ratio ) - # TODO: update name - - # if 'mtpy' in capacity_unit: - # # convert from amount/annual steel to amount/unit pig iron - # total_feedstock_usage = coeff_df.loc[i_update]["Model"]*steel_plant_capacity - # feedstock_usage_ratio = total_feedstock_usage/iron_plant_capacity - # if 'mt ' in capacity_unit: - - if "MWh" in old_unit: - old_unit = old_unit.replace("MWh", "(MW*h)") - if "mlt" in old_unit: # millon long tons - old_unit = old_unit.replace("mlt", "(2240*Mlb)") - if "lt" in old_unit: # dry long tons - old_unit = old_unit.replace("lt", "(2240*lb)") - if "mt" in old_unit: # metric tonne - old_unit = old_unit.replace("mt", "t") - if "wt %" in old_unit: - old_unit = old_unit.replace("wt %", "unitless") - if "deg N" in old_unit or "deg E" in old_unit: - old_unit = "deg" + + capacity_unit = capacity_unit.replace("steel", "").strip() + feedstock_unit = feedstock_unit.strip() + coeff_df.loc[i_update]["Type"] = f"{coeff_df.loc[i_update]['Type'].values[0]}/iron" + + if capacity_unit == "mtpy": + # some feedstocks had mislead units, + # where 'kW/ mtpy steel' is actually 'kW/mt steel' + # NOTE: perhaps these ones need to be modified with the steel efficiency? + capacity_unit = "mt" + + old_unit = f"{feedstock_unit}/{capacity_unit}" + + if "H2O" in old_unit: + # convert t to kg then convert cubic meters to liters + coeff_df.loc[i_update]["Value"] = ( + coeff_df.loc[i_update]["Value"] * 1e3 * 1e3 / self.config.water_density + ) + old_unit = f"L/{capacity_unit}" + + old_unit = old_unit.replace("-LHV NG", "").replace("%", "percent") + old_unit = ( + old_unit.replace("m3 catalyst", "(m**3)") + .replace("MWh", "(MW*h)") + .replace(" ore", "") + ) + old_unit = ( + old_unit.replace("mtpy", "(t/yr)").replace("mt", "t").replace("kW/", "(kW*h)/") + ) + # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' + # replace % with "unitless" unit_rename_mapper.update({old_units[ii]: old_unit}) coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) convert_units_dict = { - "(kW*h)/(2240*lb)": "(kW*h)/t", - "(2240*Mlb)": "t", - "(2240*lb)/yr": "t/yr", + "GJ/t": "MMBtu/t", + "L/t": "galUS/t", + "(MW*h)/t": "(kW*h)/t", + "percent": "unitless", } for i in coeff_df.index.to_list(): if coeff_df.loc[i, "Unit"] in convert_units_dict: @@ -191,70 +197,69 @@ def format_coeff_df(self, coeff_df): coeff_df.loc[i, "Value"], current_units, desired_units ) coeff_df.loc[i, "Unit"] = desired_units - + # NOTE: not sure if percent is actually being converted to unitless return coeff_df def compute(self, inputs, outputs): - # calculate crude ore required per amount of ore processed - ref_Orefeedstock = self.coeff_df[self.coeff_df["Name"] == "Crude ore processed"][ - "Value" - ].values - ref_Oreproduced = self.coeff_df[self.coeff_df["Name"] == "Ore pellets produced"][ - "Value" - ].values - crude_ore_usage_per_processed_ore = ref_Orefeedstock / ref_Oreproduced - - # energy consumption based on ore production - energy_usage_per_processed_ore = self.coeff_df[ - self.coeff_df["Type"] == "energy use/pellet" - ]["Value"].sum() - # check that units work out - energy_usage_unit = self.coeff_df[self.coeff_df["Type"] == "energy use/pellet"][ - "Unit" - ].values[0] - energy_usage_per_processed_ore = units.convert_units( - energy_usage_per_processed_ore, f"(t/h)*({energy_usage_unit})", "(kW*h)/h" - ) + feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() + feedstocks_usage_rates = { + "natural_gas": feedstocks[feedstocks["Unit"] == "MMBtu/t"]["Value"].sum(), + "water": feedstocks[feedstocks["Unit"] == "galUS/t"]["Value"].sum(), + "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), + "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"]["Value"].sum(), + "reformer_catalyst": feedstocks[feedstocks["Name"] == "Reformer Catalyst"][ + "Value" + ].sum(), + } - # calculate max inputs/outputs based on rated capacity - max_crude_ore_consumption = inputs["system_capacity"] * crude_ore_usage_per_processed_ore - max_energy_consumption = inputs["system_capacity"] * energy_usage_per_processed_ore + inputs["system_capacity"] # iron ore demand, saturated at maximum rated system capacity - processed_ore_demand = np.where( - inputs["iron_ore_demand"] > inputs["system_capacity"], + pig_iron_demand = np.where( + inputs["pig_iron_demand"] > inputs["system_capacity"], inputs["system_capacity"], - inputs["iron_ore_demand"], - ) - - # available feedstocks, saturated at maximum system feedstock consumption - crude_ore_available = np.where( - inputs["crude_ore_in"] > max_crude_ore_consumption, - max_crude_ore_consumption, - inputs["crude_ore_in"], + inputs["pig_iron_demand"], ) - energy_available = np.where( - inputs["electricity_in"] > max_energy_consumption, - max_energy_consumption, - inputs["electricity_in"], + pig_iron_from_feedstocks = np.zeros( + (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) ) - - # how much output can be produced from each of the feedstocks - processed_ore_from_electricity = energy_available / energy_usage_per_processed_ore - processed_ore_from_crude_ore = crude_ore_available / crude_ore_usage_per_processed_ore + pig_iron_from_feedstocks[0] = pig_iron_demand + ii = 1 + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + # calculate max inputs/outputs based on rated capacity + max_feedstock_consumption = inputs["system_capacity"] * consumption_rate + # available feedstocks, saturated at maximum system feedstock consumption + feedstock_available = np.where( + inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, + max_feedstock_consumption, + inputs[f"{feedstock_type}_in"], + ) + # how much output can be produced from each of the feedstocks + pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate # output is minimum between available feedstocks and output demand - processed_ore_production = np.minimum.reduce( - [processed_ore_from_crude_ore, processed_ore_from_electricity, processed_ore_demand] - ) - - # energy consumption - energy_consumed = processed_ore_production * energy_usage_per_processed_ore - - # crude ore consumption - crude_ore_consumption = processed_ore_production * crude_ore_usage_per_processed_ore - - outputs["iron_ore_out"] = processed_ore_production - outputs["electricity_consumed"] = energy_consumed - outputs["crude_ore_consumed"] = crude_ore_consumption + pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) + + # feedstock consumption + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate + + +if __name__ == "__main__": + prob = om.Problem() + + plant_config = {"plant": {"simulation": {"n_timesteps": 8760}}} + iron_dri_config_rosner_ng = { + # 'plant_capacity_mtpy': 1418095 + "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, + "reduction_chemical": "ng", + } + iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + plant_config=plant_config, + tech_config={"model_inputs": {"performance_parameters": iron_dri_config_rosner_ng}}, + driver_config={}, + ) + prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) + prob.setup() + prob.run_model() From 5f82e24b7fc0c033f5dc8e70279f54841f1933cf Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:48:47 -0700 Subject: [PATCH 03/40] updated docs in ng iron performance model --- .../converters/iron/rosner_iron_perf_model.py | 75 ++++++++++--------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index 96461f381..74b818504 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -10,22 +10,16 @@ @define class RosnerIronPlantPerformanceConfig(BaseConfig): - """Configuration class for MartinIronMinePerformanceComponent. + """Configuration class for NaturalGasIronReudctionPlantPerformanceComponent. Attributes: - taconite_pellet_type (str): type of taconite pellets, options are "std" or "drg". - mine (str): name of ore mine. Must be "Hibbing", "Northshore", "United", - "Minorca" or "Tilden" pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant in units of metric tonnes of pig iron produced per hour. + water_density (float): water density in kg/m3 to use to calculate water volume + from mass. Defaults to 1000.0 """ pig_iron_production_rate_tonnes_per_hr: float = field() - - # reduction_chemical: str = field( - # converter=(str.lower, str.strip), validator=contains(["ng", "h2"]) - # ) - water_density: float = field(default=1000) # kg/m3 @@ -47,7 +41,7 @@ def setup(self): "system_capacity", val=self.config.pig_iron_production_rate_tonnes_per_hr, units="t/h", - desc="Annual pig iron production capacity", + desc="Rated pig iron production capacity", ) feedstocks_to_units = { @@ -57,6 +51,7 @@ def setup(self): "electricity": "kW", "reformer_catalyst": "(m**3)/h", } + # Add feedstock inputs and outputs, default to 0 --> set using feedstock component for feedstock, feedstock_units in feedstocks_to_units.items(): self.add_input( @@ -74,10 +69,7 @@ def setup(self): desc=f"{feedstock} consumed for iron reduction", ) - # Add iron ore input, default to 0 --> set using feedstock component - - # Default the reduced iron demand input as the rated capacity - + # Default the pig iron demand input as the rated capacity self.add_input( "pig_iron_demand", val=self.config.pig_iron_production_rate_tonnes_per_hr, @@ -97,22 +89,22 @@ def setup(self): coeff_fpath = ( ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "perf_coeffs.csv" ) - # martin ore performance model + # rosner dri performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df) self.feedstock_to_units = feedstocks_to_units def format_coeff_df(self, coeff_df): - """Update the coefficient dataframe such that values are adjusted to standard units - and units are compatible with OpenMDAO units. Also filter the dataframe to include - only the data necessary for a given mine and pellet type. + """Update the coefficient dataframe such that feedstock values are converted to units of + feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units + and that units are compatible with OpenMDAO Units. Filter the dataframe to include + only the data necessary for Natural Gas reduction. Args: - coeff_df (pd.DataFrame): cost coefficient dataframe. - mine (str): name of mine that ore is extracted from. + coeff_df (pd.DataFrame): performance coefficient dataframe. Returns: - pd.DataFrame: cost coefficient dataframe + pd.DataFrame: filtered performance coefficient dataframe """ # only include data for the given product coeff_df = coeff_df[coeff_df["Product"] == "ng_dri"] @@ -124,16 +116,16 @@ def format_coeff_df(self, coeff_df): # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst # h2_dri needs natural gas, water, hydrogen, iron ore. - # hm.loc["ng_dri","feed"][hm.loc["ng_dri","feed"]["Model"]>0] steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] + # capacity units units are mtpy steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} - # units are mtpy, %, mt ore/mt steel, m3 catalyst/mt steel, GJ-LHV NG/mt steel, - # mt H2O/mt steel, mt CO2/mt steel, mt H20 discharged/mt H2O withdrawn, kW/mtpy steel - # convert wet long tons per year to dry long tons per year + # efficiency units are % + # feedstock units are mt ore/mt steel, m3 catalyst/mt steel, GJ-LHV NG/mt steel, + # mt H2O/mt steel, kW/mtpy steel # convert units to standardized units old_units = list(set(coeff_df["Unit"].to_list())) @@ -142,6 +134,8 @@ def format_coeff_df(self, coeff_df): continue if "steel" in old_unit: feedstock_unit, capacity_unit = old_unit.split("/") + capacity_unit = capacity_unit.replace("steel", "").strip() + feedstock_unit = feedstock_unit.strip() i_update = coeff_df[coeff_df["Unit"] == old_unit].index @@ -150,12 +144,14 @@ def format_coeff_df(self, coeff_df): coeff_df.loc[i_update]["Value"] * steel_to_iron_ratio ) - capacity_unit = capacity_unit.replace("steel", "").strip() - feedstock_unit = feedstock_unit.strip() + # update the "Type" to specify that units were changed to be per unit pig iron coeff_df.loc[i_update]["Type"] = f"{coeff_df.loc[i_update]['Type'].values[0]}/iron" - if capacity_unit == "mtpy": - # some feedstocks had mislead units, + is_capacity_type = all( + k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() + ) + if capacity_unit == "mtpy" and not is_capacity_type: + # some feedstocks had misleading units, # where 'kW/ mtpy steel' is actually 'kW/mt steel' # NOTE: perhaps these ones need to be modified with the steel efficiency? capacity_unit = "mt" @@ -163,7 +159,9 @@ def format_coeff_df(self, coeff_df): old_unit = f"{feedstock_unit}/{capacity_unit}" if "H2O" in old_unit: - # convert t to kg then convert cubic meters to liters + # convert metric tonnes to kg (value*1e3) + # convert mass to volume in cubic meters (value*1e3)/density + # then convert cubic meters to liters 1e3*(value*1e3)/density coeff_df.loc[i_update]["Value"] = ( coeff_df.loc[i_update]["Value"] * 1e3 * 1e3 / self.config.water_density ) @@ -189,6 +187,7 @@ def format_coeff_df(self, coeff_df): "(MW*h)/t": "(kW*h)/t", "percent": "unitless", } + # convert units to standard units and OpenMDAO compatible units for i in coeff_df.index.to_list(): if coeff_df.loc[i, "Unit"] in convert_units_dict: current_units = coeff_df.loc[i, "Unit"] @@ -198,10 +197,14 @@ def format_coeff_df(self, coeff_df): ) coeff_df.loc[i, "Unit"] = desired_units # NOTE: not sure if percent is actually being converted to unitless + # but not big deal since percent is not used in feedstocks return coeff_df def compute(self, inputs, outputs): + # get the feedstocks from feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() + + # get the feedstock usage rates in units/t pig iron feedstocks_usage_rates = { "natural_gas": feedstocks[feedstocks["Unit"] == "MMBtu/t"]["Value"].sum(), "water": feedstocks[feedstocks["Unit"] == "galUS/t"]["Value"].sum(), @@ -212,18 +215,19 @@ def compute(self, inputs, outputs): ].sum(), } - inputs["system_capacity"] - - # iron ore demand, saturated at maximum rated system capacity + # pig iron demand, saturated at maximum rated system capacity pig_iron_demand = np.where( inputs["pig_iron_demand"] > inputs["system_capacity"], inputs["system_capacity"], inputs["pig_iron_demand"], ) + # initialize an array of how much pig iron could be produced + # from the available feedstocks and the demand pig_iron_from_feedstocks = np.zeros( (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) ) + # first entry is the pig iron demand pig_iron_from_feedstocks[0] = pig_iron_demand ii = 1 for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): @@ -240,8 +244,9 @@ def compute(self, inputs, outputs): # output is minimum between available feedstocks and output demand pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) + outputs["pig_iron_out"] = pig_iron_production - # feedstock consumption + # feedstock consumption based on actual pig iron produced for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate @@ -253,7 +258,7 @@ def compute(self, inputs, outputs): iron_dri_config_rosner_ng = { # 'plant_capacity_mtpy': 1418095 "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - "reduction_chemical": "ng", + # "reduction_chemical": "ng", } iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( plant_config=plant_config, From 08cbefe121e833d7ab36c6ae24b4bd8e7d5c272f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:54:19 -0700 Subject: [PATCH 04/40] updated iron cost model --- .../converters/iron/rosner_iron_cost_model.py | 291 ++++++++++++------ 1 file changed, 190 insertions(+), 101 deletions(-) diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index afca55fd5..f604483f6 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -1,45 +1,73 @@ import pandas as pd +import openmdao.api as om from attrs import field, define from openmdao.utils import units from h2integrate import ROOT_DIR -from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.validators import contains +from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs +from h2integrate.core.validators import gte_zero from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci @define -class RosnerIronPlantCostConfig(BaseConfig): +class RosnerIronPlantCostConfig(CostModelBaseConfig): """Configuration class for RosnerIronPlantCostComponent. Attributes: - taconite_pellet_type (str): type of taconite pellets, options are "std" or "drg". - mine (str): name of ore mine. Must be "Hibbing", "Northshore", "United", - "Minorca" or "Tilden" - max_ore_production_rate_tonnes_per_hr (float): capacity of the pellet plant - in units of metric tonnes of pellets produced per hour. - cost_year (int): target dollar year to convert costs to + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + cost_year (int): This model uses 2022 as the base year for the cost model. + The cost year is updated based on `target_dollar_year` in the plant + config to adjust costs based on CPI/CEPCI within this model. This value + cannot be user added under `cost_parameters`. + skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr + unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr """ - max_ore_production_rate_tonnes_per_hr: float = field() + pig_iron_production_rate_tonnes_per_hr: float = field() + cost_year: int = field(converter=int) + skilled_labor_cost: float = field(validator=gte_zero) + unskilled_labor_cost: float = field(validator=gte_zero) - taconite_pellet_type: str = field( - converter=(str.lower, str.strip), validator=contains(["std", "drg"]) - ) - mine: str = field(validator=contains(["Hibbing", "Northshore", "United", "Minorca", "Tilden"])) - cost_year: int = field(converter=int) +class NaturalGasIronPlantCostComponent(CostModelBaseClass): + """_summary_ + Attributes: + config (RosnerIronPlantCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ -class RosnerIronPlantCostComponent(CostModelBaseClass): def setup(self): - self.target_dollar_year = self.options["plant_config"]["finance_parameters"][ - "cost_adjustment_parameters" - ]["target_dollar_year"] n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") - config_dict.update({"cost_year": self.target_dollar_year}) + + if "cost_year" in config_dict: + msg = ( + "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " + "The cost year cannot be modified for this cost model. " + ) + raise ValueError(msg) + + target_dollar_year = self.options["plant_config"]["finance_parameters"][ + "cost_adjustment_parameters" + ]["target_dollar_year"] + + if target_dollar_year <= 2024 and target_dollar_year >= 2010: + # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment + target_dollar_year = target_dollar_year + + elif target_dollar_year < 2010: + # adjust costs from 2022 to 2010 using CP/CEPCI adjustment + target_dollar_year = 2010 + + elif target_dollar_year > 2024: + # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment + target_dollar_year = 2024 + + config_dict.update({"cost_year": target_dollar_year}) self.config = RosnerIronPlantCostConfig.from_dict(config_dict, strict=False) @@ -47,30 +75,30 @@ def setup(self): self.add_input( "system_capacity", - val=self.config.max_ore_production_rate_tonnes_per_hr, + val=self.config.pig_iron_production_rate_tonnes_per_hr, # shape=n_timesteps, units="t/h", - desc="Annual ore production capacity", + desc="Pig ore production capacity", ) self.add_input( - "iron_ore_out", + "pig_iron_out", val=0.0, shape=n_timesteps, units="t/h", - desc="Iron ore pellets produced", + desc="Pig iron produced", ) coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "martin_ore" / "cost_coeffs.csv" + ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "cost_coeffs.csv" ) - # martin ore performance model + # rosner cost model coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) + self.coeff_df = self.format_coeff_df(coeff_df) def format_coeff_df(self, coeff_df): """Update the coefficient dataframe such that values are adjusted to standard units and units are compatible with OpenMDAO units. Also filter the dataframe to include - only the data necessary for a given dri type. + only the data necessary for natural gas DRI type. Args: coeff_df (pd.DataFrame): cost coefficient dataframe. @@ -78,83 +106,144 @@ def format_coeff_df(self, coeff_df): Returns: pd.DataFrame: cost coefficient dataframe """ + product = "ng_dri" # h2_dri + + perf_coeff_fpath = ( + ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "perf_coeffs.csv" + ) + + perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) + perf_df = perf_df[perf_df["Product"] == product] + steel_plant_capacity = perf_df[perf_df["Name"] == "Steel Production"]["Model"].values[0] + iron_plant_capacity = perf_df[perf_df["Name"] == "Pig Iron Production"]["Model"].values[0] + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + self.steel_to_iron_ratio = steel_to_iron_ratio # only include data for the given product - coeff_df = coeff_df[ - coeff_df["Product"] == f"{self.config.taconite_pellet_type}_taconite_pellets" - ] - data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] + + data_cols = ["Type", "Coeff", "Unit", product] coeff_df = coeff_df[data_cols] - coeff_df = coeff_df.rename(columns={"Model": "Value"}) - - # convert wet to dry - moisture_percent = 2.0 - dry_fraction = (100 - moisture_percent) / 100 - - # convert wet long tons per year to dry long tons per year - i_wlt = coeff_df[coeff_df["Unit"] == "wltpy"].index.to_list() - coeff_df.loc[i_wlt, "Value"] = coeff_df.loc[i_wlt, "Value"] * dry_fraction - coeff_df.loc[i_wlt, "Unit"] = "lt/yr" - - # convert kWh/wet long ton to kWh/dry long ton - i_per_wlt = coeff_df[coeff_df["Unit"] == "2021 $ per wlt pellet"].index.to_list() - coeff_df.loc[i_per_wlt, "Value"] = coeff_df.loc[i_per_wlt, "Value"] / dry_fraction - coeff_df.loc[i_per_wlt, "Unit"] = "USD/lt" - coeff_df.loc[i_per_wlt, "Type"] = "variable opex/pellet" - - # convert units to standardized units - unit_rename_mapper = {} - old_units = list(set(coeff_df["Unit"].to_list())) - for ii, old_unit in enumerate(old_units): - if "2021 $" in old_unit: - old_unit = old_unit.replace("2021 $", "USD") - if "mlt" in old_unit: # millon long tons - old_unit = old_unit.replace("mlt", "(2240*Mlb)") - if "lt" in old_unit: # dry long tons - old_unit = old_unit.replace("lt", "(2240*lb)") - if "mt" in old_unit: # metric tonne - old_unit = old_unit.replace("mt", "t") - if "wt %" in old_unit: - old_unit = old_unit.replace("wt %", "unitless") - if "deg N" in old_unit or "deg E" in old_unit: - old_unit = "deg" - unit_rename_mapper.update({old_units[ii]: old_unit}) - coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) - - convert_units_dict = { - "USD/(2240*lb)": "USD/t", - "(2240*Mlb)": "t", - "(2240*lb)/yr": "t/yr", - } - for i in coeff_df.index.to_list(): - if coeff_df.loc[i, "Unit"] in convert_units_dict: - current_units = coeff_df.loc[i, "Unit"] - desired_units = convert_units_dict[current_units] - coeff_df.loc[i, "Value"] = units.convert_units( - coeff_df.loc[i, "Value"], current_units, desired_units - ) - coeff_df.loc[i, "Unit"] = desired_units + + coeff_df = coeff_df.rename(columns={product: "Value"}) return coeff_df def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - ref_Oreproduced = self.coeff_df[self.coeff_df["Name"] == "Ore pellets produced"][ + # Calculate the capital cost for the item + dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) + + # Calculate + capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) + capital_items_df = self.coeff_df.loc[capital_items].copy() + capital_items_df = capital_items_df.reset_index(drop=False) + capital_items_df = capital_items_df.set_index( + keys=["Name", "Coeff"] + ) # costs are in USD/steel plant capacity + capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) + + ref_steel_plant_capacity_tpy = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "t/yr" + ) + + total_capex_usd = 0.0 + for item in capital_items: + if ( + capital_items_df.loc[item, "exp"]["Value"] > 0 + and capital_items_df.loc[item, "lin"]["Value"] > 0 + ): + capex = capital_items_df.loc[item, "lin"]["Value"] * ( + (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] + ) + total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) + + # Calculate Owners Costs + # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() + + # Calculated Fixed OpEx + fixed_items = list( + set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) + ) + fixed_items_df = self.coeff_df.loc[fixed_items].copy() + fixed_items_df = fixed_items_df.reset_index(drop=False) + fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) + fixed_items_df = fixed_items_df.drop(columns=["Type"]) + + property_om = ( + total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() + ) + + # Calculate labor costs + skilled_labor_cost = self.config.skilled_labor_cost * ( + fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 + ) + unskilled_labor_cost = self.config.unskilled_labor_cost * ( + fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 + ) + labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr + + ref_steel_plant_capacity_kgpd = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "kg/d" + ) + scaled_steel_plant_capacity_kgpd = ( + ref_steel_plant_capacity_kgpd + ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] + ) + + # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) + work_hrs_per_day_per_step = ( + scaled_steel_plant_capacity_kgpd + * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] + ) + + # employee-hours/day = employee-hours/day/process step * # of process steps + work_hrs_per_day = ( + work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values + ) + labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day + annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") + maintenance_labor_cost = ( + total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"] + ) + admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"]["Value"] * ( + annual_labor_cost + maintenance_labor_cost + ) + + total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost + tot_fixed_om = total_labor_related_cost + property_om + + # Calculate Variable O&M + varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ "Value" - ].values - - # get the capital cost for the reference design - ref_tot_capex = self.coeff_df[self.coeff_df["Type"] == "capital"]["Value"].sum() - ref_capex_per_anual_processed_ore = ref_tot_capex / ref_Oreproduced # USD/t/yr - ref_capex_per_processed_ore = ref_capex_per_anual_processed_ore * 8760 # USD/t/hr - tot_capex_2021USD = inputs["system_capacity"] * ref_capex_per_processed_ore # USD - - # get the variable om cost based on the total pellet production - total_pellets_produced = sum(inputs["iron_ore_out"]) - var_om_2021USD = ( - self.coeff_df[self.coeff_df["Type"] == "variable opex/pellet"]["Value"] - * total_pellets_produced - ).sum() - - # adjust costs to cost year - outputs["CapEx"] = inflate_cpi(tot_capex_2021USD, 2021, self.config.cost_year) - outputs["VarOpEx"] = inflate_cpi(var_om_2021USD, 2021, self.config.cost_year) + ].sum() # units are USD/mtpy steel + tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() + + # Adjust costs to target dollar year + tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) + tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) + tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) + + outputs["CapEx"] = tot_capex_adjusted + outputs["VarOpEx"] = tot_varopex_adjusted + outputs["OpEx"] = tot_fixed_om_adjusted + + +if __name__ == "__main__": + prob = om.Problem() + + plant_config = { + "plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}, + "finance_parameters": {"cost_adjustment_parameters": {"target_dollar_year": 2022}}, + } + iron_dri_config_rosner_ng = { + "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, + "skilled_labor_cost": 1.0, + "unskilled_labor_cost": 1.0, + } + iron_dri_perf = NaturalGasIronPlantCostComponent( + plant_config=plant_config, + tech_config={"model_inputs": {"shared_parameters": iron_dri_config_rosner_ng}}, + driver_config={}, + ) + prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) + prob.setup() + prob.run_model() From a18e9b0fee169dd351e30abb5c8d7600d498d4b3 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:57:45 -0700 Subject: [PATCH 05/40] updated coefficient paths for rosner iron plant models --- h2integrate/converters/iron/rosner_iron_cost_model.py | 8 ++------ h2integrate/converters/iron/rosner_iron_perf_model.py | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index f604483f6..462eeffc3 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -88,9 +88,7 @@ def setup(self): desc="Pig iron produced", ) - coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "cost_coeffs.csv" - ) + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" # rosner cost model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df) @@ -108,9 +106,7 @@ def format_coeff_df(self, coeff_df): """ product = "ng_dri" # h2_dri - perf_coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "perf_coeffs.csv" - ) + perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) perf_df = perf_df[perf_df["Product"] == product] diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index 74b818504..91aea273f 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -86,9 +86,7 @@ def setup(self): desc="Pig iron produced", ) - coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "rosner" / "perf_coeffs.csv" - ) + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" # rosner dri performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df) From d6482138b774ee3996bbc999c52271273055cf46 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:32:43 -0700 Subject: [PATCH 06/40] updated example 21 and fixed bugs that were found --- examples/21_iron_mn_to_il/plant_config.yaml | 13 ++- examples/21_iron_mn_to_il/tech_config.yaml | 96 +++++++++++++++---- .../converters/iron/rosner_iron_perf_model.py | 5 +- h2integrate/core/supported_models.py | 6 ++ h2integrate/transporters/pipe.py | 7 ++ 5 files changed, 104 insertions(+), 23 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index f2b237bc4..047dd25ac 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -22,11 +22,20 @@ site: # with the reverse definition. # this will naturally grow as we mature the interconnected tech technology_interconnections: [ + # connect feedstocks to iron mine ["grid_feedstock","iron_mine","electricity","cable"], ["mine_feedstock","iron_mine","crude_ore","pipe"], + # connect iron mine to iron transport + #TODO: update after updated transport model is integrated ["iron_mine","iron_transport","total_iron_ore_produced"], - ["iron_transport","iron_plant","iron_transport_cost"], - ["finance_subgroup_iron_ore","iron_plant","price_iron_ore"], + ["iron_mine","iron_plant","iron_ore","pipe"], #temp workaround until transport model is updated + # connect feedstocks to iron plant + ["dri_grid_feedstock","iron_plant","electricity","cable"], + ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], + ["water_feedstock","iron_plant","water","pipe"], + ["natural_gas_feedstock","iron_plant","natural_gas","pipe"], + # ["iron_transport","iron_plant","iron_transport_cost"], + # ["finance_subgroup_iron_ore","iron_plant","price_iron_ore"], ] plant: diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 919b9fec7..b5e0fa79e 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -2,7 +2,7 @@ name: "technology_config" description: "This hybrid plant produces iron" technologies: - grid_feedstock: + grid_feedstock: #electricity feedstock for iron ore performance_model: model: "feedstock_performance" cost_model: @@ -18,7 +18,7 @@ technologies: price: 0.0 annual_cost: 0. start_up_cost: 0. - mine_feedstock: + mine_feedstock: #iron ore feedstock performance_model: model: "feedstock_performance" cost_model: @@ -56,27 +56,85 @@ technologies: cost_parameters: transport_year: 2022 cost_year: 2022 - iron_plant: + + natural_gas_feedstock: + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "natural_gas" + units: "MMBtu" + performance_parameters: + rated_capacity: 1270. # need 1268.934 MMBtu at each timestep + cost_parameters: + cost_year: 2022 + price: 4.0 #USD 4.0/MMBtu + annual_cost: 0. + start_up_cost: 0. + + water_feedstock: #for iron reduction + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "water" + units: "galUS" #galUS/h + performance_parameters: + rated_capacity: 40. # need 38.71049649 galUS/h + cost_parameters: + cost_year: 2022 + price: 1670.0 # cost is $0.441167535/t, equal to $1670.0004398318847/galUS + annual_cost: 0. + start_up_cost: 0. + + catalyst_feedstock: #for iron reduction performance_model: - model: "iron_plant_performance" + model: "feedstock_performance" cost_model: - model: "iron_plant_cost" + model: "feedstock_cost" model_inputs: shared_parameters: - winning_type: "ng" - iron_win_capacity: 1418095 - site_name: "IL" + feedstock_type: "reformer_catalyst" + units: "(m**3)" #m**3/h performance_parameters: - win_capacity_demon: "iron" - model_name: "rosner" + rated_capacity: 0.001 # need 0.00056546 m**3/h cost_parameters: - LCOE: 58.02 - LCOH: 7.10 - LCOI_ore: 0 #125.25996463784443 - iron_transport_cost: 30.566808424134745 - ore_profit_pct: 6.0 - varom_model_name: "rosner" - installation_years: 3 - operational_year: 2035 + cost_year: 2022 + price: 17515.14 #USD 17515.14/m**3 + annual_cost: 0. + start_up_cost: 0. -# baseline 843.37604007 + dri_grid_feedstock: #electricity feedstock for iron dri + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "electricity" + units: "kW" + performance_parameters: + rated_capacity: 27000. # need 26949.46472431 kW + cost_parameters: + cost_year: 2022 + price: 0.05802 #USD/kW + annual_cost: 0. + start_up_cost: 0. + # need 263.7474 t/h of iron ore + iron_plant: + performance_model: + model: "ng_dri_performance_rosner" #"iron_plant_performance" + cost_model: + model: "ng_dri_cost_rosner" #"iron_plant_cost" + model_inputs: + shared_parameters: + pig_iron_production_rate_tonnes_per_hr: 161.8829908675799 #equivalent to 1418095 t/yr + performance_parameters: + water_density: 1000 #kg/m3 + cost_parameters: + skilled_labor_cost: 40.85 #2022 USD/hr + unskilled_labor_cost: 30.0 #2022 USD/hr diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index 91aea273f..34bd23d34 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -46,10 +46,10 @@ def setup(self): feedstocks_to_units = { "natural_gas": "MMBtu", - "water": "galUS/h", + "water": "galUS", # "galUS/h" "iron_ore": "t/h", "electricity": "kW", - "reformer_catalyst": "(m**3)/h", + "reformer_catalyst": "(m**3)", # "(m**3)/h" } # Add feedstock inputs and outputs, default to 0 --> set using feedstock component @@ -239,6 +239,7 @@ def compute(self, inputs, outputs): ) # how much output can be produced from each of the feedstocks pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate + ii += 1 # output is minimum between available feedstocks and output demand pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index ef11d126e..42a7c04c2 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -60,6 +60,10 @@ from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent +from h2integrate.converters.iron.rosner_iron_cost_model import NaturalGasIronPlantCostComponent +from h2integrate.converters.iron.rosner_iron_perf_model import ( + NaturalGasIronReudctionPlantPerformanceComponent, +) from h2integrate.converters.methanol.smr_methanol_plant import ( SMRMethanolPlantCostModel, SMRMethanolPlantFinanceModel, @@ -174,6 +178,8 @@ "iron_plant_cost": IronPlantCostComponent, "iron_mine_performance_martin": MartinIronMinePerformanceComponent, # standalone model "iron_mine_cost_martin": MartinIronMineCostComponent, # standalone model + "ng_dri_performance_rosner": NaturalGasIronReudctionPlantPerformanceComponent, + "ng_dri_cost_rosner": NaturalGasIronPlantCostComponent, # standalone model "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, diff --git a/h2integrate/transporters/pipe.py b/h2integrate/transporters/pipe.py index 524e68622..f47981e18 100644 --- a/h2integrate/transporters/pipe.py +++ b/h2integrate/transporters/pipe.py @@ -17,6 +17,9 @@ def initialize(self): "nitrogen", "natural_gas", "crude_ore", + "iron_ore", + "reformer_catalyst", + "water", ], ) @@ -27,6 +30,10 @@ def setup(self): if transport_item == "natural_gas": units = "MMBtu" + elif transport_item == "reformer_catalyst": + units = "(m**3)" + elif transport_item == "water": + units = "galUS" elif transport_item == "co2": units = "kg/h" else: From b37e190ca07bcd788603a1adfa0c9bbe475fdf99 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:06:01 -0700 Subject: [PATCH 07/40] minor updates to dri cost and perf models --- examples/21_iron_mn_to_il/plant_config.yaml | 2 +- .../converters/iron/rosner_iron_cost_model.py | 12 ++++++------ .../converters/iron/rosner_iron_perf_model.py | 2 -- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 047dd25ac..5024efa43 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -78,4 +78,4 @@ finance_parameters: pig_iron: commodity: "pig_iron" commodity_stream: "iron_plant" - technologies: ["iron_plant"] # + technologies: ["iron_plant", "dri_grid_feedstock", "catalyst_feedstock", "water_feedstock", "natural_gas_feedstock"] # diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index 462eeffc3..244db1d90 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -155,7 +155,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Calculate Owners Costs # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() - # Calculated Fixed OpEx + # Calculate Fixed OpEx, includes: fixed_items = list( set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) ) @@ -196,13 +196,13 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values ) labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day - annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") + annual_labor_cost = labor_cost_per_day * 365 # units.convert_units(1, "yr", "d") maintenance_labor_cost = ( - total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"] - ) - admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"]["Value"] * ( - annual_labor_cost + maintenance_labor_cost + total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values ) + admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ + "Value" + ].values * (annual_labor_cost + maintenance_labor_cost) total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost tot_fixed_om = total_labor_related_cost + property_om diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index 34bd23d34..d8e3037f7 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -255,9 +255,7 @@ def compute(self, inputs, outputs): plant_config = {"plant": {"simulation": {"n_timesteps": 8760}}} iron_dri_config_rosner_ng = { - # 'plant_capacity_mtpy': 1418095 "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - # "reduction_chemical": "ng", } iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( plant_config=plant_config, From c2971d2759e0fb2013de8a12edd4b6e65f19f9ec Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:07:11 -0700 Subject: [PATCH 08/40] added basic tests for new dri models --- .../converters/iron/test/test_rosner_dri.py | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 h2integrate/converters/iron/test/test_rosner_dri.py diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py new file mode 100644 index 000000000..ecc3d44f1 --- /dev/null +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -0,0 +1,180 @@ +import numpy as np +import pytest +import openmdao.api as om +from pytest import fixture + +from h2integrate import EXAMPLE_DIR +from h2integrate.core.inputs.validation import load_driver_yaml +from h2integrate.converters.iron.rosner_iron_cost_model import NaturalGasIronPlantCostComponent +from h2integrate.converters.iron.rosner_iron_perf_model import ( + NaturalGasIronReudctionPlantPerformanceComponent, +) + + +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + "finance_parameters": { + "cost_adjustment_parameters": { + "cost_year_adjustment_inflation": 0.025, + "target_dollar_year": 2022, + } + }, + } + return plant_config + + +@fixture +def driver_config(): + driver_config = load_driver_yaml(EXAMPLE_DIR / "21_iron_mn_to_il" / "driver_config.yaml") + return driver_config + + +@fixture +def ng_dri_base_config(): + tech_config = { + "model_inputs": { + "shared_parameters": { + "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, # t/h + }, + "cost_parameters": { + "skilled_labor_cost": 40.85, # 2022 USD/hr + "unskilled_labor_cost": 30.0, # 2022 USD/hr + }, + } + } + return tech_config + + +@fixture +def feedstock_availability_costs(): + feedstocks_dict = { + "electricity": { + "rated_capacity": 27000, # need 26949.46472431 kW + "units": "kW", + "price": 0.05802, # USD/kW + }, + "natural_gas": { + "rated_capacity": 1270, # need 1268.934 MMBtu at each timestep + "units": "MMBtu", + "price": 0.0, + }, + "reformer_catalyst": { + "rated_capacity": 0.001, # need 0.00056546 m**3/h + "units": "m**3", + "price": 0.0, + }, + "water": { + "rated_capacity": 40.0, # need 38.71049649 galUS/h + "units": "galUS", + "price": 1670.0, # cost is $0.441167535/t, equal to $1670.0004398318847/galUS + }, + "iron_ore": { + "rated_capacity": 263.75, + "units": "t/h", + "price": 27.5409 * 1e3, # USD/t + }, + } + return feedstocks_dict + + +def test_ng_dri_performance( + plant_config, ng_dri_base_config, feedstock_availability_costs, subtests +): + expected_pig_iron_annual_production_tpd = 3885.1917808219177 # t/d + + prob = om.Problem() + + iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) + == expected_pig_iron_annual_production_tpd + ) + + +def test_ng_dri_performance_cost( + plant_config, ng_dri_base_config, feedstock_availability_costs, subtests +): + expected_capex = 403808062.6981323 + expected_fixed_om = 60103761.59958463 + expected_pig_iron_annual_production_tpd = 3885.1917808219177 # t/d + + prob = om.Problem() + + iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + iron_dri_cost = NaturalGasIronPlantCostComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_dri_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + # difference from IronPlantCostComponent: + # IronPlantCostComponent: maintenance_materials is included in Fixed OpEx + # NaturalGasIronPlantCostComponent: maintenance_materials is the variable O&M + + # expected values from old model: + # labor_cost_annual_operation: 34892517.01488541, new is > difference of 23153.24035203 + # labor_cost_maintenance: 3180865.1610519094, new is > difference of 1416.57 + # labor_cost_admin_support: 9518345.54398433, new is > difference of 6142.454 + # property_tax_insurance: 8076161.253962645, new is > by 3596.66923786 + # maintenance_materials: 4435872.62570034 #exactly + # 365.242199 days/year vs 365 + # -> this fixed annual_labor_cost + # -> maintenance labor cost is fraction of CapEx + # -> admin labor cost depends on maintenance labor cost + # -> property om depends on CapEx + annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) + == expected_pig_iron_annual_production_tpd + ) + with subtests.test("CapEx"): + # expected difference of 0.044534% + assert pytest.approx(prob.get_val("cost.CapEx")[0], rel=1e-3) == expected_capex + with subtests.test("OpEx"): + assert ( + pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) + == expected_fixed_om + ) From e35b17b8c8ac372ad2b0f34e7b6ee4f44c5cc571 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:07:53 -0700 Subject: [PATCH 09/40] removed notes from rosner dri test --- h2integrate/converters/iron/test/test_rosner_dri.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index ecc3d44f1..278341c9e 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -153,17 +153,6 @@ def test_ng_dri_performance_cost( # IronPlantCostComponent: maintenance_materials is included in Fixed OpEx # NaturalGasIronPlantCostComponent: maintenance_materials is the variable O&M - # expected values from old model: - # labor_cost_annual_operation: 34892517.01488541, new is > difference of 23153.24035203 - # labor_cost_maintenance: 3180865.1610519094, new is > difference of 1416.57 - # labor_cost_admin_support: 9518345.54398433, new is > difference of 6142.454 - # property_tax_insurance: 8076161.253962645, new is > by 3596.66923786 - # maintenance_materials: 4435872.62570034 #exactly - # 365.242199 days/year vs 365 - # -> this fixed annual_labor_cost - # -> maintenance labor cost is fraction of CapEx - # -> admin labor cost depends on maintenance labor cost - # -> property om depends on CapEx annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) with subtests.test("Annual Pig Iron"): assert ( From c21a777ca8ae8b719288caa11962a68c1ead88ac Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:41:07 -0700 Subject: [PATCH 10/40] double checked water costs and unit conversions and cleaned up some small things --- examples/21_iron_mn_to_il/tech_config.yaml | 2 +- h2integrate/converters/iron/rosner_iron_cost_model.py | 2 +- h2integrate/converters/iron/rosner_iron_perf_model.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index b5e0fa79e..842c7e4be 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -87,7 +87,7 @@ technologies: rated_capacity: 40. # need 38.71049649 galUS/h cost_parameters: cost_year: 2022 - price: 1670.0 # cost is $0.441167535/t, equal to $1670.0004398318847/galUS + price: 0.0016700004398318847 # cost is USD0.441167535/t, converted to USD/gal annual_cost: 0. start_up_cost: 0. diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index 244db1d90..defc963cb 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -196,7 +196,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values ) labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day - annual_labor_cost = labor_cost_per_day * 365 # units.convert_units(1, "yr", "d") + annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") maintenance_labor_cost = ( total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values ) diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index d8e3037f7..77499c226 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -160,9 +160,9 @@ def format_coeff_df(self, coeff_df): # convert metric tonnes to kg (value*1e3) # convert mass to volume in cubic meters (value*1e3)/density # then convert cubic meters to liters 1e3*(value*1e3)/density - coeff_df.loc[i_update]["Value"] = ( - coeff_df.loc[i_update]["Value"] * 1e3 * 1e3 / self.config.water_density - ) + water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density + water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") + coeff_df.loc[i_update]["Value"] = water_volume_L old_unit = f"L/{capacity_unit}" old_unit = old_unit.replace("-LHV NG", "").replace("%", "percent") From ca55caa036f95683205b187450db18d3c07d662c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:57:39 -0700 Subject: [PATCH 11/40] renamed ng iron reduction models and configs --- .../converters/iron/rosner_iron_cost_model.py | 10 +++++----- .../converters/iron/rosner_iron_perf_model.py | 10 +++++----- .../converters/iron/test/test_rosner_dri.py | 14 ++++++++------ h2integrate/core/supported_models.py | 10 ++++++---- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/rosner_iron_cost_model.py index defc963cb..d53a87b72 100644 --- a/h2integrate/converters/iron/rosner_iron_cost_model.py +++ b/h2integrate/converters/iron/rosner_iron_cost_model.py @@ -11,7 +11,7 @@ @define -class RosnerIronPlantCostConfig(CostModelBaseConfig): +class NaturalGasIronReductionCostConfig(CostModelBaseConfig): """Configuration class for RosnerIronPlantCostComponent. Attributes: @@ -31,11 +31,11 @@ class RosnerIronPlantCostConfig(CostModelBaseConfig): unskilled_labor_cost: float = field(validator=gte_zero) -class NaturalGasIronPlantCostComponent(CostModelBaseClass): +class NaturalGasIronReductionPlantCostComponent(CostModelBaseClass): """_summary_ Attributes: - config (RosnerIronPlantCostConfig): configuration class + config (NaturalGasIronReductionCostConfig): configuration class coeff_df (pd.DataFrame): cost coefficient dataframe steel_to_iron_ratio (float): steel/pig iron ratio """ @@ -69,7 +69,7 @@ def setup(self): config_dict.update({"cost_year": target_dollar_year}) - self.config = RosnerIronPlantCostConfig.from_dict(config_dict, strict=False) + self.config = NaturalGasIronReductionCostConfig.from_dict(config_dict, strict=False) super().setup() @@ -235,7 +235,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): "skilled_labor_cost": 1.0, "unskilled_labor_cost": 1.0, } - iron_dri_perf = NaturalGasIronPlantCostComponent( + iron_dri_perf = NaturalGasIronReductionPlantCostComponent( plant_config=plant_config, tech_config={"model_inputs": {"shared_parameters": iron_dri_config_rosner_ng}}, driver_config={}, diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/rosner_iron_perf_model.py index 77499c226..dbd91c8af 100644 --- a/h2integrate/converters/iron/rosner_iron_perf_model.py +++ b/h2integrate/converters/iron/rosner_iron_perf_model.py @@ -9,8 +9,8 @@ @define -class RosnerIronPlantPerformanceConfig(BaseConfig): - """Configuration class for NaturalGasIronReudctionPlantPerformanceComponent. +class NaturalGasIronReductionPerformanceConfig(BaseConfig): + """Configuration class for NaturalGasIronReductionPlantPerformanceComponent. Attributes: pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant @@ -23,7 +23,7 @@ class RosnerIronPlantPerformanceConfig(BaseConfig): water_density: float = field(default=1000) # kg/m3 -class NaturalGasIronReudctionPlantPerformanceComponent(om.ExplicitComponent): +class NaturalGasIronReductionPlantPerformanceComponent(om.ExplicitComponent): def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) @@ -32,7 +32,7 @@ def initialize(self): def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.config = RosnerIronPlantPerformanceConfig.from_dict( + self.config = NaturalGasIronReductionPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=True, ) @@ -257,7 +257,7 @@ def compute(self, inputs, outputs): iron_dri_config_rosner_ng = { "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, } - iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( plant_config=plant_config, tech_config={"model_inputs": {"performance_parameters": iron_dri_config_rosner_ng}}, driver_config={}, diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index 278341c9e..13a297956 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -5,9 +5,11 @@ from h2integrate import EXAMPLE_DIR from h2integrate.core.inputs.validation import load_driver_yaml -from h2integrate.converters.iron.rosner_iron_cost_model import NaturalGasIronPlantCostComponent +from h2integrate.converters.iron.rosner_iron_cost_model import ( + NaturalGasIronReductionPlantCostComponent, +) from h2integrate.converters.iron.rosner_iron_perf_model import ( - NaturalGasIronReudctionPlantPerformanceComponent, + NaturalGasIronReductionPlantPerformanceComponent, ) @@ -92,7 +94,7 @@ def test_ng_dri_performance( prob = om.Problem() - iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( plant_config=plant_config, tech_config=ng_dri_base_config, driver_config={}, @@ -125,12 +127,12 @@ def test_ng_dri_performance_cost( prob = om.Problem() - iron_dri_perf = NaturalGasIronReudctionPlantPerformanceComponent( + iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( plant_config=plant_config, tech_config=ng_dri_base_config, driver_config={}, ) - iron_dri_cost = NaturalGasIronPlantCostComponent( + iron_dri_cost = NaturalGasIronReductionPlantCostComponent( plant_config=plant_config, tech_config=ng_dri_base_config, driver_config={}, @@ -151,7 +153,7 @@ def test_ng_dri_performance_cost( # difference from IronPlantCostComponent: # IronPlantCostComponent: maintenance_materials is included in Fixed OpEx - # NaturalGasIronPlantCostComponent: maintenance_materials is the variable O&M + # NaturalGasIronReductionPlantCostComponent: maintenance_materials is the variable O&M annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) with subtests.test("Annual Pig Iron"): diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 42a7c04c2..8d370a074 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -60,9 +60,11 @@ from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent -from h2integrate.converters.iron.rosner_iron_cost_model import NaturalGasIronPlantCostComponent +from h2integrate.converters.iron.rosner_iron_cost_model import ( + NaturalGasIronReductionPlantCostComponent, +) from h2integrate.converters.iron.rosner_iron_perf_model import ( - NaturalGasIronReudctionPlantPerformanceComponent, + NaturalGasIronReductionPlantPerformanceComponent, ) from h2integrate.converters.methanol.smr_methanol_plant import ( SMRMethanolPlantCostModel, @@ -178,8 +180,8 @@ "iron_plant_cost": IronPlantCostComponent, "iron_mine_performance_martin": MartinIronMinePerformanceComponent, # standalone model "iron_mine_cost_martin": MartinIronMineCostComponent, # standalone model - "ng_dri_performance_rosner": NaturalGasIronReudctionPlantPerformanceComponent, - "ng_dri_cost_rosner": NaturalGasIronPlantCostComponent, # standalone model + "ng_dri_performance_rosner": NaturalGasIronReductionPlantPerformanceComponent, + "ng_dri_cost_rosner": NaturalGasIronReductionPlantCostComponent, # standalone model "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, From 6a4b6de85c7da3e2ce050c294b09264db3249e0d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 23 Dec 2025 10:59:34 -0700 Subject: [PATCH 12/40] renamed ng iron dri files --- .../{rosner_iron_cost_model.py => ng_iron_dri_cost_model.py} | 0 .../{rosner_iron_perf_model.py => ng_iron_dri_perf_model.py} | 0 h2integrate/converters/iron/test/test_rosner_dri.py | 4 ++-- h2integrate/core/supported_models.py | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) rename h2integrate/converters/iron/{rosner_iron_cost_model.py => ng_iron_dri_cost_model.py} (100%) rename h2integrate/converters/iron/{rosner_iron_perf_model.py => ng_iron_dri_perf_model.py} (100%) diff --git a/h2integrate/converters/iron/rosner_iron_cost_model.py b/h2integrate/converters/iron/ng_iron_dri_cost_model.py similarity index 100% rename from h2integrate/converters/iron/rosner_iron_cost_model.py rename to h2integrate/converters/iron/ng_iron_dri_cost_model.py diff --git a/h2integrate/converters/iron/rosner_iron_perf_model.py b/h2integrate/converters/iron/ng_iron_dri_perf_model.py similarity index 100% rename from h2integrate/converters/iron/rosner_iron_perf_model.py rename to h2integrate/converters/iron/ng_iron_dri_perf_model.py diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index 13a297956..024a06616 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -5,10 +5,10 @@ from h2integrate import EXAMPLE_DIR from h2integrate.core.inputs.validation import load_driver_yaml -from h2integrate.converters.iron.rosner_iron_cost_model import ( +from h2integrate.converters.iron.ng_iron_dri_cost_model import ( NaturalGasIronReductionPlantCostComponent, ) -from h2integrate.converters.iron.rosner_iron_perf_model import ( +from h2integrate.converters.iron.ng_iron_dri_perf_model import ( NaturalGasIronReductionPlantPerformanceComponent, ) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 8d370a074..d9aa078af 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -60,10 +60,10 @@ from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent -from h2integrate.converters.iron.rosner_iron_cost_model import ( +from h2integrate.converters.iron.ng_iron_dri_cost_model import ( NaturalGasIronReductionPlantCostComponent, ) -from h2integrate.converters.iron.rosner_iron_perf_model import ( +from h2integrate.converters.iron.ng_iron_dri_perf_model import ( NaturalGasIronReductionPlantPerformanceComponent, ) from h2integrate.converters.methanol.smr_methanol_plant import ( From 25250ed092447174a1222bc8503e217257039fae Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:24:31 -0700 Subject: [PATCH 13/40] updated iron example tech config --- examples/21_iron_mn_to_il/tech_config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 842c7e4be..510cc5f4e 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -26,7 +26,7 @@ technologies: model_inputs: shared_parameters: feedstock_type: "crude_ore" - units: "kg/h" + units: "t/h" performance_parameters: rated_capacity: 2000. # kg/h cost_parameters: From f03ac12ebbc8c690432c6b87c1ea045944a30b0f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:47:04 -0700 Subject: [PATCH 14/40] added h2 dri cost and performance models --- .../converters/iron/h2_iron_dri_cost_model.py | 245 ++++++++++++++++ .../converters/iron/h2_iron_dri_perf_model.py | 262 ++++++++++++++++++ .../converters/iron/test/test_iron_plant.py | 38 +++ .../converters/iron/test/test_rosner_dri.py | 129 ++++++++- 4 files changed, 669 insertions(+), 5 deletions(-) create mode 100644 h2integrate/converters/iron/h2_iron_dri_cost_model.py create mode 100644 h2integrate/converters/iron/h2_iron_dri_perf_model.py diff --git a/h2integrate/converters/iron/h2_iron_dri_cost_model.py b/h2integrate/converters/iron/h2_iron_dri_cost_model.py new file mode 100644 index 000000000..650ab5b22 --- /dev/null +++ b/h2integrate/converters/iron/h2_iron_dri_cost_model.py @@ -0,0 +1,245 @@ +import pandas as pd +import openmdao.api as om +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs +from h2integrate.core.validators import gte_zero +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci + + +@define +class HydrogenIronReductionCostConfig(CostModelBaseConfig): + """Configuration class for RosnerIronPlantCostComponent. + + Attributes: + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + cost_year (int): This model uses 2022 as the base year for the cost model. + The cost year is updated based on `target_dollar_year` in the plant + config to adjust costs based on CPI/CEPCI within this model. This value + cannot be user added under `cost_parameters`. + skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr + unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr + """ + + pig_iron_production_rate_tonnes_per_hr: float = field() + cost_year: int = field(converter=int) + skilled_labor_cost: float = field(validator=gte_zero) + unskilled_labor_cost: float = field(validator=gte_zero) + + +class HydrogenIronReductionPlantCostComponent(CostModelBaseClass): + """_summary_ + + Attributes: + config (NaturalGasIronReductionCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") + + if "cost_year" in config_dict: + msg = ( + "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " + "The cost year cannot be modified for this cost model. " + ) + raise ValueError(msg) + + target_dollar_year = self.options["plant_config"]["finance_parameters"][ + "cost_adjustment_parameters" + ]["target_dollar_year"] + + if target_dollar_year <= 2024 and target_dollar_year >= 2010: + # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment + target_dollar_year = target_dollar_year + + elif target_dollar_year < 2010: + # adjust costs from 2022 to 2010 using CP/CEPCI adjustment + target_dollar_year = 2010 + + elif target_dollar_year > 2024: + # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment + target_dollar_year = 2024 + + config_dict.update({"cost_year": target_dollar_year}) + + self.config = HydrogenIronReductionCostConfig.from_dict(config_dict, strict=False) + + super().setup() + + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + # shape=n_timesteps, + units="t/h", + desc="Pig ore production capacity", + ) + self.add_input( + "pig_iron_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Pig iron produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" + # rosner cost model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that values are adjusted to standard units + and units are compatible with OpenMDAO units. Also filter the dataframe to include + only the data necessary for natural gas DRI type. + + Args: + coeff_df (pd.DataFrame): cost coefficient dataframe. + + Returns: + pd.DataFrame: cost coefficient dataframe + """ + product = "h2_dri" + + perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + + perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) + perf_df = perf_df[perf_df["Product"] == product] + steel_plant_capacity = perf_df[perf_df["Name"] == "Steel Production"]["Model"].values[0] + iron_plant_capacity = perf_df[perf_df["Name"] == "Pig Iron Production"]["Model"].values[0] + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + self.steel_to_iron_ratio = steel_to_iron_ratio + + # only include data for the given product + + data_cols = ["Type", "Coeff", "Unit", product] + coeff_df = coeff_df[data_cols] + + coeff_df = coeff_df.rename(columns={product: "Value"}) + + return coeff_df + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + # Calculate the capital cost for the item + dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) + + # Calculate + capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) + capital_items_df = self.coeff_df.loc[capital_items].copy() + capital_items_df = capital_items_df.reset_index(drop=False) + capital_items_df = capital_items_df.set_index( + keys=["Name", "Coeff"] + ) # costs are in USD/steel plant capacity + capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) + + ref_steel_plant_capacity_tpy = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "t/yr" + ) + + total_capex_usd = 0.0 + for item in capital_items: + if ( + capital_items_df.loc[item, "exp"]["Value"] > 0 + and capital_items_df.loc[item, "lin"]["Value"] > 0 + ): + capex = capital_items_df.loc[item, "lin"]["Value"] * ( + (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] + ) + total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) + + # Calculate Owners Costs + # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() + + # Calculate Fixed OpEx, includes: + fixed_items = list( + set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) + ) + fixed_items_df = self.coeff_df.loc[fixed_items].copy() + fixed_items_df = fixed_items_df.reset_index(drop=False) + fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) + fixed_items_df = fixed_items_df.drop(columns=["Type"]) + + property_om = ( + total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() + ) + + # Calculate labor costs + skilled_labor_cost = self.config.skilled_labor_cost * ( + fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 + ) + unskilled_labor_cost = self.config.unskilled_labor_cost * ( + fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 + ) + labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr + + ref_steel_plant_capacity_kgpd = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "kg/d" + ) + scaled_steel_plant_capacity_kgpd = ( + ref_steel_plant_capacity_kgpd + ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] + ) + + # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) + work_hrs_per_day_per_step = ( + scaled_steel_plant_capacity_kgpd + * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] + ) + + # employee-hours/day = employee-hours/day/process step * # of process steps + work_hrs_per_day = ( + work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values + ) + labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day + annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") + maintenance_labor_cost = ( + total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values + ) + admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ + "Value" + ].values * (annual_labor_cost + maintenance_labor_cost) + + total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost + tot_fixed_om = total_labor_related_cost + property_om + + # Calculate Variable O&M + varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ + "Value" + ].sum() # units are USD/mtpy steel + tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() + + # Adjust costs to target dollar year + tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) + tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) + tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) + + outputs["CapEx"] = tot_capex_adjusted + outputs["VarOpEx"] = tot_varopex_adjusted + outputs["OpEx"] = tot_fixed_om_adjusted + + +if __name__ == "__main__": + prob = om.Problem() + + plant_config = { + "plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}, + "finance_parameters": {"cost_adjustment_parameters": {"target_dollar_year": 2022}}, + } + iron_dri_config_rosner_ng = { + "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, + "skilled_labor_cost": 1.0, + "unskilled_labor_cost": 1.0, + } + iron_dri_perf = HydrogenIronReductionPlantCostComponent( + plant_config=plant_config, + tech_config={"model_inputs": {"shared_parameters": iron_dri_config_rosner_ng}}, + driver_config={}, + ) + prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) + prob.setup() + prob.run_model() diff --git a/h2integrate/converters/iron/h2_iron_dri_perf_model.py b/h2integrate/converters/iron/h2_iron_dri_perf_model.py new file mode 100644 index 000000000..2b3b7a6e4 --- /dev/null +++ b/h2integrate/converters/iron/h2_iron_dri_perf_model.py @@ -0,0 +1,262 @@ +import numpy as np +import pandas as pd +import openmdao.api as om +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import BaseConfig, merge_shared_inputs + + +@define +class HydrogenIronReductionPerformanceConfig(BaseConfig): + """Configuration class for NaturalGasIronReductionPlantPerformanceComponent. + + Attributes: + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + water_density (float): water density in kg/m3 to use to calculate water volume + from mass. Defaults to 1000.0 + """ + + pig_iron_production_rate_tonnes_per_hr: float = field() + water_density: float = field(default=1000) # kg/m3 + + +class HydrogenIronReductionPlantPerformanceComponent(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + self.config = HydrogenIronReductionPerformanceConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, + ) + + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + units="t/h", + desc="Rated pig iron production capacity", + ) + + feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "galUS", # "galUS/h" + "iron_ore": "t/h", + "electricity": "kW", + "hydrogen": "t/h", + # "reformer_catalyst": "(m**3)", # "(m**3)/h" + } + + # Add feedstock inputs and outputs, default to 0 --> set using feedstock component + for feedstock, feedstock_units in feedstocks_to_units.items(): + self.add_input( + f"{feedstock}_in", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} available for iron reduction", + ) + self.add_output( + f"{feedstock}_consumed", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} consumed for iron reduction", + ) + + # Default the pig iron demand input as the rated capacity + self.add_input( + "pig_iron_demand", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + shape=n_timesteps, + units="t/h", + desc="Pig iron demand for iron plant", + ) + + self.add_output( + "pig_iron_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Pig iron produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + # rosner dri performance model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + self.feedstock_to_units = feedstocks_to_units + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that feedstock values are converted to units of + feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units + and that units are compatible with OpenMDAO Units. Filter the dataframe to include + only the data necessary for Natural Gas reduction. + + Args: + coeff_df (pd.DataFrame): performance coefficient dataframe. + + Returns: + pd.DataFrame: filtered performance coefficient dataframe + """ + # only include data for the given product + coeff_df = coeff_df[coeff_df["Product"] == "h2_dri"] + data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] + coeff_df = coeff_df[data_cols] + coeff_df = coeff_df.rename(columns={"Model": "Value"}) + coeff_df = coeff_df[coeff_df["Value"] > 0] + coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data + + # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst + # h2_dri needs natural gas, water, hydrogen, iron ore. + steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] + iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] + + # capacity units units are mtpy + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} + + # efficiency units are % + # feedstock units are mt ore/mt steel, mt H2/mt steel, GJ-LHV NG/mt steel, + # mt H2O/mt steel, kW/mtpy steel + + # convert units to standardized units + old_units = list(set(coeff_df["Unit"].to_list())) + for ii, old_unit in enumerate(old_units): + if old_unit in unit_rename_mapper: + continue + if "steel" in old_unit: + feedstock_unit, capacity_unit = old_unit.split("/") + capacity_unit = capacity_unit.replace("steel", "").strip() + feedstock_unit = feedstock_unit.strip() + + i_update = coeff_df[coeff_df["Unit"] == old_unit].index + + # convert from feedstock per unit steel to feedstock per unit pig iron + coeff_df.loc[i_update]["Value"] = ( + coeff_df.loc[i_update]["Value"] * steel_to_iron_ratio + ) + + # update the "Type" to specify that units were changed to be per unit pig iron + coeff_df.loc[i_update]["Type"] = f"{coeff_df.loc[i_update]['Type'].values[0]}/iron" + + is_capacity_type = all( + k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() + ) + if capacity_unit == "mtpy" and not is_capacity_type: + # some feedstocks had misleading units, + # where 'kW/ mtpy steel' is actually 'kW/mt steel' + # NOTE: perhaps these ones need to be modified with the steel efficiency? + capacity_unit = "mt" + + old_unit = f"{feedstock_unit}/{capacity_unit}" + + if "H2O" in old_unit: + # convert metric tonnes to kg (value*1e3) + # convert mass to volume in cubic meters (value*1e3)/density + # then convert cubic meters to liters 1e3*(value*1e3)/density + water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density + water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") + coeff_df.loc[i_update]["Value"] = water_volume_L + old_unit = f"L/{capacity_unit}" + + old_unit = old_unit.replace("-LHV NG", "").replace("%", "percent") + old_unit = old_unit.replace("mt H2", "t").replace("MWh", "(MW*h)").replace(" ore", "") + old_unit = ( + old_unit.replace("mtpy", "(t/yr)").replace("mt", "t").replace("kW/", "(kW*h)/") + ) + # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' + # replace % with "unitless" + unit_rename_mapper.update({old_units[ii]: old_unit}) + coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) + + convert_units_dict = { + "GJ/t": "MMBtu/t", + "L/t": "galUS/t", + "(MW*h)/t": "(kW*h)/t", + "percent": "unitless", + } + # convert units to standard units and OpenMDAO compatible units + for i in coeff_df.index.to_list(): + if coeff_df.loc[i, "Unit"] in convert_units_dict: + current_units = coeff_df.loc[i, "Unit"] + desired_units = convert_units_dict[current_units] + coeff_df.loc[i, "Value"] = units.convert_units( + coeff_df.loc[i, "Value"], current_units, desired_units + ) + coeff_df.loc[i, "Unit"] = desired_units + # NOTE: not sure if percent is actually being converted to unitless + # but not big deal since percent is not used in feedstocks + return coeff_df + + def compute(self, inputs, outputs): + # get the feedstocks from + feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() + + # get the feedstock usage rates in units/t pig iron + feedstocks_usage_rates = { + "natural_gas": feedstocks[feedstocks["Unit"] == "MMBtu/t"]["Value"].sum(), + "water": feedstocks[feedstocks["Unit"] == "galUS/t"]["Value"].sum(), + "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), + "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"]["Value"].sum(), + "hydrogen": feedstocks[feedstocks["Name"] == "Hydrogen"]["Value"].sum(), + } + + # pig iron demand, saturated at maximum rated system capacity + pig_iron_demand = np.where( + inputs["pig_iron_demand"] > inputs["system_capacity"], + inputs["system_capacity"], + inputs["pig_iron_demand"], + ) + + # initialize an array of how much pig iron could be produced + # from the available feedstocks and the demand + pig_iron_from_feedstocks = np.zeros( + (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) + ) + # first entry is the pig iron demand + pig_iron_from_feedstocks[0] = pig_iron_demand + ii = 1 + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + # calculate max inputs/outputs based on rated capacity + max_feedstock_consumption = inputs["system_capacity"] * consumption_rate + # available feedstocks, saturated at maximum system feedstock consumption + feedstock_available = np.where( + inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, + max_feedstock_consumption, + inputs[f"{feedstock_type}_in"], + ) + # how much output can be produced from each of the feedstocks + pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate + ii += 1 + + # output is minimum between available feedstocks and output demand + pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) + outputs["pig_iron_out"] = pig_iron_production + + # feedstock consumption based on actual pig iron produced + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate + + +if __name__ == "__main__": + prob = om.Problem() + + plant_config = {"plant": {"simulation": {"n_timesteps": 8760}}} + iron_dri_config_rosner_ng = { + "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, + } + iron_dri_perf = HydrogenIronReductionPlantPerformanceComponent( + plant_config=plant_config, + tech_config={"model_inputs": {"performance_parameters": iron_dri_config_rosner_ng}}, + driver_config={}, + ) + prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) + prob.setup() + prob.run_model() diff --git a/h2integrate/converters/iron/test/test_iron_plant.py b/h2integrate/converters/iron/test/test_iron_plant.py index 86d87c670..d0558844f 100644 --- a/h2integrate/converters/iron/test/test_iron_plant.py +++ b/h2integrate/converters/iron/test/test_iron_plant.py @@ -101,3 +101,41 @@ def test_baseline_iron_dri_costs_rosner_ng( assert pytest.approx(prob.get_val("dri_cost.OpEx")[0], rel=1e-6) == expected_fixed_om with subtests.test("VarOpEx"): assert pytest.approx(prob.get_val("dri_cost.VarOpEx")[0], rel=1e-6) == expected_var_om + + +def test_baseline_iron_dri_costs_rosner_h2( + plant_config, driver_config, iron_dri_config_rosner_ng, subtests +): + expected_capex = 246546589.2914324 + expected_var_om = 888847481.352381 + expected_fixed_om = 53360873.348792635 + capacity = 3885.1917808219177 + + iron_dri_config_rosner_ng["model_inputs"]["shared_parameters"].update({"winning_type": "h2"}) + prob = om.Problem() + iron_dri_perf = IronPlantPerformanceComponent( + plant_config=plant_config, + tech_config=iron_dri_config_rosner_ng, + driver_config=driver_config, + ) + + iron_dri_cost = IronPlantCostComponent( + plant_config=plant_config, + tech_config=iron_dri_config_rosner_ng, + driver_config=driver_config, + ) + + prob.model.add_subsystem("dri_perf", iron_dri_perf, promotes=["*"]) + prob.model.add_subsystem("dri_cost", iron_dri_cost, promotes=["*"]) + prob.setup() + prob.run_model() + + annual_pig_iron = prob.get_val("dri_perf.total_pig_iron_produced", units="t/year") + with subtests.test("Annual Ore"): + assert pytest.approx(annual_pig_iron[0] / 365, rel=1e-3) == capacity + with subtests.test("CapEx"): + assert pytest.approx(prob.get_val("dri_cost.CapEx")[0], rel=1e-6) == expected_capex + with subtests.test("OpEx"): + assert pytest.approx(prob.get_val("dri_cost.OpEx")[0], rel=1e-6) == expected_fixed_om + with subtests.test("VarOpEx"): + assert pytest.approx(prob.get_val("dri_cost.VarOpEx")[0], rel=1e-6) == expected_var_om diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index 024a06616..dc5271c46 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -5,6 +5,12 @@ from h2integrate import EXAMPLE_DIR from h2integrate.core.inputs.validation import load_driver_yaml +from h2integrate.converters.iron.h2_iron_dri_cost_model import ( + HydrogenIronReductionPlantCostComponent, +) +from h2integrate.converters.iron.h2_iron_dri_perf_model import ( + HydrogenIronReductionPlantPerformanceComponent, +) from h2integrate.converters.iron.ng_iron_dri_cost_model import ( NaturalGasIronReductionPlantCostComponent, ) @@ -56,7 +62,7 @@ def ng_dri_base_config(): @fixture -def feedstock_availability_costs(): +def ng_feedstock_availability_costs(): feedstocks_dict = { "electricity": { "rated_capacity": 27000, # need 26949.46472431 kW @@ -87,8 +93,40 @@ def feedstock_availability_costs(): return feedstocks_dict +@fixture +def h2_feedstock_availability_costs(): + feedstocks_dict = { + "electricity": { + "rated_capacity": 19000, # need 18947.258036729818 kW + "units": "kW", + "price": 0.05802, # USD/kW TODO: update + }, + "natural_gas": { + "rated_capacity": 96.0, # need 95.4810143 MMBtu at each timestep + "units": "MMBtu", + "price": 0.0, + }, + "hydrogen": { + "rated_capacity": 11.0, # need 10.677802077625572 t/h + "units": "t/h", + "price": 0.0, + }, + "water": { + "rated_capacity": 30.0, # need 27.4952280 galUS/h + "units": "galUS", + "price": 1670.0, # TODO: update cost is $0.441167535/t, equal to $1670.0004398318847/galUS + }, + "iron_ore": { + "rated_capacity": 263.75, # need 263.75110 t/h + "units": "t/h", + "price": 27.5409 * 1e3, # USD/t TODO: update + }, + } + return feedstocks_dict + + def test_ng_dri_performance( - plant_config, ng_dri_base_config, feedstock_availability_costs, subtests + plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests ): expected_pig_iron_annual_production_tpd = 3885.1917808219177 # t/d @@ -102,7 +140,7 @@ def test_ng_dri_performance( prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) prob.setup() - for feedstock_name, feedstock_info in feedstock_availability_costs.items(): + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): prob.set_val( f"perf.{feedstock_name}_in", feedstock_info["rated_capacity"], @@ -119,7 +157,7 @@ def test_ng_dri_performance( def test_ng_dri_performance_cost( - plant_config, ng_dri_base_config, feedstock_availability_costs, subtests + plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests ): expected_capex = 403808062.6981323 expected_fixed_om = 60103761.59958463 @@ -142,7 +180,7 @@ def test_ng_dri_performance_cost( prob.model.add_subsystem("cost", iron_dri_cost, promotes=["*"]) prob.setup() - for feedstock_name, feedstock_info in feedstock_availability_costs.items(): + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): prob.set_val( f"perf.{feedstock_name}_in", feedstock_info["rated_capacity"], @@ -169,3 +207,84 @@ def test_ng_dri_performance_cost( pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) == expected_fixed_om ) + + +def test_h2_dri_performance( + plant_config, ng_dri_base_config, h2_feedstock_availability_costs, subtests +): + expected_pig_iron_annual_production_tpd = 3885.1917808219177 # t/d + + prob = om.Problem() + + iron_dri_perf = HydrogenIronReductionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in h2_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) + == expected_pig_iron_annual_production_tpd + ) + + +def test_h2_dri_performance_cost( + plant_config, ng_dri_base_config, h2_feedstock_availability_costs, subtests +): + expected_capex = 246546589.2914324 + expected_fixed_om = 53360873.348792635 + + expected_pig_iron_annual_production_tpd = 3885.1917808219177 # t/d + + prob = om.Problem() + + iron_dri_perf = HydrogenIronReductionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + iron_dri_cost = HydrogenIronReductionPlantCostComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_dri_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in h2_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) + == expected_pig_iron_annual_production_tpd + ) + with subtests.test("CapEx"): + # expected difference of 0.044534% + assert pytest.approx(prob.get_val("cost.CapEx")[0], rel=1e-3) == expected_capex + with subtests.test("OpEx"): + assert ( + pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) + == expected_fixed_om + ) From ad3f14bb30d0e140fda67143748477c1c7283ec5 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:51:07 -0700 Subject: [PATCH 15/40] added h2 dri models to supported models --- h2integrate/core/supported_models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index d9aa078af..9fe2628a8 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -58,6 +58,12 @@ from h2integrate.converters.solar.atb_res_com_pv_cost import ATBResComPVCostModel from h2integrate.converters.solar.atb_utility_pv_cost import ATBUtilityPVCostModel from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource +from h2integrate.converters.iron.h2_iron_dri_cost_model import ( + HydrogenIronReductionPlantCostComponent, +) +from h2integrate.converters.iron.h2_iron_dri_perf_model import ( + HydrogenIronReductionPlantPerformanceComponent, +) from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent from h2integrate.converters.iron.ng_iron_dri_cost_model import ( @@ -182,6 +188,8 @@ "iron_mine_cost_martin": MartinIronMineCostComponent, # standalone model "ng_dri_performance_rosner": NaturalGasIronReductionPlantPerformanceComponent, "ng_dri_cost_rosner": NaturalGasIronReductionPlantCostComponent, # standalone model + "h2_dri_performance_rosner": HydrogenIronReductionPlantPerformanceComponent, + "h2_dri_cost_rosner": HydrogenIronReductionPlantCostComponent, "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, From a33bffcff564c3d2497edc768f0db1914f2f4170 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 24 Dec 2025 11:04:22 -0600 Subject: [PATCH 16/40] update test values for wombat version bump --- CHANGELOG.md | 1 + examples/test/test_all_examples.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5545c099e..15d4d403b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 0.5.x [TBD] - Updates models for NumPy version 2.4.0 +- Update test values for WOMBAT update to 0.13.0 ## 0.5.1 [December 18, 2025] diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index c25b45ff8..9d216777d 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -801,9 +801,9 @@ def test_electrolyzer_om_example(subtests): with subtests.test("Check LCOE"): assert pytest.approx(lcoe, rel=1e-4) == 39.98869 with subtests.test("Check LCOH with lcoh_financials"): - assert pytest.approx(lcoh_with_lcoh_finance, rel=1e-4) == 13.0954678 + assert pytest.approx(lcoh_with_lcoh_finance, rel=1e-4) == 13.0858012 with subtests.test("Check LCOH with lcoe_financials"): - assert pytest.approx(lcoh_with_lcoe_finance, rel=1e-4) == 8.00321771 + assert pytest.approx(lcoh_with_lcoe_finance, rel=1e-4) == 7.9935907 def test_wombat_electrolyzer_example(subtests): From 59ed5e0c077e72268a05621c177b87fce61b71b2 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 24 Dec 2025 11:28:35 -0600 Subject: [PATCH 17/40] update unit test --- h2integrate/converters/hydrogen/test/test_wombat_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2integrate/converters/hydrogen/test/test_wombat_model.py b/h2integrate/converters/hydrogen/test/test_wombat_model.py index bc71d74d7..d675f9dcf 100644 --- a/h2integrate/converters/hydrogen/test/test_wombat_model.py +++ b/h2integrate/converters/hydrogen/test/test_wombat_model.py @@ -56,7 +56,7 @@ def test_wombat_model_outputs(subtests): with subtests.test("CapEx"): assert prob["CapEx"] == approx(51800000.0, rel=1e-2) with subtests.test("OpEx"): - assert prob["OpEx"] == approx(1015899.3984, rel=1e-2) + assert prob["OpEx"] == approx(1004502.975183, rel=1e-2) with subtests.test("percent_hydrogen_lost"): assert prob["percent_hydrogen_lost"] == approx(1.50371, rel=1e-2) with subtests.test("electrolyzer_availability"): From d356c237547d6df1cd652c37e45d133d049f609a Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 24 Dec 2025 14:20:36 -0600 Subject: [PATCH 18/40] update docstrings --- h2integrate/converters/iron/h2_iron_dri_cost_model.py | 7 ++++--- h2integrate/converters/iron/h2_iron_dri_perf_model.py | 5 ++--- h2integrate/converters/iron/ng_iron_dri_cost_model.py | 6 +++--- h2integrate/core/supported_models.py | 8 ++++++++ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/h2integrate/converters/iron/h2_iron_dri_cost_model.py b/h2integrate/converters/iron/h2_iron_dri_cost_model.py index 650ab5b22..8d9e16790 100644 --- a/h2integrate/converters/iron/h2_iron_dri_cost_model.py +++ b/h2integrate/converters/iron/h2_iron_dri_cost_model.py @@ -12,7 +12,7 @@ @define class HydrogenIronReductionCostConfig(CostModelBaseConfig): - """Configuration class for RosnerIronPlantCostComponent. + """Configuration class for HydrogenIronReductionPlantCostComponent. Attributes: pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant @@ -32,10 +32,11 @@ class HydrogenIronReductionCostConfig(CostModelBaseConfig): class HydrogenIronReductionPlantCostComponent(CostModelBaseClass): - """_summary_ + """Cost component for hydrogen-based direct reduced iron (DRI) plant + using the Rosner cost model. Attributes: - config (NaturalGasIronReductionCostConfig): configuration class + config (HydrogenIronReductionCostConfig): configuration class coeff_df (pd.DataFrame): cost coefficient dataframe steel_to_iron_ratio (float): steel/pig iron ratio """ diff --git a/h2integrate/converters/iron/h2_iron_dri_perf_model.py b/h2integrate/converters/iron/h2_iron_dri_perf_model.py index 2b3b7a6e4..81b4befed 100644 --- a/h2integrate/converters/iron/h2_iron_dri_perf_model.py +++ b/h2integrate/converters/iron/h2_iron_dri_perf_model.py @@ -10,7 +10,7 @@ @define class HydrogenIronReductionPerformanceConfig(BaseConfig): - """Configuration class for NaturalGasIronReductionPlantPerformanceComponent. + """Configuration class for HydrogenIronReductionPlantPerformanceComponent. Attributes: pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant @@ -50,7 +50,6 @@ def setup(self): "iron_ore": "t/h", "electricity": "kW", "hydrogen": "t/h", - # "reformer_catalyst": "(m**3)", # "(m**3)/h" } # Add feedstock inputs and outputs, default to 0 --> set using feedstock component @@ -97,7 +96,7 @@ def format_coeff_df(self, coeff_df): """Update the coefficient dataframe such that feedstock values are converted to units of feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units and that units are compatible with OpenMDAO Units. Filter the dataframe to include - only the data necessary for Natural Gas reduction. + only the data necessary for hydrogen reduction. Args: coeff_df (pd.DataFrame): performance coefficient dataframe. diff --git a/h2integrate/converters/iron/ng_iron_dri_cost_model.py b/h2integrate/converters/iron/ng_iron_dri_cost_model.py index d53a87b72..92c0b91d0 100644 --- a/h2integrate/converters/iron/ng_iron_dri_cost_model.py +++ b/h2integrate/converters/iron/ng_iron_dri_cost_model.py @@ -12,7 +12,7 @@ @define class NaturalGasIronReductionCostConfig(CostModelBaseConfig): - """Configuration class for RosnerIronPlantCostComponent. + """Configuration class for NaturalGasIronReductionPlantCostComponent. Attributes: pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant @@ -32,7 +32,8 @@ class NaturalGasIronReductionCostConfig(CostModelBaseConfig): class NaturalGasIronReductionPlantCostComponent(CostModelBaseClass): - """_summary_ + """Calculates the capital and operating costs for a natural gas-based direct + iron reduction plant using the Rosner cost model. Attributes: config (NaturalGasIronReductionCostConfig): configuration class @@ -76,7 +77,6 @@ def setup(self): self.add_input( "system_capacity", val=self.config.pig_iron_production_rate_tonnes_per_hr, - # shape=n_timesteps, units="t/h", desc="Pig ore production capacity", ) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index d9aa078af..2c390e404 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -58,6 +58,12 @@ from h2integrate.converters.solar.atb_res_com_pv_cost import ATBResComPVCostModel from h2integrate.converters.solar.atb_utility_pv_cost import ATBUtilityPVCostModel from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource +from h2integrate.converters.iron.h2_iron_dri_cost_model import ( + HydrogenIronReductionPlantCostComponent, +) +from h2integrate.converters.iron.h2_iron_dri_perf_model import ( + HydrogenIronReductionPlantPerformanceComponent, +) from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent from h2integrate.converters.iron.ng_iron_dri_cost_model import ( @@ -182,6 +188,8 @@ "iron_mine_cost_martin": MartinIronMineCostComponent, # standalone model "ng_dri_performance_rosner": NaturalGasIronReductionPlantPerformanceComponent, "ng_dri_cost_rosner": NaturalGasIronReductionPlantCostComponent, # standalone model + "h2_dri_performance_rosner": HydrogenIronReductionPlantPerformanceComponent, + "h2_dri_cost_rosner": HydrogenIronReductionPlantCostComponent, # standalone model "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, From 868603a6b042146c6e8e6516746df318492b9cbf Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 24 Dec 2025 16:21:13 -0600 Subject: [PATCH 19/40] update feedstocks to be per ton pig iron --- .../converters/iron/h2_iron_dri_perf_model.py | 24 ++++++++++++------- .../converters/iron/test/test_rosner_dri.py | 11 +++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/h2integrate/converters/iron/h2_iron_dri_perf_model.py b/h2integrate/converters/iron/h2_iron_dri_perf_model.py index 81b4befed..7b6f7bb45 100644 --- a/h2integrate/converters/iron/h2_iron_dri_perf_model.py +++ b/h2integrate/converters/iron/h2_iron_dri_perf_model.py @@ -138,12 +138,12 @@ def format_coeff_df(self, coeff_df): i_update = coeff_df[coeff_df["Unit"] == old_unit].index # convert from feedstock per unit steel to feedstock per unit pig iron - coeff_df.loc[i_update]["Value"] = ( - coeff_df.loc[i_update]["Value"] * steel_to_iron_ratio + coeff_df.loc[i_update, "Value"] = ( + coeff_df.loc[i_update, "Value"] * steel_to_iron_ratio ) # update the "Type" to specify that units were changed to be per unit pig iron - coeff_df.loc[i_update]["Type"] = f"{coeff_df.loc[i_update]['Type'].values[0]}/iron" + coeff_df.loc[i_update, "Type"] = f"{coeff_df.loc[i_update, 'Type'].values[0]}/iron" is_capacity_type = all( k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() @@ -196,15 +196,21 @@ def format_coeff_df(self, coeff_df): def compute(self, inputs, outputs): # get the feedstocks from - feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() + feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed/iron"].copy() # get the feedstock usage rates in units/t pig iron feedstocks_usage_rates = { - "natural_gas": feedstocks[feedstocks["Unit"] == "MMBtu/t"]["Value"].sum(), - "water": feedstocks[feedstocks["Unit"] == "galUS/t"]["Value"].sum(), - "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), - "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"]["Value"].sum(), - "hydrogen": feedstocks[feedstocks["Name"] == "Hydrogen"]["Value"].sum(), + "natural_gas": feedstocks[feedstocks["Name"] == "Natural Gas"][ + "Value" + ].sum(), # MMBtu/t + "water": feedstocks[feedstocks["Name"] == "Raw Water Withdrawal"][ + "Value" + ].sum(), # galUS/t + "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), # t/t + "electricity": feedstocks[feedstocks["Name"] == "Electricity"][ + "Value" + ].sum(), # (kW*h)/t + "hydrogen": feedstocks[feedstocks["Name"] == "Hydrogen"]["Value"].sum(), # t/t } # pig iron demand, saturated at maximum rated system capacity diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index dc5271c46..28e913298 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -97,27 +97,28 @@ def ng_feedstock_availability_costs(): def h2_feedstock_availability_costs(): feedstocks_dict = { "electricity": { - "rated_capacity": 19000, # need 18947.258036729818 kW + # (1418095/8760)t-pig_iron/h * 98.17925 kWh/t-pig_iron = 15893.55104 kW + "rated_capacity": 16000, # need 15893.55104 kW "units": "kW", "price": 0.05802, # USD/kW TODO: update }, "natural_gas": { - "rated_capacity": 96.0, # need 95.4810143 MMBtu at each timestep + "rated_capacity": 81.0, # need 80.101596 MMBtu at each timestep "units": "MMBtu", "price": 0.0, }, "hydrogen": { - "rated_capacity": 11.0, # need 10.677802077625572 t/h + "rated_capacity": 9.0, # need 8.957895917766855 t/h "units": "t/h", "price": 0.0, }, "water": { - "rated_capacity": 30.0, # need 27.4952280 galUS/h + "rated_capacity": 24.0, # need 23.0664878077501 galUS/h "units": "galUS", "price": 1670.0, # TODO: update cost is $0.441167535/t, equal to $1670.0004398318847/galUS }, "iron_ore": { - "rated_capacity": 263.75, # need 263.75110 t/h + "rated_capacity": 221.5, # need 221.2679060330504 t/h "units": "t/h", "price": 27.5409 * 1e3, # USD/t TODO: update }, From 76949eca386cc16abd9a7e136dadc8bbe8465d7c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 10:32:56 -0700 Subject: [PATCH 20/40] refactored DRI performance and cost models to share baseclass --- .../converters/iron/h2_iron_dri_cost_model.py | 246 --------- .../converters/iron/h2_iron_dri_perf_model.py | 267 ---------- h2integrate/converters/iron/iron_dri_base.py | 472 ++++++++++++++++++ h2integrate/converters/iron/iron_dri_plant.py | 83 +++ .../converters/iron/ng_iron_dri_cost_model.py | 245 --------- .../converters/iron/ng_iron_dri_perf_model.py | 267 ---------- .../converters/iron/test/test_rosner_dri.py | 10 +- h2integrate/core/supported_models.py | 18 +- 8 files changed, 563 insertions(+), 1045 deletions(-) delete mode 100644 h2integrate/converters/iron/h2_iron_dri_cost_model.py delete mode 100644 h2integrate/converters/iron/h2_iron_dri_perf_model.py create mode 100644 h2integrate/converters/iron/iron_dri_base.py create mode 100644 h2integrate/converters/iron/iron_dri_plant.py delete mode 100644 h2integrate/converters/iron/ng_iron_dri_cost_model.py delete mode 100644 h2integrate/converters/iron/ng_iron_dri_perf_model.py diff --git a/h2integrate/converters/iron/h2_iron_dri_cost_model.py b/h2integrate/converters/iron/h2_iron_dri_cost_model.py deleted file mode 100644 index 8d9e16790..000000000 --- a/h2integrate/converters/iron/h2_iron_dri_cost_model.py +++ /dev/null @@ -1,246 +0,0 @@ -import pandas as pd -import openmdao.api as om -from attrs import field, define -from openmdao.utils import units - -from h2integrate import ROOT_DIR -from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs -from h2integrate.core.validators import gte_zero -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci - - -@define -class HydrogenIronReductionCostConfig(CostModelBaseConfig): - """Configuration class for HydrogenIronReductionPlantCostComponent. - - Attributes: - pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant - in units of metric tonnes of pig iron produced per hour. - cost_year (int): This model uses 2022 as the base year for the cost model. - The cost year is updated based on `target_dollar_year` in the plant - config to adjust costs based on CPI/CEPCI within this model. This value - cannot be user added under `cost_parameters`. - skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr - unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr - """ - - pig_iron_production_rate_tonnes_per_hr: float = field() - cost_year: int = field(converter=int) - skilled_labor_cost: float = field(validator=gte_zero) - unskilled_labor_cost: float = field(validator=gte_zero) - - -class HydrogenIronReductionPlantCostComponent(CostModelBaseClass): - """Cost component for hydrogen-based direct reduced iron (DRI) plant - using the Rosner cost model. - - Attributes: - config (HydrogenIronReductionCostConfig): configuration class - coeff_df (pd.DataFrame): cost coefficient dataframe - steel_to_iron_ratio (float): steel/pig iron ratio - """ - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") - - if "cost_year" in config_dict: - msg = ( - "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " - "The cost year cannot be modified for this cost model. " - ) - raise ValueError(msg) - - target_dollar_year = self.options["plant_config"]["finance_parameters"][ - "cost_adjustment_parameters" - ]["target_dollar_year"] - - if target_dollar_year <= 2024 and target_dollar_year >= 2010: - # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment - target_dollar_year = target_dollar_year - - elif target_dollar_year < 2010: - # adjust costs from 2022 to 2010 using CP/CEPCI adjustment - target_dollar_year = 2010 - - elif target_dollar_year > 2024: - # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment - target_dollar_year = 2024 - - config_dict.update({"cost_year": target_dollar_year}) - - self.config = HydrogenIronReductionCostConfig.from_dict(config_dict, strict=False) - - super().setup() - - self.add_input( - "system_capacity", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - # shape=n_timesteps, - units="t/h", - desc="Pig ore production capacity", - ) - self.add_input( - "pig_iron_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Pig iron produced", - ) - - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" - # rosner cost model - coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df) - - def format_coeff_df(self, coeff_df): - """Update the coefficient dataframe such that values are adjusted to standard units - and units are compatible with OpenMDAO units. Also filter the dataframe to include - only the data necessary for natural gas DRI type. - - Args: - coeff_df (pd.DataFrame): cost coefficient dataframe. - - Returns: - pd.DataFrame: cost coefficient dataframe - """ - product = "h2_dri" - - perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" - - perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) - perf_df = perf_df[perf_df["Product"] == product] - steel_plant_capacity = perf_df[perf_df["Name"] == "Steel Production"]["Model"].values[0] - iron_plant_capacity = perf_df[perf_df["Name"] == "Pig Iron Production"]["Model"].values[0] - steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year - self.steel_to_iron_ratio = steel_to_iron_ratio - - # only include data for the given product - - data_cols = ["Type", "Coeff", "Unit", product] - coeff_df = coeff_df[data_cols] - - coeff_df = coeff_df.rename(columns={product: "Value"}) - - return coeff_df - - def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - # Calculate the capital cost for the item - dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) - - # Calculate - capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) - capital_items_df = self.coeff_df.loc[capital_items].copy() - capital_items_df = capital_items_df.reset_index(drop=False) - capital_items_df = capital_items_df.set_index( - keys=["Name", "Coeff"] - ) # costs are in USD/steel plant capacity - capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) - - ref_steel_plant_capacity_tpy = units.convert_units( - inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "t/yr" - ) - - total_capex_usd = 0.0 - for item in capital_items: - if ( - capital_items_df.loc[item, "exp"]["Value"] > 0 - and capital_items_df.loc[item, "lin"]["Value"] > 0 - ): - capex = capital_items_df.loc[item, "lin"]["Value"] * ( - (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] - ) - total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) - - # Calculate Owners Costs - # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() - - # Calculate Fixed OpEx, includes: - fixed_items = list( - set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) - ) - fixed_items_df = self.coeff_df.loc[fixed_items].copy() - fixed_items_df = fixed_items_df.reset_index(drop=False) - fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) - fixed_items_df = fixed_items_df.drop(columns=["Type"]) - - property_om = ( - total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() - ) - - # Calculate labor costs - skilled_labor_cost = self.config.skilled_labor_cost * ( - fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 - ) - unskilled_labor_cost = self.config.unskilled_labor_cost * ( - fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 - ) - labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr - - ref_steel_plant_capacity_kgpd = units.convert_units( - inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "kg/d" - ) - scaled_steel_plant_capacity_kgpd = ( - ref_steel_plant_capacity_kgpd - ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] - ) - - # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) - work_hrs_per_day_per_step = ( - scaled_steel_plant_capacity_kgpd - * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] - ) - - # employee-hours/day = employee-hours/day/process step * # of process steps - work_hrs_per_day = ( - work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values - ) - labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day - annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") - maintenance_labor_cost = ( - total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values - ) - admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ - "Value" - ].values * (annual_labor_cost + maintenance_labor_cost) - - total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost - tot_fixed_om = total_labor_related_cost + property_om - - # Calculate Variable O&M - varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ - "Value" - ].sum() # units are USD/mtpy steel - tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() - - # Adjust costs to target dollar year - tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) - tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) - tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) - - outputs["CapEx"] = tot_capex_adjusted - outputs["VarOpEx"] = tot_varopex_adjusted - outputs["OpEx"] = tot_fixed_om_adjusted - - -if __name__ == "__main__": - prob = om.Problem() - - plant_config = { - "plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}, - "finance_parameters": {"cost_adjustment_parameters": {"target_dollar_year": 2022}}, - } - iron_dri_config_rosner_ng = { - "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - "skilled_labor_cost": 1.0, - "unskilled_labor_cost": 1.0, - } - iron_dri_perf = HydrogenIronReductionPlantCostComponent( - plant_config=plant_config, - tech_config={"model_inputs": {"shared_parameters": iron_dri_config_rosner_ng}}, - driver_config={}, - ) - prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) - prob.setup() - prob.run_model() diff --git a/h2integrate/converters/iron/h2_iron_dri_perf_model.py b/h2integrate/converters/iron/h2_iron_dri_perf_model.py deleted file mode 100644 index 7b6f7bb45..000000000 --- a/h2integrate/converters/iron/h2_iron_dri_perf_model.py +++ /dev/null @@ -1,267 +0,0 @@ -import numpy as np -import pandas as pd -import openmdao.api as om -from attrs import field, define -from openmdao.utils import units - -from h2integrate import ROOT_DIR -from h2integrate.core.utilities import BaseConfig, merge_shared_inputs - - -@define -class HydrogenIronReductionPerformanceConfig(BaseConfig): - """Configuration class for HydrogenIronReductionPlantPerformanceComponent. - - Attributes: - pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant - in units of metric tonnes of pig iron produced per hour. - water_density (float): water density in kg/m3 to use to calculate water volume - from mass. Defaults to 1000.0 - """ - - pig_iron_production_rate_tonnes_per_hr: float = field() - water_density: float = field(default=1000) # kg/m3 - - -class HydrogenIronReductionPlantPerformanceComponent(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - - self.config = HydrogenIronReductionPerformanceConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), - strict=True, - ) - - self.add_input( - "system_capacity", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - units="t/h", - desc="Rated pig iron production capacity", - ) - - feedstocks_to_units = { - "natural_gas": "MMBtu", - "water": "galUS", # "galUS/h" - "iron_ore": "t/h", - "electricity": "kW", - "hydrogen": "t/h", - } - - # Add feedstock inputs and outputs, default to 0 --> set using feedstock component - for feedstock, feedstock_units in feedstocks_to_units.items(): - self.add_input( - f"{feedstock}_in", - val=0.0, - shape=n_timesteps, - units=feedstock_units, - desc=f"{feedstock} available for iron reduction", - ) - self.add_output( - f"{feedstock}_consumed", - val=0.0, - shape=n_timesteps, - units=feedstock_units, - desc=f"{feedstock} consumed for iron reduction", - ) - - # Default the pig iron demand input as the rated capacity - self.add_input( - "pig_iron_demand", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - shape=n_timesteps, - units="t/h", - desc="Pig iron demand for iron plant", - ) - - self.add_output( - "pig_iron_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Pig iron produced", - ) - - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" - # rosner dri performance model - coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df) - self.feedstock_to_units = feedstocks_to_units - - def format_coeff_df(self, coeff_df): - """Update the coefficient dataframe such that feedstock values are converted to units of - feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units - and that units are compatible with OpenMDAO Units. Filter the dataframe to include - only the data necessary for hydrogen reduction. - - Args: - coeff_df (pd.DataFrame): performance coefficient dataframe. - - Returns: - pd.DataFrame: filtered performance coefficient dataframe - """ - # only include data for the given product - coeff_df = coeff_df[coeff_df["Product"] == "h2_dri"] - data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] - coeff_df = coeff_df[data_cols] - coeff_df = coeff_df.rename(columns={"Model": "Value"}) - coeff_df = coeff_df[coeff_df["Value"] > 0] - coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data - - # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst - # h2_dri needs natural gas, water, hydrogen, iron ore. - steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] - iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] - - # capacity units units are mtpy - steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year - unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} - - # efficiency units are % - # feedstock units are mt ore/mt steel, mt H2/mt steel, GJ-LHV NG/mt steel, - # mt H2O/mt steel, kW/mtpy steel - - # convert units to standardized units - old_units = list(set(coeff_df["Unit"].to_list())) - for ii, old_unit in enumerate(old_units): - if old_unit in unit_rename_mapper: - continue - if "steel" in old_unit: - feedstock_unit, capacity_unit = old_unit.split("/") - capacity_unit = capacity_unit.replace("steel", "").strip() - feedstock_unit = feedstock_unit.strip() - - i_update = coeff_df[coeff_df["Unit"] == old_unit].index - - # convert from feedstock per unit steel to feedstock per unit pig iron - coeff_df.loc[i_update, "Value"] = ( - coeff_df.loc[i_update, "Value"] * steel_to_iron_ratio - ) - - # update the "Type" to specify that units were changed to be per unit pig iron - coeff_df.loc[i_update, "Type"] = f"{coeff_df.loc[i_update, 'Type'].values[0]}/iron" - - is_capacity_type = all( - k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() - ) - if capacity_unit == "mtpy" and not is_capacity_type: - # some feedstocks had misleading units, - # where 'kW/ mtpy steel' is actually 'kW/mt steel' - # NOTE: perhaps these ones need to be modified with the steel efficiency? - capacity_unit = "mt" - - old_unit = f"{feedstock_unit}/{capacity_unit}" - - if "H2O" in old_unit: - # convert metric tonnes to kg (value*1e3) - # convert mass to volume in cubic meters (value*1e3)/density - # then convert cubic meters to liters 1e3*(value*1e3)/density - water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density - water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") - coeff_df.loc[i_update]["Value"] = water_volume_L - old_unit = f"L/{capacity_unit}" - - old_unit = old_unit.replace("-LHV NG", "").replace("%", "percent") - old_unit = old_unit.replace("mt H2", "t").replace("MWh", "(MW*h)").replace(" ore", "") - old_unit = ( - old_unit.replace("mtpy", "(t/yr)").replace("mt", "t").replace("kW/", "(kW*h)/") - ) - # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' - # replace % with "unitless" - unit_rename_mapper.update({old_units[ii]: old_unit}) - coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) - - convert_units_dict = { - "GJ/t": "MMBtu/t", - "L/t": "galUS/t", - "(MW*h)/t": "(kW*h)/t", - "percent": "unitless", - } - # convert units to standard units and OpenMDAO compatible units - for i in coeff_df.index.to_list(): - if coeff_df.loc[i, "Unit"] in convert_units_dict: - current_units = coeff_df.loc[i, "Unit"] - desired_units = convert_units_dict[current_units] - coeff_df.loc[i, "Value"] = units.convert_units( - coeff_df.loc[i, "Value"], current_units, desired_units - ) - coeff_df.loc[i, "Unit"] = desired_units - # NOTE: not sure if percent is actually being converted to unitless - # but not big deal since percent is not used in feedstocks - return coeff_df - - def compute(self, inputs, outputs): - # get the feedstocks from - feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed/iron"].copy() - - # get the feedstock usage rates in units/t pig iron - feedstocks_usage_rates = { - "natural_gas": feedstocks[feedstocks["Name"] == "Natural Gas"][ - "Value" - ].sum(), # MMBtu/t - "water": feedstocks[feedstocks["Name"] == "Raw Water Withdrawal"][ - "Value" - ].sum(), # galUS/t - "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), # t/t - "electricity": feedstocks[feedstocks["Name"] == "Electricity"][ - "Value" - ].sum(), # (kW*h)/t - "hydrogen": feedstocks[feedstocks["Name"] == "Hydrogen"]["Value"].sum(), # t/t - } - - # pig iron demand, saturated at maximum rated system capacity - pig_iron_demand = np.where( - inputs["pig_iron_demand"] > inputs["system_capacity"], - inputs["system_capacity"], - inputs["pig_iron_demand"], - ) - - # initialize an array of how much pig iron could be produced - # from the available feedstocks and the demand - pig_iron_from_feedstocks = np.zeros( - (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) - ) - # first entry is the pig iron demand - pig_iron_from_feedstocks[0] = pig_iron_demand - ii = 1 - for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): - # calculate max inputs/outputs based on rated capacity - max_feedstock_consumption = inputs["system_capacity"] * consumption_rate - # available feedstocks, saturated at maximum system feedstock consumption - feedstock_available = np.where( - inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, - max_feedstock_consumption, - inputs[f"{feedstock_type}_in"], - ) - # how much output can be produced from each of the feedstocks - pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate - ii += 1 - - # output is minimum between available feedstocks and output demand - pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) - outputs["pig_iron_out"] = pig_iron_production - - # feedstock consumption based on actual pig iron produced - for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): - outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate - - -if __name__ == "__main__": - prob = om.Problem() - - plant_config = {"plant": {"simulation": {"n_timesteps": 8760}}} - iron_dri_config_rosner_ng = { - "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - } - iron_dri_perf = HydrogenIronReductionPlantPerformanceComponent( - plant_config=plant_config, - tech_config={"model_inputs": {"performance_parameters": iron_dri_config_rosner_ng}}, - driver_config={}, - ) - prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) - prob.setup() - prob.run_model() diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py new file mode 100644 index 000000000..1d588aea0 --- /dev/null +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -0,0 +1,472 @@ +import numpy as np +import pandas as pd +import openmdao.api as om +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import BaseConfig, CostModelBaseConfig, merge_shared_inputs +from h2integrate.core.validators import gte_zero +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci + + +@define +class IronReductionPerformanceBaseConfig(BaseConfig): + """Configuration baseclass for IronReductionPlantBasePerformanceComponent. + + Attributes: + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + water_density (float): water density in kg/m3 to use to calculate water volume + from mass. Defaults to 1000.0 + """ + + pig_iron_production_rate_tonnes_per_hr: float = field() + water_density: float = field(default=1000) # kg/m3 + + +class IronReductionPlantBasePerformanceComponent(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + self.config = IronReductionPerformanceBaseConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, + ) + + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + units="t/h", + desc="Rated pig iron production capacity", + ) + + # Add feedstock inputs and outputs, default to 0 --> set using feedstock component + for feedstock, feedstock_units in self.feedstocks_to_units.items(): + self.add_input( + f"{feedstock}_in", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} available for iron reduction", + ) + self.add_output( + f"{feedstock}_consumed", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} consumed for iron reduction", + ) + + # Default the pig iron demand input as the rated capacity + self.add_input( + "pig_iron_demand", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + shape=n_timesteps, + units="t/h", + desc="Pig iron demand for iron plant", + ) + + self.add_output( + "pig_iron_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Pig iron produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + # rosner dri performance model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that feedstock values are converted to units of + feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units + and that units are compatible with OpenMDAO Units. Filter the dataframe to include + only the data necessary for the specified type of reduction. + + Args: + coeff_df (pd.DataFrame): performance coefficient dataframe. + + Returns: + pd.DataFrame: filtered performance coefficient dataframe + """ + # only include data for the given product + coeff_df = coeff_df[coeff_df["Product"] == self.product] + data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] + coeff_df = coeff_df[data_cols] + coeff_df = coeff_df.rename(columns={"Model": "Value"}) + coeff_df = coeff_df[coeff_df["Value"] > 0] + coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data + + # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst + # h2_dri needs natural gas, water, hydrogen, iron ore. + steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] + iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] + + # capacity units units are mtpy + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} + + # efficiency units are % + # feedstock units are mt ore/mt steel, mt H2/mt steel, GJ-LHV NG/mt steel, + # mt H2O/mt steel, kW/mtpy steel + + # convert units to standardized units + old_units = list(set(coeff_df["Unit"].to_list())) + for ii, old_unit in enumerate(old_units): + if old_unit in unit_rename_mapper: + continue + if "steel" in old_unit: + feedstock_unit, capacity_unit = old_unit.split("/") + capacity_unit = capacity_unit.replace("steel", "").strip() + feedstock_unit = feedstock_unit.strip() + + i_update = coeff_df[coeff_df["Unit"] == old_unit].index + + # convert from feedstock per unit steel to feedstock per unit pig iron + coeff_df.loc[i_update, "Value"] = ( + coeff_df.loc[i_update, "Value"] * steel_to_iron_ratio + ) + + # update the "Type" to specify that units were changed to be per unit pig iron + coeff_df.loc[i_update, "Type"] = f"{coeff_df.loc[i_update, 'Type'].values[0]}/iron" + + is_capacity_type = all( + k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() + ) + if capacity_unit == "mtpy" and not is_capacity_type: + # some feedstocks had misleading units, + # where 'kW/ mtpy steel' is actually 'kW/mt steel' + # NOTE: perhaps these ones need to be modified with the steel efficiency? + capacity_unit = "mt" + + old_unit = f"{feedstock_unit}/{capacity_unit}" + + if "H2O" in old_unit: + # convert metric tonnes to kg (value*1e3) + # convert mass to volume in cubic meters (value*1e3)/density + # then convert cubic meters to liters 1e3*(value*1e3)/density + water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density + water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") + coeff_df.loc[i_update]["Value"] = water_volume_L + old_unit = f"L/{capacity_unit}" + + old_unit = ( + old_unit.replace("-LHV NG", "") + .replace("%", "percent") + .replace("m3 catalyst", "(m**3)") + ) + old_unit = old_unit.replace("mt H2", "t").replace("MWh", "(MW*h)").replace(" ore", "") + old_unit = ( + old_unit.replace("mtpy", "(t/yr)").replace("mt", "t").replace("kW/", "(kW*h)/") + ) + # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' + # replace % with "unitless" + unit_rename_mapper.update({old_units[ii]: old_unit}) + coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) + + convert_units_dict = { + "GJ/t": "MMBtu/t", + "L/t": "galUS/t", + "(MW*h)/t": "(kW*h)/t", + "percent": "unitless", + } + # convert units to standard units and OpenMDAO compatible units + for i in coeff_df.index.to_list(): + if coeff_df.loc[i, "Unit"] in convert_units_dict: + current_units = coeff_df.loc[i, "Unit"] + desired_units = convert_units_dict[current_units] + coeff_df.loc[i, "Value"] = units.convert_units( + coeff_df.loc[i, "Value"], current_units, desired_units + ) + coeff_df.loc[i, "Unit"] = desired_units + # NOTE: not sure if percent is actually being converted to unitless + # but not big deal since percent is not used in feedstocks + return coeff_df + + def compute(self, inputs, outputs): + # get the feedstocks from + feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed/iron"].copy() + + # get the feedstock usage rates in units/t pig iron + feedstocks_usage_rates = { + "natural_gas": feedstocks[feedstocks["Name"] == "Natural Gas"][ + "Value" + ].sum(), # MMBtu/t + "water": feedstocks[feedstocks["Name"] == "Raw Water Withdrawal"][ + "Value" + ].sum(), # galUS/t + "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), # t/t + "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"][ + "Value" + ].sum(), # electricity + } + if "hydrogen" in self.feedstocks_to_units: + # t/t + feedstocks_usage_rates["hydrogen"] = feedstocks[feedstocks["Name"] == "Hydrogen"][ + "Value" + ].sum() + + if "reformer_catalyst" in self.feedstocks_to_units: + # m**3/t + feedstocks_usage_rates["reformer_catalyst"] = feedstocks[ + feedstocks["Name"] == "Reformer Catalyst" + ]["Value"].sum() + + # pig iron demand, saturated at maximum rated system capacity + pig_iron_demand = np.where( + inputs["pig_iron_demand"] > inputs["system_capacity"], + inputs["system_capacity"], + inputs["pig_iron_demand"], + ) + + # initialize an array of how much pig iron could be produced + # from the available feedstocks and the demand + pig_iron_from_feedstocks = np.zeros( + (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) + ) + # first entry is the pig iron demand + pig_iron_from_feedstocks[0] = pig_iron_demand + ii = 1 + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + # calculate max inputs/outputs based on rated capacity + max_feedstock_consumption = inputs["system_capacity"] * consumption_rate + # available feedstocks, saturated at maximum system feedstock consumption + feedstock_available = np.where( + inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, + max_feedstock_consumption, + inputs[f"{feedstock_type}_in"], + ) + # how much output can be produced from each of the feedstocks + pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate + ii += 1 + + # output is minimum between available feedstocks and output demand + pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) + outputs["pig_iron_out"] = pig_iron_production + + # feedstock consumption based on actual pig iron produced + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate + + +@define +class IronReductionCostBaseConfig(CostModelBaseConfig): + """Configuration baseclass for IronReductionPlantBaseCostComponent. + + Attributes: + pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant + in units of metric tonnes of pig iron produced per hour. + cost_year (int): This model uses 2022 as the base year for the cost model. + The cost year is updated based on `target_dollar_year` in the plant + config to adjust costs based on CPI/CEPCI within this model. This value + cannot be user added under `cost_parameters`. + skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr + unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr + """ + + pig_iron_production_rate_tonnes_per_hr: float = field() + cost_year: int = field(converter=int) + skilled_labor_cost: float = field(validator=gte_zero) + unskilled_labor_cost: float = field(validator=gte_zero) + + +class IronReductionPlantBaseCostComponent(CostModelBaseClass): + """Cost component for direct reduced iron (DRI) plant + using the Rosner cost model. + + Attributes: + config (IronReductionCostBaseConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") + + if "cost_year" in config_dict: + msg = ( + "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " + "The cost year cannot be modified for this cost model. " + ) + raise ValueError(msg) + + target_dollar_year = self.options["plant_config"]["finance_parameters"][ + "cost_adjustment_parameters" + ]["target_dollar_year"] + + if target_dollar_year <= 2024 and target_dollar_year >= 2010: + # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment + target_dollar_year = target_dollar_year + + elif target_dollar_year < 2010: + # adjust costs from 2022 to 2010 using CP/CEPCI adjustment + target_dollar_year = 2010 + + elif target_dollar_year > 2024: + # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment + target_dollar_year = 2024 + + config_dict.update({"cost_year": target_dollar_year}) + + self.config = IronReductionCostBaseConfig.from_dict(config_dict, strict=False) + + super().setup() + + self.add_input( + "system_capacity", + val=self.config.pig_iron_production_rate_tonnes_per_hr, + units="t/h", + desc="Pig ore production capacity", + ) + self.add_input( + "pig_iron_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Pig iron produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" + + # rosner cost model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that values are adjusted to standard units + and units are compatible with OpenMDAO units. Also filter the dataframe to include + only the data necessary for natural gas DRI type. + + Args: + coeff_df (pd.DataFrame): cost coefficient dataframe. + + Returns: + pd.DataFrame: cost coefficient dataframe + """ + + perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + + perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) + perf_df = perf_df[perf_df["Product"] == self.product] + steel_plant_capacity = perf_df[perf_df["Name"] == "Steel Production"]["Model"].values[0] + iron_plant_capacity = perf_df[perf_df["Name"] == "Pig Iron Production"]["Model"].values[0] + steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year + self.steel_to_iron_ratio = steel_to_iron_ratio + + # only include data for the given product + + data_cols = ["Type", "Coeff", "Unit", self.product] + coeff_df = coeff_df[data_cols] + + coeff_df = coeff_df.rename(columns={self.product: "Value"}) + + return coeff_df + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + # Calculate the capital cost for the item + dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) + + # Calculate + capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) + capital_items_df = self.coeff_df.loc[capital_items].copy() + capital_items_df = capital_items_df.reset_index(drop=False) + capital_items_df = capital_items_df.set_index( + keys=["Name", "Coeff"] + ) # costs are in USD/steel plant capacity + capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) + + ref_steel_plant_capacity_tpy = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "t/yr" + ) + + total_capex_usd = 0.0 + for item in capital_items: + if ( + capital_items_df.loc[item, "exp"]["Value"] > 0 + and capital_items_df.loc[item, "lin"]["Value"] > 0 + ): + capex = capital_items_df.loc[item, "lin"]["Value"] * ( + (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] + ) + total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) + + # Calculate Owners Costs + # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() + + # Calculate Fixed OpEx, includes: + fixed_items = list( + set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) + ) + fixed_items_df = self.coeff_df.loc[fixed_items].copy() + fixed_items_df = fixed_items_df.reset_index(drop=False) + fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) + fixed_items_df = fixed_items_df.drop(columns=["Type"]) + + property_om = ( + total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() + ) + + # Calculate labor costs + skilled_labor_cost = self.config.skilled_labor_cost * ( + fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 + ) + unskilled_labor_cost = self.config.unskilled_labor_cost * ( + fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 + ) + labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr + + ref_steel_plant_capacity_kgpd = units.convert_units( + inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "kg/d" + ) + scaled_steel_plant_capacity_kgpd = ( + ref_steel_plant_capacity_kgpd + ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] + ) + + # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) + work_hrs_per_day_per_step = ( + scaled_steel_plant_capacity_kgpd + * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] + ) + + # employee-hours/day = employee-hours/day/process step * # of process steps + work_hrs_per_day = ( + work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values + ) + labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day + annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") + maintenance_labor_cost = ( + total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values + ) + admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ + "Value" + ].values * (annual_labor_cost + maintenance_labor_cost) + + total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost + tot_fixed_om = total_labor_related_cost + property_om + + # Calculate Variable O&M + varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ + "Value" + ].sum() # units are USD/mtpy steel + tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() + + # Adjust costs to target dollar year + tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) + tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) + tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) + + outputs["CapEx"] = tot_capex_adjusted + outputs["VarOpEx"] = tot_varopex_adjusted + outputs["OpEx"] = tot_fixed_om_adjusted diff --git a/h2integrate/converters/iron/iron_dri_plant.py b/h2integrate/converters/iron/iron_dri_plant.py new file mode 100644 index 000000000..872bde5c5 --- /dev/null +++ b/h2integrate/converters/iron/iron_dri_plant.py @@ -0,0 +1,83 @@ +from h2integrate.converters.iron.iron_dri_base import ( + IronReductionPlantBaseCostComponent, + IronReductionPlantBasePerformanceComponent, +) + + +class HydrogenIronReductionPlantCostComponent(IronReductionPlantBaseCostComponent): + """Cost component for hydrogen-based direct reduced iron (DRI) plant + using the Rosner cost model. + + Attributes: + product (str): 'h2_dri' + config (HydrogenIronReductionCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + self.product = "h2_dri" + super().setup() + + +class NaturalGasIronReductionPlantCostComponent(IronReductionPlantBaseCostComponent): + """Cost component for natural gas-based direct reduced iron (DRI) plant + using the Rosner cost model. + + Attributes: + product (str): 'ng_dri' + config (HydrogenIronReductionCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + self.product = "ng_dri" + super().setup() + + +class HydrogenIronReductionPlantPerformanceComponent(IronReductionPlantBasePerformanceComponent): + """Cost component for hydrogen-based direct reduced iron (DRI) plant + using the Rosner cost model. + + Attributes: + product (str): 'h2_dri' + config (HydrogenIronReductionCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + self.product = "h2_dri" + self.feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "galUS", # "galUS/h" + "iron_ore": "t/h", + "electricity": "kW", + "hydrogen": "t/h", + } + super().setup() + + +class NaturalGasIronReductionPlantPerformanceComponent(IronReductionPlantBasePerformanceComponent): + """Cost component for natural gas-based direct reduced iron (DRI) plant + using the Rosner cost model. + + Attributes: + product (str): 'ng_dri' + config (HydrogenIronReductionCostConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + self.feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "galUS", # "galUS/h" + "iron_ore": "t/h", + "electricity": "kW", + "reformer_catalyst": "(m**3)", # "(m**3)/h" + } + + self.product = "ng_dri" + super().setup() diff --git a/h2integrate/converters/iron/ng_iron_dri_cost_model.py b/h2integrate/converters/iron/ng_iron_dri_cost_model.py deleted file mode 100644 index 92c0b91d0..000000000 --- a/h2integrate/converters/iron/ng_iron_dri_cost_model.py +++ /dev/null @@ -1,245 +0,0 @@ -import pandas as pd -import openmdao.api as om -from attrs import field, define -from openmdao.utils import units - -from h2integrate import ROOT_DIR -from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs -from h2integrate.core.validators import gte_zero -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci - - -@define -class NaturalGasIronReductionCostConfig(CostModelBaseConfig): - """Configuration class for NaturalGasIronReductionPlantCostComponent. - - Attributes: - pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant - in units of metric tonnes of pig iron produced per hour. - cost_year (int): This model uses 2022 as the base year for the cost model. - The cost year is updated based on `target_dollar_year` in the plant - config to adjust costs based on CPI/CEPCI within this model. This value - cannot be user added under `cost_parameters`. - skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr - unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr - """ - - pig_iron_production_rate_tonnes_per_hr: float = field() - cost_year: int = field(converter=int) - skilled_labor_cost: float = field(validator=gte_zero) - unskilled_labor_cost: float = field(validator=gte_zero) - - -class NaturalGasIronReductionPlantCostComponent(CostModelBaseClass): - """Calculates the capital and operating costs for a natural gas-based direct - iron reduction plant using the Rosner cost model. - - Attributes: - config (NaturalGasIronReductionCostConfig): configuration class - coeff_df (pd.DataFrame): cost coefficient dataframe - steel_to_iron_ratio (float): steel/pig iron ratio - """ - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") - - if "cost_year" in config_dict: - msg = ( - "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " - "The cost year cannot be modified for this cost model. " - ) - raise ValueError(msg) - - target_dollar_year = self.options["plant_config"]["finance_parameters"][ - "cost_adjustment_parameters" - ]["target_dollar_year"] - - if target_dollar_year <= 2024 and target_dollar_year >= 2010: - # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment - target_dollar_year = target_dollar_year - - elif target_dollar_year < 2010: - # adjust costs from 2022 to 2010 using CP/CEPCI adjustment - target_dollar_year = 2010 - - elif target_dollar_year > 2024: - # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment - target_dollar_year = 2024 - - config_dict.update({"cost_year": target_dollar_year}) - - self.config = NaturalGasIronReductionCostConfig.from_dict(config_dict, strict=False) - - super().setup() - - self.add_input( - "system_capacity", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - units="t/h", - desc="Pig ore production capacity", - ) - self.add_input( - "pig_iron_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Pig iron produced", - ) - - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" - # rosner cost model - coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df) - - def format_coeff_df(self, coeff_df): - """Update the coefficient dataframe such that values are adjusted to standard units - and units are compatible with OpenMDAO units. Also filter the dataframe to include - only the data necessary for natural gas DRI type. - - Args: - coeff_df (pd.DataFrame): cost coefficient dataframe. - - Returns: - pd.DataFrame: cost coefficient dataframe - """ - product = "ng_dri" # h2_dri - - perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" - - perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) - perf_df = perf_df[perf_df["Product"] == product] - steel_plant_capacity = perf_df[perf_df["Name"] == "Steel Production"]["Model"].values[0] - iron_plant_capacity = perf_df[perf_df["Name"] == "Pig Iron Production"]["Model"].values[0] - steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year - self.steel_to_iron_ratio = steel_to_iron_ratio - - # only include data for the given product - - data_cols = ["Type", "Coeff", "Unit", product] - coeff_df = coeff_df[data_cols] - - coeff_df = coeff_df.rename(columns={product: "Value"}) - - return coeff_df - - def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - # Calculate the capital cost for the item - dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) - - # Calculate - capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) - capital_items_df = self.coeff_df.loc[capital_items].copy() - capital_items_df = capital_items_df.reset_index(drop=False) - capital_items_df = capital_items_df.set_index( - keys=["Name", "Coeff"] - ) # costs are in USD/steel plant capacity - capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) - - ref_steel_plant_capacity_tpy = units.convert_units( - inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "t/yr" - ) - - total_capex_usd = 0.0 - for item in capital_items: - if ( - capital_items_df.loc[item, "exp"]["Value"] > 0 - and capital_items_df.loc[item, "lin"]["Value"] > 0 - ): - capex = capital_items_df.loc[item, "lin"]["Value"] * ( - (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] - ) - total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) - - # Calculate Owners Costs - # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() - - # Calculate Fixed OpEx, includes: - fixed_items = list( - set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) - ) - fixed_items_df = self.coeff_df.loc[fixed_items].copy() - fixed_items_df = fixed_items_df.reset_index(drop=False) - fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) - fixed_items_df = fixed_items_df.drop(columns=["Type"]) - - property_om = ( - total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() - ) - - # Calculate labor costs - skilled_labor_cost = self.config.skilled_labor_cost * ( - fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 - ) - unskilled_labor_cost = self.config.unskilled_labor_cost * ( - fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 - ) - labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr - - ref_steel_plant_capacity_kgpd = units.convert_units( - inputs["system_capacity"] * self.steel_to_iron_ratio, "t/h", "kg/d" - ) - scaled_steel_plant_capacity_kgpd = ( - ref_steel_plant_capacity_kgpd - ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] - ) - - # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) - work_hrs_per_day_per_step = ( - scaled_steel_plant_capacity_kgpd - * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] - ) - - # employee-hours/day = employee-hours/day/process step * # of process steps - work_hrs_per_day = ( - work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values - ) - labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day - annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") - maintenance_labor_cost = ( - total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values - ) - admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ - "Value" - ].values * (annual_labor_cost + maintenance_labor_cost) - - total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost - tot_fixed_om = total_labor_related_cost + property_om - - # Calculate Variable O&M - varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ - "Value" - ].sum() # units are USD/mtpy steel - tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() - - # Adjust costs to target dollar year - tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) - tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) - tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) - - outputs["CapEx"] = tot_capex_adjusted - outputs["VarOpEx"] = tot_varopex_adjusted - outputs["OpEx"] = tot_fixed_om_adjusted - - -if __name__ == "__main__": - prob = om.Problem() - - plant_config = { - "plant": {"plant_life": 30, "simulation": {"n_timesteps": 8760}}, - "finance_parameters": {"cost_adjustment_parameters": {"target_dollar_year": 2022}}, - } - iron_dri_config_rosner_ng = { - "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - "skilled_labor_cost": 1.0, - "unskilled_labor_cost": 1.0, - } - iron_dri_perf = NaturalGasIronReductionPlantCostComponent( - plant_config=plant_config, - tech_config={"model_inputs": {"shared_parameters": iron_dri_config_rosner_ng}}, - driver_config={}, - ) - prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) - prob.setup() - prob.run_model() diff --git a/h2integrate/converters/iron/ng_iron_dri_perf_model.py b/h2integrate/converters/iron/ng_iron_dri_perf_model.py deleted file mode 100644 index dbd91c8af..000000000 --- a/h2integrate/converters/iron/ng_iron_dri_perf_model.py +++ /dev/null @@ -1,267 +0,0 @@ -import numpy as np -import pandas as pd -import openmdao.api as om -from attrs import field, define -from openmdao.utils import units - -from h2integrate import ROOT_DIR -from h2integrate.core.utilities import BaseConfig, merge_shared_inputs - - -@define -class NaturalGasIronReductionPerformanceConfig(BaseConfig): - """Configuration class for NaturalGasIronReductionPlantPerformanceComponent. - - Attributes: - pig_iron_production_rate_tonnes_per_hr (float): capacity of the iron processing plant - in units of metric tonnes of pig iron produced per hour. - water_density (float): water density in kg/m3 to use to calculate water volume - from mass. Defaults to 1000.0 - """ - - pig_iron_production_rate_tonnes_per_hr: float = field() - water_density: float = field(default=1000) # kg/m3 - - -class NaturalGasIronReductionPlantPerformanceComponent(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - - self.config = NaturalGasIronReductionPerformanceConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), - strict=True, - ) - - self.add_input( - "system_capacity", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - units="t/h", - desc="Rated pig iron production capacity", - ) - - feedstocks_to_units = { - "natural_gas": "MMBtu", - "water": "galUS", # "galUS/h" - "iron_ore": "t/h", - "electricity": "kW", - "reformer_catalyst": "(m**3)", # "(m**3)/h" - } - - # Add feedstock inputs and outputs, default to 0 --> set using feedstock component - for feedstock, feedstock_units in feedstocks_to_units.items(): - self.add_input( - f"{feedstock}_in", - val=0.0, - shape=n_timesteps, - units=feedstock_units, - desc=f"{feedstock} available for iron reduction", - ) - self.add_output( - f"{feedstock}_consumed", - val=0.0, - shape=n_timesteps, - units=feedstock_units, - desc=f"{feedstock} consumed for iron reduction", - ) - - # Default the pig iron demand input as the rated capacity - self.add_input( - "pig_iron_demand", - val=self.config.pig_iron_production_rate_tonnes_per_hr, - shape=n_timesteps, - units="t/h", - desc="Pig iron demand for iron plant", - ) - - self.add_output( - "pig_iron_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Pig iron produced", - ) - - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" - # rosner dri performance model - coeff_df = pd.read_csv(coeff_fpath, index_col=0) - self.coeff_df = self.format_coeff_df(coeff_df) - self.feedstock_to_units = feedstocks_to_units - - def format_coeff_df(self, coeff_df): - """Update the coefficient dataframe such that feedstock values are converted to units of - feedstock unit / unit pig iron, e.g., 'galUS/t'. Also convert values to standard units - and that units are compatible with OpenMDAO Units. Filter the dataframe to include - only the data necessary for Natural Gas reduction. - - Args: - coeff_df (pd.DataFrame): performance coefficient dataframe. - - Returns: - pd.DataFrame: filtered performance coefficient dataframe - """ - # only include data for the given product - coeff_df = coeff_df[coeff_df["Product"] == "ng_dri"] - data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] - coeff_df = coeff_df[data_cols] - coeff_df = coeff_df.rename(columns={"Model": "Value"}) - coeff_df = coeff_df[coeff_df["Value"] > 0] - coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data - - # ng DRI needs natural gas, water, electricity, iron ore and reformer catalyst - # h2_dri needs natural gas, water, hydrogen, iron ore. - steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] - iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] - - # capacity units units are mtpy - steel_to_iron_ratio = steel_plant_capacity / iron_plant_capacity # both in metric tons/year - unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} - - # efficiency units are % - # feedstock units are mt ore/mt steel, m3 catalyst/mt steel, GJ-LHV NG/mt steel, - # mt H2O/mt steel, kW/mtpy steel - - # convert units to standardized units - old_units = list(set(coeff_df["Unit"].to_list())) - for ii, old_unit in enumerate(old_units): - if old_unit in unit_rename_mapper: - continue - if "steel" in old_unit: - feedstock_unit, capacity_unit = old_unit.split("/") - capacity_unit = capacity_unit.replace("steel", "").strip() - feedstock_unit = feedstock_unit.strip() - - i_update = coeff_df[coeff_df["Unit"] == old_unit].index - - # convert from feedstock per unit steel to feedstock per unit pig iron - coeff_df.loc[i_update]["Value"] = ( - coeff_df.loc[i_update]["Value"] * steel_to_iron_ratio - ) - - # update the "Type" to specify that units were changed to be per unit pig iron - coeff_df.loc[i_update]["Type"] = f"{coeff_df.loc[i_update]['Type'].values[0]}/iron" - - is_capacity_type = all( - k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() - ) - if capacity_unit == "mtpy" and not is_capacity_type: - # some feedstocks had misleading units, - # where 'kW/ mtpy steel' is actually 'kW/mt steel' - # NOTE: perhaps these ones need to be modified with the steel efficiency? - capacity_unit = "mt" - - old_unit = f"{feedstock_unit}/{capacity_unit}" - - if "H2O" in old_unit: - # convert metric tonnes to kg (value*1e3) - # convert mass to volume in cubic meters (value*1e3)/density - # then convert cubic meters to liters 1e3*(value*1e3)/density - water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density - water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") - coeff_df.loc[i_update]["Value"] = water_volume_L - old_unit = f"L/{capacity_unit}" - - old_unit = old_unit.replace("-LHV NG", "").replace("%", "percent") - old_unit = ( - old_unit.replace("m3 catalyst", "(m**3)") - .replace("MWh", "(MW*h)") - .replace(" ore", "") - ) - old_unit = ( - old_unit.replace("mtpy", "(t/yr)").replace("mt", "t").replace("kW/", "(kW*h)/") - ) - # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' - # replace % with "unitless" - unit_rename_mapper.update({old_units[ii]: old_unit}) - coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) - - convert_units_dict = { - "GJ/t": "MMBtu/t", - "L/t": "galUS/t", - "(MW*h)/t": "(kW*h)/t", - "percent": "unitless", - } - # convert units to standard units and OpenMDAO compatible units - for i in coeff_df.index.to_list(): - if coeff_df.loc[i, "Unit"] in convert_units_dict: - current_units = coeff_df.loc[i, "Unit"] - desired_units = convert_units_dict[current_units] - coeff_df.loc[i, "Value"] = units.convert_units( - coeff_df.loc[i, "Value"], current_units, desired_units - ) - coeff_df.loc[i, "Unit"] = desired_units - # NOTE: not sure if percent is actually being converted to unitless - # but not big deal since percent is not used in feedstocks - return coeff_df - - def compute(self, inputs, outputs): - # get the feedstocks from - feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() - - # get the feedstock usage rates in units/t pig iron - feedstocks_usage_rates = { - "natural_gas": feedstocks[feedstocks["Unit"] == "MMBtu/t"]["Value"].sum(), - "water": feedstocks[feedstocks["Unit"] == "galUS/t"]["Value"].sum(), - "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), - "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"]["Value"].sum(), - "reformer_catalyst": feedstocks[feedstocks["Name"] == "Reformer Catalyst"][ - "Value" - ].sum(), - } - - # pig iron demand, saturated at maximum rated system capacity - pig_iron_demand = np.where( - inputs["pig_iron_demand"] > inputs["system_capacity"], - inputs["system_capacity"], - inputs["pig_iron_demand"], - ) - - # initialize an array of how much pig iron could be produced - # from the available feedstocks and the demand - pig_iron_from_feedstocks = np.zeros( - (len(feedstocks_usage_rates) + 1, len(inputs["pig_iron_demand"])) - ) - # first entry is the pig iron demand - pig_iron_from_feedstocks[0] = pig_iron_demand - ii = 1 - for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): - # calculate max inputs/outputs based on rated capacity - max_feedstock_consumption = inputs["system_capacity"] * consumption_rate - # available feedstocks, saturated at maximum system feedstock consumption - feedstock_available = np.where( - inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, - max_feedstock_consumption, - inputs[f"{feedstock_type}_in"], - ) - # how much output can be produced from each of the feedstocks - pig_iron_from_feedstocks[ii] = feedstock_available / consumption_rate - ii += 1 - - # output is minimum between available feedstocks and output demand - pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) - outputs["pig_iron_out"] = pig_iron_production - - # feedstock consumption based on actual pig iron produced - for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): - outputs[f"{feedstock_type}_consumed"] = pig_iron_production * consumption_rate - - -if __name__ == "__main__": - prob = om.Problem() - - plant_config = {"plant": {"simulation": {"n_timesteps": 8760}}} - iron_dri_config_rosner_ng = { - "pig_iron_production_rate_tonnes_per_hr": 1418095 / 8760, - } - iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( - plant_config=plant_config, - tech_config={"model_inputs": {"performance_parameters": iron_dri_config_rosner_ng}}, - driver_config={}, - ) - prob.model.add_subsystem("dri", iron_dri_perf, promotes=["*"]) - prob.setup() - prob.run_model() diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index 28e913298..0064ba995 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -5,16 +5,10 @@ from h2integrate import EXAMPLE_DIR from h2integrate.core.inputs.validation import load_driver_yaml -from h2integrate.converters.iron.h2_iron_dri_cost_model import ( +from h2integrate.converters.iron.iron_dri_plant import ( HydrogenIronReductionPlantCostComponent, -) -from h2integrate.converters.iron.h2_iron_dri_perf_model import ( - HydrogenIronReductionPlantPerformanceComponent, -) -from h2integrate.converters.iron.ng_iron_dri_cost_model import ( NaturalGasIronReductionPlantCostComponent, -) -from h2integrate.converters.iron.ng_iron_dri_perf_model import ( + HydrogenIronReductionPlantPerformanceComponent, NaturalGasIronReductionPlantPerformanceComponent, ) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 2c390e404..9781182d2 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -27,6 +27,12 @@ from h2integrate.storage.battery.pysam_battery import PySAMBatteryPerformanceModel from h2integrate.transporters.generic_combiner import GenericCombinerPerformanceModel from h2integrate.transporters.generic_splitter import GenericSplitterPerformanceModel +from h2integrate.converters.iron.iron_dri_plant import ( + HydrogenIronReductionPlantCostComponent, + NaturalGasIronReductionPlantCostComponent, + HydrogenIronReductionPlantPerformanceComponent, + NaturalGasIronReductionPlantPerformanceComponent, +) from h2integrate.converters.iron.iron_transport import ( IronTransportCostComponent, IronTransportPerformanceComponent, @@ -58,20 +64,8 @@ from h2integrate.converters.solar.atb_res_com_pv_cost import ATBResComPVCostModel from h2integrate.converters.solar.atb_utility_pv_cost import ATBUtilityPVCostModel from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource -from h2integrate.converters.iron.h2_iron_dri_cost_model import ( - HydrogenIronReductionPlantCostComponent, -) -from h2integrate.converters.iron.h2_iron_dri_perf_model import ( - HydrogenIronReductionPlantPerformanceComponent, -) from h2integrate.converters.iron.martin_mine_cost_model import MartinIronMineCostComponent from h2integrate.converters.iron.martin_mine_perf_model import MartinIronMinePerformanceComponent -from h2integrate.converters.iron.ng_iron_dri_cost_model import ( - NaturalGasIronReductionPlantCostComponent, -) -from h2integrate.converters.iron.ng_iron_dri_perf_model import ( - NaturalGasIronReductionPlantPerformanceComponent, -) from h2integrate.converters.methanol.smr_methanol_plant import ( SMRMethanolPlantCostModel, SMRMethanolPlantFinanceModel, From ebd56ae644cbb5ab05eadcc0833c776e1aff836a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:32:39 -0700 Subject: [PATCH 21/40] updated electricity to not double count --- h2integrate/converters/iron/iron_dri_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index 1d588aea0..182e966f3 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -205,9 +205,9 @@ def compute(self, inputs, outputs): "Value" ].sum(), # galUS/t "iron_ore": feedstocks[feedstocks["Name"] == "Iron Ore"]["Value"].sum(), # t/t - "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"][ + "electricity": feedstocks[feedstocks["Name"] == "Electricity"][ "Value" - ].sum(), # electricity + ].sum(), # (kW*h)/t } if "hydrogen" in self.feedstocks_to_units: # t/t From 7e33bdfee5bb0db9ed74db2b3102d198471e4935 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:53:15 -0700 Subject: [PATCH 22/40] added test for performance with limited feedstock availability --- .../converters/iron/test/test_rosner_dri.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index 0064ba995..a132e5866 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -151,6 +151,45 @@ def test_ng_dri_performance( ) +def test_ng_dri_performance_limited_feedstock( + plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests +): + expected_pig_iron_annual_production_tpd = 3885.1917808219177 / 2 # t/d + # make iron ore feedstock half of whats needed + water_usage_rate_gal_pr_tonne = 0.20060957937294563 + water_half_availability_gal_pr_hr = ( + water_usage_rate_gal_pr_tonne * expected_pig_iron_annual_production_tpd / 24 + ) + ng_feedstock_availability_costs["water"].update( + {"rated_capacity": water_half_availability_gal_pr_hr} + ) + + prob = om.Problem() + + iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_pig_iron = np.sum(prob.get_val("perf.pig_iron_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) + == expected_pig_iron_annual_production_tpd + ) + + def test_ng_dri_performance_cost( plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests ): From 5410238bec3cf767bb6e31ba5cf029758056204d Mon Sep 17 00:00:00 2001 From: kbrunik Date: Mon, 29 Dec 2025 14:55:23 -0600 Subject: [PATCH 23/40] eaf wip --- .../converters/steel/steel_eaf_base.py | 461 ++++++++++++++++++ .../converters/steel/steel_eaf_plant.py | 79 +++ h2integrate/core/supported_models.py | 10 + 3 files changed, 550 insertions(+) create mode 100644 h2integrate/converters/steel/steel_eaf_base.py create mode 100644 h2integrate/converters/steel/steel_eaf_plant.py diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py new file mode 100644 index 000000000..9b8875953 --- /dev/null +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -0,0 +1,461 @@ +import numpy as np +import pandas as pd +import openmdao.api as om +from attrs import field, define +from openmdao.utils import units + +from h2integrate import ROOT_DIR +from h2integrate.core.utilities import BaseConfig, CostModelBaseConfig, merge_shared_inputs +from h2integrate.core.validators import gte_zero +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci + + +@define +class ElectricArcFurnacePerformanceBaseConfig(BaseConfig): + """Configuration baseclass for ElectricArcFurnacePlantBasePerformanceComponent. + + Attributes: + steel_production_rate_tonnes_per_hr (float): capacity of the steel processing plant + in units of metric tonnes of steel produced per hour. + water_density (float): water density in kg/m3 to use to calculate water volume + from mass. Defaults to 1000.0 + """ + + steel_production_rate_tonnes_per_hr: float = field() + water_density: float = field(default=1000) # kg/m3 + + +class ElectricArcFurnacePlantBasePerformanceComponent(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + self.config = ElectricArcFurnacePerformanceBaseConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, + ) + + self.add_input( + "system_capacity", + val=self.config.steel_production_rate_tonnes_per_hr, + units="t/h", + desc="Rated steel production capacity", + ) + + # Add feedstock inputs and outputs, default to 0 --> set using feedstock component + for feedstock, feedstock_units in self.feedstocks_to_units.items(): + self.add_input( + f"{feedstock}_in", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} available for steel production", + ) + self.add_output( + f"{feedstock}_consumed", + val=0.0, + shape=n_timesteps, + units=feedstock_units, + desc=f"{feedstock} consumed for steel production", + ) + + # Default the steel demand input as the rated capacity + self.add_input( + "steel_demand", + val=self.config.steel_production_rate_tonnes_per_hr, + shape=n_timesteps, + units="t/h", + desc="Steel demand for steel plant", + ) + + self.add_output( + "steel_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Steel produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + # rosner dri performance model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that feedstock values are converted to standard + units and that units are compatible with OpenMDAO Units. Filter the dataframe to include + only the data necessary for the specified type of reduction. + + Args: + coeff_df (pd.DataFrame): performance coefficient dataframe. + + Returns: + pd.DataFrame: filtered performance coefficient dataframe + """ + # only include data for the given product + coeff_df = coeff_df[coeff_df["Product"] == self.product] + data_cols = ["Name", "Type", "Coeff", "Unit", "Model"] + coeff_df = coeff_df[data_cols] + coeff_df = coeff_df.rename(columns={"Model": "Value"}) + coeff_df = coeff_df[coeff_df["Value"] > 0] + coeff_df = coeff_df[coeff_df["Type"] != "emission"] # dont include emission data + + # H2 EAF needs natural gas, electricity, carbon, lime, water + # NG EAF needs natural gas and electricity. + # add pig iron feedstock to dataframe + steel_plant_capacity = coeff_df[coeff_df["Name"] == "Steel Production"]["Value"].values[0] + iron_plant_capacity = coeff_df[coeff_df["Name"] == "Pig Iron Production"]["Value"].values[0] + iron_to_steel_ratio = iron_plant_capacity / steel_plant_capacity # both in metric tons/year + # add to dataframe + pig_iron_row = pd.DataFrame( + { + "Name": ["Pig Iron"], + "Type": ["feed"], + "Coeff": ["lin"], + "Unit": ["mt pig iron/mt steel"], + "Value": [iron_to_steel_ratio], + } + ) + coeff_df = pd.concat([coeff_df, pig_iron_row], ignore_index=True) + + # capacity units units are mtpy + unit_rename_mapper = {"mtpy": "t/yr", "%": "unitless"} + + # efficiency units are % + # feedstock units are GJ-LHV NG/mt steel, + # mt H2O/mt steel, kW/mtpy steel + + # convert units to standardized units + old_units = list(set(coeff_df["Unit"].to_list())) + for ii, old_unit in enumerate(old_units): + if old_unit in unit_rename_mapper: + continue + if "steel" in old_unit: + feedstock_unit, capacity_unit = old_unit.split("/") + capacity_unit = capacity_unit.replace("steel", "").strip() + feedstock_unit = feedstock_unit.strip() + + i_update = coeff_df[coeff_df["Unit"] == old_unit].index + + is_capacity_type = all( + k == "capacity" for k in coeff_df.loc[i_update]["Type"].to_list() + ) + if capacity_unit == "mtpy" and not is_capacity_type: + # some feedstocks had misleading units, + # where 'kW/ mtpy steel' is actually 'kW/mt steel' + # NOTE: perhaps these ones need to be modified with the steel efficiency? + capacity_unit = "mt" + + old_unit = f"{feedstock_unit}/{capacity_unit}" + + if "H2O" in old_unit: + # convert metric tonnes to kg (value*1e3) + # convert mass to volume in cubic meters (value*1e3)/density + # then convert cubic meters to liters 1e3*(value*1e3)/density + water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density + water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") + coeff_df.loc[i_update]["Value"] = water_volume_L + old_unit = f"L/{capacity_unit}" + + old_unit = ( + old_unit.replace("-LHV NG", "") + .replace("MWh", "(MW*h)") + .replace("mt C", "t") + .replace("mt lime", "t") + .replace("%", "percent") + .replace("mtpy", "(t/yr)") + .replace("mt", "t") + .replace("kW/", "(kW*h)/") + ) + # NOTE: how would 'kW / mtpy steel' be different than 'kW / mt steel' + # replace % with "unitless" + unit_rename_mapper.update({old_units[ii]: old_unit}) + coeff_df["Unit"] = coeff_df["Unit"].replace(to_replace=unit_rename_mapper) + + convert_units_dict = { + "GJ/t": "MMBtu/t", + "L/t": "galUS/t", + "(MW*h)/t": "(kW*h)/t", + "percent": "unitless", + } + # convert units to standard units and OpenMDAO compatible units + for i in coeff_df.index.to_list(): + if coeff_df.loc[i, "Unit"] in convert_units_dict: + current_units = coeff_df.loc[i, "Unit"] + desired_units = convert_units_dict[current_units] + coeff_df.loc[i, "Value"] = units.convert_units( + coeff_df.loc[i, "Value"], current_units, desired_units + ) + coeff_df.loc[i, "Unit"] = desired_units + # NOTE: not sure if percent is actually being converted to unitless + # but not big deal since percent is not used in feedstocks + return coeff_df + + def compute(self, inputs, outputs): + # get the feedstocks from + feedstocks = self.coeff_df[self.coeff_df["Type"] == "feed"].copy() + + # get the feedstock usage rates in units/t steel + feedstocks_usage_rates = { + "natural_gas": feedstocks[feedstocks["Name"] == "Natural Gas"][ + "Value" + ].sum(), # MMBtu/t + "carbon": feedstocks[feedstocks["Name"] == "Carbon (Coke)"]["Value"].sum(), # t/t + "lime": feedstocks[feedstocks["Name"] == "Lime"]["Value"].sum(), # t/t + "water": feedstocks[feedstocks["Name"] == "Raw Water Withdrawal"][ + "Value" + ].sum(), # galUS/t + "pig_iron": feedstocks[feedstocks["Name"] == "Pig Iron"]["Value"].sum(), # t/t + "electricity": feedstocks[feedstocks["Unit"] == "(kW*h)/t"][ + "Value" + ].sum(), # electricity + } + + # steel demand, saturated at maximum rated system capacity + steel_demand = np.where( + inputs["steel_demand"] > inputs["system_capacity"], + inputs["system_capacity"], + inputs["steel_demand"], + ) + + # initialize an array of how much steel could be produced + # from the available feedstocks and the demand + steel_from_feedstocks = np.zeros( + (len(feedstocks_usage_rates) + 1, len(inputs["steel_demand"])) + ) + # first entry is the steel demand + steel_from_feedstocks[0] = steel_demand + ii = 1 + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + # calculate max inputs/outputs based on rated capacity + max_feedstock_consumption = inputs["system_capacity"] * consumption_rate + # available feedstocks, saturated at maximum system feedstock consumption + feedstock_available = np.where( + inputs[f"{feedstock_type}_in"] > max_feedstock_consumption, + max_feedstock_consumption, + inputs[f"{feedstock_type}_in"], + ) + # how much output can be produced from each of the feedstocks + steel_from_feedstocks[ii] = feedstock_available / consumption_rate + ii += 1 + + # output is minimum between available feedstocks and output demand + steel_production = np.minimum.reduce(steel_from_feedstocks) + outputs["steel_out"] = steel_production + + # feedstock consumption based on actual steel produced + for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): + outputs[f"{feedstock_type}_consumed"] = steel_production * consumption_rate + + +@define +class ElectricArcFurnaceCostBaseConfig(CostModelBaseConfig): + """Configuration baseclass for ElectricArcFurnacePlantBaseCostComponent. + + Attributes: + steel_production_rate_tonnes_per_hr (float): capacity of the steel processing plant + in units of metric tonnes of steel produced per hour. + cost_year (int): This model uses 2022 as the base year for the cost model. + The cost year is updated based on `target_dollar_year` in the plant + config to adjust costs based on CPI/CEPCI within this model. This value + cannot be user added under `cost_parameters`. + skilled_labor_cost (float): Skilled labor cost in 2022 USD/hr + unskilled_labor_cost (float): Unskilled labor cost in 2022 USD/hr + """ + + steel_production_rate_tonnes_per_hr: float = field() + cost_year: int = field(converter=int) + skilled_labor_cost: float = field(validator=gte_zero) + unskilled_labor_cost: float = field(validator=gte_zero) + + +class ElectricArcFurnacePlantBaseCostComponent(CostModelBaseClass): + """Cost component for electric arc furnace (EAF) plant + using the Rosner cost model. + + Attributes: + config (ElectricArcFurnaceCostBaseConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + steel_to_iron_ratio (float): steel/pig iron ratio + """ + + def setup(self): + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + config_dict = merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") + + if "cost_year" in config_dict: + msg = ( + "This cost model is based on 2022 costs and adjusts costs using CPI and CEPCI. " + "The cost year cannot be modified for this cost model. " + ) + raise ValueError(msg) + + target_dollar_year = self.options["plant_config"]["finance_parameters"][ + "cost_adjustment_parameters" + ]["target_dollar_year"] + + if target_dollar_year <= 2024 and target_dollar_year >= 2010: + # adjust costs from 2022 to target dollar year using CPI/CEPCI adjustment + target_dollar_year = target_dollar_year + + elif target_dollar_year < 2010: + # adjust costs from 2022 to 2010 using CP/CEPCI adjustment + target_dollar_year = 2010 + + elif target_dollar_year > 2024: + # adjust costs from 2022 to 2024 using CPI/CEPCI adjustment + target_dollar_year = 2024 + + config_dict.update({"cost_year": target_dollar_year}) + + self.config = ElectricArcFurnaceCostBaseConfig.from_dict(config_dict, strict=False) + + super().setup() + + self.add_input( + "system_capacity", + val=self.config.steel_production_rate_tonnes_per_hr, + units="t/h", + desc="Steel production capacity", + ) + self.add_input( + "steel_out", + val=0.0, + shape=n_timesteps, + units="t/h", + desc="Steel produced", + ) + + coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "cost_coeffs.csv" + + # rosner cost model + coeff_df = pd.read_csv(coeff_fpath, index_col=0) + self.coeff_df = self.format_coeff_df(coeff_df) + + def format_coeff_df(self, coeff_df): + """Update the coefficient dataframe such that values are adjusted to standard units + and units are compatible with OpenMDAO units. Also filter the dataframe to include + only the data necessary for natural gas DRI type. + + Args: + coeff_df (pd.DataFrame): cost coefficient dataframe. + + Returns: + pd.DataFrame: cost coefficient dataframe + """ + + perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + + perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) + perf_df = perf_df[perf_df["Product"] == self.product] + + # only include data for the given product + + data_cols = ["Type", "Coeff", "Unit", self.product] + coeff_df = coeff_df[data_cols] + + coeff_df = coeff_df.rename(columns={self.product: "Value"}) + + return coeff_df + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + # Calculate the capital cost for the item + dollar_year = self.coeff_df.loc["Dollar Year", "Value"].astype(int) + + # Calculate + capital_items = list(set(self.coeff_df[self.coeff_df["Type"] == "capital"].index.to_list())) + capital_items_df = self.coeff_df.loc[capital_items].copy() + capital_items_df = capital_items_df.reset_index(drop=False) + capital_items_df = capital_items_df.set_index( + keys=["Name", "Coeff"] + ) # costs are in USD/steel plant capacity + capital_items_df = capital_items_df.drop(columns=["Unit", "Type"]) + + ref_steel_plant_capacity_tpy = units.convert_units(inputs["system_capacity"], "t/h", "t/yr") + + total_capex_usd = 0.0 + for item in capital_items: + if ( + capital_items_df.loc[item, "exp"]["Value"] > 0 + and capital_items_df.loc[item, "lin"]["Value"] > 0 + ): + capex = capital_items_df.loc[item, "lin"]["Value"] * ( + (ref_steel_plant_capacity_tpy) ** capital_items_df.loc[item, "exp"]["Value"] + ) + total_capex_usd += inflate_cepci(capex, dollar_year, self.config.cost_year) + + # Calculate Owners Costs + # owner_costs_frac_tpc = self.coeff_df[self.coeff_df["Type"]=="owner"]["Value"].sum() + + # Calculate Fixed OpEx, includes: + fixed_items = list( + set(self.coeff_df[self.coeff_df["Type"] == "fixed opex"].index.to_list()) + ) + fixed_items_df = self.coeff_df.loc[fixed_items].copy() + fixed_items_df = fixed_items_df.reset_index(drop=False) + fixed_items_df = fixed_items_df.set_index(keys=["Name", "Coeff"]) + fixed_items_df = fixed_items_df.drop(columns=["Type"]) + + property_om = ( + total_capex_usd * fixed_items_df.loc["Property Tax & Insurance"]["Value"].sum() + ) + + # Calculate labor costs + skilled_labor_cost = self.config.skilled_labor_cost * ( + fixed_items_df.loc["% Skilled Labor"]["Value"].values / 100 + ) + unskilled_labor_cost = self.config.unskilled_labor_cost * ( + fixed_items_df.loc["% Unskilled Labor"]["Value"].values / 100 + ) + labor_cost_per_hr = skilled_labor_cost + unskilled_labor_cost # USD/hr + + ref_steel_plant_capacity_kgpd = units.convert_units( + inputs["system_capacity"], "t/h", "kg/d" + ) + scaled_steel_plant_capacity_kgpd = ( + ref_steel_plant_capacity_kgpd + ** fixed_items_df.loc["Annual Operating Labor Cost", "exp"]["Value"] + ) + + # employee-hours/day/process step = ((employee-hours/day/process step)/(kg/day))*(kg/day) + work_hrs_per_day_per_step = ( + scaled_steel_plant_capacity_kgpd + * fixed_items_df.loc["Annual Operating Labor Cost", "lin"]["Value"] + ) + + # employee-hours/day = employee-hours/day/process step * # of process steps + work_hrs_per_day = ( + work_hrs_per_day_per_step * fixed_items_df.loc["Processing Steps"]["Value"].values + ) + labor_cost_per_day = labor_cost_per_hr * work_hrs_per_day + annual_labor_cost = labor_cost_per_day * units.convert_units(1, "yr", "d") + maintenance_labor_cost = ( + total_capex_usd * fixed_items_df.loc["Maintenance Labor Cost"]["Value"].values + ) + admin_labor_cost = fixed_items_df.loc["Administrative & Support Labor Cost"][ + "Value" + ].values * (annual_labor_cost + maintenance_labor_cost) + + total_labor_related_cost = admin_labor_cost + maintenance_labor_cost + annual_labor_cost + tot_fixed_om = total_labor_related_cost + property_om + + # Calculate Variable O&M + varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ + "Value" + ].sum() # units are USD/mtpy steel + tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() + + # Adjust costs to target dollar year + tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) + tot_fixed_om_adjusted = inflate_cpi(tot_fixed_om, dollar_year, self.config.cost_year) + tot_varopex_adjusted = inflate_cpi(tot_varopex, dollar_year, self.config.cost_year) + + outputs["CapEx"] = tot_capex_adjusted + outputs["VarOpEx"] = tot_varopex_adjusted + outputs["OpEx"] = tot_fixed_om_adjusted diff --git a/h2integrate/converters/steel/steel_eaf_plant.py b/h2integrate/converters/steel/steel_eaf_plant.py new file mode 100644 index 000000000..6b8a1b216 --- /dev/null +++ b/h2integrate/converters/steel/steel_eaf_plant.py @@ -0,0 +1,79 @@ +from h2integrate.converters.steel.steel_eaf_base import ( + ElectricArcFurnacePlantBaseCostComponent, + ElectricArcFurnacePlantBasePerformanceComponent, +) + + +class HydrogenEAFPlantCostComponent(ElectricArcFurnacePlantBaseCostComponent): + """Cost component for hydrogen-based electric arc furnace (EAF) plant + using the Rosner cost model. + + Attributes: + product (str): 'h2_eaf' + config (ElectricArcFurnaceCostBaseConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + """ + + def setup(self): + self.product = "h2_eaf" + super().setup() + + +class NaturalGasEAFPlantCostComponent(ElectricArcFurnacePlantBaseCostComponent): + """Cost component for natural gas-based electric arc furnace (EAF) plant + using the Rosner cost model. + + Attributes: + product (str): 'ng_eaf' + config (ElectricArcFurnaceCostBaseConfig): configuration class + coeff_df (pd.DataFrame): cost coefficient dataframe + """ + + def setup(self): + self.product = "ng_eaf" + super().setup() + + +class HydrogenEAFPlantPerformanceComponent(ElectricArcFurnacePlantBasePerformanceComponent): + """Performance component for hydrogen-based electric arc furnace (EAF) plant + using the Rosner performance model. + + Attributes: + product (str): 'h2_eaf' + config (ElectricArcFurnacePerformanceBaseConfig): configuration class + coeff_df (pd.DataFrame): performance coefficient dataframe + """ + + def setup(self): + self.product = "h2_eaf" + self.feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "galUS", # "galUS/h" + "carbon": "t/h", + "lime": "t/h", + "pig_iron": "t/h", + "electricity": "kW", + } + super().setup() + + +class NaturalGasEAFPlantPerformanceComponent(ElectricArcFurnacePlantBasePerformanceComponent): + """Performance component for natural gas-based electric arc furnace (EAF) plant + using the Rosner performance model. + + Attributes: + product (str): 'ng_eaf' + config (ElectricArcFurnacePerformanceBaseConfig): configuration class + coeff_df (pd.DataFrame): performance coefficient dataframe + """ + + def setup(self): + self.feedstocks_to_units = { + "natural_gas": "MMBtu", + "water": "galUS", # "galUS/h" + "pig_iron": "t/h", + "electricity": "kW", + } + + self.product = "ng_eaf" + super().setup() diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 9781182d2..58a6916de 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -44,6 +44,12 @@ HydrogenTankPerformanceModel, ) from h2integrate.converters.hydrogen.wombat_model import WOMBATElectrolyzerModel +from h2integrate.converters.steel.steel_eaf_plant import ( + HydrogenEAFPlantCostComponent, + NaturalGasEAFPlantCostComponent, + HydrogenEAFPlantPerformanceComponent, + NaturalGasEAFPlantPerformanceComponent, +) from h2integrate.storage.battery.atb_battery_cost import ATBBatteryCostModel from h2integrate.storage.hydrogen.h2_storage_cost import ( PipeStorageCostModel, @@ -184,6 +190,10 @@ "ng_dri_cost_rosner": NaturalGasIronReductionPlantCostComponent, # standalone model "h2_dri_performance_rosner": HydrogenIronReductionPlantPerformanceComponent, "h2_dri_cost_rosner": HydrogenIronReductionPlantCostComponent, # standalone model + "ng_eaf_performance_rosner": NaturalGasEAFPlantPerformanceComponent, + "ng_eaf_cost_rosner": NaturalGasEAFPlantCostComponent, # standalone model + "h2_eaf_performance_rosner": HydrogenEAFPlantPerformanceComponent, + "h2_eaf_cost_rosner": HydrogenEAFPlantCostComponent, # standalone model "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, From 14e918bb546f754638516b139ce6ee09cc94bf29 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Mon, 29 Dec 2025 16:12:37 -0600 Subject: [PATCH 24/40] steel tests wip --- .../converters/iron/rosner/cost_coeffs.csv | 76 ++-- .../converters/steel/steel_eaf_base.py | 17 +- .../converters/steel/test/test_rosner_eaf.py | 327 ++++++++++++++++++ 3 files changed, 378 insertions(+), 42 deletions(-) create mode 100644 h2integrate/converters/steel/test/test_rosner_eaf.py diff --git a/h2integrate/converters/iron/rosner/cost_coeffs.csv b/h2integrate/converters/iron/rosner/cost_coeffs.csv index 30dbb8598..b2c7a681f 100644 --- a/h2integrate/converters/iron/rosner/cost_coeffs.csv +++ b/h2integrate/converters/iron/rosner/cost_coeffs.csv @@ -1,38 +1,38 @@ -Name,Type,Coeff,Unit,h2_dri_eaf,h2_dri,ng_dri_eaf,ng_dri,h2_eaf,ng_eaf -Dollar Year,all,-,-,2022.0,2022.0,2022.0,2022.0,2022.0,2022.0 -EAF & Casting,capital,lin,$,352191.5240169328,9.999999999999783e-11,348441.82271277835,9.999999999999996e-11,352191.5240169328,348441.82271277835 -Shaft Furnace,capital,lin,$,489.68060552071756,498.4011587515349,12706.35540748921,12706.35540748921,9.999999999999783e-11,9.999999999999996e-11 -Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,9.999999999999996e-11 -Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199391,956.7744259199391,0.0,9.999999999999996e-11 -Oxygen Supply,capital,lin,$,1715.21508561117,1364.556123425206,1204.989085989581,1024.5621894371002,393.8307342018522,201.3153857876692 -H2 Pre-heating,capital,lin,$,45.69122789208198,45.69122789208198,0.0,0.0,9.999999999999783e-11,0.0 -Cooling Tower,capital,lin,$,2513.0831352600603,2349.15226276371,1790.4037306002654,1774.6459078322403,195.45026173489327,428.90723393800783 -Piping,capital,lin,$,11815.72718570437,172.83401368816206,25651.756587058582,3917.34823234076,49970.89662834664,51294.534191987485 -Electrical & Instrumentation,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,33313.93113452472,34196.35613320244 -"Buildings, Storage, Water Service",capital,lin,$,1097.818759618821,630.531300861788,1077.3955282629602,618.8012346570356,467.2874587570587,458.59429360590775 -Other Miscellaneous Cost,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,32124.80020341839,34196.35613320244 -EAF & Casting,capital,exp,-,0.4559999999420573,1.3155100341401327e-15,0.45599999989917783,-3.9460856373682605e-16,0.4559999999420573,0.45599999989917783 -Shaft Furnace,capital,exp,-,0.8874110806752277,0.8862693772994416,0.654083508755392,0.654083508755392,1.3155100341401327e-15,-3.9460856373682605e-16 -Reformer,capital,exp,-,0.0,0.0,0.6504523630280986,0.6504523630280986,0.0,-3.9460856373682605e-16 -Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012126,0.7100000000012126,0.0,-3.9460856373682605e-16 -Oxygen Supply,capital,exp,-,0.6457441946722564,0.634272491606804,0.6448555996459648,0.6370664161269146,0.6699999996017935,0.6699999991468175 -H2 Pre-heating,capital,exp,-,0.8656365854575331,0.8656365854575331,0.0,0.0,1.3155100341401327e-15,0.0 -Cooling Tower,capital,exp,-,0.6332532740097354,0.6286978709250873,0.6302820063981899,0.6308163319151914,0.6659797264431847,0.25999986981600937 -Piping,capital,exp,-,0.5998309612457874,0.8331608480295299,0.5641056316548689,0.6551154484929355,0.461960765339552,0.45807183935606205 -Electrical & Instrumentation,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.4619607652382664,0.4580718393497376 -"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942431,0.7999999999824277,0.7999999998654156,0.7999999999666229,0.7999999997752477,0.7999999997288553 -Other Miscellaneous Cost,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.46389753648982157,0.4580718393497376 -Preproduction,owner,lin,frac of TPC,0.02,0.02,0.02,0.02,0.02,0.02 -Spare Parts,owner,lin,frac of TPC,0.005,0.005,0.005,0.005,0.005,0.005 -"Initial Catalyst, Sorbent & Chemicals",owner,lin,frac of TPC,0.0,0.0,0.250835587,0.250835587,0.0,0.250835587 -Land,owner,lin,frac of TPC,0.775,0.775,0.775,0.775,0.775,0.775 -Other Owners's Costs,owner,lin,frac of TPC,0.15,0.15,0.15,0.15,0.15,0.15 -Processing Steps,fixed opex,lin,-,29.0,14.5,29.0,14.5,14.5,14.5 -% Skilled Labor,fixed opex,lin,%,35.0,35.0,35.0,35.0,35.0,35.0 -% Unskilled Labor,fixed opex,lin,%,65.0,65.0,65.0,65.0,65.0,65.0 -Annual Operating Labor Cost,fixed opex,lin,hr/day/step/(kg/day),4.42732,4.42732,4.42732,4.42732,4.42732,4.42732 -Annual Operating Labor Cost,fixed opex,exp,-,0.25242,0.25242,0.25242,0.25242,0.25242,0.25242 -Maintenance Labor Cost,fixed opex,lin,frac of TPC,0.00863,0.00637674,0.007877171,0.007877171,0.00225326,0.007877171 -Administrative & Support Labor Cost,fixed opex,lin,frac of O&M labor,0.25,0.25,0.25,0.25,0.0,0.0 -Property Tax & Insurance,fixed opex,lin,frac of TPC,0.02,0.02,0.02,0.02,0.0,0.0 -Maintenance Materials,variable opex,lin,$/mtpy steel,7.72,2.394841843,8.82,3.72863263,5.325158157,5.09136737 +Name , Type , Coeff, Unit , h2_dri_eaf , h2_dri , ng_dri_eaf , ng_dri , h2_eaf , ng_eaf +Dollar Year , all , - , - , 2022.0 , 2022.0 , 2022.0 , 2022.0 , 2022.0 , 2022.0 +EAF & Casting , capital , lin , $ , 352191.5240169328 , 9.999999999999783e-11 , 348441.82271277835 , 9.999999999999996e-11 , 352191.5240169328 , 348441.82271277835 +Shaft Furnace , capital , lin , $ , 489.68060552071756 , 498.4011587515349 , 12706.35540748921 , 12706.35540748921 , 9.999999999999783e-11 , 9.999999999999996e-11 +Reformer , capital , lin , $ , 0.0 , 0.0 , 12585.824569411547 , 12585.824569411547 , 0.0 , 9.999999999999996e-11 +Recycle Compressor , capital , lin , $ , 0.0 , 0.0 , 956.7744259199391 , 956.7744259199391 , 0.0 , 9.999999999999996e-11 +Oxygen Supply , capital , lin , $ , 1715.21508561117 , 1364.556123425206 , 1204.989085989581 , 1024.5621894371002 , 393.8307342018522 , 201.3153857876692 +H2 Pre-heating , capital , lin , $ , 45.69122789208198 , 45.69122789208198 , 0.0 , 0.0 , 9.999999999999783e-11 , 0.0 +Cooling Tower , capital , lin , $ , 2513.0831352600603 , 2349.15226276371 , 1790.4037306002654 , 1774.6459078322403 , 195.45026173489327 , 428.90723393800783 +Piping , capital , lin , $ , 11815.72718570437 , 172.83401368816206 , 25651.756587058582 , 3917.34823234076 , 49970.89662834664 , 51294.534191987485 +Electrical & Instrumentation , capital , lin , $ , 7877.151460413984 , 115.2226758287703 , 17101.171070383258 , 2611.5654909389123 , 33313.93113452472 , 34196.35613320244 +"Buildings, Storage, Water Service" , capital , lin , $ , 1097.818759618821 , 630.531300861788 , 1077.3955282629602 , 618.8012346570356 , 467.2874587570587 , 458.59429360590775 +Other Miscellaneous Cost , capital , lin , $ , 7877.151460413984 , 115.2226758287703 , 17101.171070383258 , 2611.5654909389123 , 32124.80020341839 , 34196.35613320244 +EAF & Casting , capital , exp , - , 0.4559999999420573, 1.3155100341401327e-15, 0.45599999989917783, -3.9460856373682605e-16, 0.4559999999420573 , 0.45599999989917783 +Shaft Furnace , capital , exp , - , 0.8874110806752277, 0.8862693772994416 , 0.654083508755392 , 0.654083508755392 , 1.3155100341401327e-15, -3.9460856373682605e-16 +Reformer , capital , exp , - , 0.0 , 0.0 , 0.6504523630280986 , 0.6504523630280986 , 0.0 , -3.9460856373682605e-16 +Recycle Compressor , capital , exp , - , 0.0 , 0.0 , 0.7100000000012126 , 0.7100000000012126 , 0.0 , -3.9460856373682605e-16 +Oxygen Supply , capital , exp , - , 0.6457441946722564, 0.634272491606804 , 0.6448555996459648 , 0.6370664161269146 , 0.6699999996017935 , 0.6699999991468175 +H2 Pre-heating , capital , exp , - , 0.8656365854575331, 0.8656365854575331 , 0.0 , 0.0 , 1.3155100341401327e-15, 0.0 +Cooling Tower , capital , exp , - , 0.6332532740097354, 0.6286978709250873 , 0.6302820063981899 , 0.6308163319151914 , 0.6659797264431847 , 0.25999986981600937 +Piping , capital , exp , - , 0.5998309612457874, 0.8331608480295299 , 0.5641056316548689 , 0.6551154484929355 , 0.461960765339552 , 0.45807183935606205 +Electrical & Instrumentation , capital , exp , - , 0.599830961215121 , 0.8331608479997342 , 0.5641056316058728 , 0.6551154484197809 , 0.4619607652382664 , 0.4580718393497376 +"Buildings, Storage, Water Service" , capital , exp , - , 0.7999999998942431, 0.7999999999824277 , 0.7999999998654156 , 0.7999999999666229 , 0.7999999997752477 , 0.7999999997288553 +Other Miscellaneous Cost , capital , exp , - , 0.599830961215121 , 0.8331608479997342 , 0.5641056316058728 , 0.6551154484197809 , 0.46389753648982157 , 0.4580718393497376 +Preproduction , owner , lin , frac of TPC , 0.02 , 0.02 , 0.02 , 0.02 , 0.02 , 0.02 +Spare Parts , owner , lin , frac of TPC , 0.005 , 0.005 , 0.005 , 0.005 , 0.005 , 0.005 +"Initial Catalyst, Sorbent & Chemicals", owner , lin , frac of TPC , 0.0 , 0.0 , 0.250835587 , 0.250835587 , 0.0 , 0.250835587 +Land , owner , lin , frac of TPC , 0.775 , 0.775 , 0.775 , 0.775 , 0.775 , 0.775 +Other Owners's Costs , owner , lin , frac of TPC , 0.15 , 0.15 , 0.15 , 0.15 , 0.15 , 0.15 +Processing Steps , fixed opex , lin , - , 29.0 , 14.5 , 29.0 , 14.5 , 14.5 , 14.5 +% Skilled Labor , fixed opex , lin , % , 35.0 , 35.0 , 35.0 , 35.0 , 35.0 , 35.0 +% Unskilled Labor , fixed opex , lin , % , 65.0 , 65.0 , 65.0 , 65.0 , 65.0 , 65.0 +Annual Operating Labor Cost , fixed opex , lin , hr/day/step/(kg/day), 4.42732 , 4.42732 , 4.42732 , 4.42732 , 4.42732 , 4.42732 +Annual Operating Labor Cost , fixed opex , exp , - , 0.25242 , 0.25242 , 0.25242 , 0.25242 , 0.25242 , 0.25242 +Maintenance Labor Cost , fixed opex , lin , frac of TPC , 0.00863 , 0.00637674 , 0.007877171 , 0.007877171 , 0.00225326 , 0.007877171 +Administrative & Support Labor Cost , fixed opex , lin , frac of O&M labor , 0.25 , 0.25 , 0.25 , 0.25 , 0.0 , 0.0 +Property Tax & Insurance , fixed opex , lin , frac of TPC , 0.02 , 0.02 , 0.02 , 0.02 , 0.0 , 0.0 +Maintenance Materials , variable opex, lin , $/mtpy steel , 7.72 , 2.394841843 , 8.82 , 3.72863263 , 5.325158157 , 5.09136737 diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 9b8875953..40a8c29b5 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -205,8 +205,6 @@ def compute(self, inputs, outputs): "natural_gas": feedstocks[feedstocks["Name"] == "Natural Gas"][ "Value" ].sum(), # MMBtu/t - "carbon": feedstocks[feedstocks["Name"] == "Carbon (Coke)"]["Value"].sum(), # t/t - "lime": feedstocks[feedstocks["Name"] == "Lime"]["Value"].sum(), # t/t "water": feedstocks[feedstocks["Name"] == "Raw Water Withdrawal"][ "Value" ].sum(), # galUS/t @@ -216,6 +214,18 @@ def compute(self, inputs, outputs): ].sum(), # electricity } + if "carbon" in self.feedstocks_to_units: + # t/t + feedstocks_usage_rates["carbon"] = feedstocks[feedstocks["Name"] == "Carbon (Coke)"][ + "Value" + ].sum() # t/t + + if "lime" in self.feedstocks_to_units: + # m**3/t + feedstocks_usage_rates["lime"] = feedstocks[feedstocks["Name"] == "Lime"][ + "Value" + ].sum() # t/t + # steel demand, saturated at maximum rated system capacity steel_demand = np.where( inputs["steel_demand"] > inputs["system_capacity"], @@ -281,7 +291,6 @@ class ElectricArcFurnacePlantBaseCostComponent(CostModelBaseClass): Attributes: config (ElectricArcFurnaceCostBaseConfig): configuration class coeff_df (pd.DataFrame): cost coefficient dataframe - steel_to_iron_ratio (float): steel/pig iron ratio """ def setup(self): @@ -449,7 +458,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): varom = self.coeff_df[self.coeff_df["Type"] == "variable opex"][ "Value" ].sum() # units are USD/mtpy steel - tot_varopex = varom * self.steel_to_iron_ratio * inputs["pig_iron_out"].sum() + tot_varopex = varom * inputs["steel_out"].sum() # Adjust costs to target dollar year tot_capex_adjusted = inflate_cepci(total_capex_usd, dollar_year, self.config.cost_year) diff --git a/h2integrate/converters/steel/test/test_rosner_eaf.py b/h2integrate/converters/steel/test/test_rosner_eaf.py new file mode 100644 index 000000000..fe83ff067 --- /dev/null +++ b/h2integrate/converters/steel/test/test_rosner_eaf.py @@ -0,0 +1,327 @@ +import numpy as np +import pytest +import openmdao.api as om +from pytest import fixture + +from h2integrate import EXAMPLE_DIR +from h2integrate.core.inputs.validation import load_driver_yaml +from h2integrate.converters.steel.steel_eaf_plant import ( + HydrogenEAFPlantCostComponent, + NaturalGasEAFPlantCostComponent, + HydrogenEAFPlantPerformanceComponent, + NaturalGasEAFPlantPerformanceComponent, +) + + +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + "finance_parameters": { + "cost_adjustment_parameters": { + "cost_year_adjustment_inflation": 0.025, + "target_dollar_year": 2022, + } + }, + } + return plant_config + + +@fixture +def driver_config(): + driver_config = load_driver_yaml(EXAMPLE_DIR / "21_iron_mn_to_il" / "driver_config.yaml") + return driver_config + + +@fixture +def ng_eaf_base_config(): + tech_config = { + "model_inputs": { + "shared_parameters": { + "steel_production_rate_tonnes_per_hr": 1189772 / 8760, # t/h + }, + "cost_parameters": { + "skilled_labor_cost": 40.85, # 2022 USD/hr + "unskilled_labor_cost": 30.0, # 2022 USD/hr + }, + } + } + return tech_config + + +@fixture +def ng_feedstock_availability_costs(): + feedstocks_dict = { + "electricity": { + "rated_capacity": 56640, # need 56636.07192 kW + "units": "kW", + "price": 0.05802, # USD/kW + }, + "natural_gas": { + "rated_capacity": 277, # need 276.5024456918746 MMBtu at each timestep + "units": "MMBtu", + "price": 0.0, + }, + "water": { + "rated_capacity": 10.0, # need 9.08297025163801 galUS/h + "units": "galUS", + "price": 1670.0, # cost is $0.441167535/t, equal to $1670.0004398318847/galUS + }, + "pig_iron": { + "rated_capacity": 162, # need 161.88297569673742 t/h + "units": "t/h", + "price": 27.5409 * 1e3, # USD/t TODO UPDATE + }, + } + return feedstocks_dict + + +@fixture +def h2_feedstock_availability_costs(): + feedstocks_dict = { + "electricity": { + # (1189678/8760)t-steel/h * 433.170439 kWh/t-steel = 58828.00702 kW + "rated_capacity": 58830, # need 58828.00702 kW + "units": "kW", + "price": 0.05802, # USD/kW TODO: update + }, + "natural_gas": { + "rated_capacity": 13.0, # need 12.136117946872957 MMBtu at each timestep + "units": "MMBtu", + "price": 0.0, + }, + "carbon": { + "rated_capacity": 8.0, # need 7.306469908675799 t/h + "units": "t/h", + "price": 0.0, + }, + "lime": { + "rated_capacity": 2.5, # need 2.460840794520548 t/h + "units": "t/h", + "price": 0.0, + }, + "water": { + "rated_capacity": 6.0, # need 5.766528266260271 galUS/h + "units": "galUS", + "price": 1670.0, # TODO: update cost is $0.441167535/t, equal to $1670.0004398318847/galUS + }, + "pig_iron": { + "rated_capacity": 162, # need 161.88297569673742 t/h + "units": "t/h", + "price": 27.5409 * 1e3, # USD/t TODO: update + }, + } + return feedstocks_dict + + +def test_ng_eaf_performance( + plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests +): + expected_steel_annual_production_tpd = 3259.391781 # t/d + + prob = om.Problem() + + iron_dri_perf = NaturalGasEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_pig_iron = np.sum(prob.get_val("perf.steel_out", units="t/h")) + with subtests.test("Annual Steel"): + assert ( + pytest.approx(annual_pig_iron / 365, rel=1e-3) == expected_steel_annual_production_tpd + ) + + +def test_ng_eaf_performance_limited_feedstock( + plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests +): + expected_steel_annual_production_tpd = 3259.391781 / 2 # t/d + # make steel feedstock half of whats needed + water_usage_rate_gal_pr_tonne = 0.066881 + water_half_availability_gal_pr_hr = ( + water_usage_rate_gal_pr_tonne * expected_steel_annual_production_tpd / 24 + ) + ng_feedstock_availability_costs["water"].update( + {"rated_capacity": water_half_availability_gal_pr_hr} + ) + + prob = om.Problem() + + iron_eaf_perf = NaturalGasEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_eaf_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_steel = np.sum(prob.get_val("perf.steel_out", units="t/h")) + with subtests.test("Annual Steel"): + assert pytest.approx(annual_steel / 365, rel=1e-3) == expected_steel_annual_production_tpd + + +def test_ng_eaf_performance_cost( + plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests +): + expected_capex = 264034898.3329662 + expected_fixed_om = 38298777.651658 + expected_var_om = 32.09095 + expected_steel_annual_production_tpd = 3259.391781 # t/d + + prob = om.Problem() + + iron_eaf_perf = NaturalGasEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + iron_eaf_cost = NaturalGasEAFPlantCostComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + + prob.model.add_subsystem("perf", iron_eaf_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_eaf_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + # difference from IronPlantCostComponent: + # IronPlantCostComponent: maintenance_materials is included in Fixed OpEx + # NaturalGasIronReductionPlantCostComponent: maintenance_materials is the variable O&M + + annual_steel = np.sum(prob.get_val("perf.steel_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert pytest.approx(annual_steel / 365, rel=1e-3) == expected_steel_annual_production_tpd + with subtests.test("CapEx"): + # expected difference of 0.044534% + assert pytest.approx(prob.get_val("cost.CapEx")[0], rel=1e-3) == expected_capex + with subtests.test("OpEx"): + assert ( + pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) + == expected_fixed_om + ) + with subtests.test("VarOpEx"): + assert ( + pytest.approx( + prob.get_val("cost.VarOpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3 + ) + == expected_var_om + ) + + +def test_h2_eaf_performance( + plant_config, ng_eaf_base_config, h2_feedstock_availability_costs, subtests +): + expected_steel_annual_production_tpd = 3259.391781 # t/d + + prob = om.Problem() + + iron_dri_perf = HydrogenEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in h2_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + + annual_steel = np.sum(prob.get_val("perf.steel_out", units="t/h")) + with subtests.test("Annual Steel"): + assert pytest.approx(annual_steel / 365, rel=1e-3) == expected_steel_annual_production_tpd + + +def test_h2_eaf_performance_cost( + plant_config, ng_eaf_base_config, h2_feedstock_availability_costs, subtests +): + expected_capex = 246546589.2914324 + expected_fixed_om = 53360873.348792635 + expected_var_om = 888847481.352381 + + expected_steel_annual_production_tpd = 3259.391781 # t/d + + prob = om.Problem() + + iron_dri_perf = HydrogenEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + iron_dri_cost = HydrogenEAFPlantCostComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + + prob.model.add_subsystem("perf", iron_dri_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_dri_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in h2_feedstock_availability_costs.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + annual_steel = np.sum(prob.get_val("perf.steel_out", units="t/h")) + with subtests.test("Annual Pig Iron"): + assert pytest.approx(annual_steel / 365, rel=1e-3) == expected_steel_annual_production_tpd + with subtests.test("CapEx"): + # expected difference of 0.044534% + assert pytest.approx(prob.get_val("cost.CapEx")[0], rel=1e-3) == expected_capex + with subtests.test("OpEx"): + assert ( + pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) + == expected_fixed_om + ) + with subtests.test("VarOpEx"): + assert ( + pytest.approx( + prob.get_val("cost.VarOpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3 + ) + == expected_var_om + ) From e8aca5031fad5a259f96871cebe82e054d494c4e Mon Sep 17 00:00:00 2001 From: kbrunik Date: Mon, 29 Dec 2025 16:34:51 -0600 Subject: [PATCH 25/40] undo align --- .../converters/iron/rosner/cost_coeffs.csv | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/h2integrate/converters/iron/rosner/cost_coeffs.csv b/h2integrate/converters/iron/rosner/cost_coeffs.csv index b2c7a681f..30dbb8598 100644 --- a/h2integrate/converters/iron/rosner/cost_coeffs.csv +++ b/h2integrate/converters/iron/rosner/cost_coeffs.csv @@ -1,38 +1,38 @@ -Name , Type , Coeff, Unit , h2_dri_eaf , h2_dri , ng_dri_eaf , ng_dri , h2_eaf , ng_eaf -Dollar Year , all , - , - , 2022.0 , 2022.0 , 2022.0 , 2022.0 , 2022.0 , 2022.0 -EAF & Casting , capital , lin , $ , 352191.5240169328 , 9.999999999999783e-11 , 348441.82271277835 , 9.999999999999996e-11 , 352191.5240169328 , 348441.82271277835 -Shaft Furnace , capital , lin , $ , 489.68060552071756 , 498.4011587515349 , 12706.35540748921 , 12706.35540748921 , 9.999999999999783e-11 , 9.999999999999996e-11 -Reformer , capital , lin , $ , 0.0 , 0.0 , 12585.824569411547 , 12585.824569411547 , 0.0 , 9.999999999999996e-11 -Recycle Compressor , capital , lin , $ , 0.0 , 0.0 , 956.7744259199391 , 956.7744259199391 , 0.0 , 9.999999999999996e-11 -Oxygen Supply , capital , lin , $ , 1715.21508561117 , 1364.556123425206 , 1204.989085989581 , 1024.5621894371002 , 393.8307342018522 , 201.3153857876692 -H2 Pre-heating , capital , lin , $ , 45.69122789208198 , 45.69122789208198 , 0.0 , 0.0 , 9.999999999999783e-11 , 0.0 -Cooling Tower , capital , lin , $ , 2513.0831352600603 , 2349.15226276371 , 1790.4037306002654 , 1774.6459078322403 , 195.45026173489327 , 428.90723393800783 -Piping , capital , lin , $ , 11815.72718570437 , 172.83401368816206 , 25651.756587058582 , 3917.34823234076 , 49970.89662834664 , 51294.534191987485 -Electrical & Instrumentation , capital , lin , $ , 7877.151460413984 , 115.2226758287703 , 17101.171070383258 , 2611.5654909389123 , 33313.93113452472 , 34196.35613320244 -"Buildings, Storage, Water Service" , capital , lin , $ , 1097.818759618821 , 630.531300861788 , 1077.3955282629602 , 618.8012346570356 , 467.2874587570587 , 458.59429360590775 -Other Miscellaneous Cost , capital , lin , $ , 7877.151460413984 , 115.2226758287703 , 17101.171070383258 , 2611.5654909389123 , 32124.80020341839 , 34196.35613320244 -EAF & Casting , capital , exp , - , 0.4559999999420573, 1.3155100341401327e-15, 0.45599999989917783, -3.9460856373682605e-16, 0.4559999999420573 , 0.45599999989917783 -Shaft Furnace , capital , exp , - , 0.8874110806752277, 0.8862693772994416 , 0.654083508755392 , 0.654083508755392 , 1.3155100341401327e-15, -3.9460856373682605e-16 -Reformer , capital , exp , - , 0.0 , 0.0 , 0.6504523630280986 , 0.6504523630280986 , 0.0 , -3.9460856373682605e-16 -Recycle Compressor , capital , exp , - , 0.0 , 0.0 , 0.7100000000012126 , 0.7100000000012126 , 0.0 , -3.9460856373682605e-16 -Oxygen Supply , capital , exp , - , 0.6457441946722564, 0.634272491606804 , 0.6448555996459648 , 0.6370664161269146 , 0.6699999996017935 , 0.6699999991468175 -H2 Pre-heating , capital , exp , - , 0.8656365854575331, 0.8656365854575331 , 0.0 , 0.0 , 1.3155100341401327e-15, 0.0 -Cooling Tower , capital , exp , - , 0.6332532740097354, 0.6286978709250873 , 0.6302820063981899 , 0.6308163319151914 , 0.6659797264431847 , 0.25999986981600937 -Piping , capital , exp , - , 0.5998309612457874, 0.8331608480295299 , 0.5641056316548689 , 0.6551154484929355 , 0.461960765339552 , 0.45807183935606205 -Electrical & Instrumentation , capital , exp , - , 0.599830961215121 , 0.8331608479997342 , 0.5641056316058728 , 0.6551154484197809 , 0.4619607652382664 , 0.4580718393497376 -"Buildings, Storage, Water Service" , capital , exp , - , 0.7999999998942431, 0.7999999999824277 , 0.7999999998654156 , 0.7999999999666229 , 0.7999999997752477 , 0.7999999997288553 -Other Miscellaneous Cost , capital , exp , - , 0.599830961215121 , 0.8331608479997342 , 0.5641056316058728 , 0.6551154484197809 , 0.46389753648982157 , 0.4580718393497376 -Preproduction , owner , lin , frac of TPC , 0.02 , 0.02 , 0.02 , 0.02 , 0.02 , 0.02 -Spare Parts , owner , lin , frac of TPC , 0.005 , 0.005 , 0.005 , 0.005 , 0.005 , 0.005 -"Initial Catalyst, Sorbent & Chemicals", owner , lin , frac of TPC , 0.0 , 0.0 , 0.250835587 , 0.250835587 , 0.0 , 0.250835587 -Land , owner , lin , frac of TPC , 0.775 , 0.775 , 0.775 , 0.775 , 0.775 , 0.775 -Other Owners's Costs , owner , lin , frac of TPC , 0.15 , 0.15 , 0.15 , 0.15 , 0.15 , 0.15 -Processing Steps , fixed opex , lin , - , 29.0 , 14.5 , 29.0 , 14.5 , 14.5 , 14.5 -% Skilled Labor , fixed opex , lin , % , 35.0 , 35.0 , 35.0 , 35.0 , 35.0 , 35.0 -% Unskilled Labor , fixed opex , lin , % , 65.0 , 65.0 , 65.0 , 65.0 , 65.0 , 65.0 -Annual Operating Labor Cost , fixed opex , lin , hr/day/step/(kg/day), 4.42732 , 4.42732 , 4.42732 , 4.42732 , 4.42732 , 4.42732 -Annual Operating Labor Cost , fixed opex , exp , - , 0.25242 , 0.25242 , 0.25242 , 0.25242 , 0.25242 , 0.25242 -Maintenance Labor Cost , fixed opex , lin , frac of TPC , 0.00863 , 0.00637674 , 0.007877171 , 0.007877171 , 0.00225326 , 0.007877171 -Administrative & Support Labor Cost , fixed opex , lin , frac of O&M labor , 0.25 , 0.25 , 0.25 , 0.25 , 0.0 , 0.0 -Property Tax & Insurance , fixed opex , lin , frac of TPC , 0.02 , 0.02 , 0.02 , 0.02 , 0.0 , 0.0 -Maintenance Materials , variable opex, lin , $/mtpy steel , 7.72 , 2.394841843 , 8.82 , 3.72863263 , 5.325158157 , 5.09136737 +Name,Type,Coeff,Unit,h2_dri_eaf,h2_dri,ng_dri_eaf,ng_dri,h2_eaf,ng_eaf +Dollar Year,all,-,-,2022.0,2022.0,2022.0,2022.0,2022.0,2022.0 +EAF & Casting,capital,lin,$,352191.5240169328,9.999999999999783e-11,348441.82271277835,9.999999999999996e-11,352191.5240169328,348441.82271277835 +Shaft Furnace,capital,lin,$,489.68060552071756,498.4011587515349,12706.35540748921,12706.35540748921,9.999999999999783e-11,9.999999999999996e-11 +Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,9.999999999999996e-11 +Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199391,956.7744259199391,0.0,9.999999999999996e-11 +Oxygen Supply,capital,lin,$,1715.21508561117,1364.556123425206,1204.989085989581,1024.5621894371002,393.8307342018522,201.3153857876692 +H2 Pre-heating,capital,lin,$,45.69122789208198,45.69122789208198,0.0,0.0,9.999999999999783e-11,0.0 +Cooling Tower,capital,lin,$,2513.0831352600603,2349.15226276371,1790.4037306002654,1774.6459078322403,195.45026173489327,428.90723393800783 +Piping,capital,lin,$,11815.72718570437,172.83401368816206,25651.756587058582,3917.34823234076,49970.89662834664,51294.534191987485 +Electrical & Instrumentation,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,33313.93113452472,34196.35613320244 +"Buildings, Storage, Water Service",capital,lin,$,1097.818759618821,630.531300861788,1077.3955282629602,618.8012346570356,467.2874587570587,458.59429360590775 +Other Miscellaneous Cost,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,32124.80020341839,34196.35613320244 +EAF & Casting,capital,exp,-,0.4559999999420573,1.3155100341401327e-15,0.45599999989917783,-3.9460856373682605e-16,0.4559999999420573,0.45599999989917783 +Shaft Furnace,capital,exp,-,0.8874110806752277,0.8862693772994416,0.654083508755392,0.654083508755392,1.3155100341401327e-15,-3.9460856373682605e-16 +Reformer,capital,exp,-,0.0,0.0,0.6504523630280986,0.6504523630280986,0.0,-3.9460856373682605e-16 +Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012126,0.7100000000012126,0.0,-3.9460856373682605e-16 +Oxygen Supply,capital,exp,-,0.6457441946722564,0.634272491606804,0.6448555996459648,0.6370664161269146,0.6699999996017935,0.6699999991468175 +H2 Pre-heating,capital,exp,-,0.8656365854575331,0.8656365854575331,0.0,0.0,1.3155100341401327e-15,0.0 +Cooling Tower,capital,exp,-,0.6332532740097354,0.6286978709250873,0.6302820063981899,0.6308163319151914,0.6659797264431847,0.25999986981600937 +Piping,capital,exp,-,0.5998309612457874,0.8331608480295299,0.5641056316548689,0.6551154484929355,0.461960765339552,0.45807183935606205 +Electrical & Instrumentation,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.4619607652382664,0.4580718393497376 +"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942431,0.7999999999824277,0.7999999998654156,0.7999999999666229,0.7999999997752477,0.7999999997288553 +Other Miscellaneous Cost,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.46389753648982157,0.4580718393497376 +Preproduction,owner,lin,frac of TPC,0.02,0.02,0.02,0.02,0.02,0.02 +Spare Parts,owner,lin,frac of TPC,0.005,0.005,0.005,0.005,0.005,0.005 +"Initial Catalyst, Sorbent & Chemicals",owner,lin,frac of TPC,0.0,0.0,0.250835587,0.250835587,0.0,0.250835587 +Land,owner,lin,frac of TPC,0.775,0.775,0.775,0.775,0.775,0.775 +Other Owners's Costs,owner,lin,frac of TPC,0.15,0.15,0.15,0.15,0.15,0.15 +Processing Steps,fixed opex,lin,-,29.0,14.5,29.0,14.5,14.5,14.5 +% Skilled Labor,fixed opex,lin,%,35.0,35.0,35.0,35.0,35.0,35.0 +% Unskilled Labor,fixed opex,lin,%,65.0,65.0,65.0,65.0,65.0,65.0 +Annual Operating Labor Cost,fixed opex,lin,hr/day/step/(kg/day),4.42732,4.42732,4.42732,4.42732,4.42732,4.42732 +Annual Operating Labor Cost,fixed opex,exp,-,0.25242,0.25242,0.25242,0.25242,0.25242,0.25242 +Maintenance Labor Cost,fixed opex,lin,frac of TPC,0.00863,0.00637674,0.007877171,0.007877171,0.00225326,0.007877171 +Administrative & Support Labor Cost,fixed opex,lin,frac of O&M labor,0.25,0.25,0.25,0.25,0.0,0.0 +Property Tax & Insurance,fixed opex,lin,frac of TPC,0.02,0.02,0.02,0.02,0.0,0.0 +Maintenance Materials,variable opex,lin,$/mtpy steel,7.72,2.394841843,8.82,3.72863263,5.325158157,5.09136737 From 5b2050a97c8277d709aca867b8844b8149010fc7 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:00:09 -0700 Subject: [PATCH 26/40] minor update on handling negative exponents in steel eaf cost model --- h2integrate/converters/steel/steel_eaf_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 40a8c29b5..4ce62e4c3 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -359,10 +359,10 @@ def format_coeff_df(self, coeff_df): pd.DataFrame: cost coefficient dataframe """ - perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" + # perf_coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" - perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) - perf_df = perf_df[perf_df["Product"] == self.product] + # perf_df = pd.read_csv(perf_coeff_fpath, index_col=0) + # perf_df = perf_df[perf_df["Product"] == self.product] # only include data for the given product @@ -391,7 +391,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): total_capex_usd = 0.0 for item in capital_items: if ( - capital_items_df.loc[item, "exp"]["Value"] > 0 + np.abs(capital_items_df.loc[item, "exp"]["Value"]) > 0 and capital_items_df.loc[item, "lin"]["Value"] > 0 ): capex = capital_items_df.loc[item, "lin"]["Value"] * ( From 1383f2b0acb7e40f09c4f05c2fe4931a125ebed6 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:13:53 -0700 Subject: [PATCH 27/40] added carbon and lime to pipe --- h2integrate/transporters/pipe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/h2integrate/transporters/pipe.py b/h2integrate/transporters/pipe.py index 459882d1c..eeefa1f44 100644 --- a/h2integrate/transporters/pipe.py +++ b/h2integrate/transporters/pipe.py @@ -20,6 +20,8 @@ def initialize(self): "iron_ore", "reformer_catalyst", "water", + "carbon", + "lime", ], ) From 991a716ac62bf4c29a8d3626f86bae5abc3fefcc Mon Sep 17 00:00:00 2001 From: kbrunik Date: Mon, 29 Dec 2025 17:21:12 -0600 Subject: [PATCH 28/40] update rosner eaf test --- .../converters/steel/test/test_rosner_eaf.py | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/h2integrate/converters/steel/test/test_rosner_eaf.py b/h2integrate/converters/steel/test/test_rosner_eaf.py index fe83ff067..842e420ee 100644 --- a/h2integrate/converters/steel/test/test_rosner_eaf.py +++ b/h2integrate/converters/steel/test/test_rosner_eaf.py @@ -189,10 +189,12 @@ def test_ng_eaf_performance_limited_feedstock( def test_ng_eaf_performance_cost( plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests ): + ng_eaf_base_config["model_inputs"]["shared_parameters"][ + "steel_production_rate_tonnes_per_hr" + ] = 838926.9489 / 8760 expected_capex = 264034898.3329662 expected_fixed_om = 38298777.651658 - expected_var_om = 32.09095 - expected_steel_annual_production_tpd = 3259.391781 # t/d + expected_steel_annual_production_tpd = 2298.43 # t/d prob = om.Problem() @@ -235,13 +237,6 @@ def test_ng_eaf_performance_cost( pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) == expected_fixed_om ) - with subtests.test("VarOpEx"): - assert ( - pytest.approx( - prob.get_val("cost.VarOpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3 - ) - == expected_var_om - ) def test_h2_eaf_performance( @@ -275,11 +270,13 @@ def test_h2_eaf_performance( def test_h2_eaf_performance_cost( plant_config, ng_eaf_base_config, h2_feedstock_availability_costs, subtests ): - expected_capex = 246546589.2914324 - expected_fixed_om = 53360873.348792635 - expected_var_om = 888847481.352381 + ng_eaf_base_config["model_inputs"]["shared_parameters"][ + "steel_production_rate_tonnes_per_hr" + ] = 838926.9489 / 8760 + expected_capex = 271492352.2740321 + expected_fixed_om = 37048005.00181486 - expected_steel_annual_production_tpd = 3259.391781 # t/d + expected_steel_annual_production_tpd = 2298.43 # t/d prob = om.Problem() @@ -318,10 +315,3 @@ def test_h2_eaf_performance_cost( pytest.approx(prob.get_val("cost.OpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3) == expected_fixed_om ) - with subtests.test("VarOpEx"): - assert ( - pytest.approx( - prob.get_val("cost.VarOpEx")[0] + prob.get_val("cost.VarOpEx")[0], rel=1e-3 - ) - == expected_var_om - ) From 0baf6d752aa863137dbd4f5f6052c408cc67a9c6 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 29 Dec 2025 16:22:19 -0700 Subject: [PATCH 29/40] draft update to iron example --- examples/21_iron_mn_to_il/tech_config.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 510cc5f4e..bc31add1b 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -138,3 +138,17 @@ technologies: cost_parameters: skilled_labor_cost: 40.85 #2022 USD/hr unskilled_labor_cost: 30.0 #2022 USD/hr + + steel_plant: + performance_model: + model: "ng_eaf_performance_rosner" + cost_model: + model: "ng_eaf_cost_rosner" + model_inputs: + shared_parameters: + steel_production_rate_tonnes_per_hr: 135.8187214611872 #equivalent to 1189772 t/yr + performance_parameters: + water_density: 1000 #kg/m3 + cost_parameters: + skilled_labor_cost: 40.85 #2022 USD/hr + unskilled_labor_cost: 30.0 #2022 USD/hr From 343708de0c817099514f6f7423f702d83709ea1b Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:33:52 -0700 Subject: [PATCH 30/40] updated iron example to include steel eaf --- examples/21_iron_mn_to_il/plant_config.yaml | 36 ++++++++++- examples/21_iron_mn_to_il/tech_config.yaml | 70 ++++++++++++++++++--- h2integrate/transporters/pipe.py | 1 + 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 5024efa43..465c429b1 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -34,8 +34,12 @@ technology_interconnections: [ ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], ["natural_gas_feedstock","iron_plant","natural_gas","pipe"], + # connect feedstocks to steel plant + ["eaf_grid_feedstock","steel_plant","electricity","cable"], + ["eaf_water_feedstock","steel_plant","water","pipe"], + ["eaf_natural_gas_feedstock","steel_plant","natural_gas","pipe"], + ["iron_plant","steel_plant","pig_iron","pipe"], # ["iron_transport","iron_plant","iron_transport_cost"], - # ["finance_subgroup_iron_ore","iron_plant","price_iron_ore"], ] plant: @@ -74,8 +78,34 @@ finance_parameters: iron_ore: commodity: "iron_ore" commodity_stream: "iron_mine" - technologies: ["iron_mine"] + technologies: ["iron_mine", "grid_feedstock", "mine_feedstock"] pig_iron: commodity: "pig_iron" commodity_stream: "iron_plant" - technologies: ["iron_plant", "dri_grid_feedstock", "catalyst_feedstock", "water_feedstock", "natural_gas_feedstock"] # + technologies: + - "iron_mine" + - "grid_feedstock" + - "mine_feedstock" + - "iron_transport" + - "iron_plant" + - "dri_grid_feedstock" + - "catalyst_feedstock" + - "water_feedstock" + - "natural_gas_feedstock" + steel: + commodity: "steel" + commodity_stream: "steel_plant" + technologies: + - "iron_mine" + - "grid_feedstock" + - "mine_feedstock" + - "iron_transport" + - "iron_plant" + - "dri_grid_feedstock" + - "catalyst_feedstock" + - "water_feedstock" + - "natural_gas_feedstock" + - "eaf_water_feedstock" + - "eaf_natural_gas_feedstock" + - "eaf_grid_feedstock" + - "steel_plant" diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index bc31add1b..0bb1d4444 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -12,12 +12,13 @@ technologies: feedstock_type: "electricity" units: "MW" performance_parameters: - rated_capacity: 750. # MW + rated_capacity: 30. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 - price: 0.0 + price: 58.02 #USD/MW annual_cost: 0. start_up_cost: 0. + mine_feedstock: #iron ore feedstock performance_model: model: "feedstock_performance" @@ -28,12 +29,13 @@ technologies: feedstock_type: "crude_ore" units: "t/h" performance_parameters: - rated_capacity: 2000. # kg/h + rated_capacity: 2000. # need 828.50385048 t/h cost_parameters: cost_year: 2022 price: 0.0 annual_cost: 0. start_up_cost: 0. + iron_mine: performance_model: model: "iron_mine_performance_martin" @@ -43,7 +45,8 @@ technologies: shared_parameters: mine: "Northshore" taconite_pellet_type: "drg" - max_ore_production_rate_tonnes_per_hr: 516.0497610311598 + max_ore_production_rate_tonnes_per_hr: 221.2592636 #516.0497610311598 + iron_transport: performance_model: model: "iron_transport_performance" @@ -91,7 +94,7 @@ technologies: annual_cost: 0. start_up_cost: 0. - catalyst_feedstock: #for iron reduction + catalyst_feedstock: #for NG iron reduction performance_model: model: "feedstock_performance" cost_model: @@ -124,12 +127,12 @@ technologies: price: 0.05802 #USD/kW annual_cost: 0. start_up_cost: 0. - # need 263.7474 t/h of iron ore + iron_plant: performance_model: - model: "ng_dri_performance_rosner" #"iron_plant_performance" + model: "ng_dri_performance_rosner" cost_model: - model: "ng_dri_cost_rosner" #"iron_plant_cost" + model: "ng_dri_cost_rosner" model_inputs: shared_parameters: pig_iron_production_rate_tonnes_per_hr: 161.8829908675799 #equivalent to 1418095 t/yr @@ -139,6 +142,57 @@ technologies: skilled_labor_cost: 40.85 #2022 USD/hr unskilled_labor_cost: 30.0 #2022 USD/hr + eaf_grid_feedstock: #electricity feedstock for EAF + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "electricity" + units: "kW" + performance_parameters: + rated_capacity: 56650. # need 56642.327357 kW + cost_parameters: + cost_year: 2022 + price: 0.05802 #USD/kW + annual_cost: 0. + start_up_cost: 0. + + eaf_water_feedstock: #for EAF + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "water" + units: "galUS" #galUS/h + performance_parameters: + rated_capacity: 10. # need 9.083687924154146 galUS/h + cost_parameters: + cost_year: 2022 + price: 0.0016700004398318847 # cost is USD 0.441167535/t, converted to USD/gal + annual_cost: 0. + start_up_cost: 0. + + eaf_natural_gas_feedstock: + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "natural_gas" + units: "MMBtu" + performance_parameters: + rated_capacity: 280. # need 276.5242929731515 MMBtu at each timestep + cost_parameters: + cost_year: 2022 + price: 4.0 #USD 4.0/MMBtu + annual_cost: 0. + start_up_cost: 0. + steel_plant: performance_model: model: "ng_eaf_performance_rosner" diff --git a/h2integrate/transporters/pipe.py b/h2integrate/transporters/pipe.py index eeefa1f44..40b8d64cc 100644 --- a/h2integrate/transporters/pipe.py +++ b/h2integrate/transporters/pipe.py @@ -22,6 +22,7 @@ def initialize(self): "water", "carbon", "lime", + "pig_iron", ], ) From ba7692f6019acb567615111d8979f10c6149fcbb Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:42:08 -0700 Subject: [PATCH 31/40] updated doc page and changelog --- CHANGELOG.md | 1 + docs/user_guide/model_overview.md | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d4d403b..1b4272690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.5.x [TBD] - Updates models for NumPy version 2.4.0 - Update test values for WOMBAT update to 0.13.0 +- Added standlone iron DRI and steel EAF performance and cost models ## 0.5.1 [December 18, 2025] diff --git a/docs/user_guide/model_overview.md b/docs/user_guide/model_overview.md index da1768ac0..f79c277ab 100644 --- a/docs/user_guide/model_overview.md +++ b/docs/user_guide/model_overview.md @@ -166,8 +166,13 @@ Below summarizes the available performance, cost, and financial models for each - `steel`: steel production - performance models: + `'steel_performance'` + + `'ng_eaf_performance_rosner'` + + `'h2_eaf_performance_rosner'` - combined cost and financial models: + `'steel_cost'` + - cost models: + + `'ng_eaf_cost_rosner'` + + `'h2_eaf_cost_rosner'` - `ammonia`: ammonia synthesis - performance models: + `'simple_ammonia_performance'` @@ -214,6 +219,18 @@ Below summarizes the available performance, cost, and financial models for each + `'grid_performance'` - cost models: + `'grid_cost'` +- `iron_ore`: iron ore mining and refining + - performance models: + + `'iron_mine_performance_martin'` + - cost models: + + `'iron_mine_cost_martin'` +- `iron_dri`: iron ore direct reduction + - performance models: + + `'ng_dri_performance_rosner'` + + `'h2_dri_performance_rosner'` + - cost models: + + `'ng_dri_cost_rosner'` + + `'h2_dri_cost_rosner'` (transport-models)= ## Transport Models @@ -222,7 +239,7 @@ Below summarizes the available performance, cost, and financial models for each + `'cable'`: specific to `electricity` commodity - `pipe`: - performance models: - + `'pipe'`: currently compatible with the commodities "hydrogen", "co2", "methanol", "ammonia", "nitrogen", "natural_gas" + + `'pipe'`: currently compatible with the commodities "hydrogen", "co2", "methanol", "ammonia", "nitrogen", "natural_gas", "pig_iron", "reformer_catalyst", "water", "carbon", "iron_ore", and "lime" - `combiner`: - performance models: + `'combiner_performance'`: can be used for any commodity From cf04d4ac3bc5a85027f9ca53dde07a43a58b9c6d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:48:21 -0700 Subject: [PATCH 32/40] added small test for example 21 --- examples/test/test_all_examples.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 9d216777d..767b4a694 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -1293,3 +1293,23 @@ def test_24_solar_battery_grid_example(subtests): with subtests.test("Value check on LCOE"): lcoe = model.prob.get_val("finance_subgroup_renewables.LCOE", units="USD/(MW*h)")[0] assert pytest.approx(lcoe, rel=1e-4) == 91.7057887 + + +def test_21_iron_dri_eaf_example(subtests): + os.chdir(EXAMPLE_DIR / "21_iron_mn_to_il") + + h2i = H2IntegrateModel("21_iron.yaml") + + h2i.run() + + with subtests.test("Value check on LCOI"): + lcoi = h2i.model.get_val("finance_subgroup_iron_ore.LCOI", units="USD/t")[0] + assert pytest.approx(lcoi, rel=1e-4) == 143.3495266638054 + + with subtests.test("Value check on LCOP"): + lcop = h2i.model.get_val("finance_subgroup_pig_iron.LCOP", units="USD/t")[0] + assert pytest.approx(lcop, rel=1e-4) == 353.63339139124 + + with subtests.test("Value check on LCOS"): + lcos = h2i.model.get_val("finance_subgroup_steel.LCOS", units="USD/t")[0] + assert pytest.approx(lcos, rel=1e-4) == 524.2665698971817 From d0fbd6a4e81fc8f99434ccde070eabf5b5b32f5c Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Tue, 30 Dec 2025 10:55:44 -0700 Subject: [PATCH 33/40] Fix run_iron.py to actually compare old and new --- examples/21_iron_mn_to_il/run_iron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/21_iron_mn_to_il/run_iron.py b/examples/21_iron_mn_to_il/run_iron.py index 70f1f7c38..40ff5f014 100644 --- a/examples/21_iron_mn_to_il/run_iron.py +++ b/examples/21_iron_mn_to_il/run_iron.py @@ -29,7 +29,7 @@ model_old.run() model.post_process() model_old.post_process() - lcois.append(float(model_old.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) + lcois.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) lcois_old.append(float(model_old.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) # Compare the LCOIs from iron_wrapper and modular iron From cf0a839c59ebf97053c3e8a1b739b0a8da4d47e9 Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Tue, 30 Dec 2025 16:47:54 -0700 Subject: [PATCH 34/40] Comparing old and new models --- examples/21_iron_mn_to_il/plant_config.yaml | 17 +++++----- examples/21_iron_mn_to_il/run_iron.py | 27 ++++++++++++++- examples/21_iron_mn_to_il/tech_config.yaml | 33 +++++++++++++++++-- h2integrate/converters/iron/iron_dri_base.py | 2 +- h2integrate/converters/iron/iron_plant.py | 4 +-- h2integrate/converters/iron/iron_transport.py | 14 ++++++-- .../converters/iron/martin_mine_cost_model.py | 2 +- .../martin_ore/cost_coeffs_elec_split.csv | 27 +++++++++++++++ .../martin_ore/cost_inputs_elec_split.csv | 27 +++++++++++++++ h2integrate/core/feedstocks.py | 8 ++++- h2integrate/transporters/generic_splitter.py | 33 +++++++++++++++++-- 11 files changed, 172 insertions(+), 22 deletions(-) create mode 100644 h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv create mode 100644 h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 465c429b1..739966f5d 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -27,9 +27,14 @@ technology_interconnections: [ ["mine_feedstock","iron_mine","crude_ore","pipe"], # connect iron mine to iron transport #TODO: update after updated transport model is integrated - ["iron_mine","iron_transport","total_iron_ore_produced"], - ["iron_mine","iron_plant","iron_ore","pipe"], #temp workaround until transport model is updated + ["iron_mine","ore_splitter","iron_ore","pipe"], + ["iron_mine","ore_splitter","total_iron_ore_produced"], + ["iron_plant","ore_splitter",["iron_ore_consumed","prescribed_commodity_to_priority_tech"]], + ["ore_splitter","iron_transport",["total_iron_ore_produced1","total_iron_ore_produced"]], + # connect iron ore price to feedstock + ["finance_subgroup_iron_ore","ore_feedstock",["price_iron_ore","price"]], # connect feedstocks to iron plant + ["ore_feedstock","iron_plant","iron_ore","pipe"], ["dri_grid_feedstock","iron_plant","electricity","cable"], ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], @@ -83,9 +88,7 @@ finance_parameters: commodity: "pig_iron" commodity_stream: "iron_plant" technologies: - - "iron_mine" - - "grid_feedstock" - - "mine_feedstock" + - "ore_feedstock" - "iron_transport" - "iron_plant" - "dri_grid_feedstock" @@ -96,9 +99,7 @@ finance_parameters: commodity: "steel" commodity_stream: "steel_plant" technologies: - - "iron_mine" - - "grid_feedstock" - - "mine_feedstock" + - "ore_feedstock" - "iron_transport" - "iron_plant" - "dri_grid_feedstock" diff --git a/examples/21_iron_mn_to_il/run_iron.py b/examples/21_iron_mn_to_il/run_iron.py index 40ff5f014..b6c8e7871 100644 --- a/examples/21_iron_mn_to_il/run_iron.py +++ b/examples/21_iron_mn_to_il/run_iron.py @@ -21,6 +21,12 @@ ] lcois = [] lcois_old = [] +capexes = [] +fopexes = [] +vopexes = [] +capexes_old = [] +fopexes_old = [] +vopexes_old = [] for casename in casenames: model = modify_tech_config(model, cases[casename]) @@ -31,7 +37,26 @@ model_old.post_process() lcois.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) lcois_old.append(float(model_old.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) - + capexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0])) + capexes_old.append( + float(model_old.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0]) + ) + fopexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0])) + fopexes_old.append( + float(model_old.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0]) + ) + vopexes.append( + float(model.model.get_val("finance_subgroup_pig_iron.total_varopex_adjusted")[0]) + ) + vopexes_old.append( + float(model_old.model.get_val("finance_subgroup_pig_iron.total_varopex_adjusted")[0]) + ) # Compare the LCOIs from iron_wrapper and modular iron +print(capexes) +print(capexes_old) +print(fopexes) +print(fopexes_old) +print(vopexes) +print(vopexes_old) print(lcois) print(lcois_old) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 0bb1d4444..4e8aabbd1 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -12,7 +12,7 @@ technologies: feedstock_type: "electricity" units: "MW" performance_parameters: - rated_capacity: 30. # MW, need 27.913 MW per timestep for iron ore + rated_capacity: 100. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 price: 58.02 #USD/MW @@ -36,6 +36,23 @@ technologies: annual_cost: 0. start_up_cost: 0. + ore_feedstock: #iron ore feedstock + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "iron_ore" + units: "t/h" + performance_parameters: + rated_capacity: 2000. # need 828.50385048 t/h + cost_parameters: + cost_year: 2022 + price: 0.0 + annual_cost: 0. + start_up_cost: 0. + iron_mine: performance_model: model: "iron_mine_performance_martin" @@ -45,7 +62,17 @@ technologies: shared_parameters: mine: "Northshore" taconite_pellet_type: "drg" - max_ore_production_rate_tonnes_per_hr: 221.2592636 #516.0497610311598 + max_ore_production_rate_tonnes_per_hr: 516.0497610311598 + + ore_splitter: + performance_model: + model: "splitter_performance" + model_inputs: + performance_parameters: + split_mode: "prescribed_commodity" + commodity: "iron_ore" + commodity_units: "t/h" + prescribed_commodity_to_priority_tech: 221.2592636 iron_transport: performance_model: @@ -87,7 +114,7 @@ technologies: feedstock_type: "water" units: "galUS" #galUS/h performance_parameters: - rated_capacity: 40. # need 38.71049649 galUS/h + rated_capacity: 40000. # need 38.71049649 galUS/h cost_parameters: cost_year: 2022 price: 0.0016700004398318847 # cost is USD0.441167535/t, converted to USD/gal diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index 182e966f3..0331f0f96 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -156,7 +156,7 @@ def format_coeff_df(self, coeff_df): # then convert cubic meters to liters 1e3*(value*1e3)/density water_volume_m3 = coeff_df.loc[i_update]["Value"] * 1e3 / self.config.water_density water_volume_L = units.convert_units(water_volume_m3.values, "m**3", "L") - coeff_df.loc[i_update]["Value"] = water_volume_L + coeff_df.loc[i_update, "Value"] = water_volume_L old_unit = f"L/{capacity_unit}" old_unit = ( diff --git a/h2integrate/converters/iron/iron_plant.py b/h2integrate/converters/iron/iron_plant.py index dfe964f09..e377b38b4 100644 --- a/h2integrate/converters/iron/iron_plant.py +++ b/h2integrate/converters/iron/iron_plant.py @@ -256,7 +256,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # TODO: make natural gas costs an input natural_gas_prices_MMBTU = coeff_dict["Natural Gas"]["values"][indices].astype(float) - natural_gas_prices_GJ = natural_gas_prices_MMBTU * 1.05506 # Convert to GJ + natural_gas_prices_GJ = natural_gas_prices_MMBTU / 1.05506 # Convert to GJ iron_ore_pellet_unitcost_tonne = inputs["price_iron_ore"] if inputs["iron_transport_cost"] > 0: @@ -264,7 +264,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): ore_profit_pct = inputs["ore_profit_pct"][0] iron_ore_pellet_unitcost_tonne = ( iron_ore_pellet_unitcost_tonne + iron_transport_cost_tonne - ) * (1 + ore_profit_pct / 100) + ) * (1 + ore_profit_pct * 0 / 100) v_start = years.index(self.config.operational_year) - years.index(analysis_start) + 1 variable_om += perf_ds["Raw Water Withdrawal"] * raw_water_unitcost_tonne diff --git a/h2integrate/converters/iron/iron_transport.py b/h2integrate/converters/iron/iron_transport.py index be72e2568..0f16d8ee8 100644 --- a/h2integrate/converters/iron/iron_transport.py +++ b/h2integrate/converters/iron/iron_transport.py @@ -183,12 +183,20 @@ def setup(self): self.add_input("land_transport_distance", val=0.0, units="mi") self.add_input("water_transport_distance", val=0.0, units="mi") self.add_input("total_transport_distance", val=0.0, units="mi") + # self.add_input("iron_ore_consumed", val=0.0, units="t/h") self.add_input("total_iron_ore_produced", val=0.0, units="t/year") self.add_output("iron_transport_cost", val=0.0, units="USD/t") self.add_output("ore_profit_margin", val=0.0, units="USD/t") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + # ore_consumed = inputs["iron_ore_consumed"] + # ore_consumed_sum = np.sum(ore_consumed) + # if ore_consumed_sum > 0: + # total_ore = ore_consumed_sum + # else: + total_ore = inputs["total_iron_ore_produced"] + water_coeff_dict = load_top_down_coeffs( ["Barge Shipping Cost"], cost_year=self.config.cost_year ) @@ -200,7 +208,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): water_ship_cost_dol_per_ton = ( water_ship_cost_dol_tonne_mi * inputs["water_transport_distance"] ) - water_ship_cost_USD = inputs["total_iron_ore_produced"] * water_ship_cost_dol_per_ton + water_ship_cost_USD = total_ore * water_ship_cost_dol_per_ton land_coeff_dict = load_top_down_coeffs( ["Land Shipping Cost"], cost_year=self.config.cost_year @@ -209,7 +217,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): land_ship_cost_dol_tonne_mi = land_coeff_dict["Land Shipping Cost"]["values"][land_year_idx] land_ship_cost_dol_per_ton = land_ship_cost_dol_tonne_mi * inputs["land_transport_distance"] - land_ship_cost_USD = inputs["total_iron_ore_produced"] * land_ship_cost_dol_per_ton + land_ship_cost_USD = total_ore * land_ship_cost_dol_per_ton total_shipment_cost = water_ship_cost_USD + land_ship_cost_USD @@ -219,5 +227,5 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): pm_year_idx ] - outputs["iron_transport_cost"] = total_shipment_cost / inputs["total_iron_ore_produced"] + outputs["iron_transport_cost"] = total_shipment_cost / total_ore outputs["VarOpEx"] = total_shipment_cost diff --git a/h2integrate/converters/iron/martin_mine_cost_model.py b/h2integrate/converters/iron/martin_mine_cost_model.py index da6594bca..ab40cd489 100644 --- a/h2integrate/converters/iron/martin_mine_cost_model.py +++ b/h2integrate/converters/iron/martin_mine_cost_model.py @@ -91,7 +91,7 @@ def setup(self): desc="Iron ore pellets produced", ) - coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "cost_coeffs.csv" + coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "cost_coeffs_elec_split.csv" # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) diff --git a/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv b/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv new file mode 100644 index 000000000..53d29b736 --- /dev/null +++ b/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv @@ -0,0 +1,27 @@ +,Product,Name,Type,Coeff,Unit,Northshore,United,Hibbing,Minorca,Tilden +0,std_taconite_pellets,Latitude,location,constant,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 +1,std_taconite_pellets,Longitude,location,constant,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 +2,std_taconite_pellets,Crude ore processed,capacity,constant,wltpy,17000000.0,15000000.0,24000000.0,8600000.0,20500000.0 +3,std_taconite_pellets,Ore pellets produced,capacity,constant,wltpy,4540000.0,4600000.0,6300000.0,2790000.0,7700000.0 +4,std_taconite_pellets,Estimated Reserves,capacity,constant,mlt,1158.0,774.6,109.0,109.7,520.0 +5,std_taconite_pellets,Buildings and other structures,capital,constant,2021 $,802586723.3,782080397.9,1078561311.0,552238204.9,1340439971.0 +6,std_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807.0 +7,std_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 +8,std_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 +9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,33.21653565,31.13507091,28.61082938,37.70637508,35.4138672 +10,std_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 +11,std_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 +12,std_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 +13,drg_taconite_pellets,Latitude,location,constant,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 +14,drg_taconite_pellets,Longitude,location,constant,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 +15,drg_taconite_pellets,Crude ore processed,capacity,constant,wltpy,17000000.0,15000000.0,24000000.0,0.0,0.0 +16,drg_taconite_pellets,Ore pellets produced,capacity,constant,wltpy,4540000.0,4600000.0,6300000.0,0.0,0.0 +17,drg_taconite_pellets,Estimated Reserves,capacity,constant,mlt,1158.0,774.6,109.0,0.0,0.0 +18,drg_taconite_pellets,Buildings and other structures,capital,constant,2021 $,1012588246.0,1118066189.0,949301480.9,0.0,0.0 +19,drg_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,117130702.2,129331817.0,109810033.3,0.0,0.0 +20,drg_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1313966.21,1450837.691,1231843.322,0.0,0.0 +21,drg_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0.0,0.0 +22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,36.26424693,42.71867478,36.27057293,0,0 +23,drg_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0.0,0.0 +24,drg_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,0.0,0.0 +25,drg_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0.0,0.0 diff --git a/h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv b/h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv new file mode 100644 index 000000000..bbbd38ca4 --- /dev/null +++ b/h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv @@ -0,0 +1,27 @@ +Product,Name,Type,Unit,Northshore,United,Hibbing,Minorca,Tilden +std_taconite_pellets,Latitude,location,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 +std_taconite_pellets,Longitude,location,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 +std_taconite_pellets,Crude ore processed,capacity,wltpy,17000000,15000000,24000000,8600000,20500000 +std_taconite_pellets,Ore pellets produced,capacity,wltpy,4540000,4600000,6300000,2790000,7700000 +std_taconite_pellets,Estimated Reserves,capacity,mlt,1158,774.6,109,109.7,520 +std_taconite_pellets,Buildings and other structures,capital,2021 $,802586723.3,782080397.9,1078561311,552238204.9,1340439971 +std_taconite_pellets,Mineral exploration and development,capital,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807 +std_taconite_pellets,Mineral land and rights,capital,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 +std_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 +std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,33.21653565,31.13507091,28.61082938,37.70637508,35.4138672 +std_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 +std_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 +std_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 +drg_taconite_pellets,Latitude,location,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 +drg_taconite_pellets,Longitude,location,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 +drg_taconite_pellets,Crude ore processed,capacity,wltpy,17000000,15000000,24000000,0,0 +drg_taconite_pellets,Ore pellets produced,capacity,wltpy,4540000,4600000,6300000,0,0 +drg_taconite_pellets,Estimated Reserves,capacity,mlt,1158,774.6,109,0,0 +drg_taconite_pellets,Buildings and other structures,capital,2021 $,1012588246,1118066189,949301480.9,0,0 +drg_taconite_pellets,Mineral exploration and development,capital,2021 $,117130702.2,129331817,109810033.3,0,0 +drg_taconite_pellets,Mineral land and rights,capital,2021 $,1313966.21,1450837.691,1231843.322,0,0 +drg_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0,0 +drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,36.26424693,42.71867478,36.27057293,0,0 +drg_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0,0 +drg_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,0,0 +drg_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0,0 diff --git a/h2integrate/core/feedstocks.py b/h2integrate/core/feedstocks.py index d8129d999..b100656e2 100644 --- a/h2integrate/core/feedstocks.py +++ b/h2integrate/core/feedstocks.py @@ -84,10 +84,16 @@ def setup(self): units=self.config.units, desc=f"Consumption profile of {feedstock_type}", ) + self.add_input( + "price", + val=self.config.price, + units="USD/(" + self.config.units + ")/h", + desc=f"Consumption profile of {feedstock_type}", + ) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): feedstock_type = self.config.feedstock_type - price = self.config.price + price = inputs["price"] hourly_consumption = inputs[f"{feedstock_type}_consumed"] cost_per_year = sum(price * hourly_consumption) diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index a1259fa38..3cbae16c3 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -63,8 +63,10 @@ class GenericSplitterPerformanceModel(om.ExplicitComponent): The priority_tech parameter determines which technology receives the primary allocation. The outputs are: - - {commodity}_out1: commodity sent to the first technology - - {commodity}_out2: commodity sent to the second technology + - {commodity}_out1: profile of commodity sent to the first technology + - {commodity}_out2: profile of commodity sent to the second technology + - total_{commodity}_produced1: total commodity sent to the first technology + - total_{commodity}_produced2: total commodity sent to the second technology This component is purposefully simple; a more realistic case might include losses or other considerations from system components. @@ -87,6 +89,12 @@ def setup(self): shape_by_conn=True, units=self.config.commodity_units, ) + self.add_input( + f"total_{self.config.commodity}_produced", + val=-1.0, + units=self.config.commodity_units, + desc=f"Total {self.config.commodity} input to the splitter", + ) split_mode = self.config.split_mode @@ -119,6 +127,18 @@ def setup(self): units=self.config.commodity_units, desc=f"{self.config.commodity} output to the second technology", ) + self.add_output( + f"total_{self.config.commodity}_produced1", + val=-1.0, + units=self.config.commodity_units, + desc=f"Total {self.config.commodity} output to the first technology", + ) + self.add_output( + f"total_{self.config.commodity}_produced2", + val=-1.0, + units=self.config.commodity_units, + desc=f"Total {self.config.commodity} output to the second technology", + ) def compute(self, inputs, outputs): commodity_in = inputs[f"{self.config.commodity}_in"] @@ -138,6 +158,13 @@ def compute(self, inputs, outputs): requested_amount = np.maximum(0.0, prescribed_to_priority) commodity_to_priority = np.minimum(requested_amount, available_commodity) commodity_to_other = commodity_in - commodity_to_priority + total_priority = np.sum(commodity_to_priority) + total_other = np.sum(commodity_to_other) + # Need to do this in case _out is different units than _producced + frac_priority = total_priority / (total_priority + total_other) + total_produced = inputs[f"total_{self.config.commodity}_produced"] + total_priority = total_produced * frac_priority + total_other = total_produced * (1 - frac_priority) # Determine which output gets priority allocation based on plant config # This requires mapping priority_tech to output1 or output2 @@ -145,3 +172,5 @@ def compute(self, inputs, outputs): # TODO: This mapping logic should be enhanced based on plant configuration outputs[f"{self.config.commodity}_out1"] = commodity_to_priority outputs[f"{self.config.commodity}_out2"] = commodity_to_other + outputs[f"total_{self.config.commodity}_produced1"] = total_priority + outputs[f"total_{self.config.commodity}_produced2"] = total_other From 755435c56a44fe3fc58d18260dd8c581e358d9b0 Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Fri, 2 Jan 2026 13:43:13 -0700 Subject: [PATCH 35/40] Old and new models matched up --- examples/21_iron_mn_to_il/plant_config.yaml | 3 +-- examples/21_iron_mn_to_il/tech_config.yaml | 4 ++-- h2integrate/converters/iron/iron_transport.py | 2 +- h2integrate/converters/iron/martin_mine_cost_model.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index fb15febb6..85c7baa90 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -22,7 +22,6 @@ site: # with the reverse definition. # this will naturally grow as we mature the interconnected tech technology_interconnections: [ - # connect feedstocks to iron mine # connect feedstocks to iron mine ["grid_feedstock","iron_mine","electricity","cable"], ["mine_feedstock","iron_mine","crude_ore","pipe"], @@ -31,7 +30,7 @@ technology_interconnections: [ ["iron_mine","ore_splitter","iron_ore","pipe"], ["iron_mine","ore_splitter","total_iron_ore_produced"], ["iron_plant","ore_splitter",["iron_ore_consumed","prescribed_commodity_to_priority_tech"]], - ["ore_splitter","iron_transport",["total_iron_ore_produced1","total_iron_ore_produced"]], + ["ore_splitter","iron_transport","iron_ore","pipe"], # connect iron ore price to feedstock ["finance_subgroup_iron_ore","ore_feedstock",["price_iron_ore","price"]], # connect feedstocks to iron plant diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 481810322..1b787225b 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -16,7 +16,7 @@ technologies: rated_capacity: 100. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 - price: 58.02 #USD/MW + price: 0. #USD/MW annual_cost: 0. start_up_cost: 0. @@ -33,7 +33,7 @@ technologies: rated_capacity: 2000. # need 828.50385048 t/h cost_parameters: cost_year: 2022 - price: 58.02 #USD/MW + price: 0. #USD/t annual_cost: 0. start_up_cost: 0. diff --git a/h2integrate/converters/iron/iron_transport.py b/h2integrate/converters/iron/iron_transport.py index 4d27e1efd..dfb8a9cd5 100644 --- a/h2integrate/converters/iron/iron_transport.py +++ b/h2integrate/converters/iron/iron_transport.py @@ -200,7 +200,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # if ore_consumed_sum > 0: # total_ore = ore_consumed_sum # else: - inputs["total_iron_ore_produced"] + # total_ore = inputs["total_iron_ore_produced"] water_coeff_dict = load_top_down_coeffs( ["Barge Shipping Cost"], cost_year=self.config.cost_year diff --git a/h2integrate/converters/iron/martin_mine_cost_model.py b/h2integrate/converters/iron/martin_mine_cost_model.py index ab40cd489..da6594bca 100644 --- a/h2integrate/converters/iron/martin_mine_cost_model.py +++ b/h2integrate/converters/iron/martin_mine_cost_model.py @@ -91,7 +91,7 @@ def setup(self): desc="Iron ore pellets produced", ) - coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "cost_coeffs_elec_split.csv" + coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "cost_coeffs.csv" # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) From 059c2ea0dc5782a6754ddfc3cd7285f56d819010 Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Fri, 23 Jan 2026 18:50:50 -0700 Subject: [PATCH 36/40] Cleaning up PR --- docs/_toc.yml | 1 + docs/technology_models/iron_mine.md | 34 ++++++++++ examples/21_iron_mn_to_il/21_iron.yaml | 2 +- examples/21_iron_mn_to_il/21_iron_old.yaml | 7 -- examples/21_iron_mn_to_il/driver_config.yaml | 2 +- examples/21_iron_mn_to_il/plant_config.yaml | 30 ++------- .../21_iron_mn_to_il/plant_config_old.yaml | 66 ------------------ examples/21_iron_mn_to_il/run_iron.py | 45 +++++-------- examples/21_iron_mn_to_il/tech_config.yaml | 61 ++++++----------- .../21_iron_mn_to_il/tech_config_old.yaml | 67 ------------------- h2integrate/converters/iron/iron_plant.py | 2 +- h2integrate/converters/iron/iron_transport.py | 7 -- .../iron/martin_ore/cost_inputs.csv | 4 +- .../converters/iron/rosner/cost_coeffs.csv | 44 ++++++------ h2integrate/transporters/generic_splitter.py | 33 ++++----- 15 files changed, 118 insertions(+), 287 deletions(-) create mode 100644 docs/technology_models/iron_mine.md delete mode 100644 examples/21_iron_mn_to_il/21_iron_old.yaml delete mode 100644 examples/21_iron_mn_to_il/plant_config_old.yaml delete mode 100644 examples/21_iron_mn_to_il/tech_config_old.yaml diff --git a/docs/_toc.yml b/docs/_toc.yml index de35982ed..c87cce720 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -41,6 +41,7 @@ parts: - file: technology_models/geologic_hydrogen.md - file: technology_models/grid - file: technology_models/hydrogen_storage.md + - file: technology_models/iron_ewin.md - caption: Resource Models chapters: diff --git a/docs/technology_models/iron_mine.md b/docs/technology_models/iron_mine.md new file mode 100644 index 000000000..4cffc6604 --- /dev/null +++ b/docs/technology_models/iron_mine.md @@ -0,0 +1,34 @@ +# Iron mine model + +H2I contains an iron mine model that simulates the extraction of crude ore and its processing into iron ore pellets. +The main input feedstock is `crude_ore`, i.e. the unprocessed ore in the earth containing iron oxide. +The output commodity is `iron_ore` in the form of pellets that can be shipped to other plants for further processing (`iron_plant` for instance). + +This model was developed in conjunction with the University of Minnesota's Natural Resource Research Institute (NRRI). +NRRI compiled cost and production data from 5 existing mines and provided expertise for analysis at NLR to determine the energy input and cost trends across these mines. +Four of the mines (Northshore, United, Hibbing, and Minorca) are located in Minnesota, while one (Tilden) is located in Michigan. + +There are two potential grades of ore produced from an iron mine in this model: +- Standard or Blast Furnace (BF) grade pellets (62-65% Fe) +- Direct Reduction (DR) grade pellets (>67% Fe) + +It was determined that 3 of these mines (Northshore, United, and Hibbing) had crude reserves sufficient to produce DR-grade pellets, although only one (Northshore) reported production data on DR-grade pellets, with the rest reporting their data strictly on standard ore pellets. +The increases in cost and energy usage reported at the Northshore mine were used to project the potential performance and cost of DR-grade production at United and Hibbing. +The results of this analysis are compiled in the directory `h2integreate/converters/iron/martin_ore/`. +Performance data are included in `perf_inputs.csv`, with cost data in `cost_inputs.csv`. + +These data were compiled from two sources: +- The EPA's "Taconite Iron Ore NESHAP Economic Impact Analysis" by [Heller et al.](https://www.epa.gov/sites/default/files/2020-07/documents/taconite_eia_neshap_final_08-2003.pdf) - Capex estimates + - This document esitmated the total percentage of cost spent throughout the entire industry on capital as a percentage of total production costs - 5.4%. This percentage is applied to the total annual production costs of each plant to find the estimated Capex. +- Cleveland-Cliffs Inc.'s Technical Report Summaries for individual mines - Opex and performance data + - [Northshore Mine](https://minedocs.com/22/Northshore-TR-12312021.pdf) + - [United Mine](https://minedocs.com/22/United-Taconite-TR-12312021.pdf) + - [Hibbing Mine](https://minedocs.com/22/Hibbing-Taconite-TR-12312021.pdf) + - [Minorca Mine](https://minedocs.com/22/Minorca-TR-12312021.pdf) + - [Tilden Mine](https://minedocs.com/22/Tilden-TR-12312021.pdf) + +To use this model, specify `"iron_mine_performance_martin"` as the performance model and `"iron_mine_cost_martin"` as the cost model. +Currently, no complex calculations occur beyond importing performance and costs. +In the performance model, the "wet long tons" (wlt) that ore production is typically reported in are converted to dry metric tons for use in H2I. +In the cost model, the total capex costs for a plant are scaled by the amount of are produced annualy. +Besides these calculations, previously-calculated performance and cost metrics are simply loaded from the input spreadsheets. diff --git a/examples/21_iron_mn_to_il/21_iron.yaml b/examples/21_iron_mn_to_il/21_iron.yaml index 33588912d..6028d728e 100644 --- a/examples/21_iron_mn_to_il/21_iron.yaml +++ b/examples/21_iron_mn_to_il/21_iron.yaml @@ -1,6 +1,6 @@ name: "H2Integrate_config" -system_summary: "This reference hybrid plant is located in Minnesota and contains wind, solar, and battery storage technologies. The system is designed to produce hydrogen using an electrolyzer and also produce steel using a grid-connected plant." +system_summary: "An iron mine producing ore pellets and a separately-located iron plant performing direct reduction." driver_config: "driver_config.yaml" technology_config: "tech_config.yaml" diff --git a/examples/21_iron_mn_to_il/21_iron_old.yaml b/examples/21_iron_mn_to_il/21_iron_old.yaml deleted file mode 100644 index 96cfa1943..000000000 --- a/examples/21_iron_mn_to_il/21_iron_old.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: "H2Integrate_config" - -system_summary: "This reference hybrid plant is located in Minnesota and contains wind, solar, and battery storage technologies. The system is designed to produce hydrogen using an electrolyzer and also produce steel using a grid-connected plant." - -driver_config: "driver_config.yaml" -technology_config: "tech_config_old.yaml" -plant_config: "plant_config_old.yaml" diff --git a/examples/21_iron_mn_to_il/driver_config.yaml b/examples/21_iron_mn_to_il/driver_config.yaml index d3b06a03e..c279b7f1b 100644 --- a/examples/21_iron_mn_to_il/driver_config.yaml +++ b/examples/21_iron_mn_to_il/driver_config.yaml @@ -1,5 +1,5 @@ name: "driver_config" -description: "This analysis runs a hybrid plant to match the first example in H2Integrate" +description: "Simply setting up an outputs folder, nothing fancy" general: folder_output: outputs diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 85c7baa90..cef45b2bf 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -5,46 +5,28 @@ site: latitude: 41.717 longitude: -88.398 - # array of polygons defining boundaries with x/y coords - boundaries: [ - { - x: [0.0, 1000.0, 1000.0, 0.0], - y: [0.0, 0.0, 100.0, 1000.0], - }, - { - x: [2000.0, 2500.0, 2000.0], - y: [2000.0, 2000.0, 2500.0], - } - ] - -# array of arrays containing left-to-right technology -# interconnections; can support bidirectional connections -# with the reverse definition. -# this will naturally grow as we mature the interconnected tech technology_interconnections: [ - # connect feedstocks to iron mine + # Connect feedstocks to iron mine. ["grid_feedstock","iron_mine","electricity","cable"], ["mine_feedstock","iron_mine","crude_ore","pipe"], - # connect iron mine to iron transport - #TODO: update after updated transport model is integrated + # Connect iron_ore price to ore_feedstock + ["finance_subgroup_iron_ore","ore_feedstock",["price_iron_ore","price"]], + # Connect iron_ore flow to iron transport - need to split off how much the iron_plant needs ["iron_mine","ore_splitter","iron_ore","pipe"], ["iron_mine","ore_splitter","total_iron_ore_produced"], ["iron_plant","ore_splitter",["iron_ore_consumed","prescribed_commodity_to_priority_tech"]], ["ore_splitter","iron_transport","iron_ore","pipe"], - # connect iron ore price to feedstock - ["finance_subgroup_iron_ore","ore_feedstock",["price_iron_ore","price"]], - # connect feedstocks to iron plant + # Connect feedstocks to iron plant ["ore_feedstock","iron_plant","iron_ore","pipe"], ["dri_grid_feedstock","iron_plant","electricity","cable"], ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], ["natural_gas_feedstock","iron_plant","natural_gas","pipe"], - # connect feedstocks to steel plant + # Connect feedstocks to steel plant ["eaf_grid_feedstock","steel_plant","electricity","cable"], ["eaf_water_feedstock","steel_plant","water","pipe"], ["eaf_natural_gas_feedstock","steel_plant","natural_gas","pipe"], ["iron_plant","steel_plant","pig_iron","pipe"], - # ["iron_transport","iron_plant","iron_transport_cost"], ] plant: diff --git a/examples/21_iron_mn_to_il/plant_config_old.yaml b/examples/21_iron_mn_to_il/plant_config_old.yaml deleted file mode 100644 index 86a345298..000000000 --- a/examples/21_iron_mn_to_il/plant_config_old.yaml +++ /dev/null @@ -1,66 +0,0 @@ -name: "plant_config" -description: "This plant is located in MN, USA..." - -site: - latitude: 41.717 - longitude: -88.398 - - # array of polygons defining boundaries with x/y coords - boundaries: [ - { - x: [0.0, 1000.0, 1000.0, 0.0], - y: [0.0, 0.0, 100.0, 1000.0], - }, - { - x: [2000.0, 2500.0, 2000.0], - y: [2000.0, 2000.0, 2500.0], - } - ] - -# array of arrays containing left-to-right technology -# interconnections; can support bidirectional connections -# with the reverse definition. -# this will naturally grow as we mature the interconnected tech -technology_interconnections: [ - ["iron_mine","iron_plant","iron_ore","iron_transport"], - ["iron_transport","iron_plant","iron_transport_cost"], - ["finance_subgroup_iron_ore","iron_plant","price_iron_ore"], -] - -plant: - plant_life: 30 -finance_parameters: - finance_groups: - finance_model: "ProFastComp" - model_inputs: - params: - analysis_start_year: 2032 - installation_time: 36 # months - inflation_rate: 0.0 # 0 for nominal analysis - discount_rate: 0.09 # nominal return based on 2024 ATB baseline workbook for land-based wind - debt_equity_ratio: 2.62 # 2024 ATB uses 72.4% debt for land-based wind - property_tax_and_insurance: 0.03 # percent of CAPEX estimated based on https://www.nrel.gov/docs/fy25osti/91775.pdf https://www.house.mn.gov/hrd/issinfo/clsrates.aspx - total_income_tax_rate: 0.257 # 0.257 tax rate in 2024 atb baseline workbook, value here is based on federal (21%) and state in MN (9.8) - capital_gains_tax_rate: 0.15 # H2FAST default - sales_tax_rate: 0.07375 # total state and local sales tax in St. Louis County https://taxmaps.state.mn.us/salestax/ - debt_interest_rate: 0.07 # based on 2024 ATB nominal interest rate for land-based wind - debt_type: "Revolving debt" # can be "Revolving debt" or "One time loan". Revolving debt is H2FAST default and leads to much lower LCOH - loan_period_if_used: 0 # H2FAST default, not used for revolving debt - cash_onhand_months: 1 # H2FAST default - admin_expense: 0.00 # percent of sales H2FAST default - capital_items: - depr_type: "MACRS" # can be "MACRS" or "Straight line" - depr_period: 5 # 5 years - for clean energy facilities as specified by the IRS MACRS schedule https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - refurb: [0.] - cost_adjustment_parameters: - cost_year_adjustment_inflation: 0.025 - target_dollar_year: 2022 - finance_subgroups: - iron_ore: - commodity: "iron_ore" - commodity_stream: "iron_mine" - technologies: ["iron_mine"] - pig_iron: - commodity: "pig_iron" - commodity_stream: "iron_plant" - technologies: ["iron_plant"] # diff --git a/examples/21_iron_mn_to_il/run_iron.py b/examples/21_iron_mn_to_il/run_iron.py index b6c8e7871..13953076f 100644 --- a/examples/21_iron_mn_to_il/run_iron.py +++ b/examples/21_iron_mn_to_il/run_iron.py @@ -1,12 +1,14 @@ from pathlib import Path +import numpy as np +import pandas as pd + from h2integrate.tools.run_cases import modify_tech_config, load_tech_config_cases from h2integrate.core.h2integrate_model import H2IntegrateModel -# Create H2Integrate models - comparing old and new +# Create H2Integrate model model = H2IntegrateModel("21_iron.yaml") -model_old = H2IntegrateModel("21_iron_old.yaml") # Load cases case_file = Path("test_inputs.csv") @@ -20,43 +22,28 @@ "Case 4", ] lcois = [] -lcois_old = [] capexes = [] fopexes = [] vopexes = [] -capexes_old = [] -fopexes_old = [] -vopexes_old = [] for casename in casenames: model = modify_tech_config(model, cases[casename]) - model_old = modify_tech_config(model_old, cases[casename]) model.run() - model_old.run() - model.post_process() - model_old.post_process() lcois.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) - lcois_old.append(float(model_old.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) capexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0])) - capexes_old.append( - float(model_old.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0]) - ) fopexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0])) - fopexes_old.append( - float(model_old.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0]) - ) vopexes.append( float(model.model.get_val("finance_subgroup_pig_iron.total_varopex_adjusted")[0]) ) - vopexes_old.append( - float(model_old.model.get_val("finance_subgroup_pig_iron.total_varopex_adjusted")[0]) - ) -# Compare the LCOIs from iron_wrapper and modular iron -print(capexes) -print(capexes_old) -print(fopexes) -print(fopexes_old) -print(vopexes) -print(vopexes_old) -print(lcois) -print(lcois_old) + +# Compare the Capex, Fixed Opex, and Variable Opex across the 4 cases +columns = [ + "Capex [Million USD]", + "Fixed Opex [Million USD/year]", + "Variable Opex [Million USD/year]", + "LCOI [USD/kg]", +] +df = pd.DataFrame( + np.transpose(np.vstack([capexes, fopexes, vopexes, lcois])), index=casenames, columns=columns +) +print(df) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 1b787225b..0b3205517 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -2,7 +2,6 @@ name: "technology_config" description: "This hybrid plant produces iron" technologies: - grid_feedstock: #electricity feedstock for iron ore grid_feedstock: #electricity feedstock for iron ore performance_model: model: "feedstock_performance" @@ -16,7 +15,7 @@ technologies: rated_capacity: 100. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 - price: 0. #USD/MW + price: 58.02 #USD/MW annual_cost: 0. start_up_cost: 0. @@ -37,25 +36,6 @@ technologies: annual_cost: 0. start_up_cost: 0. - ore_feedstock: #iron ore feedstock - performance_model: - model: "feedstock_performance" - cost_model: - model: "feedstock_cost" - model_inputs: - shared_parameters: - feedstock_type: "iron_ore" - units: "t/h" - performance_parameters: - rated_capacity: 2000. # need 828.50385048 t/h - rated_capacity: 2000. # need 828.50385048 t/h - cost_parameters: - cost_year: 2022 - price: 0.0 - annual_cost: 0. - start_up_cost: 0. - - iron_mine: performance_model: model: "iron_mine_performance_martin" @@ -67,6 +47,7 @@ technologies: taconite_pellet_type: "drg" max_ore_production_rate_tonnes_per_hr: 516.0497610311598 + ore_splitter: performance_model: model: "splitter_performance" @@ -90,6 +71,25 @@ technologies: transport_year: 2022 cost_year: 2022 + + ore_feedstock: #iron ore feedstock + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "iron_ore" + units: "t/h" + performance_parameters: + rated_capacity: 2000. # need 828.50385048 t/h + rated_capacity: 2000. # need 828.50385048 t/h + cost_parameters: + cost_year: 2022 + price: 0.0 + annual_cost: 0. + start_up_cost: 0. + natural_gas_feedstock: performance_model: model: "feedstock_performance" @@ -172,6 +172,7 @@ technologies: skilled_labor_cost: 40.85 #2022 USD/hr unskilled_labor_cost: 30.0 #2022 USD/hr + eaf_grid_feedstock: #electricity feedstock for EAF performance_model: model: "feedstock_performance" @@ -227,24 +228,6 @@ technologies: annual_cost: 0. start_up_cost: 0. - steel_plant: - performance_model: - model: "ng_eaf_performance_rosner" - cost_model: - model: "ng_eaf_cost_rosner" - model_inputs: - shared_parameters: - steel_production_rate_tonnes_per_hr: 135.8187214611872 #equivalent to 1189772 t/yr - performance_parameters: - water_density: 1000 #kg/m3 - cost_parameters: - skilled_labor_cost: 40.85 #2022 USD/hr - unskilled_labor_cost: 30.0 #2022 USD/hr - cost_year: 2022 - price: 4.0 #USD 4.0/MMBtu - annual_cost: 0. - start_up_cost: 0. - steel_plant: performance_model: model: "ng_eaf_performance_rosner" diff --git a/examples/21_iron_mn_to_il/tech_config_old.yaml b/examples/21_iron_mn_to_il/tech_config_old.yaml deleted file mode 100644 index afe79faf9..000000000 --- a/examples/21_iron_mn_to_il/tech_config_old.yaml +++ /dev/null @@ -1,67 +0,0 @@ -name: "technology_config" -description: "This hybrid plant produces iron" - -technologies: - iron_mine: - performance_model: - model: "iron_mine_performance" - cost_model: - model: "iron_mine_cost" - model_inputs: - shared_parameters: - mine: "Northshore" - taconite_pellet_type: "drg" - performance_parameters: - ore_cf_estimate: 0.9 - model_name: "martin_ore" - cost_parameters: - LCOE: 58.02 - LCOH: 7.10 - model_name: "martin_ore" - varom_model_name: "martin_ore" - installation_years: 3 - operational_year: 2035 - plant_life: 30 - gen_inflation: 0.025 - financial_assumptions: - total income tax rate: 0.2574 - capital gains tax rate: 0.15 - leverage after tax nominal discount rate: 0.10893 - debt equity ratio of initial financing: 0.624788 - debt interest rate: 0.050049 - iron_transport: - performance_model: - model: "iron_transport_performance" - cost_model: - model: "iron_transport_cost" - model_inputs: - performance_parameters: - find_closest_ship_site: False - shipment_site: "Chicago" - cost_parameters: - transport_year: 2022 - cost_year: 2022 - iron_plant: - performance_model: - model: "iron_plant_performance" - cost_model: - model: "iron_plant_cost" - model_inputs: - shared_parameters: - winning_type: "ng" - iron_win_capacity: 1418095 - site_name: "IL" - performance_parameters: - win_capacity_demon: "iron" - model_name: "rosner" - cost_parameters: - LCOE: 58.02 - LCOH: 7.10 - LCOI_ore: 0 #125.25996463784443 - iron_transport_cost: 30.566808424134745 - ore_profit_pct: 6.0 - varom_model_name: "rosner" - installation_years: 3 - operational_year: 2035 - -# baseline 843.37604007 diff --git a/h2integrate/converters/iron/iron_plant.py b/h2integrate/converters/iron/iron_plant.py index 657517d41..1cd784e02 100644 --- a/h2integrate/converters/iron/iron_plant.py +++ b/h2integrate/converters/iron/iron_plant.py @@ -264,7 +264,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): ore_profit_pct = inputs["ore_profit_pct"][0] iron_ore_pellet_unitcost_tonne = ( iron_ore_pellet_unitcost_tonne + iron_transport_cost_tonne - ) * (1 + ore_profit_pct * 0 / 100) + ) * (1 + ore_profit_pct / 100) v_start = years.index(self.config.operational_year) - years.index(analysis_start) + 1 variable_om += perf_ds["Raw Water Withdrawal"] * raw_water_unitcost_tonne diff --git a/h2integrate/converters/iron/iron_transport.py b/h2integrate/converters/iron/iron_transport.py index dfb8a9cd5..dca811925 100644 --- a/h2integrate/converters/iron/iron_transport.py +++ b/h2integrate/converters/iron/iron_transport.py @@ -195,13 +195,6 @@ def setup(self): self.add_output("ore_profit_margin", val=0.0, units="USD/t") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - # ore_consumed = inputs["iron_ore_consumed"] - # ore_consumed_sum = np.sum(ore_consumed) - # if ore_consumed_sum > 0: - # total_ore = ore_consumed_sum - # else: - # total_ore = inputs["total_iron_ore_produced"] - water_coeff_dict = load_top_down_coeffs( ["Barge Shipping Cost"], cost_year=self.config.cost_year ) diff --git a/h2integrate/converters/iron/martin_ore/cost_inputs.csv b/h2integrate/converters/iron/martin_ore/cost_inputs.csv index baddd2ad2..4a55d9d9b 100644 --- a/h2integrate/converters/iron/martin_ore/cost_inputs.csv +++ b/h2integrate/converters/iron/martin_ore/cost_inputs.csv @@ -8,7 +8,7 @@ std_taconite_pellets,Buildings and other structures,capital,2021 $,802586723.3,7 std_taconite_pellets,Mineral exploration and development,capital,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807 std_taconite_pellets,Mineral land and rights,capital,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 std_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 +std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,33.22,31.40,25.11,38.24,38.12 std_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 std_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 std_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 @@ -21,7 +21,7 @@ drg_taconite_pellets,Buildings and other structures,capital,2021 $,1012588246,11 drg_taconite_pellets,Mineral exploration and development,capital,2021 $,117130702.2,129331817,109810033.3,0,0 drg_taconite_pellets,Mineral land and rights,capital,2021 $,1313966.21,1450837.691,1231843.322,0,0 drg_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0,0 -drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0,0 +drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,36.54,42.68,36.24,0,0 drg_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0,0 drg_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,0,0 drg_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0,0 diff --git a/h2integrate/converters/iron/rosner/cost_coeffs.csv b/h2integrate/converters/iron/rosner/cost_coeffs.csv index 30dbb8598..68d52643b 100644 --- a/h2integrate/converters/iron/rosner/cost_coeffs.csv +++ b/h2integrate/converters/iron/rosner/cost_coeffs.csv @@ -1,27 +1,27 @@ Name,Type,Coeff,Unit,h2_dri_eaf,h2_dri,ng_dri_eaf,ng_dri,h2_eaf,ng_eaf Dollar Year,all,-,-,2022.0,2022.0,2022.0,2022.0,2022.0,2022.0 -EAF & Casting,capital,lin,$,352191.5240169328,9.999999999999783e-11,348441.82271277835,9.999999999999996e-11,352191.5240169328,348441.82271277835 -Shaft Furnace,capital,lin,$,489.68060552071756,498.4011587515349,12706.35540748921,12706.35540748921,9.999999999999783e-11,9.999999999999996e-11 -Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,9.999999999999996e-11 -Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199391,956.7744259199391,0.0,9.999999999999996e-11 -Oxygen Supply,capital,lin,$,1715.21508561117,1364.556123425206,1204.989085989581,1024.5621894371002,393.8307342018522,201.3153857876692 -H2 Pre-heating,capital,lin,$,45.69122789208198,45.69122789208198,0.0,0.0,9.999999999999783e-11,0.0 -Cooling Tower,capital,lin,$,2513.0831352600603,2349.15226276371,1790.4037306002654,1774.6459078322403,195.45026173489327,428.90723393800783 -Piping,capital,lin,$,11815.72718570437,172.83401368816206,25651.756587058582,3917.34823234076,49970.89662834664,51294.534191987485 -Electrical & Instrumentation,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,33313.93113452472,34196.35613320244 -"Buildings, Storage, Water Service",capital,lin,$,1097.818759618821,630.531300861788,1077.3955282629602,618.8012346570356,467.2874587570587,458.59429360590775 -Other Miscellaneous Cost,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,32124.80020341839,34196.35613320244 -EAF & Casting,capital,exp,-,0.4559999999420573,1.3155100341401327e-15,0.45599999989917783,-3.9460856373682605e-16,0.4559999999420573,0.45599999989917783 -Shaft Furnace,capital,exp,-,0.8874110806752277,0.8862693772994416,0.654083508755392,0.654083508755392,1.3155100341401327e-15,-3.9460856373682605e-16 -Reformer,capital,exp,-,0.0,0.0,0.6504523630280986,0.6504523630280986,0.0,-3.9460856373682605e-16 -Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012126,0.7100000000012126,0.0,-3.9460856373682605e-16 -Oxygen Supply,capital,exp,-,0.6457441946722564,0.634272491606804,0.6448555996459648,0.6370664161269146,0.6699999996017935,0.6699999991468175 -H2 Pre-heating,capital,exp,-,0.8656365854575331,0.8656365854575331,0.0,0.0,1.3155100341401327e-15,0.0 -Cooling Tower,capital,exp,-,0.6332532740097354,0.6286978709250873,0.6302820063981899,0.6308163319151914,0.6659797264431847,0.25999986981600937 -Piping,capital,exp,-,0.5998309612457874,0.8331608480295299,0.5641056316548689,0.6551154484929355,0.461960765339552,0.45807183935606205 -Electrical & Instrumentation,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.4619607652382664,0.4580718393497376 -"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942431,0.7999999999824277,0.7999999998654156,0.7999999999666229,0.7999999997752477,0.7999999997288553 -Other Miscellaneous Cost,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.46389753648982157,0.4580718393497376 +EAF & Casting,capital,lin,$,352191.52401693846,1.0000000000000423e-10,348441.822712761,1.0000000000000493e-10,352191.52401693846,348441.822712761 +Shaft Furnace,capital,lin,$,489.6806055207384,498.40115875153936,12706.355407489165,12706.355407489165,1.0000000000000423e-10,1.0000000000000493e-10 +Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,1.0000000000000493e-10 +Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199501,956.7744259199501,0.0,1.0000000000000493e-10 +Oxygen Supply,capital,lin,$,1715.2150856111075,1364.5561234251998,1204.9890859895831,1024.5621894371047,393.8307342018581,201.31538578767027 +H2 Pre-heating,capital,lin,$,45.691227892080846,45.691227892080846,0.0,0.0,1.0000000000000423e-10,0.0 +Cooling Tower,capital,lin,$,2513.0831352601745,2349.1522627637223,1790.4037306003052,1774.6459078322134,195.45026173489256,428.907233937993 +Piping,capital,lin,$,11815.727185705,172.8340136881622,25651.756587058124,3917.3482323406975,49970.8966283462,51294.53419198649 +Electrical & Instrumentation,capital,lin,$,7877.15146041425,115.22267582877082,17101.1710703832,2611.5654909388777,33313.9311345246,34196.35613320214 +"Buildings, Storage, Water Service",capital,lin,$,1097.8187596188288,630.5313008617629,1077.3955282629765,618.801234657034,467.28745875705994,458.59429360591184 +Other Miscellaneous Cost,capital,lin,$,7877.15146041425,115.22267582877082,17101.1710703832,2611.5654909388777,32124.800203417988,34196.35613320214 +EAF & Casting,capital,exp,-,0.45599999994205614,-3.208623172686777e-15,0.4559999998991816,-3.5137286842850213e-15,0.45599999994205614,0.4559999998991816 +Shaft Furnace,capital,exp,-,0.8874110806752245,0.8862693772994409,0.6540835087553919,0.6540835087553919,-3.208623172686777e-15,-3.5137286842850213e-15 +Reformer,capital,exp,-,0.0,0.0,0.6504523630280983,0.6504523630280983,0.0,-3.5137286842850213e-15 +Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012111,0.7100000000012111,0.0,-3.5137286842850213e-15 +Oxygen Supply,capital,exp,-,0.6457441946722592,0.6342724916068043,0.6448555996459645,0.6370664161269142,0.6699999996017922,0.6699999991468166 +H2 Pre-heating,capital,exp,-,0.8656365854575351,0.8656365854575351,0.0,0.0,-3.208623172686777e-15,0.0 +Cooling Tower,capital,exp,-,0.6332532740097315,0.6286978709250869,0.6302820063981879,0.6308163319151925,0.665979726443185,0.2599998698160119 +Piping,capital,exp,-,0.5998309612457832,0.8331608480295302,0.5641056316548696,0.6551154484929362,0.4619607653395523,0.45807183935606327 +Electrical & Instrumentation,capital,exp,-,0.5998309612151184,0.8331608479997338,0.5641056316058727,0.6551154484197816,0.46196076523826657,0.4580718393497376 +"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942425,0.7999999999824305,0.7999999998654145,0.7999999999666231,0.7999999997752475,0.7999999997288543 +Other Miscellaneous Cost,capital,exp,-,0.5998309612151184,0.8331608479997338,0.5641056316058727,0.6551154484197816,0.4638975364898229,0.4580718393497376 Preproduction,owner,lin,frac of TPC,0.02,0.02,0.02,0.02,0.02,0.02 Spare Parts,owner,lin,frac of TPC,0.005,0.005,0.005,0.005,0.005,0.005 "Initial Catalyst, Sorbent & Chemicals",owner,lin,frac of TPC,0.0,0.0,0.250835587,0.250835587,0.0,0.250835587 diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index 3cbae16c3..96a5c423b 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -127,18 +127,18 @@ def setup(self): units=self.config.commodity_units, desc=f"{self.config.commodity} output to the second technology", ) - self.add_output( - f"total_{self.config.commodity}_produced1", - val=-1.0, - units=self.config.commodity_units, - desc=f"Total {self.config.commodity} output to the first technology", - ) - self.add_output( - f"total_{self.config.commodity}_produced2", - val=-1.0, - units=self.config.commodity_units, - desc=f"Total {self.config.commodity} output to the second technology", - ) + # self.add_output( + # f"total_{self.config.commodity}_produced1", + # val=-1.0, + # units=self.config.commodity_units, + # desc=f"Total {self.config.commodity} output to the first technology", + # ) + # self.add_output( + # f"total_{self.config.commodity}_produced2", + # val=-1.0, + # units=self.config.commodity_units, + # desc=f"Total {self.config.commodity} output to the second technology", + # ) def compute(self, inputs, outputs): commodity_in = inputs[f"{self.config.commodity}_in"] @@ -158,13 +158,6 @@ def compute(self, inputs, outputs): requested_amount = np.maximum(0.0, prescribed_to_priority) commodity_to_priority = np.minimum(requested_amount, available_commodity) commodity_to_other = commodity_in - commodity_to_priority - total_priority = np.sum(commodity_to_priority) - total_other = np.sum(commodity_to_other) - # Need to do this in case _out is different units than _producced - frac_priority = total_priority / (total_priority + total_other) - total_produced = inputs[f"total_{self.config.commodity}_produced"] - total_priority = total_produced * frac_priority - total_other = total_produced * (1 - frac_priority) # Determine which output gets priority allocation based on plant config # This requires mapping priority_tech to output1 or output2 @@ -172,5 +165,3 @@ def compute(self, inputs, outputs): # TODO: This mapping logic should be enhanced based on plant configuration outputs[f"{self.config.commodity}_out1"] = commodity_to_priority outputs[f"{self.config.commodity}_out2"] = commodity_to_other - outputs[f"total_{self.config.commodity}_produced1"] = total_priority - outputs[f"total_{self.config.commodity}_produced2"] = total_other From 0fd1485ffe8a6eebf006912c3d9d85abf825af5d Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Mon, 26 Jan 2026 19:02:06 -0700 Subject: [PATCH 37/40] Less goofy plant config and other cleanup --- examples/21_iron_mn_to_il/plant_config.yaml | 25 ++++----- examples/21_iron_mn_to_il/run_iron.py | 24 ++++++++- examples/21_iron_mn_to_il/tech_config.yaml | 54 +++++++------------ .../iron/martin_ore/cost_coeffs.csv | 4 +- .../martin_ore/cost_coeffs_elec_split.csv | 27 ---------- ...uts_elec_split.csv => cost_inputs_old.csv} | 4 +- .../converters/iron/model_locations.yaml | 2 +- .../converters/iron/rosner/cost_coeffs.csv | 44 +++++++-------- h2integrate/transporters/generic_splitter.py | 24 +-------- 9 files changed, 82 insertions(+), 126 deletions(-) delete mode 100644 h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv rename h2integrate/converters/iron/martin_ore/{cost_inputs_elec_split.csv => cost_inputs_old.csv} (96%) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 013c417eb..2867e6f69 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -8,17 +8,13 @@ sites: technology_interconnections: [ # Connect feedstocks to iron mine. - ["grid_feedstock","iron_mine","electricity","cable"], - ["mine_feedstock","iron_mine","crude_ore","pipe"], - # Connect iron_ore price to ore_feedstock - ["finance_subgroup_iron_ore","ore_feedstock",["price_iron_ore","price"]], - # Connect iron_ore flow to iron transport - need to split off how much the iron_plant needs - ["iron_mine","ore_splitter","iron_ore","pipe"], - ["iron_mine","ore_splitter","total_iron_ore_produced"], - ["iron_plant","ore_splitter",["iron_ore_consumed","prescribed_commodity_to_priority_tech"]], - ["ore_splitter","iron_transport","iron_ore","pipe"], - # Connect feedstocks to iron plant - ["ore_feedstock","iron_plant","iron_ore","pipe"], + ["mine_electricity_feedstock","iron_mine","electricity","cable"], + ["mine_crude_ore_feedstock","iron_mine","crude_ore","pipe"], + # Connect iron_ore price and processed_ore_feedstock to iron_transport + ["finance_subgroup_iron_ore","processed_ore_feedstock",["price_iron_ore","price"]], + ["processed_ore_feedstock","iron_plant","iron_ore","iron_transport"], + # 'iron_transport.iron_ore_in' is connected to 'processed_ore_feedstock_source.iron_ore_out' + # but it SHOULD be connected to 'processed_ore_feedstock_source.iron_ore_consumed' ["dri_grid_feedstock","iron_plant","electricity","cable"], ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], @@ -66,13 +62,12 @@ finance_parameters: iron_ore: commodity: "iron_ore" commodity_stream: "iron_mine" - technologies: ["iron_mine", "grid_feedstock", "mine_feedstock"] - technologies: ["iron_mine", "grid_feedstock", "mine_feedstock"] + technologies: ["iron_mine", "mine_electricity_feedstock", "mine_crude_ore_feedstock"] pig_iron: commodity: "pig_iron" commodity_stream: "iron_plant" technologies: - - "ore_feedstock" + - "processed_ore_feedstock" - "iron_transport" - "iron_plant" - "dri_grid_feedstock" @@ -83,7 +78,7 @@ finance_parameters: commodity: "steel" commodity_stream: "steel_plant" technologies: - - "ore_feedstock" + - "processed_ore_feedstock" - "iron_transport" - "iron_plant" - "dri_grid_feedstock" diff --git a/examples/21_iron_mn_to_il/run_iron.py b/examples/21_iron_mn_to_il/run_iron.py index 13953076f..711fe5c4f 100644 --- a/examples/21_iron_mn_to_il/run_iron.py +++ b/examples/21_iron_mn_to_il/run_iron.py @@ -21,6 +21,10 @@ "Case 3", "Case 4", ] +lcois_ore = [] +capexes_ore = [] +fopexes_ore = [] +vopexes_ore = [] lcois = [] capexes = [] fopexes = [] @@ -29,6 +33,17 @@ for casename in casenames: model = modify_tech_config(model, cases[casename]) model.run() + model.post_process() + lcois_ore.append(float(model.model.get_val("finance_subgroup_iron_ore.price_iron_ore")[0])) + capexes_ore.append( + float(model.model.get_val("finance_subgroup_iron_ore.total_capex_adjusted")[0]) + ) + fopexes_ore.append( + float(model.model.get_val("finance_subgroup_iron_ore.total_opex_adjusted")[0]) + ) + vopexes_ore.append( + float(model.model.get_val("finance_subgroup_iron_ore.total_varopex_adjusted")[0]) + ) lcois.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) capexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0])) fopexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0])) @@ -38,11 +53,18 @@ # Compare the Capex, Fixed Opex, and Variable Opex across the 4 cases columns = [ + "LCO Iron Ore [USD/kg]", "Capex [Million USD]", "Fixed Opex [Million USD/year]", "Variable Opex [Million USD/year]", - "LCOI [USD/kg]", ] +df_ore = pd.DataFrame( + np.transpose(np.vstack([capexes_ore, fopexes_ore, vopexes_ore, lcois_ore])), + index=casenames, + columns=columns, +) +print(df_ore) +columns[0] = "LCO Pig Iron [USD/kg]" df = pd.DataFrame( np.transpose(np.vstack([capexes, fopexes, vopexes, lcois])), index=casenames, columns=columns ) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index 0b3205517..a6c5be49a 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -2,7 +2,7 @@ name: "technology_config" description: "This hybrid plant produces iron" technologies: - grid_feedstock: #electricity feedstock for iron ore + mine_electricity_feedstock: #electricity feedstock for iron ore performance_model: model: "feedstock_performance" cost_model: @@ -12,14 +12,14 @@ technologies: feedstock_type: "electricity" units: "MW" performance_parameters: - rated_capacity: 100. # MW, need 27.913 MW per timestep for iron ore + rated_capacity: 70. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 price: 58.02 #USD/MW annual_cost: 0. start_up_cost: 0. - mine_feedstock: #iron ore feedstock + mine_crude_ore_feedstock: #iron ore feedstock performance_model: model: "feedstock_performance" cost_model: @@ -29,7 +29,7 @@ technologies: feedstock_type: "crude_ore" units: "t/h" performance_parameters: - rated_capacity: 2000. # need 828.50385048 t/h + rated_capacity: 2500. # need 828.50385048 t/h cost_parameters: cost_year: 2022 price: 0. #USD/t @@ -45,18 +45,25 @@ technologies: shared_parameters: mine: "Northshore" taconite_pellet_type: "drg" - max_ore_production_rate_tonnes_per_hr: 516.0497610311598 + max_ore_production_rate_tonnes_per_hr: 500 - ore_splitter: + processed_ore_feedstock: #iron ore feedstock performance_model: - model: "splitter_performance" + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" model_inputs: + shared_parameters: + feedstock_type: "iron_ore" + units: "t/h" performance_parameters: - split_mode: "prescribed_commodity" - commodity: "iron_ore" - commodity_units: "t/h" - prescribed_commodity_to_priority_tech: 221.2592636 + rated_capacity: 250. # need 828.50385048 t/h + cost_parameters: + cost_year: 2022 + price: 0.0 + annual_cost: 0. + start_up_cost: 0. iron_transport: performance_model: @@ -71,25 +78,6 @@ technologies: transport_year: 2022 cost_year: 2022 - - ore_feedstock: #iron ore feedstock - performance_model: - model: "feedstock_performance" - cost_model: - model: "feedstock_cost" - model_inputs: - shared_parameters: - feedstock_type: "iron_ore" - units: "t/h" - performance_parameters: - rated_capacity: 2000. # need 828.50385048 t/h - rated_capacity: 2000. # need 828.50385048 t/h - cost_parameters: - cost_year: 2022 - price: 0.0 - annual_cost: 0. - start_up_cost: 0. - natural_gas_feedstock: performance_model: model: "feedstock_performance" @@ -117,10 +105,10 @@ technologies: feedstock_type: "water" units: "galUS" #galUS/h performance_parameters: - rated_capacity: 40000. # need 38.71049649 galUS/h + rated_capacity: 40000. # need 38710.49649 galUS/h cost_parameters: cost_year: 2022 - price: 0.0016700004398318847 # cost is USD0.441167535/t, converted to USD/gal + price: 0.0016700004398318847 # cost is USD 0.441167535/t, converted to USD/gal annual_cost: 0. start_up_cost: 0. @@ -212,7 +200,6 @@ technologies: model: "feedstock_performance" cost_model: model: "feedstock_cost" - model: "feedstock_cost" model_inputs: shared_parameters: feedstock_type: "natural_gas" @@ -221,7 +208,6 @@ technologies: units: "MMBtu" performance_parameters: rated_capacity: 280. # need 276.5242929731515 MMBtu at each timestep - rated_capacity: 280. # need 276.5242929731515 MMBtu at each timestep cost_parameters: cost_year: 2022 price: 4.0 #USD 4.0/MMBtu diff --git a/h2integrate/converters/iron/martin_ore/cost_coeffs.csv b/h2integrate/converters/iron/martin_ore/cost_coeffs.csv index 8f31a0b32..5b469f66d 100644 --- a/h2integrate/converters/iron/martin_ore/cost_coeffs.csv +++ b/h2integrate/converters/iron/martin_ore/cost_coeffs.csv @@ -8,7 +8,7 @@ 6,std_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807.0 7,std_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 8,std_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 +9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,33.22,31.4,25.11,38.24,38.12 10,std_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 11,std_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 12,std_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 @@ -21,7 +21,7 @@ 19,drg_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,117130702.2,129331817.0,109810033.3,0.0,0.0 20,drg_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1313966.21,1450837.691,1231843.322,0.0,0.0 21,drg_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0.0,0.0 -22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0.0,0.0 +22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,36.54,42.68,36.24,0.0,0.0 23,drg_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0.0,0.0 24,drg_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,0.0,0.0 25,drg_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0.0,0.0 diff --git a/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv b/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv deleted file mode 100644 index 53d29b736..000000000 --- a/h2integrate/converters/iron/martin_ore/cost_coeffs_elec_split.csv +++ /dev/null @@ -1,27 +0,0 @@ -,Product,Name,Type,Coeff,Unit,Northshore,United,Hibbing,Minorca,Tilden -0,std_taconite_pellets,Latitude,location,constant,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 -1,std_taconite_pellets,Longitude,location,constant,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 -2,std_taconite_pellets,Crude ore processed,capacity,constant,wltpy,17000000.0,15000000.0,24000000.0,8600000.0,20500000.0 -3,std_taconite_pellets,Ore pellets produced,capacity,constant,wltpy,4540000.0,4600000.0,6300000.0,2790000.0,7700000.0 -4,std_taconite_pellets,Estimated Reserves,capacity,constant,mlt,1158.0,774.6,109.0,109.7,520.0 -5,std_taconite_pellets,Buildings and other structures,capital,constant,2021 $,802586723.3,782080397.9,1078561311.0,552238204.9,1340439971.0 -6,std_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807.0 -7,std_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 -8,std_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,33.21653565,31.13507091,28.61082938,37.70637508,35.4138672 -10,std_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 -11,std_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 -12,std_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 -13,drg_taconite_pellets,Latitude,location,constant,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 -14,drg_taconite_pellets,Longitude,location,constant,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 -15,drg_taconite_pellets,Crude ore processed,capacity,constant,wltpy,17000000.0,15000000.0,24000000.0,0.0,0.0 -16,drg_taconite_pellets,Ore pellets produced,capacity,constant,wltpy,4540000.0,4600000.0,6300000.0,0.0,0.0 -17,drg_taconite_pellets,Estimated Reserves,capacity,constant,mlt,1158.0,774.6,109.0,0.0,0.0 -18,drg_taconite_pellets,Buildings and other structures,capital,constant,2021 $,1012588246.0,1118066189.0,949301480.9,0.0,0.0 -19,drg_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,117130702.2,129331817.0,109810033.3,0.0,0.0 -20,drg_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1313966.21,1450837.691,1231843.322,0.0,0.0 -21,drg_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0.0,0.0 -22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,36.26424693,42.71867478,36.27057293,0,0 -23,drg_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0.0,0.0 -24,drg_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,0.0,0.0 -25,drg_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0.0,0.0 diff --git a/h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv b/h2integrate/converters/iron/martin_ore/cost_inputs_old.csv similarity index 96% rename from h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv rename to h2integrate/converters/iron/martin_ore/cost_inputs_old.csv index bbbd38ca4..baddd2ad2 100644 --- a/h2integrate/converters/iron/martin_ore/cost_inputs_elec_split.csv +++ b/h2integrate/converters/iron/martin_ore/cost_inputs_old.csv @@ -8,7 +8,7 @@ std_taconite_pellets,Buildings and other structures,capital,2021 $,802586723.3,7 std_taconite_pellets,Mineral exploration and development,capital,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807 std_taconite_pellets,Mineral land and rights,capital,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 std_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,33.21653565,31.13507091,28.61082938,37.70637508,35.4138672 +std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 std_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 std_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 std_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 @@ -21,7 +21,7 @@ drg_taconite_pellets,Buildings and other structures,capital,2021 $,1012588246,11 drg_taconite_pellets,Mineral exploration and development,capital,2021 $,117130702.2,129331817,109810033.3,0,0 drg_taconite_pellets,Mineral land and rights,capital,2021 $,1313966.21,1450837.691,1231843.322,0,0 drg_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0,0 -drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,36.26424693,42.71867478,36.27057293,0,0 +drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0,0 drg_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0,0 drg_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,0,0 drg_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0,0 diff --git a/h2integrate/converters/iron/model_locations.yaml b/h2integrate/converters/iron/model_locations.yaml index 9a1b2d509..d1fd47fca 100644 --- a/h2integrate/converters/iron/model_locations.yaml +++ b/h2integrate/converters/iron/model_locations.yaml @@ -13,7 +13,7 @@ cost: martin_ore: compatible_tech: ['h2_dri','ng_dri'] model: 'h2integrate.converters.iron.martin_ore.cost_model' - inputs: 'cost_inputs.csv' + inputs: 'cost_inputs_old.csv' coeffs: 'cost_coeffs.csv' rosner: compatible_tech: ['h2_dri','ng_dri'] diff --git a/h2integrate/converters/iron/rosner/cost_coeffs.csv b/h2integrate/converters/iron/rosner/cost_coeffs.csv index 68d52643b..30dbb8598 100644 --- a/h2integrate/converters/iron/rosner/cost_coeffs.csv +++ b/h2integrate/converters/iron/rosner/cost_coeffs.csv @@ -1,27 +1,27 @@ Name,Type,Coeff,Unit,h2_dri_eaf,h2_dri,ng_dri_eaf,ng_dri,h2_eaf,ng_eaf Dollar Year,all,-,-,2022.0,2022.0,2022.0,2022.0,2022.0,2022.0 -EAF & Casting,capital,lin,$,352191.52401693846,1.0000000000000423e-10,348441.822712761,1.0000000000000493e-10,352191.52401693846,348441.822712761 -Shaft Furnace,capital,lin,$,489.6806055207384,498.40115875153936,12706.355407489165,12706.355407489165,1.0000000000000423e-10,1.0000000000000493e-10 -Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,1.0000000000000493e-10 -Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199501,956.7744259199501,0.0,1.0000000000000493e-10 -Oxygen Supply,capital,lin,$,1715.2150856111075,1364.5561234251998,1204.9890859895831,1024.5621894371047,393.8307342018581,201.31538578767027 -H2 Pre-heating,capital,lin,$,45.691227892080846,45.691227892080846,0.0,0.0,1.0000000000000423e-10,0.0 -Cooling Tower,capital,lin,$,2513.0831352601745,2349.1522627637223,1790.4037306003052,1774.6459078322134,195.45026173489256,428.907233937993 -Piping,capital,lin,$,11815.727185705,172.8340136881622,25651.756587058124,3917.3482323406975,49970.8966283462,51294.53419198649 -Electrical & Instrumentation,capital,lin,$,7877.15146041425,115.22267582877082,17101.1710703832,2611.5654909388777,33313.9311345246,34196.35613320214 -"Buildings, Storage, Water Service",capital,lin,$,1097.8187596188288,630.5313008617629,1077.3955282629765,618.801234657034,467.28745875705994,458.59429360591184 -Other Miscellaneous Cost,capital,lin,$,7877.15146041425,115.22267582877082,17101.1710703832,2611.5654909388777,32124.800203417988,34196.35613320214 -EAF & Casting,capital,exp,-,0.45599999994205614,-3.208623172686777e-15,0.4559999998991816,-3.5137286842850213e-15,0.45599999994205614,0.4559999998991816 -Shaft Furnace,capital,exp,-,0.8874110806752245,0.8862693772994409,0.6540835087553919,0.6540835087553919,-3.208623172686777e-15,-3.5137286842850213e-15 -Reformer,capital,exp,-,0.0,0.0,0.6504523630280983,0.6504523630280983,0.0,-3.5137286842850213e-15 -Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012111,0.7100000000012111,0.0,-3.5137286842850213e-15 -Oxygen Supply,capital,exp,-,0.6457441946722592,0.6342724916068043,0.6448555996459645,0.6370664161269142,0.6699999996017922,0.6699999991468166 -H2 Pre-heating,capital,exp,-,0.8656365854575351,0.8656365854575351,0.0,0.0,-3.208623172686777e-15,0.0 -Cooling Tower,capital,exp,-,0.6332532740097315,0.6286978709250869,0.6302820063981879,0.6308163319151925,0.665979726443185,0.2599998698160119 -Piping,capital,exp,-,0.5998309612457832,0.8331608480295302,0.5641056316548696,0.6551154484929362,0.4619607653395523,0.45807183935606327 -Electrical & Instrumentation,capital,exp,-,0.5998309612151184,0.8331608479997338,0.5641056316058727,0.6551154484197816,0.46196076523826657,0.4580718393497376 -"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942425,0.7999999999824305,0.7999999998654145,0.7999999999666231,0.7999999997752475,0.7999999997288543 -Other Miscellaneous Cost,capital,exp,-,0.5998309612151184,0.8331608479997338,0.5641056316058727,0.6551154484197816,0.4638975364898229,0.4580718393497376 +EAF & Casting,capital,lin,$,352191.5240169328,9.999999999999783e-11,348441.82271277835,9.999999999999996e-11,352191.5240169328,348441.82271277835 +Shaft Furnace,capital,lin,$,489.68060552071756,498.4011587515349,12706.35540748921,12706.35540748921,9.999999999999783e-11,9.999999999999996e-11 +Reformer,capital,lin,$,0.0,0.0,12585.824569411547,12585.824569411547,0.0,9.999999999999996e-11 +Recycle Compressor,capital,lin,$,0.0,0.0,956.7744259199391,956.7744259199391,0.0,9.999999999999996e-11 +Oxygen Supply,capital,lin,$,1715.21508561117,1364.556123425206,1204.989085989581,1024.5621894371002,393.8307342018522,201.3153857876692 +H2 Pre-heating,capital,lin,$,45.69122789208198,45.69122789208198,0.0,0.0,9.999999999999783e-11,0.0 +Cooling Tower,capital,lin,$,2513.0831352600603,2349.15226276371,1790.4037306002654,1774.6459078322403,195.45026173489327,428.90723393800783 +Piping,capital,lin,$,11815.72718570437,172.83401368816206,25651.756587058582,3917.34823234076,49970.89662834664,51294.534191987485 +Electrical & Instrumentation,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,33313.93113452472,34196.35613320244 +"Buildings, Storage, Water Service",capital,lin,$,1097.818759618821,630.531300861788,1077.3955282629602,618.8012346570356,467.2874587570587,458.59429360590775 +Other Miscellaneous Cost,capital,lin,$,7877.151460413984,115.2226758287703,17101.171070383258,2611.5654909389123,32124.80020341839,34196.35613320244 +EAF & Casting,capital,exp,-,0.4559999999420573,1.3155100341401327e-15,0.45599999989917783,-3.9460856373682605e-16,0.4559999999420573,0.45599999989917783 +Shaft Furnace,capital,exp,-,0.8874110806752277,0.8862693772994416,0.654083508755392,0.654083508755392,1.3155100341401327e-15,-3.9460856373682605e-16 +Reformer,capital,exp,-,0.0,0.0,0.6504523630280986,0.6504523630280986,0.0,-3.9460856373682605e-16 +Recycle Compressor,capital,exp,-,0.0,0.0,0.7100000000012126,0.7100000000012126,0.0,-3.9460856373682605e-16 +Oxygen Supply,capital,exp,-,0.6457441946722564,0.634272491606804,0.6448555996459648,0.6370664161269146,0.6699999996017935,0.6699999991468175 +H2 Pre-heating,capital,exp,-,0.8656365854575331,0.8656365854575331,0.0,0.0,1.3155100341401327e-15,0.0 +Cooling Tower,capital,exp,-,0.6332532740097354,0.6286978709250873,0.6302820063981899,0.6308163319151914,0.6659797264431847,0.25999986981600937 +Piping,capital,exp,-,0.5998309612457874,0.8331608480295299,0.5641056316548689,0.6551154484929355,0.461960765339552,0.45807183935606205 +Electrical & Instrumentation,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.4619607652382664,0.4580718393497376 +"Buildings, Storage, Water Service",capital,exp,-,0.7999999998942431,0.7999999999824277,0.7999999998654156,0.7999999999666229,0.7999999997752477,0.7999999997288553 +Other Miscellaneous Cost,capital,exp,-,0.599830961215121,0.8331608479997342,0.5641056316058728,0.6551154484197809,0.46389753648982157,0.4580718393497376 Preproduction,owner,lin,frac of TPC,0.02,0.02,0.02,0.02,0.02,0.02 Spare Parts,owner,lin,frac of TPC,0.005,0.005,0.005,0.005,0.005,0.005 "Initial Catalyst, Sorbent & Chemicals",owner,lin,frac of TPC,0.0,0.0,0.250835587,0.250835587,0.0,0.250835587 diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index 96a5c423b..a1259fa38 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -63,10 +63,8 @@ class GenericSplitterPerformanceModel(om.ExplicitComponent): The priority_tech parameter determines which technology receives the primary allocation. The outputs are: - - {commodity}_out1: profile of commodity sent to the first technology - - {commodity}_out2: profile of commodity sent to the second technology - - total_{commodity}_produced1: total commodity sent to the first technology - - total_{commodity}_produced2: total commodity sent to the second technology + - {commodity}_out1: commodity sent to the first technology + - {commodity}_out2: commodity sent to the second technology This component is purposefully simple; a more realistic case might include losses or other considerations from system components. @@ -89,12 +87,6 @@ def setup(self): shape_by_conn=True, units=self.config.commodity_units, ) - self.add_input( - f"total_{self.config.commodity}_produced", - val=-1.0, - units=self.config.commodity_units, - desc=f"Total {self.config.commodity} input to the splitter", - ) split_mode = self.config.split_mode @@ -127,18 +119,6 @@ def setup(self): units=self.config.commodity_units, desc=f"{self.config.commodity} output to the second technology", ) - # self.add_output( - # f"total_{self.config.commodity}_produced1", - # val=-1.0, - # units=self.config.commodity_units, - # desc=f"Total {self.config.commodity} output to the first technology", - # ) - # self.add_output( - # f"total_{self.config.commodity}_produced2", - # val=-1.0, - # units=self.config.commodity_units, - # desc=f"Total {self.config.commodity} output to the second technology", - # ) def compute(self, inputs, outputs): commodity_in = inputs[f"{self.config.commodity}_in"] From 03a4ba77352d1b13b9cf70c20e4388e412ac970f Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Mon, 26 Jan 2026 19:12:17 -0700 Subject: [PATCH 38/40] Going back to old coefficients with electricity in processing cost --- examples/21_iron_mn_to_il/tech_config.yaml | 2 +- .../iron/martin_ore/cost_coeffs.csv | 4 +-- .../iron/martin_ore/cost_inputs.csv | 4 +-- .../iron/martin_ore/cost_inputs_old.csv | 27 ------------------- 4 files changed, 5 insertions(+), 32 deletions(-) delete mode 100644 h2integrate/converters/iron/martin_ore/cost_inputs_old.csv diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index a6c5be49a..a3486489b 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -15,7 +15,7 @@ technologies: rated_capacity: 70. # MW, need 27.913 MW per timestep for iron ore cost_parameters: cost_year: 2022 - price: 58.02 #USD/MW + price: 0. # Electricity costs are already priced in to current mine model annual_cost: 0. start_up_cost: 0. diff --git a/h2integrate/converters/iron/martin_ore/cost_coeffs.csv b/h2integrate/converters/iron/martin_ore/cost_coeffs.csv index 5b469f66d..8f31a0b32 100644 --- a/h2integrate/converters/iron/martin_ore/cost_coeffs.csv +++ b/h2integrate/converters/iron/martin_ore/cost_coeffs.csv @@ -8,7 +8,7 @@ 6,std_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807.0 7,std_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 8,std_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,33.22,31.4,25.11,38.24,38.12 +9,std_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 10,std_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 11,std_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 12,std_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 @@ -21,7 +21,7 @@ 19,drg_taconite_pellets,Mineral exploration and development,capital,constant,2021 $,117130702.2,129331817.0,109810033.3,0.0,0.0 20,drg_taconite_pellets,Mineral land and rights,capital,constant,2021 $,1313966.21,1450837.691,1231843.322,0.0,0.0 21,drg_taconite_pellets,Mining,variable opex,constant,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0.0,0.0 -22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,36.54,42.68,36.24,0.0,0.0 +22,drg_taconite_pellets,Processing,variable opex,constant,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0.0,0.0 23,drg_taconite_pellets,Site administration,variable opex,constant,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0.0,0.0 24,drg_taconite_pellets,New equipment purchases,variable opex,constant,2021 $ per wlt pellet,4.2,4.46,4.54,0.0,0.0 25,drg_taconite_pellets,General/other,variable opex,constant,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0.0,0.0 diff --git a/h2integrate/converters/iron/martin_ore/cost_inputs.csv b/h2integrate/converters/iron/martin_ore/cost_inputs.csv index 4a55d9d9b..baddd2ad2 100644 --- a/h2integrate/converters/iron/martin_ore/cost_inputs.csv +++ b/h2integrate/converters/iron/martin_ore/cost_inputs.csv @@ -8,7 +8,7 @@ std_taconite_pellets,Buildings and other structures,capital,2021 $,802586723.3,7 std_taconite_pellets,Mineral exploration and development,capital,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807 std_taconite_pellets,Mineral land and rights,capital,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 std_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,33.22,31.40,25.11,38.24,38.12 +std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 std_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 std_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 std_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 @@ -21,7 +21,7 @@ drg_taconite_pellets,Buildings and other structures,capital,2021 $,1012588246,11 drg_taconite_pellets,Mineral exploration and development,capital,2021 $,117130702.2,129331817,109810033.3,0,0 drg_taconite_pellets,Mineral land and rights,capital,2021 $,1313966.21,1450837.691,1231843.322,0,0 drg_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0,0 -drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,36.54,42.68,36.24,0,0 +drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0,0 drg_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0,0 drg_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,0,0 drg_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0,0 diff --git a/h2integrate/converters/iron/martin_ore/cost_inputs_old.csv b/h2integrate/converters/iron/martin_ore/cost_inputs_old.csv deleted file mode 100644 index baddd2ad2..000000000 --- a/h2integrate/converters/iron/martin_ore/cost_inputs_old.csv +++ /dev/null @@ -1,27 +0,0 @@ -Product,Name,Type,Unit,Northshore,United,Hibbing,Minorca,Tilden -std_taconite_pellets,Latitude,location,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 -std_taconite_pellets,Longitude,location,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 -std_taconite_pellets,Crude ore processed,capacity,wltpy,17000000,15000000,24000000,8600000,20500000 -std_taconite_pellets,Ore pellets produced,capacity,wltpy,4540000,4600000,6300000,2790000,7700000 -std_taconite_pellets,Estimated Reserves,capacity,mlt,1158,774.6,109,109.7,520 -std_taconite_pellets,Buildings and other structures,capital,2021 $,802586723.3,782080397.9,1078561311,552238204.9,1340439971 -std_taconite_pellets,Mineral exploration and development,capital,2021 $,92838867.94,90466807.72,124762107.6,63879912.64,155054807 -std_taconite_pellets,Mineral land and rights,capital,2021 $,1041461.66,1014852.01,1399574.925,716601.5841,1739396.873 -std_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,16.875,15.49,19.87,16.84,15.3 -std_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,40.135,37.62,34.57,45.56,42.79 -std_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,2.37,2.14,2.3,2.2,2.84 -std_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,5.63,4.65 -std_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,8.165,9.29,8.2,10.1,5.07 -drg_taconite_pellets,Latitude,location,deg N,47.29415278,47.34858889,47.53,47.55833333,46.48333333 -drg_taconite_pellets,Longitude,location,deg E,-91.25649444,-92.58361944,-92.91,-92.525,-87.66666667 -drg_taconite_pellets,Crude ore processed,capacity,wltpy,17000000,15000000,24000000,0,0 -drg_taconite_pellets,Ore pellets produced,capacity,wltpy,4540000,4600000,6300000,0,0 -drg_taconite_pellets,Estimated Reserves,capacity,mlt,1158,774.6,109,0,0 -drg_taconite_pellets,Buildings and other structures,capital,2021 $,1012588246,1118066189,949301480.9,0,0 -drg_taconite_pellets,Mineral exploration and development,capital,2021 $,117130702.2,129331817,109810033.3,0,0 -drg_taconite_pellets,Mineral land and rights,capital,2021 $,1313966.21,1450837.691,1231843.322,0,0 -drg_taconite_pellets,Mining,variable opex,2021 $ per wlt pellet,22.1175,26.60498583,22.58913891,0,0 -drg_taconite_pellets,Processing,variable opex,2021 $ per wlt pellet,43.8175,51.61628024,43.8251436,0,0 -drg_taconite_pellets,Site administration,variable opex,2021 $ per wlt pellet,4.515,5.521117114,4.687740946,0,0 -drg_taconite_pellets,New equipment purchases,variable opex,2021 $ per wlt pellet,4.2,4.46,4.54,0,0 -drg_taconite_pellets,General/other,variable opex,2021 $ per wlt pellet,15.8675,15.39868427,13.07435456,0,0 From 84323935f609fee26fb9a60f0bbfacc9b512f1b5 Mon Sep 17 00:00:00 2001 From: jmartin4 Date: Tue, 27 Jan 2026 16:54:29 -0700 Subject: [PATCH 39/40] Fixed tests, need to document --- examples/21_iron_mn_to_il/plant_config.yaml | 2 - examples/21_iron_mn_to_il/run_iron.py | 70 +++++++++++++------ examples/21_iron_mn_to_il/tech_config.yaml | 2 +- examples/21_iron_mn_to_il/test_inputs.csv | 7 +- examples/test/test_all_examples.py | 6 +- .../converters/iron/model_locations.yaml | 2 +- 6 files changed, 58 insertions(+), 31 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index 2867e6f69..bf5481119 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -13,8 +13,6 @@ technology_interconnections: [ # Connect iron_ore price and processed_ore_feedstock to iron_transport ["finance_subgroup_iron_ore","processed_ore_feedstock",["price_iron_ore","price"]], ["processed_ore_feedstock","iron_plant","iron_ore","iron_transport"], - # 'iron_transport.iron_ore_in' is connected to 'processed_ore_feedstock_source.iron_ore_out' - # but it SHOULD be connected to 'processed_ore_feedstock_source.iron_ore_consumed' ["dri_grid_feedstock","iron_plant","electricity","cable"], ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], diff --git a/examples/21_iron_mn_to_il/run_iron.py b/examples/21_iron_mn_to_il/run_iron.py index 711fe5c4f..a5ed81252 100644 --- a/examples/21_iron_mn_to_il/run_iron.py +++ b/examples/21_iron_mn_to_il/run_iron.py @@ -16,19 +16,25 @@ # Modify and run the model for different cases casenames = [ - "Case 1", - "Case 2", - "Case 3", - "Case 4", + "Standard Iron - Hibbing", + "Standard Iron - Northshore", + "DR Grade Iron - Northshore", + "DR Grade Iron - Northshore (adjusted)", ] + lcois_ore = [] capexes_ore = [] fopexes_ore = [] vopexes_ore = [] -lcois = [] -capexes = [] -fopexes = [] -vopexes = [] +lcois_iron = [] +capexes_iron = [] +fopexes_iron = [] +vopexes_iron = [] +lcois_steel = [] +capexes_steel = [] +fopexes_steel = [] +vopexes_steel = [] + for casename in casenames: model = modify_tech_config(model, cases[casename]) @@ -44,28 +50,50 @@ vopexes_ore.append( float(model.model.get_val("finance_subgroup_iron_ore.total_varopex_adjusted")[0]) ) - lcois.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) - capexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0])) - fopexes.append(float(model.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0])) - vopexes.append( + lcois_iron.append(float(model.model.get_val("finance_subgroup_pig_iron.price_pig_iron")[0])) + capexes_iron.append( + float(model.model.get_val("finance_subgroup_pig_iron.total_capex_adjusted")[0]) + ) + fopexes_iron.append( + float(model.model.get_val("finance_subgroup_pig_iron.total_opex_adjusted")[0]) + ) + vopexes_iron.append( float(model.model.get_val("finance_subgroup_pig_iron.total_varopex_adjusted")[0]) ) + lcois_steel.append(float(model.model.get_val("finance_subgroup_steel.price_steel")[0])) + capexes_steel.append( + float(model.model.get_val("finance_subgroup_steel.total_capex_adjusted")[0]) + ) + fopexes_steel.append( + float(model.model.get_val("finance_subgroup_steel.total_opex_adjusted")[0]) + ) + vopexes_steel.append( + float(model.model.get_val("finance_subgroup_steel.total_varopex_adjusted")[0]) + ) # Compare the Capex, Fixed Opex, and Variable Opex across the 4 cases columns = [ - "LCO Iron Ore [USD/kg]", - "Capex [Million USD]", - "Fixed Opex [Million USD/year]", - "Variable Opex [Million USD/year]", + "Levelized Cost\n[USD/kg]", + "Capex\n[USD]", + "Fixed Opex\n[USD/year]", + "Variable Opex\n[USD/year]", ] df_ore = pd.DataFrame( - np.transpose(np.vstack([capexes_ore, fopexes_ore, vopexes_ore, lcois_ore])), + np.transpose(np.vstack([lcois_ore, capexes_ore, fopexes_ore, vopexes_ore])), index=casenames, columns=columns, ) print(df_ore) -columns[0] = "LCO Pig Iron [USD/kg]" -df = pd.DataFrame( - np.transpose(np.vstack([capexes, fopexes, vopexes, lcois])), index=casenames, columns=columns +df_iron = pd.DataFrame( + np.transpose(np.vstack([lcois_iron, capexes_iron, fopexes_iron, vopexes_iron])), + index=casenames, + columns=columns, +) +print(df_iron) +df_steel = pd.DataFrame( + np.transpose(np.vstack([lcois_steel, capexes_steel, fopexes_steel, vopexes_steel])), + index=casenames, + columns=columns, ) -print(df) +df_steel = df_steel.iloc[2:] +print(df_steel) diff --git a/examples/21_iron_mn_to_il/tech_config.yaml b/examples/21_iron_mn_to_il/tech_config.yaml index a3486489b..266e12c8c 100644 --- a/examples/21_iron_mn_to_il/tech_config.yaml +++ b/examples/21_iron_mn_to_il/tech_config.yaml @@ -188,7 +188,7 @@ technologies: feedstock_type: "water" units: "galUS" #galUS/h performance_parameters: - rated_capacity: 10. # need 9.083687924154146 galUS/h + rated_capacity: 10000. # need 9083.687924154146 galUS/h cost_parameters: cost_year: 2022 price: 0.0016700004398318847 # cost is USD 0.441167535/t, converted to USD/gal diff --git a/examples/21_iron_mn_to_il/test_inputs.csv b/examples/21_iron_mn_to_il/test_inputs.csv index 66387f458..c51fadc41 100644 --- a/examples/21_iron_mn_to_il/test_inputs.csv +++ b/examples/21_iron_mn_to_il/test_inputs.csv @@ -1,3 +1,4 @@ -Index 0,Index 1,Index 2,Index 3,Index 4,Type,Case 1,Case 2,Case 3,Case 4, -technologies,iron_mine,model_inputs,shared_parameters,mine,str,Northshore,Hibbing,Northshore,Hibbing, -technologies,iron_mine,model_inputs,shared_parameters,taconite_pellet_type,str,std,std,drg,drg, +Index 0,Index 1,Index 2,Index 3,Index 4,Type,Standard Iron - Hibbing,Standard Iron - Northshore,DR Grade Iron - Northshore,DR Grade Iron - Northshore (adjusted) +technologies,iron_mine,model_inputs,shared_parameters,mine,str,Hibbing,Northshore,Northshore,Northshore +technologies,iron_mine,model_inputs,shared_parameters,taconite_pellet_type,str,std,std,drg,drg +technologies,processed_ore_feedstock,model_inputs,performance_parameters,rated_capacity,float,250,250,250,221.2592636 diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index a0a49358b..7b91fe191 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -1628,15 +1628,15 @@ def test_21_iron_dri_eaf_example(subtests): with subtests.test("Value check on LCOI"): lcoi = h2i.model.get_val("finance_subgroup_iron_ore.LCOI", units="USD/t")[0] - assert pytest.approx(lcoi, rel=1e-4) == 143.3495266638054 + assert pytest.approx(lcoi, rel=1e-4) == 135.3741358811098 with subtests.test("Value check on LCOP"): lcop = h2i.model.get_val("finance_subgroup_pig_iron.LCOP", units="USD/t")[0] - assert pytest.approx(lcop, rel=1e-4) == 353.63339139124 + assert pytest.approx(lcop, rel=1e-4) == 365.38855928418843 with subtests.test("Value check on LCOS"): lcos = h2i.model.get_val("finance_subgroup_steel.LCOS", units="USD/t")[0] - assert pytest.approx(lcos, rel=1e-4) == 524.8228189073025 + assert pytest.approx(lcos, rel=1e-4) == 538.4002908756322 def test_sweeping_different_resource_sites_doe(subtests): diff --git a/h2integrate/converters/iron/model_locations.yaml b/h2integrate/converters/iron/model_locations.yaml index d1fd47fca..9a1b2d509 100644 --- a/h2integrate/converters/iron/model_locations.yaml +++ b/h2integrate/converters/iron/model_locations.yaml @@ -13,7 +13,7 @@ cost: martin_ore: compatible_tech: ['h2_dri','ng_dri'] model: 'h2integrate.converters.iron.martin_ore.cost_model' - inputs: 'cost_inputs_old.csv' + inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' rosner: compatible_tech: ['h2_dri','ng_dri'] From 7f63887b1d069dceee82cf006ee28ae014ca3776 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:41:51 -0700 Subject: [PATCH 40/40] updated example 21 so iron_ore_consumed is used for transport cost --- examples/21_iron_mn_to_il/plant_config.yaml | 6 +++++- examples/test/test_all_examples.py | 4 ++-- h2integrate/core/feedstocks.py | 19 +++++++++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/examples/21_iron_mn_to_il/plant_config.yaml b/examples/21_iron_mn_to_il/plant_config.yaml index bf5481119..98006ef08 100644 --- a/examples/21_iron_mn_to_il/plant_config.yaml +++ b/examples/21_iron_mn_to_il/plant_config.yaml @@ -11,12 +11,16 @@ technology_interconnections: [ ["mine_electricity_feedstock","iron_mine","electricity","cable"], ["mine_crude_ore_feedstock","iron_mine","crude_ore","pipe"], # Connect iron_ore price and processed_ore_feedstock to iron_transport + # Connect the LCOI of iron ore to the price of a feedstock component representing the ore to be used in DRI ["finance_subgroup_iron_ore","processed_ore_feedstock",["price_iron_ore","price"]], - ["processed_ore_feedstock","iron_plant","iron_ore","iron_transport"], + # Connect the ore feedstock available to the iron plant + ["processed_ore_feedstock","iron_plant","iron_ore","pipe"], ["dri_grid_feedstock","iron_plant","electricity","cable"], ["catalyst_feedstock","iron_plant","reformer_catalyst","pipe"], ["water_feedstock","iron_plant","water","pipe"], ["natural_gas_feedstock","iron_plant","natural_gas","pipe"], + # Use the iron ore consumed by the iron plant to calculate transport costs + ["iron_plant","iron_transport", ["iron_ore_consumed","iron_ore_in"]], # Connect feedstocks to steel plant ["eaf_grid_feedstock","steel_plant","electricity","cable"], ["eaf_water_feedstock","steel_plant","water","pipe"], diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 1b9449be7..cfbb59a66 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -1788,11 +1788,11 @@ def test_21_iron_dri_eaf_example(subtests): with subtests.test("Value check on LCOP"): lcop = h2i.model.get_val("finance_subgroup_pig_iron.LCOP", units="USD/t")[0] - assert pytest.approx(lcop, rel=1e-4) == 365.38855928418843 + assert pytest.approx(lcop, rel=1e-4) == 359.670379351 with subtests.test("Value check on LCOS"): lcos = h2i.model.get_val("finance_subgroup_steel.LCOS", units="USD/t")[0] - assert pytest.approx(lcos, rel=1e-4) == 538.4002908756322 + assert pytest.approx(lcos, rel=1e-4) == 531.5842266865 def test_sweeping_different_resource_sites_doe(subtests): diff --git a/h2integrate/core/feedstocks.py b/h2integrate/core/feedstocks.py index e516d99b2..ce8d296a8 100644 --- a/h2integrate/core/feedstocks.py +++ b/h2integrate/core/feedstocks.py @@ -32,16 +32,23 @@ def setup(self): self.config = FeedstockPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - feedstock_type = self.config.feedstock_type + self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + self.add_input( + f"{self.config.feedstock_type}_capacity", + val=self.config.rated_capacity, + units=self.config.units, + ) - self.add_output(f"{feedstock_type}_out", shape=n_timesteps, units=self.config.units) + self.add_output( + f"{self.config.feedstock_type}_out", shape=self.n_timesteps, units=self.config.units + ) def compute(self, inputs, outputs): - feedstock_type = self.config.feedstock_type - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] # Generate feedstock array operating at full capacity for the full year - outputs[f"{feedstock_type}_out"] = np.full(n_timesteps, self.config.rated_capacity) + outputs[f"{self.config.feedstock_type}_out"] = np.full( + self.n_timesteps, inputs[f"{self.config.feedstock_type}_capacity"][0] + ) @define(kw_only=True)