Skip to content

Nonannual resource - #933

Open
jaredthomas68 wants to merge 42 commits into
developfrom
nonannual-resource
Open

jaredthomas68 wants to merge 42 commits into
developfrom
nonannual-resource

Conversation

@jaredthomas68

@jaredthomas68 jaredthomas68 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

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:

resource_parameters:
  upsample_method: time
  downsample_method: mean

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 -> nonannual stack, targeting develop.
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

  • Feature Enhancement
    • Framework
    • New Model
    • Updated Model
    • Tools/Utilities
    • Other (please describe): N/A
  • Bug Fix
  • Documentation Update
  • CI Changes
  • Other (please describe): N/A

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 8: New Model Checklist (if applicable): N/A; no new model.

TODO:

  • Step 1: Update the changelog link from the previous PR Resource resampling #893 to PR Nonannual resource #933.
  • Step 2: Confirm local tests and a fresh documentation build, and address review feedback.
  • Step 3: Confirm remote CI and Read the Docs results for the pushed revision.

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

  • 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): N/A; demonstration is in the docs.
  • 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.

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 before
    clipping, explicitly annotate filename lists, and clarify TMY site-change errors.
  • h2integrate/resource/utilities/download_tools.py: Convert legacy OpenMeteo timestamps to
    fixed-offset local standard time to prevent artificial daylight-saving timestep mismatches.
  • h2integrate/resource/wind/nlr_developer_wtk_api_models.py: Explicitly annotate lists of
    string or Path filenames.
  • h2integrate/resource/wind/test/test_openmeteo_wind_api.py: Verify legacy hourly timestamps
    remain 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 processing
    with 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 without
    changing annual-only validation.

Section 6: Additional Supporting Information

  • Compatibility: When timesteps already match, resampling is a no-op. A mismatch requires an
    explicit method. The existing resource_filename="" convention remains supported;
    changing filename defaults is outside this PR's scope.
  • Interpretation: With downsample_method="mean", each result averages samples in the interval
    starting at its timestamp, rather than retaining the original value at that instant.
    Upsampling interpolates intermediate values; edge filling holds the final value where needed.
  • Timestamp handling: Standard-time conversion fixes the Read the Docs notebook failure in
    the turbine-library guide and the two OpenMeteo wind AEP regression mismatches. No
    regression expectations were changed, and resampling remains explicitly opt-in.
  • Documentation: The resource guide includes pandas method links and a Python figure generated
    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.

python -m pytest h2integrate/resource/utilities/test/test_time_tools.py \
  h2integrate/resource/test/test_resource_api_baseclass.py --disable-warnings -q

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:

python -m pytest h2integrate/resource/wind/test/test_openmeteo_wind_api.py \
  h2integrate/resource/wind/test/test_wind_model_integration.py --disable-warnings -q --tb=short

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_download expects no API email to be configured. Locally configured
email 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.

  • Model Structure:
    • Follows established naming conventions outlined in docs/developer_guide/coding_guidelines.md
    • Configuration class is named {ModelName}Config, where {ModelName} is the complete model class name registered with @register
    • 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 PerformanceModelBaseClass and CostModelBaseClass
  • Integration: Model has been properly integrated into H2Integrate
    • Add the new model to the appropriate __init__.py file so it can be imported from its subpackage
    • Decorated the model class with @register from h2integrate/core/supported_models.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 documentation page added to the appropriate docs/ section
      • Path to model documentation page is added to the _toc.yml
    • Update the class hierarchy diagram docs/_static/class_hierarchy.html by doing one of the below options:
      • Navigate to docs folder and run sh build_book.sh
      • Run generate_class_hierarchy.py to update the class hierarchy diagram in docs/developer_guide/class_structure.md. You may need to delete the _autosummary folder before doing this.

@jaredthomas68
jaredthomas68 added this pull request to stack #935 October 14, 2026 15:48
@jaredthomas68 jaredthomas68 self-assigned this Oct 14, 2026
@jaredthomas68
jaredthomas68 marked this pull request as ready for review October 14, 2026 17:40
This was referenced Oct 14, 2026
@jaredthomas68 jaredthomas68 added the ready for review This PR is ready for input from folks label Oct 14, 2026
@coveralls

coveralls commented Oct 14, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 90.668% — nonannual-resource into develop. No base build found for develop.

@elenya-grant elenya-grant left a comment

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.

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_start before 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?

Comment thread h2integrate/resource/wind/nlr_developer_wtk_api_models.py
Comment thread h2integrate/resource/test/conftest.py
Comment thread h2integrate/resource/test/test_resource_api_baseclass.py Outdated
Comment on lines +115 to +116
standard_offset = dt_t0.utcoffset() - dt_t0.dst()
dt_t0 = dt_t0.astimezone(datetime_timezone(standard_offset))

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.

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)

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

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.

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():

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.

can you add some inline comments to this test to explain what's going on?

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.

Done with subtests descriptions.

Comment thread h2integrate/resource/utilities/time_tools.py Outdated
Comment thread h2integrate/resource/utilities/time_tools.py Outdated
Comment thread h2integrate/resource/utilities/time_tools.py Outdated
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))

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.

why do we need the - 1e-9?

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.

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.

Comment thread h2integrate/resource/utilities/time_tools.py Outdated
Comment thread CHANGELOG.md Outdated

@elenya-grant elenya-grant left a comment

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.

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!

jaredthomas68 and others added 3 commits October 15, 2026 16:49
@jaredthomas68
jaredthomas68 requested review from johnjasa and removed request for johnjasa October 16, 2026 15:15

@genevievestarke genevievestarke left a comment

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.

Really nice, @jaredthomas68! I just had a few comments throughout!

name: python3
---

# Resource data

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.

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.

Comment thread docs/resource/resource_index.md Outdated
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

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.

I love the plot here! Very clear!

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.

Thanks!

# 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

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.

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?

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.

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:

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.

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

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.

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:

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

This branch has not been deployed

No deployments
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.

5 participants