diff --git a/CHANGELOG.md b/CHANGELOG.md index 486553cc9..4be6e6403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Updates models for NumPy version 2.4.0 - Update test values for WOMBAT update to 0.13.0 - Added standalone iron DRI and steel EAF performance and cost models +- Added iron electrowinning model - Added capability to have transport models that require user input parameters - Add geologic hydrogen surface processing converter - Add baseclass for caching functionality diff --git a/docs/_toc.yml b/docs/_toc.yml index 65a8d886b..5bdabecd7 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -43,6 +43,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_ewin.md b/docs/technology_models/iron_ewin.md new file mode 100644 index 000000000..f368b43c3 --- /dev/null +++ b/docs/technology_models/iron_ewin.md @@ -0,0 +1,58 @@ +# Iron electrowinning models + +H2I contains iron electrowinning models to simulate the reduction of iron oxide to pure iron and removal of impurities. +The main input feedstock is iron ore, while the output commodity is "sponge iron", i.e. iron that is typically brittle ("spongey") and contains less carbon than most steel alloys. +This sponge iron can then be used in an electric arc furnace (EAF) to produce steel. + +There are currently three iron electrowinning processes modeled in H2I: + - Aqueous Hydroxide Electrolysis (AHE) + - Molten Salt Electrolysis (MSE) + - Molten Oxide Electrolysis (MOE) + +In reality, the exact composition and structure of the resulting sponge iron will differ depending on the process and the conditions. +Currently, H2I models do not make these distinctions, as the technology is new and we are still building out the capability. +Instead, the models in their current form are based on two recent studies of electrowinning technology as a whole. + +The first study is by [Humbert et al.](doi.org/10.1007/s40831-024-00878-3), who focus specifically on iron and the three technologies above. +These authors gather information on the specific energy required for electrolysis and associated pretreatments needed, which is applied in the `humbert_electrowinning_performance` performance model. +In their supporting information, they also model the full operational expenditures for each process, which is applied in the `humbert_stinn_electrowinning_cost` cost model. + +The second study is by [Stinn & Allanore](doi.org/10.1149.2/2.F06202IF), who present a generalized capital cost model for electrowinning of many different metals. +These authors use both cost data and physical parameters from existing studies to fit the model to be applicable to any metal, including iron. +This model is applied in the `humbert_stinn_electrowinning_cost` cost model. + +To use this model, specify `"humbert_electrowinning_performance"` as the performance model and `"humbert_stinn_electrowinning_cost"` as the cost model. +The performance model will Humbert et al.'s energy consumption data to consume electricity as a feedstock and feed this information to the cost model. +The cost model will calculate capex costs based on the Stinn correlations and opex costs based on the Humbert SI. + +## Performance Model + +```{eval-rst} +.. autoclass:: h2integrate.converters.iron.humbert_ewin_perf.HumbertEwinConfig + :members: + :undoc-members: + :show-inheritance: +``` + +```{eval-rst} +.. autoclass:: h2integrate.converters.iron.humbert_ewin_perf.HumbertEwinPerformanceComponent + :members: + :undoc-members: + :show-inheritance: +``` + +## Cost Model + +```{eval-rst} +.. autoclass:: h2integrate.converters.iron.humbert_stinn_ewin_cost.HumbertStinnEwinCostConfig + :members: + :undoc-members: + :show-inheritance: +``` + +```{eval-rst} +.. autoclass:: h2integrate.converters.iron.humbert_stinn_ewin_cost.HumbertStinnEwinCostComponent + :members: + :undoc-members: + :show-inheritance: +``` diff --git a/docs/user_guide/model_overview.md b/docs/user_guide/model_overview.md index 80eada037..64e938155 100644 --- a/docs/user_guide/model_overview.md +++ b/docs/user_guide/model_overview.md @@ -44,7 +44,7 @@ The inputs, outputs, and corresponding technology that are currently available i | `hopp` | electricity | N/A | | `electrolyzer` | hydrogen | electricity | | `geoh2` | hydrogen | rock type | -| `steel` | steel | hydrogen | +| `steel` | steel | iron ore | | `ammonia` | ammonia | nitrogen, hydrogen | | `doc` | co2 | electricity | | `oae` | co2 | electricity | @@ -225,13 +225,18 @@ Below summarizes the available performance, cost, and financial models for each + `'iron_mine_performance_martin'` - cost models: + `'iron_mine_cost_martin'` -- `iron_dri`: iron ore direct reduction +- `iron_dri`: direct reduced iron - performance models: + `'ng_dri_performance_rosner'` + `'h2_dri_performance_rosner'` - cost models: + `'ng_dri_cost_rosner'` + `'h2_dri_cost_rosner'` +- `iron_ewin`: iron electrowinning + - performance models: + + `'humbert_electrowinning_performance'` + - cost models: + + `'humbert_stinn_electrowinning_cost'` (transport-models)= ## Transport Models diff --git a/examples/27_iron_electrowinning/27_iron_electrowinning.yaml b/examples/27_iron_electrowinning/27_iron_electrowinning.yaml new file mode 100644 index 000000000..e600534b0 --- /dev/null +++ b/examples/27_iron_electrowinning/27_iron_electrowinning.yaml @@ -0,0 +1,7 @@ +name: "H2Integrate_config" + +system_summary: "An iron plant using processed ore pellets for electrowinning." + +driver_config: "driver_config.yaml" +technology_config: "tech_config.yaml" +plant_config: "plant_config.yaml" diff --git a/examples/27_iron_electrowinning/driver_config.yaml b/examples/27_iron_electrowinning/driver_config.yaml new file mode 100644 index 000000000..c279b7f1b --- /dev/null +++ b/examples/27_iron_electrowinning/driver_config.yaml @@ -0,0 +1,5 @@ +name: "driver_config" +description: "Simply setting up an outputs folder, nothing fancy" + +general: + folder_output: outputs diff --git a/examples/27_iron_electrowinning/plant_config.yaml b/examples/27_iron_electrowinning/plant_config.yaml new file mode 100644 index 000000000..8f36db502 --- /dev/null +++ b/examples/27_iron_electrowinning/plant_config.yaml @@ -0,0 +1,65 @@ +name: "plant_config" +description: "Configures an iron electrowinning system." + +sites: + ewin_site: + latitude: 41.717 + longitude: -88.398 + +technology_interconnections: [ + # Connect feedstocks to iron mine. + # Requires `electricity` and `crude ore`. + # Both are set up as generic feedstock components - see tech_config. + ["grid_feedstock","iron_mine","electricity","cable"], + ["mine_feedstock","iron_mine","crude_ore","pipe"], + # Connect feedstocks to iron electrowinning plant. + # Requires `electricity` and `iron_ore`. + # Electricity is set up as generic feedstock component, + # but iron_ore is set up as an output of an iron_mine. + ["iron_mine","iron_plant","iron_ore","iron_transport"], + ["ewin_grid_feedstock","iron_plant","electricity","cable"], +] + +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", "grid_feedstock", "mine_feedstock"] + sponge_iron: + commodity: "sponge_iron" + commodity_stream: "iron_plant" + technologies: + - "iron_mine" + - "grid_feedstock" + - "mine_feedstock" + - "iron_transport" + - "iron_plant" + - "ewin_grid_feedstock" diff --git a/examples/27_iron_electrowinning/run_iron.py b/examples/27_iron_electrowinning/run_iron.py new file mode 100644 index 000000000..3fd77f81b --- /dev/null +++ b/examples/27_iron_electrowinning/run_iron.py @@ -0,0 +1,43 @@ +"""Comparing three different iron electrowinning technologies + +This script runs an end-to-end iron production system (including the mine) and compares the +levelized cost of sponge_iron across three different iron electrowinning technologies to see +how their costs compare: + - Aqueous Hydroxide Electrolysis (AHE) + - Molten Salt Electrolysis (MSE) + - Molten Oxide Electrolysis (MOE) + +New users may find it helpful to look at the tech_config.yaml (particularly the iron_plant) to see +how the technologies were set up, as well as the plant_config.yaml (particularly the +technology_interconnections) to see how the technologies were connected. + +""" + +from h2integrate.core.h2integrate_model import H2IntegrateModel + + +# Create H2Integrate model +model = H2IntegrateModel("27_iron_electrowinning.yaml") + +# Define the electrowinning types as a list +electrolysis_types = ["ahe", "mse", "moe"] +lcois = [] + +for electrolysis_type in electrolysis_types: + # Set the technology config value directly + model.technology_config["technologies"]["iron_plant"]["model_inputs"]["shared_parameters"][ + "electrolysis_type" + ] = electrolysis_type + model.setup() # re-setup the model after changing config + model.run() + model.post_process() + lcois.append( + float( + model.model.get_val("finance_subgroup_sponge_iron.price_sponge_iron", units="USD/kg")[0] + ) + ) + +# Compare the LCOIs from each electrowinning type +print("Levelized Cost of Iron (LCOI) by Electrowinning Type:") +for electrolysis_type, lcoi in zip(electrolysis_types, lcois): + print(f" {electrolysis_type.upper()}: ${lcoi:,.2f} per kg of sponge iron") diff --git a/examples/27_iron_electrowinning/tech_config.yaml b/examples/27_iron_electrowinning/tech_config.yaml new file mode 100644 index 000000000..a7c18ddf0 --- /dev/null +++ b/examples/27_iron_electrowinning/tech_config.yaml @@ -0,0 +1,90 @@ +name: "technology_config" +description: "Set up technology for an iron mine/iron electrowinning system" + +technologies: + grid_feedstock: #electricity feedstock for iron ore - comes from grid + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "electricity" + units: "MW" + performance_parameters: + rated_capacity: 30. + cost_parameters: + cost_year: 2022 + price: 58.02 #USD/MW + annual_cost: 0. + start_up_cost: 0. + + mine_feedstock: #crude iron ore feedstock - comes from the ground + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "crude_ore" + units: "t/h" + performance_parameters: + rated_capacity: 2000. + cost_parameters: + cost_year: 2022 + price: 0.0 + annual_cost: 0. + start_up_cost: 0. + + iron_mine: # iron mine - turns crude_iron into iron_ore + performance_model: + model: "iron_mine_performance_martin" + cost_model: + model: "iron_mine_cost_martin" + model_inputs: + shared_parameters: + mine: "Northshore" + taconite_pellet_type: "drg" + max_ore_production_rate_tonnes_per_hr: 250 + + iron_transport: # ship iron_ore from iron_mine to iron_plant + 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 + + ewin_grid_feedstock: #electricity feedstock for iron electrowinning + performance_model: + model: "feedstock_performance" + cost_model: + model: "feedstock_cost" + model_inputs: + shared_parameters: + feedstock_type: "electricity" + units: "kW" + performance_parameters: + rated_capacity: 600000. + cost_parameters: + cost_year: 2022 + price: 0.05802 #USD/kW + annual_cost: 0. + start_up_cost: 0. + + iron_plant: # iron plant - turns iron_ore into sponge_iron + performance_model: + model: "humbert_electrowinning_performance" + cost_model: + model: "humbert_stinn_electrowinning_cost" + model_inputs: + shared_parameters: + electrolysis_type: "ahe" + performance_parameters: + ore_fe_wt_pct: 65 + capacity_mw: 600 diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 8141b5497..8f82c97dd 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -1795,6 +1795,39 @@ def test_21_iron_dri_eaf_example(subtests): assert pytest.approx(lcos, rel=1e-4) == 524.8228189073025 +def test_27_iron_electrowinning_example(subtests): + os.chdir(EXAMPLE_DIR / "27_iron_electrowinning") + + model = H2IntegrateModel("27_iron_electrowinning.yaml") + + with subtests.test("Value check on AHE"): + model.technology_config["technologies"]["iron_plant"]["model_inputs"]["shared_parameters"][ + "electrolysis_type" + ] = "ahe" + model.setup() + model.run() + lcoi = model.model.get_val("finance_subgroup_sponge_iron.LCOS", units="USD/kg")[0] + assert pytest.approx(lcoi, rel=1e-4) == 2.187928233525775 + + with subtests.test("Value check on MSE"): + model.technology_config["technologies"]["iron_plant"]["model_inputs"]["shared_parameters"][ + "electrolysis_type" + ] = "mse" + model.setup() + model.run() + lcoi = model.model.get_val("finance_subgroup_sponge_iron.LCOS", units="USD/kg")[0] + assert pytest.approx(lcoi, rel=1e-4) == 3.3410182461323226 + + with subtests.test("Value check on MOE"): + model.technology_config["technologies"]["iron_plant"]["model_inputs"]["shared_parameters"][ + "electrolysis_type" + ] = "moe" + model.setup() + model.run() + lcoi = model.model.get_val("finance_subgroup_sponge_iron.LCOS", units="USD/kg")[0] + assert pytest.approx(lcoi, rel=1e-4) == 2.2832248695268893 + + def test_sweeping_different_resource_sites_doe(subtests): os.chdir(EXAMPLE_DIR / "27_site_doe_diff") import pandas as pd diff --git a/h2integrate/converters/iron/humbert_ewin_perf.py b/h2integrate/converters/iron/humbert_ewin_perf.py new file mode 100644 index 000000000..6aa0cc818 --- /dev/null +++ b/h2integrate/converters/iron/humbert_ewin_perf.py @@ -0,0 +1,168 @@ +"""Iron electronwinning performance model based on Humbert et al. + +This module contains H2I performance configs and components for modeling iron electrowinning. It is +based on the work of Humbert et al. (doi.org/10.1007/s40831-024-00878-3) which reviews performance +and TEA literature for three different types of iron electrowinning: + - Aqueous Hydroxide Electrolysis (AHE) + - Molten Salt Electrolysis (MSE) + - Molten Oxide Electrolysis (MOE) + +This technology is selected in the tech_config as the performance_model +"humbert_electrowinning_performance" + +Classes: + HumbertEwinConfig: Sets the required model_inputs fields. + HumbertEwinPerformanceComponent: Defines initialize(), setup(), and compute() methods. + +""" + +import numpy as np +import openmdao.api as om +from attrs import field, define + +from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.validators import contains + + +@define +class HumbertEwinConfig(BaseConfig): + """Configuration class for the Humbert iron electrowinning performance model. + + Args: + electrolysis_type (str): The type of electrowinning being performed. Options: + "ahe": Aqueous Hydroxide Electrolysis (AHE) + "mse": Molten Salt Electrolysis (MSE) + "moe": Molten Oxide Electrolysis (MOE) + ore_fe_wt_pct (float): The iron content of the ore coming in, expressed as a percentage. + capacity_mw (float): The MW electrical capacity of the electrowinning plant. + + """ + + electrolysis_type: str = field( + kw_only=True, converter=(str.lower, str.strip), validator=contains(["ahe", "mse", "moe"]) + ) # product selection + ore_fe_wt_pct: float = field(kw_only=True) + capacity_mw: float = field(kw_only=True) + + +class HumbertEwinPerformanceComponent(om.ExplicitComponent): + """OpenMDAO component for the Humbert iron electrowinning performance model. + + Attributes: + OpenMDAO Inputs: + + electricity_in (array): Electric power input available in kW for each timestep. + iron_ore_in (array): Iron ore mass flow available in kg/h for each timestep. + ore_fe_concentration (float): The iron content of the ore coming in, given as a percentage. + spec_energy_cons_fe (float): The specific electrical energy consumption required to win + pure iron (Fe) from iron ore. These are currently calculated as averages between the + high and low stated values in Table 10 of Humbert et al., but this is exposed as an + OpenMDAO variable to probe the effect of specific energy consumption on iron cost. + capacity (float): The electrical capacity of the electrowinning plant in MW. + + OpenMDAO Outputs: + + electricity_consumed (array): Electric power consumption in kW for each timestep. + limiting_input (array): An array of integers indicating which input is the limiting factor + for iron production at each timestep: 0 = iron ore, 1 = electricity, 2 = capacity + sponge_iron_out (array): Sponge iron production in kg/h for each timestep. + total_sponge_iron_produced (float): Total annual sponge iron production in kg/y. + output_capacity (float): Maximum possible annual sponge iron production in kg/y. + + """ + + 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 = HumbertEwinConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=False, + ) + + ewin_type = self.config.electrolysis_type + # Look up performance parameters for each electrolysis type from Humbert Table 10 + if ewin_type == "ahe": + E_all_lo = 2.781 + E_all_hi = 3.779 + E_electrolysis_lo = 1.869 + E_electrolysis_hi = 2.72 + elif ewin_type == "mse": + E_all_lo = 2.720 + E_all_hi = 3.138 + E_electrolysis_lo = 1.81 + E_electrolysis_hi = 2.08 + elif ewin_type == "moe": + E_all_lo = 2.89 + E_all_hi = 4.45 + E_electrolysis_lo = 2.89 + E_electrolysis_hi = 4.45 + E_all = (E_all_lo + E_all_hi) / 2 # kWh/kg_Fe + E_electrolysis = (E_electrolysis_lo + E_electrolysis_hi) / 2 # kWh/kg_Fe + + self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") + self.add_input("iron_ore_in", val=0.0, shape=n_timesteps, units="kg/h") + self.add_input("ore_fe_concentration", val=self.config.ore_fe_wt_pct, units="percent") + self.add_input("spec_energy_all", val=E_all, units="kW*h/kg") + self.add_input("spec_energy_electrolysis", val=E_electrolysis, units="kW*h/kg") + self.add_input("capacity", val=self.config.capacity_mw, units="MW") + + self.add_output( + "electricity_consumed", + val=0.0, + shape=n_timesteps, + units="kW", + desc="Electricity consumed", + ) + self.add_output("limiting_input", val=0.0, shape=n_timesteps, units=None) + self.add_output("sponge_iron_out", val=0.0, shape=n_timesteps, units="kg/h") + self.add_output("total_sponge_iron_produced", val=0.0, units="kg/year") + self.add_output("output_capacity", val=0.0, units="kg/year") + self.add_output("specific_energy_electrolysis", val=0.0, units="kW*h/kg") + + def compute(self, inputs, outputs): + self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + # Parse inputs + elec_in = inputs["electricity_in"] + ore_in = inputs["iron_ore_in"] + pct_fe = inputs["ore_fe_concentration"] + kwh_kg_fe = inputs["spec_energy_all"] + kwh_kg_electrolysis = inputs["spec_energy_electrolysis"] + cap_kw = inputs["capacity"] * 1000 + + # Calculate max iron production for each input + fe_from_ore = ore_in * pct_fe / 100 + fe_from_elec = elec_in / kwh_kg_fe + + # Limit iron production per hour by each input + fe_prod = np.minimum.reduce([fe_from_ore, fe_from_elec]) + + # If production is limited by available ore at any timestep i, limiters[i] = 0 + # If limited by electricity, limiters[i] = 1 + limiters = np.argmin([fe_from_ore, fe_from_elec], axis=0) + + # Limiting iron production per hour by capacity + fe_prod = np.minimum.reduce([fe_prod, np.full(len(fe_prod), cap_kw / kwh_kg_fe)]) + + # If capacity limits production at any timestep i, cap_lim[i] = 1 + # Otherwise, cap_lim[i] = 0 + cap_lim = 1 - np.argmax([fe_prod, np.full(len(fe_prod), cap_kw / kwh_kg_fe)], axis=0) + + # Determine what the limiting factor is for each hour + # At each timestep: 0 = iron ore, 1 = electricity, 2 = capacity + limiters = np.maximum.reduce([cap_lim * 2, limiters]) + outputs["limiting_input"] = limiters + + # Determine actual electricity consumption from iron consumption + elec_consume = fe_prod * kwh_kg_fe + + # Return iron production + outputs["sponge_iron_out"] = fe_prod + outputs["electricity_consumed"] = elec_consume + outputs["total_sponge_iron_produced"] = np.sum(fe_prod) + outputs["output_capacity"] = cap_kw / kwh_kg_fe * 8760 + outputs["specific_energy_electrolysis"] = kwh_kg_electrolysis diff --git a/h2integrate/converters/iron/humbert_stinn_ewin_cost.py b/h2integrate/converters/iron/humbert_stinn_ewin_cost.py new file mode 100644 index 000000000..89614b69f --- /dev/null +++ b/h2integrate/converters/iron/humbert_stinn_ewin_cost.py @@ -0,0 +1,330 @@ +"""Iron electronwinning cost model based on Humbert et al. and Stinn and Allanore + +This module contains H2I cost configs and components for modeling iron electrowinning. It is +based on the work of Humbert et al. (doi.org/10.1007/s40831-024-00878-3), which contains relevant +iron electrowinning performance and cost data, and Stinn & Allanore (doi.org/10.1149.2/2.F06202IF), +which presents an empirical capex model for electrowinning of many different metals based on many +physical parameters of the electrowinning process. + +The opex model developed by Humbert et al. is imported from ./humbert/cost_model.py + +The capex model developed by Stinn & Allanore is imported from ./stinn/cost_model.py + +This technology is selected in the tech_config as the cost_model +"humbert_stinn_electrowinning_cost" + +Classes: + HumbertEwinCostConfig: Sets the required model_inputs fields. + HumbertEwinCostComponent: Defines initialize(), setup(), and compute() methods. + +""" + +import numpy as np +from attrs import field, define + +from h2integrate.core.utilities import merge_shared_inputs +from h2integrate.core.validators import contains, must_equal +from h2integrate.tools.constants import FE_MW, faraday +from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig + + +@define +class HumbertStinnEwinCostConfig(CostModelBaseConfig): + """Configuration class for the Humbert iron electrowinning performance model. + + Args: + electrolysis_type (str): The type of electrowinning being performed. Options: + "ahe": Aqueous Hydroxide Electrolysis (AHE) + "mse": Molten Salt Electrolysis (MSE) + "moe": Molten Oxide Electrolysis (MOE) + cost_year (int): The dollar year of costs output by the model. Defaults to 2018, the dollar + year in which data was given in the Stinn paper + """ + + electrolysis_type: str = field( + kw_only=True, converter=(str.lower, str.strip), validator=contains(["ahe", "mse", "moe"]) + ) # product selection + # Set cost year to 2018 - fixed for Stinn modeling + cost_year: int = field(default=2018, converter=int, validator=must_equal(2018)) + + +class HumbertStinnEwinCostComponent(CostModelBaseClass): + """OpenMDAO component for the Humbert/Stinn iron electrowinning cost model. + + Default values for many inputs are set for 3 technology classes: + + - Aqueous Hydroxide Electrolysis (AHE) + - Molten Salt Electrolysis (MSE) + - Molten Oxide Electrolysis (MOE) + + All of these values come from the SI spreadsheet for the Humbert paper that can be downloaded + at doi.org/10.1007/s40831-024-00878-3 except for the default anode replacement interval. + These are exposed to OpenMDAO for potential future optimization/sensitivity analysis. + + We calculate both CapEx and OpEx in this component. + CapEx is calculated using the Stinn & Allanore model. + OpEx is calculated using the Humbert et al. model. + + Attributes: + OpenMDAO Inputs: + + output_capacity (float): Maximum annual iron production capacity in kg/year. + iron_ore_in (array): Iron ore mass flow available in kg/h for each timestep. + iron_transport_cost (float): Cost to transport iron ore in USD/kg. + price_iron_ore (float): Price of iron ore in USD/kg. + electricity_in (array): Electric power input available in kW for each timestep. + price_electricity (float): Price of electricity in USD/kWh. + specific_energy_electrolysis (float): The specific electrical energy consumption required + to win pure iron (Fe) from iron ore - JUST the electrolysis step. + electrolysis_temp (float): Electrolysis temperature (°C). + electron_moles (float): Moles of electrons per mole of iron product. + current_density (float): Current density (A/m²). + electrode_area (float): Electrode area per cell (m²). + current_efficiency (float): Current efficiency (dimensionless). + cell_voltage (float): Cell operating voltage (V). + rectifier_lines (float): Number of rectifier lines. + positions (float): Labor rate (position-years/tonne). + NaOH_ratio (float): Ratio of NaOH consumed to Fe produced. + CaCl2_ratio (float): Ratio of CaCl2 consumed to Fe produced. + limestone_ratio (float): Ratio of limestone consumed to Fe produced. + anode_ratio (float): Ratio of anode mass to annual iron production. + anode_replacement_interval (float): Replacement interval of anodes (years). + + OpenMDAO Outputs: + + CapEx (float): Total capital cost of the electrowinning plant (USD). + OpEx (float): Yearly operating expenses in USD/year which do NOT depend on plant output. + VarOpEx (float): Yearly operating expenses in USD/year which DO depend on plant output. + processing_capex (float): Portion of the capex that is apportioned to preprocessing of ore. + electrolysis_capex (float): Portion of the capex that is apportioned to electrolysis. + rectifier_capex (float): Portion of the capex that is apportioned to rectifiers. + labor_opex (float): Portion of the opex that is apportioned to labor. + NaOH_opex (float): Portion of the opex that is apportioned to NaOH. + CaCl2_opex (float): Portion of the opex that is apportioned to CaCl2. + limestone_opex (float): Portion of the opex that is apportioned to limestone. + anode_opex (float): Portion of the opex that is apportioned to anodes. + ore_opex (float): Portion of the opex that is apportioned to ore. + elec_opex (float): Portion of the opex that is apportioned to electricity. + + """ + + 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 = HumbertStinnEwinCostConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=False, + ) + super().setup() + + ewin_type = self.config.electrolysis_type + + # Lookup specific inputs for electrowinning types, mostly from the Humbert SI spreadsheet + # (noted where values did not come from this spreadsheet) + if ewin_type == "ahe": + # AHE - Capex + T = 100 # Electrolysis temperature (°C) + z = 2 # Moles of electrons per mole of iron product + V = 1.7 # Cell operating voltage (V) + j = 1000 # Current density (A/m²) + A = 250 # Electrode area per cell (m²) + e = 0.66 # Current efficiency (dimensionless) + N = 12 # Number of rectifier lines + + # AHE - Opex + positions = 739.2 / 2e6 # Labor rate (position-years/tonne) + NaOH_ratio = 25130.2 * 0.1 / 2e6 # Ratio of NaOH consumption to annual iron production + CaCl2_ratio = 0 # Ratio of CaCl2 consumption to annual iron production + limestone_ratio = 0 # Ratio of limestone consumption to annual iron production + anode_ratio = 0 # Ratio of anode mass to annual iron production + # Anode replacement interval not considered by Humbert, 3 years assumed here + anode_replace_int = 3 # Replacement interval of anodes (years) + + elif ewin_type == "mse": + # MSE - Capex + T = 900 # Temperature (deg C) + z = 3 # Moles of electrons per mole of iron product + V = 3 # Cell operating voltage (V) + j = 300 # Current density (A/m²) + A = 250 # Electrode area per cell (m²) + e = 0.66 # Current efficiency (dimensionless) + N = 8 # Number of rectifier lines + + # MSE - Opex + positions = 499.2 / 2e6 # Labor rate (position-years/tonne) + NaOH_ratio = 0 # Ratio of NaOH consumption to annual iron production + CaCl2_ratio = 23138 * 0.1 / 2e6 # Ratio of CaCl2 consumption to annual iron production + limestone_ratio = 0 # Ratio of limestone consumption to annual iron production + anode_ratio = 1589.3 / 2e6 # Ratio of anode mass to annual iron production + # Anode replacement interval not considered by Humbert, 3 years assumed here + anode_replace_int = 3 # Replacement interval of anodes (years) + + elif ewin_type == "moe": + # MOE - Capex + T = 1600 # Temperature (deg C) + z = 2 # Moles of electrons per mole of iron product + V = 4.22 # Cell operating voltage (V) + j = 10000 # Current density (A/m²) + A = 30 # Electrode area per cell (m²) + e = 0.95 # Current efficiency (dimensionless) + N = 6 # Number of rectifier lines + + # AHE - Opex + positions = 230.4 / 2e6 # Labor rate (position-years/tonne) + NaOH_ratio = 0 # Ratio of NaOH consumption to annual iron production + CaCl2_ratio = 0 # Ratio of CaCl2 consumption to annual iron production + limestone_ratio = 0 # Ratio of limestone consumption to annual iron production + anode_ratio = 8365.6 / 2e6 # Ratio of anode mass to annual iron production + # Anode replacement interval not considered by Humbert, 3 years assumed here + anode_replace_int = 3 # Replacement interval of anodes (years) + + # Set up connected inputs + self.add_input("output_capacity", val=0.0, units="Mg/year") # Mg = tonnes + self.add_input("iron_ore_in", val=0.0, shape=n_timesteps, units="kg/h") + self.add_input("iron_transport_cost", val=0.0, units="USD/t") + self.add_input("price_iron_ore", val=0.0, units="USD/Mg") + self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") + self.add_input("price_electricity", val=0.0, units="USD/kW/h") + self.add_input("specific_energy_electrolysis", val=0.0, units="kW*h/kg") + + # Set inputs for Stinn Capex model + self.add_input("electrolysis_temp", val=T, units="C") + self.add_input("electron_moles", val=z, units=None) + self.add_input("current_density", val=j, units="A/m**2") + self.add_input("electrode_area", val=A, units="m**2") + self.add_input("current_efficiency", val=e, units=None) + self.add_input("cell_voltage", val=V, units="V") + self.add_input("rectifier_lines", val=N, units=None) + + # Set outputs for Stinn Capex model + self.add_output("processing_capex", val=0.0, units="USD") + self.add_output("electrolysis_capex", val=0.0, units="USD") + self.add_output("rectifier_capex", val=0.0, units="USD") + + # Set inputs for Humbert Opex model + self.add_input("positions", val=positions, units="year/Mg") + self.add_input("NaOH_ratio", val=NaOH_ratio, units=None) + self.add_input("CaCl2_ratio", val=CaCl2_ratio, units=None) + self.add_input("limestone_ratio", val=limestone_ratio, units=None) + self.add_input("anode_ratio", val=anode_ratio, units=None) + self.add_input("anode_replacement_interval", val=anode_replace_int, units="year") + + # Set outputs for Humbert Opex model + self.add_output("labor_opex", val=0.0, units="USD/year") + self.add_output("NaOH_opex", val=0.0, units="USD/year") + self.add_output("CaCl2_opex", val=0.0, units="USD/year") + self.add_output("limestone_opex", val=0.0, units="USD/year") + self.add_output("anode_opex", val=0.0, units="USD/year") + self.add_output("ore_opex", val=0.0, units="USD/year") + self.add_output("elec_opex", val=0.0, units="USD/year") + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + # Physical constants + F = faraday # Faraday constant: Electric charge per mole of electrons (C/mol) + M = FE_MW / 1000 # Fe molar mass (kg/mol) + + # Parse inputs for Stinn Capex model (doi.org/10.1149/2.F06202IF) + T = inputs["electrolysis_temp"] + z = inputs["electron_moles"] + j = inputs["current_density"] + A = inputs["electrode_area"] + e = inputs["current_efficiency"] + V = inputs["cell_voltage"] + N = inputs["rectifier_lines"] + E_spec = inputs["specific_energy_electrolysis"] + P = inputs["output_capacity"] + p = P * 1000 / 8760 / 3600 # kg/s + + # Calculate total power + j_cell = A * j # current/cell [A] + Q_cell = j_cell * V # power/cell [W] + P_cell = Q_cell * 8760 / E_spec # annual production capacity/cell [kg] + N_cell = P * 1e6 / P_cell # number of cells [-] + Q = Q_cell * N_cell / 1e6 # total installed power [MW] + + # Stinn Capex model - Equation (7) from doi.org/10.1149/2.F06202IF + # Default coefficients + a1n = 51010 + a1d = -3.82e-03 + a1t = -631 + a2n = 5634000 + a2d = -7.1e-03 + a2t = 349 + a3n = 750000 + e1 = 0.8 + e2 = 0.9 + e3 = 0.15 + e4 = 0.5 + + # Alpha coefficients + a1 = a1n / (1 + np.exp(a1d * (T - a1t))) + a2 = a2n / (1 + np.exp(a2d * (T - a2t))) + a3 = a3n * Q + + # Pre-costs calculation + processing_capex = a1 * P**e1 + + # Electrolysis and product handling contribution to total cost + electrolysis_capex = a2 * ((p * z * F) / (j * A * e * M)) ** e2 + + # Power rectifying contribution + rectifier_capex = a3 * V**e3 * N**e4 + + # Capex outputs + outputs["CapEx"] = processing_capex + electrolysis_capex + rectifier_capex + outputs["processing_capex"] = processing_capex + outputs["electrolysis_capex"] = electrolysis_capex + outputs["rectifier_capex"] = rectifier_capex + + # Parse inputs for Humbert Opex model (doi.org/10.1007/s40831-024-00878-3) + positions = inputs["positions"] + NaOH_ratio = inputs["NaOH_ratio"] + CaCl2_ratio = inputs["CaCl2_ratio"] + limestone_ratio = inputs["limestone_ratio"] + anode_ratio = inputs["anode_ratio"] + anode_interval = inputs["anode_replacement_interval"] + ore_in = inputs["iron_ore_in"] + ore_price = inputs["price_iron_ore"] + ore_transport_cost = inputs["iron_transport_cost"] + elec_in = inputs["electricity_in"] + elec_price = inputs["price_electricity"] + + # Add ore transport cost TODO: turn iron_transport into proper transporter + ore_price += ore_transport_cost + + # Humbert Opex model - from SI spreadsheet (doi.org/10.1007/s40831-024-00878-3) + # Default costs - adjusted to 2018 to match Stinn via CPI + labor_rate = 55.90 # USD/person-hour + NaOH_cost = 415.179 # USD/tonne + CaCl2_cost = 207.59 # USD/tonne + limestone_cost = 0 + anode_cost = 1660.716 # USD/tonne + hours = 2000 # hours/position-year + + # All linear OpEx for now - TODO: apply scaling models + labor_opex = labor_rate * P * positions * hours # Labor OpEx USD/year + NaOH_opex = NaOH_ratio * P * NaOH_cost # NaOH VarOpEx USD/year + CaCl2_opex = CaCl2_ratio * P * CaCl2_cost # CaCl2 VarOpEx USD/year + limestone_opex = limestone_ratio * P * limestone_cost # Limestone VarOpEx USD/year + anode_opex = anode_ratio * P * anode_cost / anode_interval # Anode VarOpEx USD/year + ore_opex = np.sum(ore_in * ore_price, keepdims=True) # Ore VarOpEx USD/year + elec_opex = np.sum(elec_in * elec_price, keepdims=True) # Electricity VarOpEx USD/year + + # Opex outputs + outputs["OpEx"] = ( + labor_opex + NaOH_opex + CaCl2_opex + limestone_opex + anode_opex + ore_opex + elec_opex + ) + outputs["VarOpEx"] = ( + NaOH_opex + CaCl2_opex + limestone_opex + anode_opex + ore_opex + elec_opex + ) + outputs["labor_opex"] = labor_opex + outputs["NaOH_opex"] = NaOH_opex + outputs["CaCl2_opex"] = CaCl2_opex + outputs["limestone_opex"] = limestone_opex + outputs["anode_opex"] = anode_opex + outputs["ore_opex"] = ore_opex + outputs["elec_opex"] = elec_opex diff --git a/h2integrate/converters/iron/stinn/cost_coeffs.csv b/h2integrate/converters/iron/stinn/cost_coeffs.csv deleted file mode 100644 index d63844b77..000000000 --- a/h2integrate/converters/iron/stinn/cost_coeffs.csv +++ /dev/null @@ -1,8 +0,0 @@ -Name,Type,Coeff,Unit -alpha_1_numerator,capital,51010.,- -alpha_1_denominator,capital,-3.823e-3,- -alpha_1_temp_offset,capital,631,- -alpha_2_numerator,capital,5634000.,- -alpha_2_denominator,capital,-7.813e-3,- -alpha_2_temp_offset,capital,349,- -alpha_3,capital,750000,- diff --git a/h2integrate/converters/iron/stinn/cost_model.py b/h2integrate/converters/iron/stinn/cost_model.py deleted file mode 100644 index 65d6a2b9d..000000000 --- a/h2integrate/converters/iron/stinn/cost_model.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Calculates the total direct capital cost (C) in 2018 US dollars. - -From: -Estimating the Capital Costs of Electrowinning Processes -Caspar Stinn and Antoine Allanore 2020 Electrochem. Soc. Interface 29 44 -https://iopscience.iop.org/article/10.1149/2.F06202IF -""" - -from pathlib import Path - -import numpy as np -import pandas as pd - - -CD = Path(__file__).parent - - -def main(config): - """ - Calculates the total direct capital cost of an electrowinning system in 2018 US dollars. - - The cost estimation is based on the methodology from: - "Estimating the Capital Costs of Electrowinning Processes" - by Caspar Stinn and Antoine Allanore (2020). - *Electrochem. Soc. Interface*, 29, 44. - DOI: https://iopscience.iop.org/article/10.1149/2.F06202IF - - Args: - config (object): Configuration object containing model inputs, including: - cost_model (dict): Dictionary with the file path to cost coefficients. - electrolysis_temp (float): Electrolysis temperature in degrees Celsius (°C). - pressure (float): System pressure. - production_rate (float): Production rate in kilograms per second (kg/s). - electron_moles (int): Moles of electrons per mole of product. - faraday_const (float): Faraday constant in coulombs per mole (C/mol). - current_density (float): Current density in amperes per square meter (A/m²). - electrode_area (float): Electrode area in square meters (m²). - current_efficiency (float): Current efficiency (dimensionless, fraction). - molar_mass (float): Molar mass of the electrolysis product - in kilograms per mole (kg/mol). - installed_capacity (float): Installed power capacity in megawatts (MW). - cell_voltage (float): Cell operating voltage in volts (V). - rectifier_lines (int): Number of rectifier lines. - - Returns: - dict: A dictionary containing: - pre_costs (float): Pre-costs related to pressure and system preparation. - electrowinning_costs (float): Costs associated with electrolysis - and power rectification. - total_costs (float): Sum of pre-costs and electrowinning costs. - """ - # Load coefficients - coeffs_fp = CD / config.cost_model["coeffs_fp"] - coeffs_df = pd.read_csv(coeffs_fp) - coeffs = coeffs_df.set_index("Name")["Coeff"].to_dict() - - # Extract coefficients - alpha_1_numerator = coeffs["alpha_1_numerator"] - alpha_1_denominator = coeffs["alpha_1_denominator"] - alpha_1_temp_offset = coeffs["alpha_1_temp_offset"] - alpha_2_numerator = coeffs["alpha_2_numerator"] - alpha_2_denominator = coeffs["alpha_2_denominator"] - alpha_2_temp_offset = coeffs["alpha_2_temp_offset"] - alpha_3 = coeffs["alpha_3"] - - # Assign inputs from config - T = config.electrolysis_temp # Electrolysis temperature (°C) - P = config.pressure # Pressure (assumed unit) - p = config.production_rate # Production rate (kg/s) - z = config.electron_moles # Moles of electrons per mole of product - F = config.faraday_const # Electric charge per mole of electrons (C/mol) - j = config.current_density # Current density (A/m²) - A = config.electrode_area # Electrode area (m²) - e = config.current_efficiency # Current efficiency (dimensionless) - M = config.molar_mass # Electrolysis product molar mass (kg/mol) - Q = config.installed_capacity # Installed power capacity (MW) - V = config.cell_voltage # Cell operating voltage (V) - N = config.rectifier_lines # Number of rectifier lines - - # Pre-costs calculation - term1 = alpha_1_numerator / (1 + np.exp(alpha_1_denominator * (T - alpha_1_temp_offset))) - term2 = alpha_2_numerator / (1 + np.exp(alpha_2_denominator * (T - alpha_2_temp_offset))) - - pre_costs = term1 * P**0.8 - - # Electrolysis and product handling contribution to total cost - electrolysis_product_handling = ((p * z * F) / (j * A * e * M)) ** 0.9 - - # Power rectifying contribution - power_rectifying_contribution = alpha_3 * Q * V**0.15 * N**0.5 - - # Electrowinning costs - electrowinning_costs = term2 * electrolysis_product_handling + power_rectifying_contribution - - # Return individual costs for modularity - return { - "pre_costs": pre_costs, - "electrowinning_costs": electrowinning_costs, - "total_costs": pre_costs + electrowinning_costs, - } - - -if __name__ == "__main__": - - class Config: - def __init__(self): - self.cost_model = {"coeffs_fp": "cost_coeffs.csv"} - # Example values for each variable (replace with actual values) - self.electrolysis_temp = 1000 # Temperature in °C, example value - self.pressure = 1.5 # Pressure, example value - self.production_rate = 1.0 # Total production rate, kg/s - self.electron_moles = 3 # Moles of electrons per mole of product, example value - self.faraday_const = ( - 96485 # Electric charge per mole of electrons (Faraday constant), C/mol - ) - self.current_density = 5000 # Current density, A/m², example value - self.electrode_area = 30.0 # Electrode area, m², example value - self.current_efficiency = 0.95 # Current efficiency (dimensionless), example value - self.molar_mass = 0.018 # Electrolysis product molar mass, kg/mol (e.g., water) - self.installed_capacity = 500.0 # Installed power capacity, MW, example value - self.cell_voltage = 4.18 # Cell operating voltage, V, example value - self.rectifier_lines = 3 # Number of rectifier lines, example value - - results = main(Config()) - print(results) diff --git a/h2integrate/converters/iron/test/test_humbert_ewin_perf.py b/h2integrate/converters/iron/test/test_humbert_ewin_perf.py new file mode 100644 index 000000000..c293554c3 --- /dev/null +++ b/h2integrate/converters/iron/test/test_humbert_ewin_perf.py @@ -0,0 +1,181 @@ +import pytest +import openmdao.api as om +from pytest import fixture + +from h2integrate.converters.iron.humbert_ewin_perf import HumbertEwinPerformanceComponent +from h2integrate.converters.iron.humbert_stinn_ewin_cost import HumbertStinnEwinCostComponent + + +@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 tech_config(): + tech_config = { + "model_inputs": { + "shared_parameters": { + "electrolysis_type": "ahe", + }, + "performance_parameters": { + "ore_fe_wt_pct": 65.0, + "capacity_mw": 600.0, + }, + } + } + return tech_config + + +@fixture +def feedstocks_dict(): + feedstocks_dict = { + "electricity": { + "rated_capacity": 600000.0, # kW + "units": "kW", + "price": 0.05802, # $/kWh + }, + "iron_ore": { + "rated_capacity": 237794.77, # kg/h + "units": "kg/h", + "price": 27.5409, # USD/kg TODO: update + }, + } + return feedstocks_dict + + +def setup_and_run(plant_config, tech_config, feedstocks_dict): + prob = om.Problem() + + iron_ewin_perf = HumbertEwinPerformanceComponent( + plant_config=plant_config, tech_config=tech_config, driver_config={} + ) + + iron_ewin_cost = HumbertStinnEwinCostComponent( + plant_config=plant_config, tech_config=tech_config, driver_config={} + ) + + prob.model.add_subsystem("perf", iron_ewin_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_ewin_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in feedstocks_dict.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + elec_consumed = prob.get_val("perf.electricity_consumed", units="kW") + iron_out = prob.get_val("perf.total_sponge_iron_produced", units="kg/year") + iron_cap = prob.get_val("perf.output_capacity", units="kg/year") + + return elec_consumed, iron_out, iron_cap + + +def test_humbert_ewin_performance_component(plant_config, tech_config, feedstocks_dict, subtests): + expected_elec_consumption_ahe = 506978.45 # kW + expected_elec_consumption_mse = 452725.57 # kW + expected_elec_consumption_moe = 567259.43 # kW + expected_sponge_iron_out_ahe = 1354003425.43 # kg/y + expected_sponge_iron_out_mse = 1354003425.43 # kg/y + expected_sponge_iron_out_moe = 1354003425.43 # kg/y + expected_output_capacity_ahe = 1602439024.39 # kg/y + expected_output_capacity_mse = 1794469102.08 # kg/y + expected_output_capacity_moe = 1432152588.56 # kg/y + + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "ahe" + elec_consumed, iron_out, iron_cap = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("ahe_electricity"): + assert ( + pytest.approx( + elec_consumed, + rel=1e-3, + ) + == expected_elec_consumption_ahe + ) + with subtests.test("ahe_production"): + assert ( + pytest.approx( + iron_out, + rel=1e-3, + ) + == expected_sponge_iron_out_ahe + ) + with subtests.test("ahe_capacity"): + assert ( + pytest.approx( + iron_cap, + rel=1e-3, + ) + == expected_output_capacity_ahe + ) + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "mse" + elec_consumed, iron_out, iron_cap = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("mse_electricity"): + assert ( + pytest.approx( + elec_consumed, + rel=1e-3, + ) + == expected_elec_consumption_mse + ) + with subtests.test("mse_production"): + assert ( + pytest.approx( + iron_out, + rel=1e-3, + ) + == expected_sponge_iron_out_mse + ) + with subtests.test("mse_capacity"): + assert ( + pytest.approx( + iron_cap, + rel=1e-3, + ) + == expected_output_capacity_mse + ) + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "moe" + elec_consumed, iron_out, iron_cap = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("moe_electricity"): + assert ( + pytest.approx( + elec_consumed, + rel=1e-3, + ) + == expected_elec_consumption_moe + ) + with subtests.test("moe_production"): + assert ( + pytest.approx( + iron_out, + rel=1e-3, + ) + == expected_sponge_iron_out_moe + ) + with subtests.test("moe_capacity"): + assert ( + pytest.approx( + iron_cap, + rel=1e-3, + ) + == expected_output_capacity_moe + ) diff --git a/h2integrate/converters/iron/test/test_humbert_stinn_ewin_cost.py b/h2integrate/converters/iron/test/test_humbert_stinn_ewin_cost.py new file mode 100644 index 000000000..f4d060781 --- /dev/null +++ b/h2integrate/converters/iron/test/test_humbert_stinn_ewin_cost.py @@ -0,0 +1,181 @@ +import pytest +import openmdao.api as om +from pytest import fixture + +from h2integrate.converters.iron.humbert_ewin_perf import HumbertEwinPerformanceComponent +from h2integrate.converters.iron.humbert_stinn_ewin_cost import HumbertStinnEwinCostComponent + + +@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 tech_config(): + tech_config = { + "model_inputs": { + "shared_parameters": { + "electrolysis_type": "ahe", + }, + "performance_parameters": { + "ore_fe_wt_pct": 65.0, + "capacity_mw": 600.0, + }, + } + } + return tech_config + + +@fixture +def feedstocks_dict(): + feedstocks_dict = { + "electricity": { + "rated_capacity": 600000.0, # kW + "units": "kW", + "price": 0.05802, # $/kWh + }, + "iron_ore": { + "rated_capacity": 237794.77, # kg/h + "units": "kg/h", + "price": 27.5409, # USD/kg TODO: update + }, + } + return feedstocks_dict + + +def setup_and_run(plant_config, tech_config, feedstocks_dict): + prob = om.Problem() + + iron_ewin_perf = HumbertEwinPerformanceComponent( + plant_config=plant_config, tech_config=tech_config, driver_config={} + ) + + iron_ewin_cost = HumbertStinnEwinCostComponent( + plant_config=plant_config, tech_config=tech_config, driver_config={} + ) + + prob.model.add_subsystem("perf", iron_ewin_perf, promotes=["*"]) + prob.model.add_subsystem("cost", iron_ewin_cost, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in feedstocks_dict.items(): + prob.set_val( + f"perf.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + + prob.run_model() + + capex = prob.get_val("cost.CapEx", units="USD") + fopex = prob.get_val("cost.OpEx", units="USD/year") + vopex = prob.get_val("cost.VarOpEx", units="USD/year") + + return capex, fopex, vopex + + +def test_humbert_stinn_ewin_cost_component(plant_config, tech_config, feedstocks_dict, subtests): + expected_capex_ahe = 6038571901.89 # USD + expected_fopex_ahe = 67050786.5 # USD/year + expected_vopex_ahe = 835954.89 # USD/year + expected_capex_mse = 19918313452.1 # USD + expected_fopex_mse = 51295503.77 # USD/year + expected_vopex_mse = 1220341.02 # USD/year + expected_capex_moe = 7307164315.34 # USD + expected_fopex_moe = 21761330.82 # USD/year + expected_vopex_moe = 3316122.05 # USD/year + + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "ahe" + capex, fopex, vopex = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("ahe_capex"): + assert ( + pytest.approx( + capex, + rel=1e-3, + ) + == expected_capex_ahe + ) + with subtests.test("ahe_fopex"): + assert ( + pytest.approx( + fopex, + rel=1e-3, + ) + == expected_fopex_ahe + ) + with subtests.test("ahe_vopex"): + assert ( + pytest.approx( + vopex, + rel=1e-3, + ) + == expected_vopex_ahe + ) + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "mse" + capex, fopex, vopex = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("mse_capex"): + assert ( + pytest.approx( + capex, + rel=1e-3, + ) + == expected_capex_mse + ) + with subtests.test("mse_fopex"): + assert ( + pytest.approx( + fopex, + rel=1e-3, + ) + == expected_fopex_mse + ) + with subtests.test("mse_vopex"): + assert ( + pytest.approx( + vopex, + rel=1e-3, + ) + == expected_vopex_mse + ) + tech_config["model_inputs"]["shared_parameters"]["electrolysis_type"] = "moe" + capex, fopex, vopex = setup_and_run(plant_config, tech_config, feedstocks_dict) + with subtests.test("moe_capex"): + assert ( + pytest.approx( + capex, + rel=1e-3, + ) + == expected_capex_moe + ) + with subtests.test("moe_fopex"): + assert ( + pytest.approx( + fopex, + rel=1e-3, + ) + == expected_fopex_moe + ) + with subtests.test("moe_vopex"): + assert ( + pytest.approx( + vopex, + rel=1e-3, + ) + == expected_vopex_moe + ) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index e030c2c45..243221a58 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -54,6 +54,7 @@ SaltCavernStorageCostModel, LinedRockCavernStorageCostModel, ) +from h2integrate.converters.iron.humbert_ewin_perf import HumbertEwinPerformanceComponent from h2integrate.converters.ammonia.ammonia_synloop import ( AmmoniaSynLoopCostModel, AmmoniaSynLoopPerformanceModel, @@ -79,6 +80,7 @@ SimpleAmmoniaCostModel, SimpleAmmoniaPerformanceModel, ) +from h2integrate.converters.iron.humbert_stinn_ewin_cost import HumbertStinnEwinCostComponent from h2integrate.converters.methanol.co2h_methanol_plant import ( CO2HMethanolPlantCostModel, CO2HMethanolPlantFinanceModel, @@ -198,6 +200,8 @@ "ng_eaf_cost_rosner": NaturalGasEAFPlantCostComponent, # standalone model "h2_eaf_performance_rosner": HydrogenEAFPlantPerformanceComponent, "h2_eaf_cost_rosner": HydrogenEAFPlantCostComponent, # standalone model + "humbert_electrowinning_performance": HumbertEwinPerformanceComponent, + "humbert_stinn_electrowinning_cost": HumbertStinnEwinCostComponent, "reverse_osmosis_desalination_performance": ReverseOsmosisPerformanceModel, "reverse_osmosis_desalination_cost": ReverseOsmosisCostModel, "simple_ammonia_performance": SimpleAmmoniaPerformanceModel, diff --git a/h2integrate/finances/profast_base.py b/h2integrate/finances/profast_base.py index 31643ea76..a0a5ce23f 100644 --- a/h2integrate/finances/profast_base.py +++ b/h2integrate/finances/profast_base.py @@ -608,6 +608,7 @@ def populate_profast(self, inputs): "methanol", "iron_ore", "pig_iron", + "sponge_iron", ] # create years of operation list diff --git a/h2integrate/tools/constants.py b/h2integrate/tools/constants.py index 6b48d8a39..4e67a8853 100644 --- a/h2integrate/tools/constants.py +++ b/h2integrate/tools/constants.py @@ -2,3 +2,5 @@ N_MW = 14.007 # Molecular weight of Nitrogen in g/mol O2_MW = 31.998 # Molecular weight of Oxygen in g/mol AR_MW = 39.948 # Molecular weight of Argon in g/mol +FE_MW = 55.845 # Molecular weight of Iron in g/mol +faraday = 96485.3321 # Faraday constant: Electric charge per mole of electrons (C/mol)