Make sure storage_baseclass handles arbitrary dt#753
Conversation
johnjasa
left a comment
There was a problem hiding this comment.
Thanks for this, Sanjana! I have one suggested change where I want your thoughts and input.
| _time_step_bounds = ( | ||
| 3600, | ||
| 3600, | ||
| ) # (min, max) time step lengths (in seconds) compatible with this model |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Here's the bounds I think each storage model should have:
StorageAutoSizingModel: (3600, 3600) - should not be changed until its testedStoragePerformanceModel: (1, 3600)PySAMBatteryPerformanceModel: (3600, 3600) - should not be changed until its testedStoragePerformanceBase: this could have (3600, 3600) or (1, 3600) since it's overwritten by the subclasses.
There was a problem hiding this comment.
For StoragePerformanceBase, I changed the bounds to (1,36000) to add the test Elenya suggested (dt > 1hr).
| 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 % | ||
| """ |
There was a problem hiding this comment.
Fantastic and clear test! Thank you for this.
elenya-grant
left a comment
There was a problem hiding this comment.
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/sas 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] |
There was a problem hiding this comment.
Just checking here - we expect that the headroom, charge_available, the input storage_dispatch_commands, etc are in commodity_rate_units?
| @pytest.mark.regression | ||
| def test_storage_half_hourly_known_outputs(subtests): |
There was a problem hiding this comment.
| @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): |
| 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 = { |
There was a problem hiding this comment.
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.
| _time_step_bounds = ( | ||
| 3600, | ||
| 3600, | ||
| ) # (min, max) time step lengths (in seconds) compatible with this model |
There was a problem hiding this comment.
Here's the bounds I think each storage model should have:
StorageAutoSizingModel: (3600, 3600) - should not be changed until its testedStoragePerformanceModel: (1, 3600)PySAMBatteryPerformanceModel: (3600, 3600) - should not be changed until its testedStoragePerformanceBase: this could have (3600, 3600) or (1, 3600) since it's overwritten by the subclasses.
Added additional tests! |
| self.dt_amount = om_units.convert_units( | ||
| self.dt, | ||
| "s", | ||
| f"({self.commodity_amount_units})/({self.commodity_rate_units})", | ||
| ) |
There was a problem hiding this comment.
Great use of the built-in units conversion here!
johnjasa
left a comment
There was a problem hiding this comment.
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.
Make sure
storage_baseclasshandles arbitrarydtstorage_baseclasscontains several unit inconsistencies whendtis non-hourly. This PR addresses them. This was brought up in #738.Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
Type of Reviewer Feedback Requested (on Draft PR)
Structural feedback:
Implementation feedback:
Other feedback:
Section 3: General PR Checklist
docs/files are up-to-date, or added when necessaryCHANGELOG.md"A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
XYZshould 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.extensionmethod1: What and why something was changed in one sentence or less.Section 5.2: Modified Files
h2integrate/storage/test/test_storage_performance_model.pyh2integrate/storage/storage_baseclass.pySection 6: Additional Supporting Information
Section 7: Test Results, if applicable
Section 8 (Optional): New Model Checklist
docs/developer_guide/coding_guidelines.mdattrsclass to define theConfigto load in attributes for the modelBaseConfigorCostModelBaseConfiginitialize()method,setup()method,compute()methodCostModelBaseClasssupported_models.pycreate_financial_modelinh2integrate_model.pytest_all_examples.pydocs/user_guide/model_overview.mddocs/section<model_name>.mdis added to the_toc.ymlgenerate_class_hierarchy.pyto update the class hierarchy diagram indocs/developer_guide/class_structure.md