Skip to content

Feature: EIA Natural Gas Price Retrieval for Feedstocks#719

Open
RHammond2 wants to merge 45 commits intoNatLabRockies:developfrom
RHammond2:feature/ng-industrial-price
Open

Feature: EIA Natural Gas Price Retrieval for Feedstocks#719
RHammond2 wants to merge 45 commits intoNatLabRockies:developfrom
RHammond2:feature/ng-industrial-price

Conversation

@RHammond2
Copy link
Copy Markdown
Collaborator

@RHammond2 RHammond2 commented May 1, 2026

Create an EIA Natural Gas API Feedstock Model

The newly created EIANaturalGasFeedstockConfig and EIANaturalGasFeedstockCostModel allow users to directly download or rely on previously downloaded EIA API data for natural gas prices for the US or any of its 50 states, for each of the available price categories. Users can use a single cost year's annual average or monthly results to create an hourly price profile to match the consumption 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

  • 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:

  • The model is functionally complete, but is awaiting any amendments based on reviewer feedback.
  • Run some more local tests to ensure things work as anticipated since the model relies on external data and API keys

Type of Reviewer Feedback Requested (on Draft PR)

Any feedback is welcome from anyone who might be interested in this or impacted by it

Structural feedback: Are there any missing considerations for when this gets scaled to a national level or for prototyping purposes?

Implementation feedback: Do the naming conventions make sense?

Other feedback: Any thoughts on integrating tests relying on local data or API keys? I wasn't sure if we crossed this bridge with H2I yet or not, so held back on it.

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

N/A

Section 5: Impacted Areas of the Software

Section 5.1: New Files

  • h2integrate/feedstocks/eia_ng_pricing.py
    • convert_to_monthly: Converts an annual timeseries to monthly.
    • convert_state_value: Converts the state input to a compliant mapping format.
    • convert_state_to_code: Maps a compliant state name to a 2-letter state code.
    • get_eia_api_key: retrieves the API key from a file or environment variable.
    • EIANaturalGasFeedstockConfig: EIA natural gas price configuration model that will also retrieve the data upon initialization to ensure the price attribute is populated for the cost model.
    • EIANaturalGasFeedstockCostModel: EIA cost model attuned to the specifics of the configuration model.
    • EIANaturalGasFeedstockPerformanceConfig: Same as FeedstockPerformanceConfig, but with fixed units and commodity values.
    • EIANaturalGasFeedstockPerformanceModel: Same as FeedstockPerformanceModel, but uses EIANaturalGasFeedstockPerformanceConfig.
  • h2integrate/feedstocks/test/test_eia_ng_feedstock.py: Adds tests for all the basic functionality, except for any step requiring access to the EIA data (downloaded or API).

Section 5.2: Modified Files

  • h2integrate/core/feedstocks.py: moved to the h2integrate/feedstocks/feedstocks.py, and updated some of the docstrings and inline commentary as I was reviewing it.
  • h2integrate/core/supported_models.py: Adds the new EIA natural gas feedstock cost and performance model.

Section 6: Additional Supporting Information

This addition is geared towards the MPPOC model for a national scale data center modeling project, but has been made broadly applicable by allowing all the pricing data to be accessed.

Neither tests for the execution of the API retrieval nor examples have been added for this addition because it relies on either an API key or downloaded file. Please let me know if we should still provide something more than the documentation for users.

Section 7: Test Results, if applicable

Tests pass.

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

@RHammond2 RHammond2 requested review from cfrontin and johnjasa May 1, 2026 00:38
@RHammond2 RHammond2 added enhancement New feature or request nationwide sweeps Functionality needed to enable nationwide sweeps (potentially ported from NEDsim) labels May 1, 2026
@RHammond2 RHammond2 changed the base branch from main to develop May 1, 2026 15:08
Comment thread h2integrate/feedstocks/feedstocks.py Outdated
desc="Capacity factor",
)
self.add_output(
"replacement_schedule",
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 is an overall comment that's acute here; but I thought of EIA NG almost as a resource, like wind. I think you're right that it's more of a specialized feedstock. but at the same time it comes with some funkiness, like how "replacement schedule" doesn't really make that much sense as a feedstock.

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.

really this is a question for a H2I pro and not me lol

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.

I originally built it as a resource system, but realized there was nowhere to model price, which is what feedstocks are used for. The replacement schedule and other such costs are more for compatibility, so it could be worthwhile that they are set to zero and not allowed to have input values because we're simply connecting to a grid for natural gas. Then again, there may be costs associated with hooking up the power plant to the pipeline, so that could be the actual intent.

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.

@elenya-grant and I are on a call right now, tagging her to chime in here as well as the whole PR!

@RHammond2 RHammond2 marked this pull request as ready for review May 1, 2026 23:23
@RHammond2 RHammond2 added ready for review This PR is ready for input from folks labels May 1, 2026
@bayc bayc self-requested a review May 5, 2026 16:56
@elenya-grant elenya-grant self-requested a review May 5, 2026 19:41
Copy link
Copy Markdown
Collaborator

@bayc bayc left a comment

Choose a reason for hiding this comment

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

Looks good to me, thanks for putting this together! Definitely useful for multiple workstreams right now. As discussed in the standup, my one request was that we can support conversion of lat/long coords to the states so it is not up to the user to change both when doing runs.

Comment on lines +179 to 186
# TODO: Update to the commodity_capacity input of the FeedstockPerformanceModel
# NOTE: Should I set this to rated_capacity if it's available?
self.add_output(
f"rated_{self.config.commodity}_production",
val=0,
units=self.config.commodity_rate_units,
)

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.

@elenya-grant and @johnjasa should this also get updated per the todo/note here and in the EIA feedstock?

Copy link
Copy Markdown
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 Rob! I think this is cool functionality - thanks for adding it in! A few higher-level thoughts:

  1. I wonder if the API download and grabbing data should instead be a pre-processing method (since, as-is, the price data wouldn't change in a site-sweep like what's done in Example 22)
  2. To get the price data to change per site (if lat/lon are being swept) would require the latitude and longitude to be inputs to this method. This would also require a lot of the functionality in the cost config class to be called in the compute() method.
  3. The H2IntegrateModel has some special handling for feedstocks - I think that if we want to make this a feedstock cost model then it should inherit the FeedstockCostModel and the logic within H2I should be updated to check if a model is or inherits the FeedstockCostModel rather than checking that the name of the model is FeedstockCostModel.

I am curious what the main purpose of this feedstock model is perhaps. Is the goal to get data downloaded once OR to make it so that the cost is updated in a sweep? My current suggestion is that someone wants to do a design sweep of site locations (like example 22) and have the natural gas price change per site, that would instead look like:

  • use the EIA API calls and functions that you added here as functions available in the preprocess folder instead. A user can make a run-script that loops through lat/lons and get the natural gas price using those methods/functions. User basically makes a data frame with columns of site.latitude, site.longitude, and natural_gas_feedstock.price.
  • Save the data frame from step 1 as a csv. Run a DOE like what's done in Example 22 (but with natural_gas_feedstock.price as a design variable too). The feedstock model would still be using the FeedstockCostModel.

This suggestion would require no changes to H2IntegrateModel, or feedstocks.py. I think that integrating the feedstocks with the site information should perhaps be targeted in future work since, and I think the above suggestion would work well enough in the meantime.

I'd be curious what folks thoughts are on the proposed approach and I'm sorry to propose such a big change! I honestly hope that my suggestion would be the easiest to get this functionality in sooner rather than later.

Please let me know if you want to talk through it at all! Happy to discuss over a call!



@define(kw_only=True)
class EIANaturalGasFeedstockPerformanceConfig(BaseConfig):
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 think the performance model or the config are necessary - aren't these identical to the FeedstockPerformanceModel?

return df[["price"]]


class EIANaturalGasFeedstockCostModel(CostModelBaseClass):
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.

Suggested change
class EIANaturalGasFeedstockCostModel(CostModelBaseClass):
class EIANaturalGasFeedstockCostModel(FeedstockCostModel):


super().setup()

self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
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 think the rest of this setup() method could be replaced with super().setup() if this class inherits the FeedstockCostModel

units="unitless",
desc="Capacity factor",
)
self.add_output(
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.

why was replacement_schedule added as an output? It shouldn't be 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.

It was already there (https://github.com/NatLabRockies/H2Integrate/tree/develop/h2integrate/core/feedstocks.py#L183), I just moved the inline comment to the output's description.

# Estimate annual consumption based on consumption over the simulation
# NOTE: once we standardize feedstock consumption outputs in models, this should
# be updated to handle consumption that varies over years of operation
# TODO: update to handle varying consumption levels when feedstock consumption is available
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'm curious why this comment was changed? I thin the previous NOTE was a bit more clear to me

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.

I converted from a note to a todo because of "this should be updated". Happy to revert it to its longer form, but I would still strongly argue this is a TODO for when the variable consumption is implemented.

attrs.validators.in_([*STATE_MAP, *STATE_MAP.values()])
),
)
latitude: float = field(default=999.9, validator=attrs.validators.instance_of(float))
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 think latitude and longitude should look like this:

    latitude: float = field(default=0.0, validator=range_val(-90.0, 90.0))
    longitude: float = field(default=0.0, validator=range_val(-180.0, 180.0))

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.

I originally used, this, but needed a value to trigger that no location data was provided. I think that we could simply riff on the original by using:

    latitude: float | None = field(default=None, validator=attrs.validators.optional(range_val(-90.0, 90.0)))
    longitude: float | None = field(default=None, validator=attrs.validators.optional(range_val(-180.0, 180.0)))

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.

yeah! I didn't even know about the optional attrs validators! Those are slick!

commodity: str = field(default="natural_gas", init=False)
commodity_rate_units: str = field(default="MMBtu/h", init=False)
commodity_amount_units: str = field(default="MMBtu", init=False)
filename: str = field(default=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.

I got an AttributeError when the filename wasn't specified because Path(None) doesnt work. I think the default should be an empty 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.

Thanks for flagging, I think I fixed this (among other things) in some commits after you would've pulled this down. I also implemented a bunch of tests around much of the configuration class and functions.



@define
class EIANaturalGasFeedstockConfig(BaseConfig):
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 think this should inherit the FeedstockCostConfig

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.

It originally did, but I need a couple pieces of the FeedstockCostConfig.setup() prior to being able to call setup, otherwise the price data won't be available. One remedy, and could work well for the next comment is to just set a default value, and get the actual data at the end of the setup. This could be ok because the price is automatically converted to monthly anyway for simplified handling of converting it to an 8760.

self.url = self.create_eia_api_url()
self.price = self.get_data()

def create_eia_api_url(self):
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 think create_eia_api_url() and get_data() should be methods in EIANaturalGasFeedstockCostModel instead of the config class.

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.

Similar to the previous comment, I originally did that as well, but the price data was required for determining the annual vs per timestep logic. Similar to the above, I think that if I use a dummy value for the price data prior to retrieval, then this could work even if the logic will be a bit out of order.

units=self.config.commodity_rate_units,
)

def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
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 this class inherits the FeedstockCostModel - then it would only need to have

super().compute(inputs, outputs)

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.

Same commentary as the above couple of responses.

@RHammond2
Copy link
Copy Markdown
Collaborator Author

Thanks for the feedback, @elenya-grant! I think pretty much everything you're saying makes good sense to me. My own concern is that this isn't inherently built around multi-site analyses, so there is still some use in having a single site version be available. Otherwise, I agree that much of the actual API functionality could become preprocessing code that a single site or generic feedstock can use. I've also scheduled a meeting so we can iterate on these ideas a bit more effectively.

@RHammond2
Copy link
Copy Markdown
Collaborator Author

The path forward based on a call with @elenya-grant is to make the following amendments:

  1. Move all the API-specific functionality to a new preprocessing module
  2. Expand the API to allow for downloading of multiple years and sites
  3. Only use a specialized cost model and configuration that inherits the main feedstock model in place of an entirely separate model for each component.

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

Labels

enhancement New feature or request nationwide sweeps Functionality needed to enable nationwide sweeps (potentially ported from NEDsim) ready for review This PR is ready for input from folks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants