Skip to content

Standardized Performance Outputs: Generic Storage & Control#493

Open
elenya-grant wants to merge 75 commits intoNatLabRockies:developfrom
elenya-grant:converter_baseclass_storage
Open

Standardized Performance Outputs: Generic Storage & Control#493
elenya-grant wants to merge 75 commits intoNatLabRockies:developfrom
elenya-grant:converter_baseclass_storage

Conversation

@elenya-grant
Copy link
Collaborator

@elenya-grant elenya-grant commented Feb 3, 2026

Standardized Performance Outputs: Generic Storage & Control

This PR is a follow-on to PR #463 and should not be merged in until after #463 is.

This PR updates the generic storage performance models and the control strategies so storage models have the same outputs as converters. There are 3 storage performance models:

  • h2integrate/storage/battery/pysam_battery.py: This was updated in PR Standardize performance model outputs #463
  • h2integrate/storage/simple_generic_storage.py: basically did nothing
  • h2integrate/storage/simple_storage_auto_sizing.py: calculates storage capacity and charge rate needed to meet a demand.

To be consistent with the pysam_battery model, the generic storage performance models needed to output the standardized outputs (like capacity factor, rated production, etc) but these generic performance models did very minimal calculations and did not output commodity_out, most calculations were done in the controllers, not the performance models.

Basically, the control strategies have been updated to output commodity_set_point instead of commodity_out. commodity_set_point is what the controller tells the performance model is the "target" commodity_out. Since the performance model may not be able to achieve the commodity_set_point (due to a variety of possible differences in the control model and the performance model, like losses), the performance model output commodity_out may not equal the commodity_set_point. The commodity_set_point is the input to the storage performance models, which now output commodity_out, annual_commodity_production, capacity_factor, etc. The computations now done in some of the storage performance models may be somewhat duplicative of the performance of some controllers, but this ensures that the storage controllers are compatible with any storage performance 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:

These TODOs are only if PR #407 gets merged in before this one is ready. Otherwise, a separate PR will include these TODOs after #407 is merged.

  • Change commodity_name to commodity for the generic storage performance models and control strategies
  • Change commodity_units to commodity_rate_units for the generic storage performance models and control strategies

Type of Reviewer Feedback Requested (on Draft PR)

Structural feedback:

Implementation feedback:

  • Thoughts on naming convention, specifically the use of commodity_set_point?
    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

This partially resolves some of Issue #486, and would resolve a step in Issue #485

New issue made: #498

Section 4: Impacted Areas of the Software

Section 4.1: New Files

N/A

Section 4.2: Modified Files

  • h2integrate/storage/simple_generic_storage.py: added commodity_set_point as an input, updated compute() method to better reflect performance of a "pass through" type storage component (this storage model has no inputs related to capacity, which is why the compute() method basically now acts like a pass-through component)
  • h2integrate/storage/simple_storage_auto_sizing.py: added commodity_set_point as an input, updated compute() method to better reflect simple performance of a storage component
  • h2integrate/control/control_strategies/demand_openloop_controller.py: changed output variable name commodity_out to commodity_set_point
  • h2integrate/control/control_strategies/passthrough_openloop_controller.py: changed output variable name commodity_out to commodity_set_point
  • h2integrate/postprocess/test/test_sql_timeseries_to_csv.py: updated subtest value, see justification in Section 6
  • pytest examples/test/test_all_examples.py::test_ammonia_synloop_example: added subtest for annual ammonia production and updated 3 subtest values that changed, see justification in Section 6.

Section 5: Additional Supporting Information

Section 6: Test Results, if applicable

  • h2integrate/postprocess/test/test_sql_timeseries_to_csv.py::test_save_csv_all_results: subtest for number of columns increased from 35 to 36 because of the new output commodity_set_point.
  • pytest examples/test/test_all_examples.py::test_ammonia_synloop_example: subtests for ammonia OpEx, total adjusted OpEx for ammonia finance subgroup, and the LCOA failed, the test values decreased.
    • The OpEx values decreased because the variable OpEx portion of the OpEx in the ammonia synloop decreased. These costs are multipliers of the annual ammonia production. The variable opex is equal to cat_opex + h2o_opex - o2_opex, all of these are multipliers of the annual ammonia produced. All these opex values increased, but the o2_opex increased much more than the increase in cat_opex and h2o_opex combined, meaning a net reduction in OpEx.
    • The annual ammonia production increased from 364,095 t/year to 406,333 t/year.
    • The max_hydrogen_capacity of the ammonia_synloop model is 10589.36 kg/h, this value has not changed.
    • The maximum hydrogen_in to the ammonia_synloop model decreased from 12549 kg/h to 9306 kg/h. This indicates that now more hydrogen is being used to make ammonia rather than being "curtailed". The total hydrogen consumed is the same as before (meaning the total hydrogen_in is the same), but the hydrogen_out of the ammonia model decreased (likely because more hydrogen is being used)
    • Previously, the limiting factor on ammonia production was primarily the ammonia plant capacity
      • capacity limited: 4863 hrs
      • hydrogen limited: 3716 hrs
      • nitrogen limited: 181
    • Now, the limiting factor on ammonia production is always hydrogen. This is because the hydrogen output is more constant because it comes from the hydrogen storage output rather than the pass-through controller output. The pass through controller hydrogen set point is equal to the hydrogen produced from the electrolyzer. Aka - beforehand the hydrogen into the ammonia model was equal to the hydrogen out of the electrolyzer. Now that the hydrogen storage auto-sizing performance model actually represents the smoothing effect that the hydrogen storage has on the hydrogen production profile, the hydrogen into the ammonia synloop has less variability and is more constant, making hydrogen the limiting factor more often which means more hydrogen is consumed because when hydrogen is not the limiting factor, hydrogen is curtailed.
    • The test values would not likely have changed if we used the "generic_storage_model" as the performance model since this acts like the pass-through controller. This would make the hydrogen into the ammonia model the same as before (equal to the hydrogen out of the electrolyzer model). But - we would have to set the storage capacity and charge rate so that the hydrogen storage cost is the same as before.
    • Overall, the LCOA decreased because the OpEx costs decreased and the ammonia production increased.

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

elenya-grant and others added 30 commits January 20, 2026 15:27
@elenya-grant elenya-grant added the ready for review This PR is ready for input from folks label Feb 3, 2026
@elenya-grant elenya-grant marked this pull request as ready for review February 3, 2026 19:08
@elenya-grant elenya-grant requested a review from johnjasa February 3, 2026 19:09
Copy link
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, Elenya! I think it's a great step in standardizing some of these storage and control models -- there's a lot of complex interactions here.

I've left some comments and pushed up minor changes. The comments are mostly questions to provoke introspection into the approaches used here or minor suggestions for clarity, nothing too major.

Comment on lines +43 to +44
# Estimate the rated commodity production as the maximum value from the commodity_set_point
outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_set_point"].max()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this the best way of getting the rated production? If the set point never reaches the actual maximum of the system, is this a problem and could lead to confusing metrics? Legitimately asking, mostly about the generality of this and if it works well in all cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Idk a better way - but this is just a pass-through component. In this case, it'd be better for this to be done in the DemandOpenLoopStorageController instead, but then we wouldn't be consistent about whether these outputs are standardized in performance models vs control strategies which would lead to errors if someone used a controller with the standard outputs with a storage model that has the standard outputs. I think issue #498 explains this though.

Comment on lines +46 to +57
# Calculate the total and annual commodity produced
outputs[f"total_{self.commodity}_produced"] = outputs[f"{self.commodity}_out"].sum()
outputs[f"annual_{self.commodity}_produced"] = outputs[
f"total_{self.commodity}_produced"
] * (1 / self.fraction_of_year_simulated)

# Calculate the maximum commodity production over the simulation
max_production = (
inputs[f"{self.commodity}_set_point"].max() * self.n_timesteps * (self.dt / 3600)
)

outputs["capacity_factor"] = outputs[f"total_{self.commodity}_produced"] / max_production
Copy link
Collaborator

Choose a reason for hiding this comment

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

Tagging @jaredthomas68 and @genevievestarke for special attention to this math to make sure it matches what you expect.


commodity_production = inputs[f"{commodity_name}_in"]
# The commodity_set_point is the production set by the controller
commodity_production = inputs[f"{self.commodity}_set_point"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that this might not match the actual production, would desired_commodity_production (or another similar name) be more apt?

Comment on lines +78 to +80
self.commodity = self.config.commodity_name
self.commodity_rate_units = self.config.commodity_units
self.commodity_amount_units = f"({self.commodity_rate_units})*h"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move this to initialize to match other components?

Comment on lines +28 to +30
self.commodity = self.config.commodity_name
self.commodity_rate_units = self.config.commodity_units
self.commodity_amount_units = f"({self.commodity_rate_units})*h"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move to initialize to match other components?

Comment on lines +17 to +18
Note: this storage performance model is intended to be used with the
`DemandOpenLoopStorageController` controller.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can it be used with other controllers? Maybe this (or whatever is appropriate)

Suggested change
Note: this storage performance model is intended to be used with the
`DemandOpenLoopStorageController` controller.
Note: this storage performance model is intended to be used with the
`DemandOpenLoopStorageController` controller and has not been tested
with other controllers, but may work.

Comment on lines +161 to +197
# Step 2: Simulate the storage performance based on the sizes calculated

# Initialize output arrays of charge and discharge
discharge_storage = np.zeros(self.n_timesteps)
charge_storage = np.zeros(self.n_timesteps)
output_array = np.zeros(self.n_timesteps)

# Initialize state-of-charge value as the soc at t=0
soc = deepcopy(commodity_storage_soc[0])

# Simulate a basic storage component
for t, demand_t in enumerate(commodity_demand):
input_flow = commodity_production[t]
available_charge = float(commodity_storage_capacity_kg - soc)
available_discharge = float(soc)

# If demand is greater than the input, discharge storage
if demand_t > input_flow:
# Discharge storage to meet demand.
discharge_needed = demand_t - input_flow
discharge = min(discharge_needed, available_discharge, storage_max_fill_rate)
# Update SOC
soc -= discharge

discharge_storage[t] = discharge
output_array[t] = input_flow + discharge

# If input is greater than the demand, charge storage
else:
# Charge storage with unused input
unused_input = input_flow - demand_t
charge = min(unused_input, available_charge, storage_max_fill_rate)
# Update SOC
soc += charge

charge_storage[t] = charge
output_array[t] = demand_t
Copy link
Collaborator

Choose a reason for hiding this comment

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

The overall reason for this code logic to be necessary here is unclear to me. Is it because this component is only meant to be used with the passthrough controller, so we need to actually calculate these charge and discharge timeseries somewhere because it's not done in the controller, and that's here?

This is the biggest question I have where chatting about it might be useful (I just messaged you).

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

Labels

ready for review This PR is ready for input from folks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants