Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
7e52698
added draft of generalizing api key tools
elenya-grant May 26, 2026
de7ddc5
generalized api methods properly
elenya-grant Jul 2, 2026
6c19042
moved base environment var functions to core
elenya-grant Jul 2, 2026
ed387c8
removed old work
elenya-grant Jul 2, 2026
2a70ff1
Merge remote-tracking branch 'h2i_upstream/develop' into generalize_api
elenya-grant Jul 2, 2026
9a0b62d
minor clean-ups to generalized stuff
elenya-grant Jul 2, 2026
05f04f3
removed commented out code
elenya-grant Jul 2, 2026
c965982
removed usage of globals()
elenya-grant Jul 2, 2026
14733b4
started adding in test file
elenya-grant Jul 2, 2026
ec139c7
Merge branch 'develop' into generalize_api
johnjasa Jul 6, 2026
e48aa67
added another test and bugfix in loading environment variable from file
elenya-grant Jul 8, 2026
fc84498
removed commented out code and added todos
elenya-grant Jul 8, 2026
3a1376d
updated so that setter method is called after _get_env_with_fallback
elenya-grant Jul 8, 2026
7ab6dae
Merge remote-tracking branch 'origin/generalize_api' into generalize_api
elenya-grant Jul 8, 2026
6b9a606
added doc strings to setter methods in nlr_developer_api_keys.py
elenya-grant Jul 9, 2026
25ec9b6
Merge remote-tracking branch 'h2i_upstream/develop' into generalize_api
elenya-grant Jul 13, 2026
a158d65
refactored environment tools with help from Rob
elenya-grant Jul 14, 2026
ea793df
minor changes to nlr_developer_api_keys
elenya-grant Jul 14, 2026
7d0b3fd
updated tests for env_tools
elenya-grant Jul 14, 2026
c05f63b
updated conftest.py files
elenya-grant Jul 14, 2026
c995469
updated doc page
elenya-grant Jul 14, 2026
f71c24f
updated conftest.py files and get_environment_variables
elenya-grant Jul 15, 2026
7c2c723
added doc page for debugging environment variable problems
elenya-grant Jul 15, 2026
7e1a67e
updated doc page so formatting is nice
elenya-grant Jul 15, 2026
e5dc052
typo fix in model_overview.md
elenya-grant Jul 15, 2026
c278ba5
Merge remote-tracking branch 'h2i_upstream/develop' into generalize_api
elenya-grant Jul 20, 2026
1c979c8
updates to doc pages
elenya-grant Jul 20, 2026
dabdc6f
added set_vars as input to get_nlr_developer_api_credential and updat…
elenya-grant Jul 20, 2026
33c3875
Merge branch 'develop' into generalize_api
johnjasa Jul 21, 2026
9909098
updated debugging environment variables doc page
elenya-grant Jul 22, 2026
e5f442f
cleaned up some of test_env_tools.py
elenya-grant Jul 22, 2026
0233866
finished cleanups to test_env_tools.py
elenya-grant Jul 22, 2026
d2185da
updated conftest.py files
elenya-grant Jul 22, 2026
ac6959d
Merge branch 'generalize_api' of github.com:elenya-grant/GreenHEART i…
elenya-grant Jul 22, 2026
643340b
updated changelog
elenya-grant Jul 22, 2026
05de2e5
Added new method to check for duplicate defined environment variables…
elenya-grant Jul 22, 2026
0997741
changed executable code cell to python code block in doc page
elenya-grant Jul 23, 2026
44612c7
trying to understand problem with openmeteo downloads
elenya-grant Jul 24, 2026
4c34c20
still trying to understand openmeteo downloads
elenya-grant Jul 24, 2026
6e3f4a9
got openmeteo solar data to work properly
elenya-grant Jul 24, 2026
53fb785
minor change to solar_resource_base
elenya-grant Jul 24, 2026
84d2a2e
cleaned up openmeteo solar data
elenya-grant Jul 24, 2026
da442e3
merged in develop
elenya-grant Jul 27, 2026
22a0159
removed print statement
elenya-grant Jul 27, 2026
e6521a6
minor cleanups to openmeteo solar model
elenya-grant Jul 27, 2026
93f1008
added all resource files that could be used for testing - will likely…
elenya-grant Jul 27, 2026
1d44105
Added big integration test of openmeteo solar models
elenya-grant Jul 27, 2026
5b93a58
updated openmeteo wind and moved make_time_index to utility file
elenya-grant Jul 27, 2026
6f3de8a
removed 3 recently added resource files
elenya-grant Jul 27, 2026
c129365
updated solar resource test that was failing
elenya-grant Jul 27, 2026
fea7c68
added doc string and comments to new function in download_tools
elenya-grant Jul 27, 2026
038ad86
removed commented out code from openmeteo solar
elenya-grant Jul 27, 2026
baf9159
updated subtest values for example 13 and 26
elenya-grant Jul 28, 2026
141cdcf
removed old download resource file and corresponding subtests
elenya-grant Jul 29, 2026
502c910
updated resource_index.md
elenya-grant Jul 29, 2026
9343d19
updated changelog
elenya-grant Jul 29, 2026
94822ca
Added fail-fast false to CI
johnjasa Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
jobs:
build:
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
- Updated `commodity_sell_price` input to `ProFastNPV` to be per year of the plant life. Also updated `BasicProFASTParameterConfig.as_dict()` so explicitly input escalation values are not overwritten to the general inflation rate [PR 799](https://github.com/NatLabRockies/H2Integrate/pull/799)
- Added `calc_azimuth_angle()` to `PYSAMSolarPlantPerformanceModel` to provide default azimuth angle based on whether the site is in the northern or southern hemisphere [PR 806](https://github.com/NatLabRockies/H2Integrate/pull/806)
- Corrected water rate units in pipe feedstock from galUS to galUS/h [PR 813](https://github.com/NatLabRockies/H2Integrate/pull/813)
- Corrected timestamps in OpenMeteo resource downloads when resource data is downloaded in local time [PR #814](https://github.com/NatLabRockies/H2Integrate/pull/814)

## 0.8 [April 15, 2026]

Expand Down
12 changes: 12 additions & 0 deletions docs/resource/resource_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
- [Solar Resource Data](solar_resource:models)
- [Tidal Resource Data](tidal_resource:models)

## Common Resource Datasets and Considerations

Both wind and solar resource data can be obtained from resource models that use the [OpenMeteo Archive API](https://open-meteo.com/en/docs/historical-weather-api) or resource models that use the resource models available in the [NLR Developer Network](https://developer.nlr.gov).

Resource models that use the [NLR Developer Network](https://developer.nlr.gov) require an API key (which can be obtained for free from [here](https://developer.nlr.gov/signup/)). Resource models that use the OpenMeteo Archive API do not require an API key.

Another key difference is that the NLR resource datasets are constrained to specific geographic regions, meaning you have to choose the resource model that corresponds to your site location. OpenMeteo resource data is available globally, for all onshore sites.

```{note}
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 developer network in local time also use a UTC offset in *standard time*.
```


## Setting resource data for a technology

Expand Down
34 changes: 17 additions & 17 deletions examples/test/test_all_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,28 +887,28 @@ def test_electrolyzer_demand(subtests, temp_copy_of_example):
lcoh = h2i.prob.get_val("finance_subgroup_hydrogen.LCOH", units="USD/kg")[0]

with subtests.test("LCOE of electricity generated"):
assert pytest.approx(217.53810477, rel=1e-6) == lcoe_gen
assert pytest.approx(138.9378311, rel=1e-6) == lcoe_gen

with subtests.test("LCOE of electrical load (battery for min power)"):
assert pytest.approx(236.15820250, rel=1e-6) == lcoe_load
assert pytest.approx(153.2393976, rel=1e-6) == lcoe_load

with subtests.test("LCOE of electrical system (battery for min power)"):
assert pytest.approx(235.43108263, rel=1e-6) == lcoe_sys
assert pytest.approx(150.374804998, rel=1e-6) == lcoe_sys

with subtests.test("LCOH (battery for min power)"):
assert pytest.approx(16.02862959, rel=1e-3) == lcoh
assert pytest.approx(10.230507642864733, rel=1e-3) == lcoh

with subtests.test("Electrolyzer capacity factor (Year 0) (battery for min power)"):
elec_cf_yr0 = h2i.prob.get_val("electrolyzer.capacity_factor", units="percent")[0]
assert pytest.approx(25.43832863, rel=1e-3) == elec_cf_yr0
assert pytest.approx(38.63085748, rel=1e-3) == elec_cf_yr0

with subtests.test("Electrical load capacity factor (battery for min power)"):
load_cf = h2i.prob.get_val("elec_load_demand.capacity_factor", units="percent")[0]
assert pytest.approx(24.29709189, rel=1e-6) == load_cf
assert pytest.approx(37.4444016, rel=1e-6) == load_cf

with subtests.test("Electricity to electrolyzer (battery for min power)"):
electricity_to_electrolyzer = h2i.prob.get_val("electrolyzer.electricity_in", "MW").sum()
assert pytest.approx(127705.51498100, rel=1e-6) == electricity_to_electrolyzer
assert pytest.approx(196807.7748, rel=1e-6) == electricity_to_electrolyzer
# Re-run where we set the battery demand equal to the electrolyzer capacity

h2i.prob.set_val("battery.electricity_set_point", electrolyzer_capacity_MW, units="MW")
Expand All @@ -921,25 +921,25 @@ def test_electrolyzer_demand(subtests, temp_copy_of_example):
lcoh = h2i.prob.get_val("finance_subgroup_hydrogen.LCOH", units="USD/kg")[0]

with subtests.test("LCOE of electrical load (battery for full power)"):
assert pytest.approx(235.46701455, rel=1e-6) == lcoe_load
assert pytest.approx(151.0430342961, rel=1e-6) == lcoe_load

with subtests.test("LCOE of electrical system (battery for full power)"):
assert pytest.approx(235.40978870, rel=1e-6) == lcoe_sys
assert pytest.approx(150.35216704, rel=1e-6) == lcoe_sys

with subtests.test("LCOH (battery for full power)"):
assert pytest.approx(17.21768237, rel=1e-6) == lcoh
assert pytest.approx(10.809740788, rel=1e-6) == lcoh

with subtests.test("Electrolyzer capacity factor (Year 0) (battery for full power)"):
elec_cf_yr0 = h2i.prob.get_val("electrolyzer.capacity_factor", units="percent")[0]
assert pytest.approx(24.96971302, rel=1e-6) == elec_cf_yr0
assert pytest.approx(38.633272786, rel=1e-6) == elec_cf_yr0

with subtests.test("Electrical load capacity factor (battery for full power)"):
load_cf = h2i.prob.get_val("elec_load_demand.capacity_factor", units="percent")[0]
assert pytest.approx(24.36841338, rel=1e-6) == load_cf
assert pytest.approx(37.988892, rel=1e-6) == load_cf

with subtests.test("Electricity to electrolyzer (battery for full power)"):
electricity_to_electrolyzer = h2i.prob.get_val("electrolyzer.electricity_in", "MW").sum()
assert pytest.approx(128080.38070512, rel=1e-6) == electricity_to_electrolyzer
assert pytest.approx(199669.6173879, rel=1e-6) == electricity_to_electrolyzer


@pytest.mark.integration
Expand Down Expand Up @@ -2063,7 +2063,7 @@ def test_floris_example(subtests, temp_copy_of_example):
pytest.approx(
h2i.prob.get_val("finance_subgroup_distributed.LCOE", units="USD/MW/h")[0], rel=1e-6
)
== 99.872209
== 99.8978994
)
with subtests.test("Utility LCOE"):
assert (
Expand All @@ -2079,7 +2079,7 @@ def test_floris_example(subtests, temp_copy_of_example):
h2i.prob.get_val("finance_subgroup_total_electricity.LCOE", units="USD/MW/h")[0],
rel=1e-6,
)
== 65.2444127137
== 65.24845058
)

with subtests.test("Distributed wind plant capacity"):
Expand All @@ -2101,7 +2101,7 @@ def test_floris_example(subtests, temp_copy_of_example):
),
rel=1e-6,
)
== 128948.21977
== 128915.05855
)

with subtests.test("Total utility electricity production"):
Expand All @@ -2118,7 +2118,7 @@ def test_floris_example(subtests, temp_copy_of_example):
h2i.prob.get_val("distributed_wind_plant.capacity_factor", units="percent")[0],
rel=1e-6,
)
== 22.30320668
== 22.297471
)

with subtests.test("Utility wind plant capacity"):
Expand Down
22 changes: 18 additions & 4 deletions h2integrate/resource/solar/openmeteo_solar.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from h2integrate.core.validators import range_val
from h2integrate.resource.resource_base import ResourceBaseAPIConfig
from h2integrate.resource.utilities.download_tools import make_time_index_openmeteo
from h2integrate.resource.solar.solar_resource_base import SolarResourceBaseAPIModel


Expand Down Expand Up @@ -97,7 +98,8 @@ def setup(self):
# "solar_zenith_angle": "deg",
"snow_depth": "m",
"rain": "mm", # "precipitable_water": "cm",
"albedo": "percent",
"albedo": "unitless",
"is_day": "unitless",
}
# get the data dictionary
data = self.get_data(self.config.latitude, self.config.longitude)
Expand Down Expand Up @@ -143,8 +145,10 @@ def create_url(self, latitude, longitude):
Returns:
str: url to use for API call.
"""

start_year = int(self.config.resource_year - 1)
end_year = int(self.config.resource_year + 1)

input_data = {
"latitude": latitude,
"longitude": longitude,
Expand Down Expand Up @@ -186,12 +190,17 @@ def download_data(self, url, fpath):

# Make time column in ISO 8601 format
time_data = pd.date_range(
start=pd.to_datetime(hourly_data.Time(), unit="s"),
end=pd.to_datetime(hourly_data.TimeEnd(), unit="s"),
start=pd.to_datetime(hourly_data.Time(), unit="s", utc=True),
end=pd.to_datetime(hourly_data.TimeEnd(), unit="s", utc=True),
freq=pd.Timedelta(seconds=hourly_data.Interval()),
inclusive="left",
)

if response.UtcOffsetSeconds() != 0:
# Data downloaded for local time
# convert timestamps to local time
time_data = time_data.tz_convert(response.Timezone().decode())

# Convert timeseries data to a DataFrame
df = pd.DataFrame(ts_data, index=time_data)
df.index.name = "time"
Expand Down Expand Up @@ -280,8 +289,13 @@ def load_data(self, fpath):

data = pd.read_csv(fpath, header=2)

time = make_time_index_openmeteo(
data,
header_dict["timezone"],
float(header_dict["latitude"]),
float(header_dict["longitude"]),
)
# Make time columns
time = pd.DatetimeIndex(data["time"])
data["Year"] = time.year
data["Month"] = time.month
data["Day"] = time.day
Expand Down
2 changes: 1 addition & 1 deletion h2integrate/resource/solar/solar_resource_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def compare_units_and_correct(self, data, data_units):
- **data_units** (*dict*): updated units of data in ``data``.
"""
for data_col, orig_units in data_units.items():
if data_col in self.output_vars_to_units:
if (data_col in self.output_vars_to_units) and (data_col in data):
desired_units = self.output_vars_to_units[data_col]
if desired_units != orig_units:
data[data_col] = units.convert_units(data[data_col], orig_units, desired_units)
Expand Down
156 changes: 155 additions & 1 deletion h2integrate/resource/solar/test/test_pvwatts_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ def pysam_performance_model():
("MeteosatPrimeMeridianSolarAPI", "solar", 41.9077, 12.4368, 2008, "nsrdb_msg_v4", 0, 2e-2, 410211.9419), # noqa: E501
pytest.param("MeteosatPrimeMeridianTMYSolarAPI", "solar", -27.3649, 152.67935, "tmy-2022", "himawari_tmy_v3", 0, 1e-3, 510709.633402, marks=pytest.mark.xfail(reason="Longitude mismatch")), # noqa: E501
("OpenMeteoHistoricalSolarResource", "solar", 44.04218, -95.19757, 2023, "openmeteo_archive_solar", 0, 1e-3, 443558.17053592583), # noqa: E501
("OpenMeteoHistoricalSolarResource", "solar", -28.454864, 114.551749, 2024, "openmeteo_archive_solar", 8, 2e-2, 192656.49240723), # noqa: E501
("OpenMeteoHistoricalSolarResource", "solar", -28.454864, 114.551749, 2024, "openmeteo_archive_solar", 8, 2e-2, 570729.3258399), # noqa: E501
("OpenMeteoHistoricalSolarResource", "solar", -28.454864, 114.551749, 2020, "openmeteo_archive_solar", 8, 2e-2, 557516.006852063), # noqa: E501
("Himawari8SolarAPI", "solar", -28.454864, 114.551749, 2020, "himawari8_v3", 8, 1e-2, 576746.92754748), # noqa: E501
],
ids=[
"Himawari7SolarAPI",
Expand All @@ -81,6 +83,8 @@ def pysam_performance_model():
"MeteosatPrimeMeridianTMYSolarAPI",
"OpenMeteoHistoricalSolarResource",
"OpenMeteoHistoricalSolarResource-LeapYear",
"OpenMeteoHistoricalSolarResource-LocalTz",
"Himawari8SolarAPI-LocalTz",
]
)
# fmt: on
Expand Down Expand Up @@ -124,3 +128,153 @@ def test_pvwatts_integration(
with subtests.test("Site longitude"):
resource_lon = prob.get_val("pv_perf.solar_resource_data").get("site_lon", 0)
assert pytest.approx(resource_lon, abs=lon_tol) == site_config["longitude"]


def make_pysam_system_openmeteo(tz):
pysam_options = {
"SystemDesign": {
"array_type": 2,
"bifaciality": 0.65,
"inv_eff": 96.0,
"losses": 14.0757,
"module_type": 0,
"rotlim": 45.0,
"gcr": 0.3,
},
"SolarResource": {
"use_wf_albedo": False,
"albedo_default": 0.3,
}
}
pysam_options["SystemDesign"].update({"tilt": 0.0})
pv_design_dict = {
"pv_capacity_kWdc": 250000.0,
"dc_ac_ratio": 1.23,
"create_model_from": "default",
"config_name": "PVWattsSingleOwner",
"tilt": 0.0,
"tilt_angle_func": "none", # "lat-func",
"pysam_options": pysam_options,
}

tech_config_dict = {
"model_inputs": {
"performance_parameters": pv_design_dict,
}
}

plant = {
"plant_life": 30,
"simulation": {
"dt": 3600,
"n_timesteps": 8760,
"start_time": "01/01/1900 00:30:00",
"timezone": tz,
},
}

plant_config = {
"plant": plant,
"site": {"latitude": 30.6617, "longitude": -101.7096, "resources": {}},
}

comp = PYSAMSolarPlantPerformanceModel(
plant_config=plant_config,
tech_config=tech_config_dict,
driver_config={},
)

return comp

@pytest.mark.integration
def test_open_meteo_timezone_pv_watts_integration(subtests):
lat = -28.454864
lon = 114.551749
resource_year = 2020

def make_resource_comp(resource_model, tz, resource_filename):
plant_simulation = {
"plant_life": 30,
"simulation": {
"dt": 3600,
"n_timesteps": 8760,
"start_time": "01/01/1900 00:30:00",
"timezone": tz,
},
}

if resource_filename is None:
resource_parameters = {
"resource_year": resource_year
}
else:
resource_parameters = {
"resource_year": resource_year,
"resource_filename": resource_filename
}
site_config = {
"latitude": lat,
"longitude": lon,
"resources": {
"solar_resource": {
"resource_model": resource_model,
"resource_parameters": resource_parameters
}
},
}
plant_config = {
"site": site_config,
"plant": plant_simulation,
}
resource_comp = supported_models[resource_model](
plant_config=plant_config,
resource_config=plant_config["site"]["resources"]["solar_resource"]["resource_parameters"],
driver_config={},
)
return resource_comp

prob = om.Problem()
# [resource_model, timezone, filename]
cases = {
"himawari_local": ["Himawari8SolarAPI", 8, None],
"web_download_local": [
"OpenMeteoHistoricalSolarResource",
8,
"open-meteo-28.44S114.55E71m-local_tz_2020.csv"
],
"new_api_local": ["OpenMeteoHistoricalSolarResource", 8, None],
}

for case_desc, case_params in cases.items():
case_group = om.Group()
model, timezone, fname = case_params
resource = make_resource_comp(model, timezone, fname)
perf = make_pysam_system_openmeteo(timezone)
case_group.add_subsystem("resource", resource, promotes=["*"])
case_group.add_subsystem("perf", perf, promotes=["*"])

prob.model.add_subsystem(case_desc, case_group)

prob.setup()
prob.run_model()

expected_aeps = {
"himawari_local": 577525.6489160028,
"web_download_local": 557469.8482059091,
"new_api_local": 557516.006852063,
}

expected_cfs = {
"himawari_local": 32.43637206240537,
"web_download_local": 31.30995037868805,
"new_api_local": 31.312542850595232,
}

for case, expected_aep in expected_aeps.items():
with subtests.test(f"{case} AEP"):
aep = prob.model.get_val(f"{case}.annual_electricity_produced", units="MW*h/year")[0]
assert pytest.approx(expected_aep, rel=1e-6) == aep
for case, expected_cf in expected_cfs.items():
with subtests.test(f"{case} CF"):
cf_result = prob.model.get_val(f"{case}.capacity_factor", units="percent")[0]
assert pytest.approx(expected_cf, rel=1e-6) == cf_result
Loading
Loading