Skip to content

Make sure storage_baseclass handles arbitrary dt#753

Merged
johnjasa merged 12 commits into
NatLabRockies:developfrom
vijay092:dt_hr
May 21, 2026
Merged

Make sure storage_baseclass handles arbitrary dt#753
johnjasa merged 12 commits into
NatLabRockies:developfrom
vijay092:dt_hr

Conversation

@vijay092
Copy link
Copy Markdown
Collaborator

Make sure storage_baseclass handles arbitrary dt

storage_baseclass contains several unit inconsistencies when dt is non-hourly. This PR addresses them. This was brought up in #738.

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)

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

  • path/to/file.extension
    • method1: What and why something was changed in one sentence or less.

Section 5.2: Modified Files

  • h2integrate/storage/test/test_storage_performance_model.py
  • h2integrate/storage/storage_baseclass.py

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

@johnjasa johnjasa requested a review from elenya-grant May 19, 2026 14:48
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.

Thanks for this, Sanjana! I have one suggested change where I want your thoughts and input.

Comment on lines 48 to 51
_time_step_bounds = (
3600,
3600,
) # (min, max) time step lengths (in seconds) compatible with this model
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 please update this so it correctly reflects a less restrictive bound on the timestep? Are there any other limitations not addressed in this PR that would affect how valid different timesteps are?

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.

Good point. I'll expand the StoragePerformanceBase._time_step_bounds to reflect that the math now handles any dt. The subclasses (StoragePerformanceModel) still have (3600, 3600). PySAMBatteryPerformanceModel also only supports hourly. @jaredthomas68 @elenya-grant could you also weigh in here?

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.

Here's the bounds I think each storage model should have:

  • StorageAutoSizingModel: (3600, 3600) - should not be changed until its tested
  • StoragePerformanceModel: (1, 3600)
  • PySAMBatteryPerformanceModel: (3600, 3600) - should not be changed until its tested
  • StoragePerformanceBase: this could have (3600, 3600) or (1, 3600) since it's overwritten by the subclasses.

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.

For StoragePerformanceBase, I changed the bounds to (1,36000) to add the test Elenya suggested (dt > 1hr).

Comment on lines +1230 to +1245
def test_storage_half_hourly_known_outputs(subtests):
"""Verify SOC, charge/discharge profiles, and scalar outputs against calculated
values for a simple scenario at dt=1800s (30-min dt).

Scenario (4 timesteps * 30 min = 2 hours total):
t0, t1: charge at 10 kg/h - stores 5 kg each step
t2, t3: discharge at 10 kg/h — removes 5 kg each step

With capacity=40 kg, init_soc=0.1, eff=1.0, min_soc=0.1, max_soc=1.0:
SOC[0] = 0.1 + 5/40 = 0.225
SOC[1] = 0.225 + 5/40 = 0.35
SOC[2] = 0.35 - 5/40 = 0.225
SOC[3] = 0.225 - 5/40 = 0.1
total_hydrogen_produced = (-10 - 10 + 10 + 10) * 0.5 hr = 0 kg
standard_capacity_factor = (10+10)*0.5 / (10 * 4 * 0.5) = 10/20 = 0.5 -> 50 %
"""
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.

Fantastic and clear test! Thank you for this.

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.

I left a few small suggestions but overall this looks good to me! Your approach is very clear and the math makes sense to me! Thanks for adding this feature!

I think some nice-to-haves (but not necessary) would be:

  • test using kg/s as the commodity rate units but using a timestep of 30 min
  • test using kW and kW*h
  • perhaps a test for dt>3600 (only if the upper limit on _time_step_bounds is changed from 3600)

headroom = (soc_max - soc) * storage_capacity / self.dt_amount

# charge available based on the available input commodity
charge_available = commodity_available[sim_start_index + t]
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.

Just checking here - we expect that the headroom, charge_available, the input storage_dispatch_commands, etc are in 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.

Correct!

Comment on lines +1229 to +1230
@pytest.mark.regression
def test_storage_half_hourly_known_outputs(subtests):
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
@pytest.mark.regression
def test_storage_half_hourly_known_outputs(subtests):
@pytest.mark.regression
@pytest.mark.parametrize("n_timesteps", [4])
def test_storage_half_hourly_known_outputs(subtests, plant_config_non_hourly):

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.

Ok done!

total_hydrogen_produced = (-10 - 10 + 10 + 10) * 0.5 hr = 0 kg
standard_capacity_factor = (10+10)*0.5 / (10 * 4 * 0.5) = 10/20 = 0.5 -> 50 %
"""
plant_config = {
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 you take the suggestion I had above, you won't need to re-define the plant config here. You could just use plant_config_non_hourly instead.

Comment on lines 48 to 51
_time_step_bounds = (
3600,
3600,
) # (min, max) time step lengths (in seconds) compatible with this model
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.

Here's the bounds I think each storage model should have:

  • StorageAutoSizingModel: (3600, 3600) - should not be changed until its tested
  • StoragePerformanceModel: (1, 3600)
  • PySAMBatteryPerformanceModel: (3600, 3600) - should not be changed until its tested
  • StoragePerformanceBase: this could have (3600, 3600) or (1, 3600) since it's overwritten by the subclasses.

@vijay092
Copy link
Copy Markdown
Collaborator Author

I left a few small suggestions but overall this looks good to me! Your approach is very clear and the math makes sense to me! Thanks for adding this feature!

I think some nice-to-haves (but not necessary) would be:

  • test using kg/s as the commodity rate units but using a timestep of 30 min
  • test using kW and kW*h
  • perhaps a test for dt>3600 (only if the upper limit on _time_step_bounds is changed from 3600)

Added additional tests!

@johnjasa johnjasa marked this pull request as ready for review May 21, 2026 19:42
Comment on lines +194 to +198
self.dt_amount = om_units.convert_units(
self.dt,
"s",
f"({self.commodity_amount_units})/({self.commodity_rate_units})",
)
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.

Great use of the built-in units conversion here!

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.

Thanks for these changes and improvements, Sanjana! I think this is a great step towards a much more flexible H2I codebase. The storage models have quite a few intricacies, so I appreciate you digging into the models and improving them.

@johnjasa johnjasa enabled auto-merge (squash) May 21, 2026 22:21
@johnjasa johnjasa merged commit b9f33f6 into NatLabRockies:develop May 21, 2026
12 checks passed
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.

3 participants