Skip to content

Iron: Adding electrowinning capabilities#432

Open
jmartin4u wants to merge 56 commits intoNatLabRockies:developfrom
jmartin4u:ewin-merge
Open

Iron: Adding electrowinning capabilities#432
jmartin4u wants to merge 56 commits intoNatLabRockies:developfrom
jmartin4u:ewin-merge

Conversation

@jmartin4u
Copy link
Collaborator

@jmartin4u jmartin4u commented Jan 2, 2026

Iron: Adding electrowinning capabilities

This PR adds three iron electrowinning technologies:

  • Aqueous hydroxide electrolysis (AHE)
  • Molten salt electrolysis (MSE)
  • Molten oxide electrolysis (MOE)

For the time being, the performance and cost are modeled fairly simply. Performance is modeled based on upper and lower bounds of energy required for each type as defined by Humbert. Cost is modeled based on correlations of capital cost developed for electrowinning of many different metals developed by Stinn and Allanore, and an opex model developed by Humbert in the spreadsheet available as SI to the linked paper, which also provides input values for the Stinn/Allanore capex model.

Section 1: Type of Contribution

  • Feature Enhancement
    • Framework
    • New Model
    • Updated Model
    • Tools/Utilities
    • Other (please describe):
  • Bug Fix
  • Documentation Update
  • CI Changes
  • Other (please describe):

Section 2: Draft PR Checklist

N/A

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback:

Implementation feedback:

Other feedback:

Section 3: General PR Checklist

  • PR description thoroughly describes the new feature, bug fix, etc.
  • Added tests for new functionality or bug fixes
  • Tests pass (If not, and this is expected, please elaborate in the Section 6: Test Results)
  • Documentation
    • Docstrings are up-to-date
    • Related docs/ files are up-to-date, or added when necessary
    • Documentation has been rebuilt successfully
    • Examples have been updated (if applicable)
  • CHANGELOG.md has been updated to describe the changes made in this PR

Section 3: Related Issues

Section 4: Impacted Areas of the Software

Section 4.1: New Files

  • h2integrate\converters\iron\:
    • humbert_ewin_perf.py: New iron electrowinning performance model humbert_electrowinning_performance based on Humbert
    • humbert_stinn_ewin_cost.py: New iron electrowinning cost model humbert_stinn_electrowinning_cost based on Humbert and Stinn and Allanore
    • stinn\table1.csv: Input data table mostly from Stinn and Allanore but iron costs from Humbert
  • h2integrate\converters\iron\stinn\cost_model.py: Modified the transcription of the Humbert SI equations to interface with the humbert_stinn_electrowinning_cost model
  • examples\27_iron_electrowinning\: New example that produces an LCOI for all 3 types of iron electrowinning

Section 4.2: Modified Files

  • h2integrate\converters\iron\stinn\:
    • cost_model.py: Modified the transcription of the Stinn and Allanore equations to interface with the humbert_stinn_electrowinning_cost model
    • cost_coeffs.csv: Added in some coefficients that were previously hard-coded into cost_model.py
  • h2integrate\core\supported_models.py: Added humbert_electrowinning_performance and humbert_stinn_electrowinning_cost as supported models
  • h2integrate\finances\profast_base.py: Added sponge_iron as a mass commodity

Section 5: Additional Supporting Information

Section 6: Test Results, if applicable

Section 7 (Optional): New Model Checklist

  • Model Structure:
    • Follows established naming conventions outlined in docs/developer_guide/coding_guidelines.md
    • Used attrs class to define the Config to load in attributes for the model
      • If applicable: inherit from BaseConfig or CostModelBaseConfig
    • Added: initialize() method, setup() method, compute() method
      • If applicable: inherit from CostModelBaseClass
  • Integration: Model has been properly integrated into H2Integrate
    • Added to supported_models.py
    • If a new commodity_type is added, update create_financial_model in h2integrate_model.py
  • Tests: Unit tests have been added for the new model
    • Pytest-style unit tests
    • Unit tests are in a "test" folder within the folder a new model was added to
    • If applicable add integration tests
  • Example: If applicable, a working example demonstrating the new model has been created
    • Input file comments
    • Run file comments
    • Example has been tested and runs successfully in test_all_examples.py
  • Documentation:
    • Write docstrings using the Google style
    • Model added to the main models list in docs/user_guide/model_overview.md
      • Model documentation page added to the appropriate docs/ section
      • <model_name>.md is added to the _toc.yml

Copy link
Collaborator

@kbrunik kbrunik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! Overall super cool and exciting to have some more functionality added to H2I. I know this isn't quite finished with regards to documentation and testing but thought I could share some initial thoughts.

Copy link
Collaborator

@elenya-grant elenya-grant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Jonathan! I think this is super cool and the example is very well done! I didn't take a super deep look into the new models but provided some initial feedback (some of it may be irrelevant depending on other changes you plan on making). Would be happy to do a deeper review once it's ready! Thanks for the work on this! Very exciting!

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't feedstock costs (lime, electricity, ore, etc) be taken care of with feedstock component(s)? In my mind, this should only output costs associated with this tech, not its feedstocks.

elec_price = inputs["price_electricity"]

# Add ore transport cost TODO: turn iron_transport into proper transporter
ore_price += ore_transport_cost
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ore transport is now a separate model, should not be included here.


# 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should labor_rate be an attribute of the configuration class?

labor_rate = 55.90 # USD/person-hour
NaOH_cost = 415.179 # USD/tonne
CaCl2_cost = 207.59 # USD/tonne
limestone_cost = 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these feedstock costs should be taken care of with the feedstock component.

@johnjasa johnjasa added the needs modifications This PR has been reviewed, at least partially, and is ready for PR author response label Jan 20, 2026
@jmartin4u jmartin4u added ready for review This PR is ready for input from folks and removed needs modifications This PR has been reviewed, at least partially, and is ready for PR author response labels Jan 23, 2026
Copy link
Collaborator

@elenya-grant elenya-grant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is coming along nicely! Thanks for the work on this! I think the performance model is looking awesome, I have some remaining questions and comments on the cost model. Another small nit-pick (non-blocking) is that links to papers can be embedded in docstrings as hyper-links if they're formatted like this:

 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 <https://link.springer.com/article/10.1007/s40831-024-00878-3>`_ 

Thanks for making a lot of the previously suggested changes. I think it's nearly ready! Let me know if you want to discuss any of the comments I left!

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add in-line comments about what each of these variables are and the units

E_all_lo = 2.781 # [kWh/kg] the something something of something

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as a comment block at the top so I didn't have to repeat 3 times, fine if you want to change

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as a comment block at the top so I didn't have to repeat 3 times, fine if you want to change

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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why strict=False - this is normally only used (as far I I know) for dispatch/storage models because theres more than 2 models connected. But in this case, it appears that its only a performance and cost model, so strict=True would be appropriate

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I need to change to strict=True or remove the whole statement - will leave in your hands!

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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add iron_ore_consumed as an output here and then calculate it in the compute() method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding


# MSE - Opex
positions = 499.2 / 2e6 # Labor rate (position-years/tonne)
NaOH_ratio = 0 # Ratio of NaOH consumption to annual iron production
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should NaOH and CaCl2 be inputs to the performance model that are consumed and may limit the output production similar to electricity and iron ore? Then the costs associated with these could be represented with a feedstock component and not included in this cost model. I could see it going either way, if it makes sense to include within this cost model, then its fine as-is

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes sense to make them into feedstocks and limit production. If you can make that change really quick great, I don't see it as a requirement - I see a wave of converting lots of these little VarOpExes into feedstocks at once across multiple models, because I bet there's more of them buried.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use t/yr instead of Mg/yr?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing

elec_in = inputs["electricity_in"]
elec_price = inputs["price_electricity"]

# Add ore transport cost TODO: turn iron_transport into proper transporter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to remove the transport cost from this model since its a separate component now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already removed, also removing reference in docstring


# 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could these costs be in the configuration class? They could default to the values you have here but could then be modified by the user.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, those could easily be config parameters. Lazy on my part. If you have time to fix, please do

outputs["VarOpEx"] = (
NaOH_opex + CaCl2_opex + limestone_opex + anode_opex + ore_opex + elec_opex
)
outputs["labor_opex"] = labor_opex
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you outputting the individual costs for testing purposes to make sure it's aligned with the results from the paper? (Just checking since only OpEx, CapEx, and VarOpEx will be used in the finance models)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to have the ability for capex and opex to be broken out in more detail - I don't want the only outputs of the cost model to be the entire sum of capex and opex. I want to be able to make plots and tables (straight from the output files) that show which parts of the capex (the electrolyzer, the processing) are sensitive to which inputs (the temperature, the iron content of the ore. Even if the finance model isn't using them I'd like to keep these outputs, and I'll put a note into the comments so there's no confusion on why they're there.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of units of None could you make these unitless?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think electricity and iron ore costs should be calculated in feedstock models

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed! Looks like you already removed, also deleting references from docstrings

@johnjasa johnjasa added the needs modifications This PR has been reviewed, at least partially, and is ready for PR author response label Feb 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs modifications This PR has been reviewed, at least partially, and is ready for PR author response ready for review This PR is ready for input from folks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants