Skip to content

Paper mill and synthetic aviation fuels - #818

Open
mariya-koleva wants to merge 9 commits into
developfrom
paper_mill
Open

Paper mill and synthetic aviation fuels#818
mariya-koleva wants to merge 9 commits into
developfrom
paper_mill

Conversation

@mariya-koleva

Copy link
Copy Markdown
Collaborator

Paper mill and synthetic aviation fuels

This feature add paper mill and synthetic aviation fuels (SAF) as converters and an example containing (1) paper mill only, (2) SAF only, (3) paper mill + SAF, depending on the need of the user. The paper mill process assumed is Kraft process. SAF is currently assuming all the lignin from the paper mill will be available for feedstock and hydrogen is delivered to SAF at a specified by the user price.

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

  • Open draft PR
  • Describe the feature that will be added
  • Fill out TODO list steps
  • Describe requested feedback from reviewers on draft PR
  • Complete Section 7: New Model Checklist (if applicable)

TODO:

  • Step 1: Currently, the financial focus is on "what would happen with the SAF plant if it's added to the paper mill?". Next step should look at "what is the financial benefit of the paper mill when SAF is collocated with it?".
  • Step 2: Refinement of inputs

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback: Please, provide comments how the structure of the added features could be improved.

Implementation feedback: Please, provide comments how the implementation of the added features could be improved.

Other feedback: None

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
    • At least one complete sentence has been provided to describe the changes made in this PR
    • After the above, a hyperlink has been provided to the PR using the following format:
      "A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
      XYZ should be replaced with the actual number.

Section 4: Related Issues

Section 5: Impacted Areas of the Software

Section 5.1: New Files

  • h2integrate/converters/saf/saf.py
  • h2integrate/converters/saf/saf_baseclass.py
    • method1: SAF converter
  • h2integrate/converters/paper_mill/paper_mill.py
  • h2integrate/converters/paper_mill/paper_mill_baseclass.py
    • method1: paper mill converter
  • `h2integrate/examples/36_paper_mill
    • method1: a folder that contains the plant, tech and driver config yaml files as well as a run_paper_mill_mn.py file

Section 5.2: Modified Files

  • h2integrate/core/supported_models.py
    • method1: Added the performance models entries of the new features.

Section 6: Additional Supporting Information

Section 7: Test Results, if applicable

Section 8 (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
    • Run generate_class_hierarchy.py to update the class hierarchy diagram in docs/developer_guide/class_structure.md

@coveralls

coveralls commented Aug 3, 2026

Copy link
Copy Markdown

Coverage Status

Coverage is 88.213%paper_mill into develop. No base build found for develop.

@johnjasa johnjasa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this PR, @mariya-koleva! I'm really glad to see you contributing your useful models to H2I, both so others can use them but also so you can get more acclimated with the code and development process. I've pushed up some changes directly and left some non-exhaustive comments and questions.

My changes I pushed up directly:

  • removed both the paper mill and SAF baseclasses in favor of just having directly performance and cost classes for them. The baseclass model makes more sense if we know we're going to have multiple technologies that inherit that, but we don't have that for paper mills and SAF, so to make the code easier to track, I removed the baseclasses.
  • reworked the example's postprocessing methods so it doesn't use an ad hoc excel spreadsheet output method; instead we use the existing sql_to_csv methods to produce .csv files. This makes the example a bit cleaner and leans on existing H2I methods for postprocessing, Let me know if that was too big of a change and you want that back in the example and we can get it back in!
  • refactored some of the inner computations for the compute() methods to make them hopefully a bit easier to read
  • added plant_capacity_mtpy as an OpenMDAO inputs so we can more easily do parameter sweeps using it
  • removed a lot of the inputs that weren't used in the performance models
  • added lignin as an available commodity in the pipe transporter and updated the example to use that pipe

Additional changes I'm requesting for this PR:

  • please add a test to test_all_examples.py for this example case
  • add to the documentation for the new paper mill and SAF models; see other technology models for examples of this
  • the SAF performance model does not actually use the lignin in any way. The cost model has lignin consumed, but the performance model doesn't use the inputs['lignin_in'] commodity stream. This should be changed so that the actual lignin produced by the paper mill is used by the SAF plant, or such that the lignin used in the SAF plant cannot exceed the inputted lignin profile. Right now, these two systems are not actually coupled because the lignin profile is not used.

Other thoughts on my mind as I went through this. I don't think we necessarily need to change these for this PR, but here's what I'm thinking:

  • because the paper mill and SAF plant operate in steady state and assume constant production, they're relatively simplistic models in an H2I sense. If they varied hourly, we'd want to expose more variables to the OpenMDAO framework instead of just using the config file inputs directly. A specific example would be the electricity input profile being an input.
  • beyond that, both the paper mill and SAF models don't have variables exposed to the OpenMDAO framework. This means that you couldn't do parameter sweeps or optimizations with these models very easily. I've made two variables into inputs to show how this would be done; if you think more variables should be exposed in this way to get more parameter sweep capabilities, we should add them as part of this PR

Overall, fantastic first big contribution to H2I! All my comments and suggestions are just in support of getting some of the code more in line with other H2I technology models so others can use and enjoy them. I'm happy to chat or work through any of this 1-on-1 with you, just let me know!

Comment on lines +90 to +91
file_path = "Breakdown_costs_per_scenario.xlsx"
df = pd.read_excel(file_path, sheet_name="Sheet1", header=None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It looks like this file depends on an Excel sheet, but this isn't included as part of this PR. Do you mean to add the sheet, or have a different script that produces it? Or maybe just remove this file entirely?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Just removing it entirely would be fine. The excel was added to help me do the plots for the E2C NE MN project.

Comment on lines +1 to +11
# # -*- coding: utf-8 -*-
# """
# Created on Fri May 15 07:38:06 2026

# @author: mkoleva
# """

# import pandas as pd
# import matplotlib.pyplot as plt

# # Load Excel file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If keeping this file, could you please add a top-level docstring explanation about what it does (i.e. what's plotted, what needs to be run beforehand)?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

As we are removing the excel file, I would suggest we remove this one, too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is the purpose of this file? Is it to test the ProFAST calculations for the paper mill, or something else? I'd suggest removing this or moving the file to a test folder and renaming the file to make it clear it's a test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The file doesn't play a role in the analysis. I will remove it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've refactored this to use existing postprocessing utilities in H2I; the SQL to .csv timeseries and summary info. I'd prefer to not have one-off postprocessing methods in the example scripts, but if you need this specific capability we can add it in a general way. Take a look at the outputted files using the built-in methods and see if it accomplishes your goal! I've also added a top-level docstring to this run script.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you!

Comment on lines +70 to +77
outputs["lignin_out"] = 0.06 * 1000 * paper_mill_production_mtpy / 8760 # tons per hour
outputs["rated_lignin_production"] = 0.06 * 1000 * self.config.plant_capacity_mtpy / 8760
outputs["total_lignin_produced"] = outputs["lignin_out"].sum()
outputs["annual_lignin_produced"] = outputs["total_lignin_produced"] * (
1 / self.fraction_of_year_simulated
)

outputs["pulp_out"] = 1.1 * paper_mill_production_mtpy / 8760

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would these multipliers, e.g. 0.06 and 1.1 ever change? As they're hardcoded currently, a user couldn't modify them without modifying the source code. I'd suggest exposing them as a config level input.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I agree with this! If you do put these in the configuration class, it could be nice to link to the reference for these numbers in the doc string!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

These hard-coded numbers shouldn't change with this process. Nevertheless, I agree - it is a good idea to have them on a config level

Comment on lines +178 to +185
# TODO: Need to update labor cost
labor_cost_annual_operation = (
69375996.9
* ((self.config.plant_capacity_mtpy / 365 * 1000) ** 0.25242)
/ ((1162077 / 365 * 1000) ** 0.25242)
)
labor_cost_maintenance = 0.00863 * total_plant_capex
0.25 * (labor_cost_annual_operation + labor_cost_maintenance)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This labor cost calculation appears to be incomplete; L185 is a calculation that isn't assigned to any variable. Perhaps this is part of your TODO note, but could you revisit this and update it as needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Certainly.

Comment on lines +193 to +212
# Sum total consumables costs and consumption
c = self.config
consumable_costs_per_mt = {
"raw_water": c.raw_water_consumption * c.raw_water_unitcost,
"wood": c.wood_consumption * (c.wood_unitcost + c.wood_transport_cost),
"calcium_carbonate": c.calcium_carbonate_consumption
* (c.calcium_carbonate_unitcost + c.calcium_carbonate_transport_cost),
"sodium_sulfide": c.sodium_sulfide_consumption
* (c.sodium_sulfide_unitcost + c.sodium_sulfide_transport_cost),
"sodium_hydroxide": c.sodium_hydroxide_consumption
* (c.sodium_hydroxide_unitcost + c.sodium_hydroxide_transport_cost),
"chlorine_dioxide": c.chlorine_dioxide_consumption
* (c.chlorine_dioxide_unitcost + c.chlorine_dioxide_transport_cost),
"hydrogen_peroxide": c.hydrogen_peroxide_consumption
* (c.hydrogen_peroxide_unitcost + c.hydrogen_peroxide_transport_cost),
"magnesium_sulfate": c.magnesium_sulfate_consumption
* (c.magnesium_sulfate_unitcost + c.magnesium_sulfate_transport_cost),
"oxygen": c.oxygen_consumption * (c.oxygen_unitcost + c.oxygen_transport_cost),
}
variable_consumables_cost = c.plant_capacity_mtpy * sum(consumable_costs_per_mt.values())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've refactored this so hopefully it's a bit more readable and extensible

hydrogen_chloride_consumption: float = field(default=1.5) # kg/MT product
electricity_consumption: float = field(default=19750) # kWh/tonne product
water_disposal_unitcost: float = field(default=0.002013) # $/kg
water_disposal_rate: float = field(default=0) # TODO: Change assumption

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Address TODO as part of this PR?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This would require some information digging so, I would suggest it is a future TODO, not a part of this PR.

@elenya-grant elenya-grant left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi Masha! So excited to see this PR and your contribution! I know it's one of your first PRs into the new H2I framework - so I was a bit overly verbose with my comments! Also - some of my comments may be in weird places since John pushed changes while I was reviewing it! I don't know much about these technologies - so some of my questions are just me trying to understand these techs. I mostly just reviewed the SAF and paper mill models so far (I haven't reviewed the example yet) - but I will do another review of the whole thing!

I think I'm curious on the separation between the performance model and the cost model. It seems like the cost models account for the feedstock usage but that wouldn't impact performance? That is OK - just curious about it!

I also think that some very basic unit or regression tests of the paper mill and SAF models would be nice to have. These tests would live in a file like h2integrate/converters/paper_mill/test/test_paper_mill.py and h2integrate/converters/saf/test/test_saf.py. I think it'd be good to test the performance and cost models separately, and perhaps also test them together! This would just help to ensure that any future changes to these models would be trackable through the tests and subtests.

Please let me know if you have any questions or want to discuss any of my comments! I agree with a lot of the suggestions from @johnjasa (and some of my comments may be repetitive of his - sorry!)

self.add_input("magnesium_sulfate_in", val=0, shape=n_timesteps, units="kg/h")
self.add_input("oxygen_in", val=0, shape=n_timesteps, units="kg/h")
self.add_output("lignin_out", val=0.0, shape=n_timesteps, units="kg/h")
self.add_output("rated_lignin_production", shape=n_timesteps, val=0.0, units="kg/h")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it seems like the feedstocks for the paper mill are wood, water, calcium carbonate, sodium sulfide, chlorine dioxide, hydrogen peroxide, magnesium sulfate, and oxygen. Could you also add outputs for the amount of these consumed? Like water_consumed, calcium_carbonate_consumed, etc? This would make the performance model compatible with the feedstock models and could perhaps be used with other finance models!

Also! Is lignin the same as paper?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Absolutely, will add the outputs and the amounts.

Lignin and paper are different in the sense that lignin could be considered by-product that is cellulose rich and is typically removed so that paper doesn't turn yellow and break down.

Comment on lines +33 to +35
self.add_output("rated_pulp_out_production", val=0.0, units="t/h")
self.add_output("total_pulp_out_produced", val=0.0, units="t")
self.add_output("annual_pulp_out_produced", val=0.0, units="t/year")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To align with the naming convention in the PerformanceModelBaseClass - I think these outputs should be rated_pulp_production, total_pulp_produced and annual_pulp_produced

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fair point.

additional_cls_name=self.__class__.__name__,
)

self.add_output("pulp_out", val=0.0, shape=n_timesteps, units="t/h")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would suggest adding an input for the rated capacity, this would allow for the paper mill capacity to be an optimization variable:

self.add_input("paper_mill_capacity", val=self.config.plant_capacity_mtpy, shape=1, units="t/y")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great suggestion!

self.add_output("annual_pulp_out_produced", val=0.0, units="t/year")

def compute(self, inputs, outputs):
paper_mill_production_mtpy = self.config.plant_capacity_mtpy * self.config.capacity_factor

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

would the paper mill production be limited by the feedstocks? I'm curious why the performance model baseclass has inputs for things like oxygen, hydrogen peroxide, etc, but those aren't used in these calculations?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The baseclass has been removed.

1 / self.fraction_of_year_simulated
)

outputs["lignin_out"] = 0.06 * 1000 * paper_mill_production_mtpy / 8760 # tons per hour

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't know much about paper mills - do they produce pulp, paper, and lignin?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That's correct.

Comment on lines +95 to +106
wood_consumption: float = field(default=0.225) # MT/MT product
raw_water_consumption: float = field(default=40693) # kg/tonne product

calcium_carbonate_consumption: float = field(default=240) # kg/MT product
sodium_sulfide_consumption: float = field(default=16.5) # kg/MT product
sodium_hydroxide_consumption: float = field(default=3.5) # kg/MT product
chlorine_dioxide_consumption: float = field(default=2) # kg/MT product
hydrogen_peroxide_consumption: float = field(default=0.5) # kg/MT product
magnesium_sulfate_consumption: float = field(default=0.2) # kg/MT product
oxygen_consumption: float = field(default=2.25) # kg/MT product

electricity_consumption: float = field(default=68.7) # kWh/tonne product

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should these consumption ratios be parameters in the PaperMillPerformanceModelConfig?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Will add them. Thank you!

Comment on lines +70 to +77
outputs["lignin_out"] = 0.06 * 1000 * paper_mill_production_mtpy / 8760 # tons per hour
outputs["rated_lignin_production"] = 0.06 * 1000 * self.config.plant_capacity_mtpy / 8760
outputs["total_lignin_produced"] = outputs["lignin_out"].sum()
outputs["annual_lignin_produced"] = outputs["total_lignin_produced"] * (
1 / self.fraction_of_year_simulated
)

outputs["pulp_out"] = 1.1 * paper_mill_production_mtpy / 8760

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I agree with this! If you do put these in the configuration class, it could be nice to link to the reference for these numbers in the doc string!


def initialize(self):
super().initialize()
self.commodity = "saf"

Copy link
Copy Markdown
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 the _control_classifier attribute to the SAFPerformanceModel - in the same place that _time_step_bounds is set? I'm not sure what this falls under (it's not dispatchable because demand is not an input, maybe it's fixed)? Any thoughts on what type of _control_classifier this should be @johnjasa

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sure!

Computes annual paper production based on plant capacity and capacity factor.
"""

_time_step_bounds = (

Copy link
Copy Markdown
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 the _control_classifier attribute to the PaperMillPerformanceModel - in the same place that _time_step_bounds is set? I'm not sure what this falls under (it's not dispatchable because demand is not an input, maybe it's fixed)? Any thoughts on what type of _control_classifier this should be @johnjasa


self.add_input("plant_capacity_mtpy", val=self.config.plant_capacity_mtpy, units="t/year")

n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick: after super().setup() is ran, the PerformanceModelBaseClass creates an attribute named n_timesteps (so you could replace n_timesteps with `self.n_timesteps)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That's on me, I reworked these classes. I'll modify it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants