Repository navigation
Nonannual resource - #933
jaredthomas68 wants to merge 42 commits into
Conversation
…ntain and use ceiling instead of round for determing the number of timesteps required
…nsitions to keep all timesteps equal
elenya-grant
left a comment
There was a problem hiding this comment.
howdy jared! I think this looks great and I'm happy to see that it can handle non-consecutive timesteps now! Most of my comments are non-blocking or are just questions (usually for my own understanding).
A few high-level suggestions/thoughts on resample_resource_data_to_dt():
- This method is very well done and does a lot of complicated stuff in a digestible way if you sit with it for a while. I think a few small changes would make it easier to follow the logic in this function from first glance:
- more descriptive variable names (I gave a few suggestions in my comments)
- variables being made closer to the code chunk that uses them (I gave a few suggestions in my comments)
- some simple in-line comments in the for-loops and for some variables (like a quick description of
block_startbefore its made)
I wanted to suggest somehow breaking up resample_resource_data_to_dt() into smaller functions, but I think that'd be more confusing. I like it as one function.
I would like to have some integration tests added into h2integrate/resource/solar/test/test_multiyear_solar_resource.py::test_solar_resource_nonannual. I think it'd be like current subtests "GOESTMY:2years-filenames" (to test TMY handling) and "Himawari7:2years-with-leapday-filenames", but consider reordering the filenames so that the years are not consecutive. I can push up some added tests if you want.
Last question - can you explain some of the changes made to the openmeteo time stuff?
| standard_offset = dt_t0.utcoffset() - dt_t0.dst() | ||
| dt_t0 = dt_t0.astimezone(datetime_timezone(standard_offset)) |
There was a problem hiding this comment.
There is some special backwards compatibility with the openmeteo timestep stuff, can you explain what these two lines are doing? Does this have sometime to do with standard time vs daylight savings time? (I'm thinking of changes that I made in PR #814)
There was a problem hiding this comment.
Good question. I found that the resample errors would trigger when converting to a timezone with daylight savings time because there is an apparent non-hourly timestep in the time series. These lines adjust to use a single offset through the year. I'm a little on the fence here because we care about the exact hours in control, but the times in the resource files are not shared with the rest of H2I right now, so it works. I'm open to suggestions on how to handle this if anyone does not like this solution.
There was a problem hiding this comment.
I think I understand? Was it a pre-existing test failing unexpectedly that made you aware of this or was it when you were playing around with it locally? I'm curious cause I want to see what the time looked like before vs now. So, does OpenMeteo downloads in local time have timesteps in daylight time and standard time depending on the time of year?
Partially noting below for myself this blurb from the doc page that came out of PR #814
OpenMeteo data can be downloaded directly from the web instead of using the models in H2I. If OpenMeteo resource data is downloaded directly from the web and in the local timezone of the site, the data is downloaded with a UTC offset in daylight time whereas data downloaded with the H2I resource models will use a UTC offset in standard time. Resource data downloaded from the NLR Developer network in local time also use a UTC offset in standard time.
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_legacy_openmeteo_timestamps_use_standard_time(): |
There was a problem hiding this comment.
can you add some inline comments to this test to explain what's going on?
There was a problem hiding this comment.
Done with subtests descriptions.
| s_i = block_starts[b] | ||
| e_i = block_starts[b + 1] | ||
| b_dur = float(np.sum(step_durations[s_i:e_i])) | ||
| block_target_ns.append(math.ceil((b_dur / float(target_dt)) - 1e-9)) |
There was a problem hiding this comment.
why do we need the - 1e-9?
There was a problem hiding this comment.
It is there to protect against small rounding errors leading to ceiling rounding up an extra integer value. However, this approach is not foolproof. I've updated it to use pd/to_timedelta which can do integer nano-second division. This is probably overkill, but should be unlikely to break.
…ars in datasets in the resample function
Co-authored-by: elenya-grant <116225007+elenya-grant@users.noreply.github.com>
Added tests with multi-year resource
elenya-grant
left a comment
There was a problem hiding this comment.
Thanks jared! This looks good from my end - thanks for making the suggested changes so quickly and answering all my questions! I appreciate it! Only small suggestion left was with the changelog. Great work! Happy to see this come in!
Co-authored-by: elenya-grant <116225007+elenya-grant@users.noreply.github.com>
genevievestarke
left a comment
There was a problem hiding this comment.
Really nice, @jaredthomas68! I just had a few comments throughout!
| name: python3 | ||
| --- | ||
|
|
||
| # Resource data |
There was a problem hiding this comment.
Small gripe with the documentation format here: it's not obvious to me why I would click on the Recource Data page itself when it is also a dropdown menu. I think it just makes things confusing, especially when the first three categories ('Getting Started', 'User Guide', and 'Technology Models') just straight up list all the pages. I would advocate for standardizing how these look in the lefthand bar in the docs.
| resampling a dataset containing both 15-minute and 2-hour intervals to a uniform 1-hour | ||
| simulation time step, applying downsampling and upsampling in the same run: | ||
|
|
||
| ```{code-cell} ipython3 |
There was a problem hiding this comment.
I love the plot here! Very clear!
| # Upper bound on skipped timestamps: every leap year up to one year past the nominal end | ||
| start_year = pd.Timestamp(start_timestamp).year | ||
| n_years = get_number_of_resource_years_needed(dt_seconds, n_timesteps, include_leap=False) | ||
| last_year = start_year + n_years + 1 # +1 needed for upper-bound calculation |
There was a problem hiding this comment.
This seems pretty drastic when you might not need another full year of data. Is there a better way we could calculate this? Perhaps something that includes and end date?
There was a problem hiding this comment.
I think this is a limitation of the APIs, they download a year of data at a time. I don't think we can change that. @elenya-grant
| # Any delta exceeding 24 hours when typical step is sub-daily represents a calendar gap | ||
| if deltas.iloc[i] > 24 * 3600 and median_dt <= 24 * 3600: | ||
| effective_deltas[i] = effective_deltas[i - 1] if i > 0 else median_dt | ||
| elif deltas.iloc[i] > 28 * 86400: |
There was a problem hiding this comment.
What is this line checking? Could you add a comment here explaining the hard-coded numbers?
| effective_deltas = deltas.to_numpy().copy() | ||
| for b_idx in range(1, len(block_starts) - 1): | ||
| trans_i = block_starts[b_idx] - 1 | ||
| effective_deltas[trans_i] = effective_deltas[trans_i - 1] if trans_i > 0 else median_dt |
There was a problem hiding this comment.
I don't quite understand this line. It seems to be saying delta(t+1) = delta(t) if t+1>0. Wouldn't this lead to the same value all the time?
| stacklevel=2, | ||
| ) | ||
|
|
||
| if upsampling and not downsampling: |
There was a problem hiding this comment.
Could you use the direction variable here instead of rewriting this logic? It's a little confusing why you seem to be doing this three times in different ways. Maybe a few more comments in this section would help?
Co-authored-by: genevievestarke <103534902+genevievestarke@users.noreply.github.com>
Add resource data resampling for simulation timesteps
Add opt-in resampling of API resource data when its native timestep differs from the simulation
timestep. Users select pandas interpolation and aggregation methods in
resource_parameters:Matching timesteps require neither option. A mismatch raises an error if the required method is
not specified; explicitly requested resampling emits a warning describing the operation.
The utility supports uniform and non-uniform intervals, including datasets requiring both
upsampling and downsampling. An elapsed-time axis preserves the supplied resource-year segment
order, including out-of-order years, and enables interpolation across segment boundaries.
Regenerated time columns preserve leap-day inclusion or exclusion represented in the input.
Scalar metadata is retained. Ceiling-based target step counts cover the available source span
before clipping to the requested simulation length.
Final resource processing removes leap days when requested, resamples, clips to the simulation
length, records timing metadata, and validates the final length. Timing metadata now reports
the full interval in seconds, including intervals longer than 24 hours.
Legacy OpenMeteo UTC timestamps are converted to fixed-offset local standard time. This avoids
artificial gaps and repeated hours at daylight-saving transitions, which otherwise trigger
unnecessary resampling and can change wind-energy results.
This is PR #933, the first PR in the
nonannual-resource->nonannual-framework->nonannualstack, targetingdevelop.It does not remove the framework's annual-only simulation restriction. Framework duration
support and concrete model adaptations belong to the subsequent PRs.
Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
Type of Reviewer Feedback Requested (on Draft PR)
Structural feedback: Resampling happens in final resource processing after leap-day
handling and before clipping, independent of framework duration support.
Implementation feedback: Mixed-resolution aggregation/interpolation, segment-boundary
handling, leap-day regeneration, and coverage for non-integer timestep ratios.
Other feedback: Opt-in configuration, standard-time handling for legacy OpenMeteo data, and the
build-time documentation figure explaining interval means and interpolation.
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.Examples and new-model checklist: N/A. The demonstration is in the resource documentation.
Local validation results and the credential-dependent test caveat are recorded in Section 7.
Remote CI and Read the Docs results for the pushed revision have not yet been verified.
Section 4: Related Issues
Partially resolves #651.
This PR resamples API resource input data to the simulation timestep shared by the models.
Dedicated resampling within individual models may be added later.
This PR supersedes PR #893.
See that PR for the earlier discussion and review history of this resource-resampling work.
Section 5: Impacted Areas of the Software
Section 5.1: New Files
N/A
Section 5.2: Modified Files
h2integrate/resource/utilities/time_tools.py: Add resampling, elapsed-time segment handling,leap-aware calendar regeneration, and full-duration timing metadata.
h2integrate/resource/resource_baseclass.py: Configure and integrate resampling beforeclipping, explicitly annotate filename lists, and clarify TMY site-change errors.
h2integrate/resource/utilities/download_tools.py: Convert legacy OpenMeteo timestamps tofixed-offset local standard time to prevent artificial daylight-saving timestep mismatches.
h2integrate/resource/wind/nlr_developer_wtk_api_models.py: Explicitly annotate lists ofstring or
Pathfilenames.h2integrate/resource/wind/test/test_openmeteo_wind_api.py: Verify legacy hourly timestampsremain uniformly spaced across daylight-saving transitions without requiring resampling.
h2integrate/resource/utilities/test/test_time_tools.py: Test methods, errors, warnings,metadata, leap days, coverage, non-uniform intervals, and out-of-order years.
h2integrate/resource/test/test_resource_api_baseclass.py: Test final resource processingwith a multi-day timestep.
h2integrate/resource/test/conftest.py: Supply resampling options in test fixtures.h2integrate/core/inputs/validation.py: Reformat the seconds-per-year constant withoutchanging annual-only validation.
Section 6: Additional Supporting Information
explicit method. The existing
resource_filename=""convention remains supported;changing filename defaults is outside this PR's scope.
downsample_method="mean", each result averages samples in the intervalstarting at its timestamp, rather than retaining the original value at that instant.
Upsampling interpolates intermediate values; edge filling holds the final value where needed.
the turbine-library guide and the two OpenMeteo wind AEP regression mismatches. No
regression expectations were changed, and resampling remains explicitly opt-in.
at documentation-build time, demonstrating downsampling and upsampling in one dataset.
Section 7: Test Results, if applicable
Focused resampling/API base-class tests: 74 tests and 45 subtests passed.
After the standard-time fix, wind-resource and integration tests: 10 tests and 75 subtests
passed, including both previously failing OpenMeteo AEP checks and the new DST regression:
Documentation: All 10 code cells in the previously failing turbine-library guide executed
successfully without cache. A full book build with a fresh output directory and notebook cache
also succeeded:
bash docs/build_book.sh --path-output "$(mktemp -d /tmp/h2integrate-docs-933.XXXXXX)"Ruff, import-order checks, and whitespace checks passed.
Earlier full resource run, before the standard-time fix: 140 passed, 5 failed, 6 skipped,
1 xfailed, 3 xpassed, 369 subtests passed. The five reported failures represented the two
wind AEP cases (each counted with its failed subtest) and one credential-dependent solar case.
The wind cases now pass in the targeted rerun above. The full directory has not been rerun
after this fix; no aggregate post-fix total is claimed.
The solar
test_forced_downloadexpects no API email to be configured. Locally configuredemail caused a dummy-key API request instead of the expected missing-email error. The test
passes with email lookup disabled in an isolated process (1 passed, 13 deselected);
credential files, production code, and regression expectations were not changed for that check.
The six skips require an HPC environment; the xfail/xpass results concern the existing Meteosat
TMY longitude check. Full repository CI and the remote docs rebuild still need confirmation.
Section 8 (Optional): New Model Checklist
N/A; no new model is introduced. The checklist is retained as required by the template.
docs/developer_guide/coding_guidelines.md{ModelName}Config, where{ModelName}is the complete model class name registered with@registerattrsclass to define theConfigto load in attributes for the modelBaseConfigorCostModelBaseConfiginitialize()method,setup()method,compute()methodPerformanceModelBaseClassandCostModelBaseClass__init__.pyfile so it can be imported from its subpackage@registerfromh2integrate/core/supported_models.pytest_all_examples.pydocs/section_toc.ymldocs/_static/class_hierarchy.htmlby doing one of the below options:docsfolder and runsh build_book.shgenerate_class_hierarchy.pyto update the class hierarchy diagram indocs/developer_guide/class_structure.md. You may need to delete the_autosummaryfolder before doing this.