Skip to content

Framework to allow for variable dt#653

Open
jaredthomas68 wants to merge 25 commits intoNatLabRockies:developfrom
jaredthomas68:variabledt
Open

Framework to allow for variable dt#653
jaredthomas68 wants to merge 25 commits intoNatLabRockies:developfrom
jaredthomas68:variabledt

Conversation

@jaredthomas68
Copy link
Copy Markdown
Collaborator

@jaredthomas68 jaredthomas68 commented Apr 3, 2026

Framework to allow for variable dt

This PR contains changes to the framework to handle variable dt between different models so we can start integrating model changes one at a time to allow variable time steps.

I also adjusted the grid components to work with variable dt to demonstrate how the framework changes will work with actual models and test the integration.

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
  • Step 2

Type of Reviewer Feedback Requested (on Draft PR)

Ready for full review, I'm looking for any and all 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
    • 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 3: Related Issues

This PR will resolve #648 and makes steps towards resolving #204.

The work on the grid components will also resolve #638.

Section 4: Impacted Areas of the Software

Section 4.1: New Files

Section 4.2: Modified Files

  • docs/developer_guide/adding_a_new_technology.md: variable time step info added
  • docs/intro.md: variable time step info added
  • docs/technology_models/grid.md: variable time step info added
  • h2integrate/converters/grid/grid.py: add 5 m to 1 h time step bounds
  • h2integrate/converters/grid/test/test_grid.py: unit and integration test for non-hour time step
  • h2integrate/core/h2integrate_model.py: check time step on a per-model basis
  • h2integrate/core/inputs/validation.py: make simulation length check account for variable dt and remove dt check
  • h2integrate/core/model_baseclasses.py: add self.dt to cost model base class
  • h2integrate/core/test/test_framework.py: add unit tests for time step check

Section 5: Additional Supporting Information

This PR is just the first step in a series of expected PRs to allow more flexibility in time step selection.

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

@jaredthomas68 jaredthomas68 changed the base branch from main to develop April 3, 2026 23:57
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.

Thanks for introducing this PR! I had a couple of high-level suggestions/questions for other folks. I think this is the cleanest and least complicated way of introducing this flexibility - nice job!

Kinda big-picture consideration - should there be a valid time-step interval defined too? Like - for the grid, what if I tried to run a timestep of 563 seconds? Maybe thats fine but it seems weird. Should there be another parameter defined as like _time_step_interval? So that models can be run for timesteps:

valid_dts = np.arange(_time_step_min, _time_step_max, _time_step_interval)

(not asking for this to be implemented, but a question I think it worth considering)

I know this is still a draft, but I would like to see a test for the grid component using a smaller timestep.


_time_step_min = 300
_time_step_max = 3600

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 like how you've added these attributes. I could see a similar thing perhaps being implemented in the future for simulation length. I'd be curious what your (and other people's thoughts) would be on whether these should be two separate variables (like you have) vs one tuple like this instead:

_time_step_bounds = (300, 3600)

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.

Another question - why can't this have a min timestep of 1 sec? Why 300?

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 making the min 300 seconds for now makes a lot of sense, since that's the smallest resource step that is available and also the smallest grid price data resolution. I think that letting the time step be 1 second is not realistic for the steady state models we have in H2I. This is something I feel pretty strongly about, but what are others' opinions about this?

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 @elenya-grant. Also, I like the idea of using a tuple.

I agree with @genevievestarke on the minimum time step, at least for now. The 5-minute resource data floor, and the fact we are not modeling any transient behavior, is why I choose to limit the grid to 5-minute minimum time steps.

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 think the only reason to not use timestep bounds would be so one or the other could be left out. Realistically, I think there will always be upper and lower limits, so I think it will be fine to use a tuple.

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 switched to using a tuple

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 wonder if in the more general implementation for the framework--like in h2integrate_model.py and the utilities files that house dt methods (not the already include converter/storage models)--we can allow dt to be less than 300 because I don't see a reason we couldn't theoretically allow users to put a signal of their choice in at a 1 second time resolution and use the other h2i capabilities such as the financial modeling with finer resolution performance data.

)
raise ValueError(msg)

elif dt != 3600:
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 an alternative to this elif catch would be to add the _time_step_min and time_step_max to the CostModelBaseClass and PerformanceModelBaseClass and have them both default to 3600. I'm not saying this is better - but its an alternative approach.

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 started doing this, but it seems like a lot of changes to accomplish a small thing. I think I'll stick with the elif for now.

Copy link
Copy Markdown
Collaborator

@kbrunik kbrunik Apr 10, 2026

Choose a reason for hiding this comment

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

I think my prefer approach would be to add _time_step_min and _time_step_max to every model and it's never inherited from the Performance and Cost base classes. Even for models that can only accept dt=3600.

Then we can remove the elif dt !=3600 because this will be caught in the above if statement.

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 we have a few different ways to essentially achieve the same checking capability here, thanks for the discussion. @kbrunik, I like your idea because of how explicit it is and how it will make it very clear what time steps each model accepts. I think this will make future updates to models as we expand out flexible dt values more trackable. I also think it will encourage (force) folks making new models to include _time_step_bounds, which will be useful to define at model creation time.

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 done with those changes and welcome input from @jaredthomas68 and @elenya-grant! Are you okay with this? What would you change?

def _check_time_step(self, model_name, model_object):
dt = int(self.plant_config["plant"]["simulation"]["dt"])

if hasattr(model_object, "_time_step_min") or hasattr(model_object, "_time_step_max"):
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.

for some reason I'm wanting to suggest getattr instead (since you can provide defaults if it doesnt have the attribute) but I think that'd make the logic more sloppy. So - I like how you have it.

Copy link
Copy Markdown
Collaborator

@johnjasa johnjasa left a comment

Choose a reason for hiding this comment

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

I like this PR! It is simple and helpful. I'd love to see your answers to @elenya-grant's questions as I had some similar thoughts.

For future work, as we build out more of the timestep handling and methods related to that, I could imagine a separate utilities file may be useful to reduce the size of h2integrate_model.py. I think as you work through other changes needed for this timestep capability, keep that in mind!

Copy link
Copy Markdown
Collaborator

@genevievestarke genevievestarke left a comment

Choose a reason for hiding this comment

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

Thank you for kicking this off, @jaredthomas68!! Just a few thoughts!


_time_step_min = 300
_time_step_max = 3600

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 making the min 300 seconds for now makes a lot of sense, since that's the smallest resource step that is available and also the smallest grid price data resolution. I think that letting the time step be 1 second is not realistic for the steady state models we have in H2I. This is something I feel pretty strongly about, but what are others' opinions about this?

def _check_time_step(self, model_name, model_object):
dt = int(self.plant_config["plant"]["simulation"]["dt"])

if hasattr(model_object, "_time_step_min") or hasattr(model_object, "_time_step_max"):
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 a little bit confused by this part. The if/else logic seems to use both values, but you're only checking for one or the other? Can a model just have one defined (i.e. only _time_step_max = 3600, and then any large step is fine)?

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 see the confusion. I switched to using a tuple as suggested by @elenya-grant, which solves the odd logic. I also think that having an upper and lower bound is important, even if they are wildly different (e.g. 300 s, 1 week), just to make sure the behavior is reasonable.

@jaredthomas68
Copy link
Copy Markdown
Collaborator Author

jaredthomas68 commented Apr 10, 2026

Thanks for introducing this PR! I had a couple of high-level suggestions/questions for other folks. I think this is the cleanest and least complicated way of introducing this flexibility - nice job!

Kinda big-picture consideration - should there be a valid time-step interval defined too? Like - for the grid, what if I tried to run a timestep of 563 seconds? Maybe thats fine but it seems weird. Should there be another parameter defined as like _time_step_interval? So that models can be run for timesteps:

valid_dts = np.arange(_time_step_min, _time_step_max, _time_step_interval)

(not asking for this to be implemented, but a question I think it worth considering)

I know this is still a draft, but I would like to see a test for the grid component using a smaller timestep.

Thanks for the review and questions. I think I would like to not include valid time step interval yet. We have essentially baked in a 1s increment. I think 563 s would be an odd time step, but there is nothing inherently wrong with it. If we do run into models that need to have an interval defined, it would not be difficult to include that capability at that point.

I have also added a unit test and an integration test for the variable dt grid component

Copy link
Copy Markdown
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.

I like where this is headed! Thanks for a functional draft and the quick testing of the added capabilities. I think we will need a lot of rigorous testing around the cost modeling and financial modeling to ensure we are handling everything correctly (especially with feedstocks).

@jaredthomas68
Copy link
Copy Markdown
Collaborator Author

I like where this is headed! Thanks for a functional draft and the quick testing of the added capabilities. I think we will need a lot of rigorous testing around the cost modeling and financial modeling to ensure we are handling everything correctly (especially with feedstocks).

Thank you @kbrunik, I don't think the cost and financial modeling will need much work because they use annual commodity amounts and capacity factor rather than the actual time-series. I looked into them but I don't think they need any changes.

@jaredthomas68 jaredthomas68 marked this pull request as ready for review April 11, 2026 00:02
@jaredthomas68 jaredthomas68 requested a review from Copilot April 11, 2026 00:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds infrastructure to support non-hourly simulation time steps by validating dt compatibility per model via _time_step_bounds, and updates the grid models/docs/tests to demonstrate variable-dt behavior.

Changes:

  • Adds per-model timestep compatibility checks in H2IntegrateModel using _time_step_bounds.
  • Updates plant-config validation to allow variable dt as long as the simulated duration equals one year.
  • Extends grid performance/cost models and tests to handle non-hourly dt, plus documentation updates.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
h2integrate/core/test/test_framework.py Adds unit tests covering the new per-model dt compatibility checks.
h2integrate/core/model_baseclasses.py Stores dt on cost model base classes for use in variable-timestep cost calculations.
h2integrate/core/inputs/validation.py Allows variable dt by validating n_timesteps * dt == 1 year (seconds).
h2integrate/core/h2integrate_model.py Introduces _check_time_step() and invokes it during model processing.
h2integrate/converters/grid/test/test_grid.py Adds unit + integration tests exercising grid behavior at non-hourly dt.
h2integrate/converters/grid/grid.py Declares timestep bounds for grid models and scales VarOpEx by dt/3600.
docs/technology_models/grid.md Documents grid component timestep compatibility (5-min to 1-hour).
docs/intro.md Notes that some models can now run at non-hourly time steps via bounds.
docs/developer_guide/adding_a_new_technology.md Documents how model authors should specify _time_step_bounds.
CHANGELOG.md Records the variable-dt infrastructure and grid bounds addition.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 596 to 604
# Generalized function to process model definitions
model_name = individual_tech_config[model_type]["model"]
model_object = self.supported_models[model_name]

self._check_time_step(model_name, model_object)

om_model_object = tech_group.add_subsystem(
model_name,
model_object(
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

_check_time_step() is only invoked through _process_model(). In create_technology_models() there are other code paths that instantiate models without calling _process_model() (e.g., the combined perf/cost model branch and the feedstock perf/cost models). Those models currently bypass the dt compatibility enforcement, which breaks the PR’s guarantee that all models in a simulation are compatible with the configured timestep. Recommendation (mandatory): ensure _check_time_step() is also called in those direct-instantiation branches, or refactor those branches to go through a single creation helper that always performs the check.

Copilot uses AI. Check for mistakes.
Comment on lines +143 to 152
n_timesteps = plant_config["plant"]["simulation"]["n_timesteps"]
dt = plant_config["plant"]["simulation"]["dt"]

if int(n_timesteps * dt) != 31536000: # seconds in simulation must be seconds/year
msg = (
"H2Integrate does not currently support simulations that are less than or "
"greater than 1-year. Please ensure that "
"plant_config['plant']['simulation']['n_timesteps'] is set to 8760."
)
raise ValueError(msg)
if int(plant_config["plant"]["simulation"]["dt"]) != 3600:
msg = (
"H2Integrate does not currently support simulations with a time step that is "
"less than or greater than 1-hour. Please ensure that "
"plant_config['plant']['simulation']['dt'] is set to 3600."
"plant_config['plant']['simulation']['n_timesteps'] times "
"plant_config['plant']['simulation']['dt'] equals 31536000 (s)."
)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The validation uses int(n_timesteps * dt), which truncates (floors) non-integer products and can incorrectly accept invalid configurations (or reject valid ones) if dt is not an integer type (e.g., 1800.5) or if n_timesteps * dt is a float with rounding error. Recommendation (mandatory): coerce n_timesteps and dt to integers first (or use exact equality on integers) and avoid truncation (e.g., compare int(n_timesteps) * int(dt) to the target, or use round(...) with an explicit tolerance if floats are truly allowed).

Copilot uses AI. Check for mistakes.
)

# dt is seconds per timestep
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

dt is used arithmetically downstream (e.g., self.dt / 3600). To keep behavior consistent across configs and avoid accidental string/float surprises, it’s safer to normalize the stored value here. Recommendation (optional): cast to an int (or float if fractional seconds are intended) and consider naming it dt_seconds to make the unit unambiguous.

Suggested change
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
self.dt = float(self.options["plant_config"]["plant"]["simulation"]["dt"])

Copilot uses AI. Check for mistakes.
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.

Make non-hourly dt error model specific

6 participants