Repository navigation
Allow for non-annual simulation. - #887
jaredthomas68 wants to merge 40 commits into
Conversation
…ent schedule for non-annual simulation
elenya-grant
left a comment
There was a problem hiding this comment.
Hi Jared! Thanks for this PR! I think that this is a neat functionality add. Thanks for putting all this work!
Responding to your requested feedback from reviewers, I think that this should be split into separate PRs so that each change can get the attention and feedback it deserves. I left some small comments for now but would want to do an in-depth review if this is going to come-in as-is. Since this touches so many baseclasses, I think that splitting out PRs based on base-class changes would be a good approach. I think that the sub-PRs would be:
- updates to the finance models (if they're needed?)
- update to resource models (I think that theres a whole lot of different implementation questions that I'd want the group's ideas on)
- update to performance model baseclass and cost model baseclass and maybe changes to 1 performance model and 1 cost model to showcase usage
- updates to other performance and cost models
I haven't dug into the changes in the model baseclasses much, I think those methods would benefit from some inline comments to make it easier to follow the logic.
I also think that this PR has changes that are broader than just making models compatible to different simulation lengths, it seems like a lot of updates are based on time-step changes which, to me, is a different category. I also think that all performance and cost models should get an attribute like _time_step_bounds that is something like _simulation_length_bounds so we can better keep track of which models work with different simulation lengths
There was a problem hiding this comment.
this change does not seem relevant to non-annual simulations but instead seems more relevant to enabling different timesteps and should perhaps be part of a different PR (given the size of this one)
| 3600, | ||
| 3600, | ||
| 900, | ||
| 14400, |
There was a problem hiding this comment.
are you sure this is a valid upper-bound? I thought PvWatts could only for 5min, 15 min, 30 min or hourly?
| @@ -140,6 +141,257 @@ def apply_curtailment(self, outputs): | |||
| command_value = self._inputs[command_value_key] | |||
| outputs[commodity_out_key] = np.minimum(uncurtailed, command_value) | |||
|
|
|||
| # TODO rewrite and reorganize this method | |||
| def calculate_annual_cf_and_replacement_schedule( | |||
There was a problem hiding this comment.
I agree with this TODO statement. This method is really large, I think it should last least be split up so that the state of health stuff is separate (not many models have degradation included - this is a new concept).
There was a problem hiding this comment.
Agreed on fully splitting out the SOH contributions outside of this PR. I also think we could separate the CF and replacement schedule computations since they have meaningfully different purposes.
| finally: | ||
| # Restore the configured resource year, which is advanced above for multi-year loads | ||
| if isinstance(base_year, int): | ||
| self.config.resource_year = base_year |
| try: | ||
| while total_timesteps < self.n_timesteps: |
There was a problem hiding this comment.
I think the while could be replaced with instead calculating the number of resource years needed to get enough resource data to achieve n_timesteps.
| try: | ||
| while total_timesteps < self.n_timesteps: | ||
| # Resolve the filename to use for this year, if any | ||
| if filename_list is not None: |
There was a problem hiding this comment.
I think a lot of these checks within the while statement could be moved outside of it.
| try: | ||
| # Setting resource_year runs the config validator, which raises | ||
| # if the year is outside the range supported by this dataset. | ||
| self.config.resource_year = base_year + offset |
There was a problem hiding this comment.
this logic will not work for the three TMY solar resource models. I also don't think we should be changing the config value here. I think that resource_year needs to made as an input to the corresponding methods.
There was a problem hiding this comment.
Resource year is coming from the config and just for the base year. The number of years needed and their values are determined from the dt and n_timesteps. I don't think we should require the user to put in every year. In the future I think we can have a string start and stop date/time format, but for now I lean towards keeping the single year spec.
I have made it not overwrite the base_year. Instead, resource year is determined in the loop and threaded through all the validators when loading multiple years. This created many small changes, but allows us to still reuse the single year loader code.
I also updated the logic slightly so that representative years can be handled (e.g. TMY) and when a representative year is specified, that same data is repeated for all requested simulation time. I thought about throwing an error, but I decided there may be other models or resources in the simulation that we do want to simulate and so I did not restrict the simulation length to match representative year data.
| return len(np.asarray(data[key])) | ||
| # Fall back to the first array-like value | ||
| for value in data.values(): | ||
| if isinstance(value, np.ndarray | list | tuple) and not isinstance(value, str | bytes): |
There was a problem hiding this comment.
I think that the check could be as simple as making sure value isn't a string, float, int, dictionary or boolean.
| # capacity factors and annual production across the plant life. | ||
| seconds_per_year = 31_536_000 # 8760 h/year * 3600 s/h | ||
| seconds_simulated = self.n_timesteps * self.dt | ||
| is_annual = abs(seconds_simulated - seconds_per_year) < self.dt / 2 |
There was a problem hiding this comment.
I thin that we could just check self.fraction_of_year_simulated instead of is_annual
| is_annual = abs(seconds_simulated - seconds_per_year) < self.dt / 2 | ||
|
|
||
| rated_production = outputs["rated_electricity_production"][0] | ||
| if is_annual: |
There was a problem hiding this comment.
I'm nervous about this logic if using lifetime performance results in PySAM
johnjasa
left a comment
There was a problem hiding this comment.
Top-level thoughts
Overall, this PR is moving in a useful direction and I'm glad we're finally crossing this bridge!
Thanks for this, @jaredthomas68.
This PR bundles several fairly large changes: annualization, multi-year resource handling, timestep definition expansion, model updates, and finance changes.
I think it would be easier to review and make robust as a sequence of smaller PRs.
I know @elenya-grant feels similarly, though I might have a slightly different suggested sequence for the sub-PRs.
I think the biggest decision points about how to subdivide these efforts revolve around project timeline needs; this will help set the order we tackle annualization, resource handling, model updates, finance changes, and lifecycle logic.
If you said "I absolutely need this before the end of the FY," then I vote you use the battery degradation branch for your project needs, but take more time and stepwise discussion with the team to ensure we're moving in the same decided direction.
Suggested potential broken-out PR order
-
Define annualization behavior at a baseclass model and framework level
Keep the basic annualization helper and validation changes together. Add tests for partial years, multi-year simulations, leap years, and non-dividing timesteps. We should also document what
tile,final_sim_value, andaverage_sim_valuemean for a partial-year run, making that behavior very clear for users. -
Add resource loading and resampling (could be the first or second sub-PR)
Heavily influenced by #888 and input from others. Keep the multi-year resource acquisition and time-tools changes together. Add at least one end-to-end test covering load, resample, concatenate, and consumption by a real performance model.
-
Enable models one at a time
Some of these might need to happen with the sub-PRs above, but otherwise: Update grid/demand first, then solar, storage, and the other technologies. Each model should get focused tests for the timestep ranges we actually support. Finance changes should be handled here, but with special care: verify that annual production, utilization, and variable OpEx match independently calculated timeseries totals for more than just electricity. Once the underlying models are proven, update examples for the supported combinations.
-
Separate SOH and degradation lifecycle logic
I think this can be split out relatively easily; this PR currently adds generic SOH-based degradation, EOL detection, replacement-cycle extrapolation, and replacement schedules. The initial non-annual work could use the existing no-degradation path and continue returning a zero replacement schedule.
Some open questions worth discussing
I expect that by tackling these as sub-PRs, it'll be easier for folks to identify questions like these and provide higher quality feedback:
- Is the default sub-annual run meant to represent a typical slice that gets scaled to a year, or an actual seasonal slice?
- Are all resource values rates, or do some need different resampling rules? i.e. would they ever be totals or integrated quantities?
- Which models and timestep ranges are intended to be supported in the first version?
- Do we enforce that a model coming into H2I has certain allowable timestep or annualization behavior?
- For the separate SOH follow-up, is replacement always recorded at plant-year boundaries if SOH reaches EOL mid-year?
| resource_filename: | ||
| - 30.6617_-101.7096_psmv3_60_2013.csv | ||
| - 30.6617_-101.7096_psmv3_60_2013.csv | ||
| # - 30.6617_-101.7096_psmv3_60_2014.csv | ||
| # - 30.6617_-101.7096_psmv3_60_2014.csv |
There was a problem hiding this comment.
Intentional inclusion of the commented-out years? I'd suggest removing the addition of the 2014 .csv file in this PR if possible
There was a problem hiding this comment.
Good catch, I meant to have the 2013 and 2014 only, but was playing around with running 4 years so I doubled each. I'm inclined to keep the addition of the 2014 csv do clearly demo multi-year data, not just giving one year twice. I'll revert to the intended two years.
| @@ -140,6 +141,257 @@ def apply_curtailment(self, outputs): | |||
| command_value = self._inputs[command_value_key] | |||
| outputs[commodity_out_key] = np.minimum(uncurtailed, command_value) | |||
|
|
|||
| # TODO rewrite and reorganize this method | |||
| def calculate_annual_cf_and_replacement_schedule( | |||
There was a problem hiding this comment.
Agreed on fully splitting out the SOH contributions outside of this PR. I also think we could separate the CF and replacement schedule computations since they have meaningfully different purposes.
Allow for non-annual simulation
This PR adds capabilities to run H2I for non-annual simulations. The approach taken allows for data and simulation on nearly any time step and number of steps. Annual simulation data is then reduced to lists of capacity factors, varopex, and a replacement schedule, using some extrapolation if needed to reach plant life. The resulting lists are then used in the ProFAST cash flow analysis to estimate life-time financial metrics.
The following is an incomplete list of changes
Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
Type of Reviewer Feedback Requested (on Draft PR)
Structural feedback:
Implementation feedback:
trystatements and while loops. I'd love any ideas you have for avoiding those as they can be error prone and hard to read.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
This PR relates to several issues, I may have missed some:
DemandOpenLoopStorageControllerSection 5: Impacted Areas of the Software
Section 5.1: New Files
calculate_annual_cf_and_replacement_schedule: Adds unit tests for annualization and replacement-schedule projection._acquire_resource_data: Adds tests for multi-year resource acquisition and ordered filename-list handling.process_leap_day,conform_resource_data_to_n_timesteps,concatenate_resource_years,resample_resource_data_to_dt: Adds tests for leap-day handling, horizon conformance, multi-year concatenation, and timestep resampling.Section 5.2: Modified Files
dtconsistently for non-hourly simulations.compute: Annualizes grid production and variable costs by simulated year so sub-annual and multi-year cases scale correctly._time_step_bounds: Widens accepted timesteps so the solar cost model can participate in non-hourly runs.compute: Uses helper-based annualization for non-annual runs while retaining PySAM annual outputs for exact annual cases.load_plant_yaml: Allows positive non-annual simulation horizons up to plant life instead of assuming a single year.calculate_annual_cf_and_replacement_schedule,calculate_annual_varopex: Centralizes annual projection logic for arbitrary simulation lengths and fixes variable OpEx annualization across timesteps.populate_profast: Reconstructs ProFAST capacity and annual production from commodity amount/rate units instead of timestep-specific assumptions._time_step_bounds: Removes hourly-only restrictions because the battery cost model is timestep-independent._time_step_bounds: Widens accepted timesteps so storage performance can run outside hourly resolution.Section 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()methodCostModelBaseClass__init__.pyfile to ensure it is properly imported and used insupported_models.pysupported_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