Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 h2integrate/resource/resource_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def setup(self):
self.resource_data = None
self.resource_site = [self.config.latitude, self.config.longitude]
self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"]
self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]
self.add_input("latitude", self.config.latitude, units="deg")
self.add_input("longitude", self.config.longitude, units="deg")

Expand Down
39 changes: 38 additions & 1 deletion h2integrate/resource/solar/openmeteo_solar.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class OpenMeteoHistoricalSolarAPIConfig(ResourceBaseAPIConfig):
Args:
resource_year (int): Year to use for resource data.
Must been between 1940 the year before the current calendar year. (inclusive).
include_leap_day (bool, optional): If False, remove data from leap day if the
resource_year is a leap year. Otherwise, leave leap day data in. Defaults to False.
resource_data (dict | object, optional): Dictionary of user-input resource data.
Defaults to an empty dictionary.
resource_dir (str | Path, optional): Folder to save resource files to or
Expand All @@ -42,6 +44,7 @@ class OpenMeteoHistoricalSolarAPIConfig(ResourceBaseAPIConfig):
"""

resource_year: int = field(converter=int, validator=range_val(1940, datetime.now().year - 1))
include_leap_day: bool = field(default=False)
dataset_desc: str = "openmeteo_archive_solar"
resource_type: str = "solar"
valid_intervals: list[int] = field(factory=lambda: [60])
Expand Down Expand Up @@ -286,7 +289,41 @@ def load_data(self, fpath):
data["Minute"] = time.minute

data = data[data["Year"] == self.config.resource_year]
# TODO: throw error if data isn't proper length

data = data.reset_index(drop=True)

# Check if data includes leap day
data_has_leap_day = int(data[data["Month"] == 2]["Day"].max()) == 29

# Remove leap day if needed
if not self.config.include_leap_day and data_has_leap_day:
# Get index of dataframe that includes leap day
leap_day_index = (
data.reset_index(drop=False)
.set_index(keys=["Month", "Day"], drop=True)
.loc[(2, 29)]["index"]
.to_list()
)
# Drop the leap day data from the dataframe
data = data.drop(index=leap_day_index)

# Check if data is the same length as the number of timesteps
if len(data) != self.n_timesteps:
leap_day_msg = ""
if data_has_leap_day and len(data) > self.n_timesteps:
# Add extra detail to error message if error may be due to leap day
leap_day_msg = (
"This may be because the resource data includes a leap day. ",
"To remove data from a leap day from resource data, please set "
"`include_leap_day` to False.",
)

msg = (
f"{self.__class__.__name__}: Resource data is not the same length as n_timesteps. "
f"Resource data has length {len(data)}, n_timesteps is {self.n_timesteps}. "
f"{leap_day_msg}"
)
raise ValueError(msg)

data, data_units = self.format_timeseries_data(data)
# make units for data in openmdao-compatible units
Expand Down
2 changes: 2 additions & 0 deletions h2integrate/resource/solar/test/test_pvwatts_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ 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
],
ids=[
"Himawari7SolarAPI",
Expand All @@ -80,6 +81,7 @@ def pysam_performance_model():
"MeteosatPrimeMeridianSolarAPI",
"MeteosatPrimeMeridianTMYSolarAPI",
"OpenMeteoHistoricalSolarResource",
"OpenMeteoHistoricalSolarResource-LeapYear",
]
)
# fmt: on
Expand Down
58 changes: 58 additions & 0 deletions h2integrate/resource/solar/test/test_resource_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,61 @@ def test_solar_resource_h2i_download(
assert solar_data["start_time"] == f"{resource_year}/01/01 00:00:00 (+0000)"
with subtests.test("Time step"):
assert solar_data["dt"] == plant_simulation["simulation"]["dt"]



# fmt: off
@pytest.mark.unit
@pytest.mark.parametrize(
"model,which,lat,lon,resource_year,model_name,timezone",
[("OpenMeteoHistoricalSolarResource", "solar", -28.454864, 114.551749, 2024, "openmeteo_archive_solar", 8)], # noqa: E501
ids=["OpenMeteoHistoricalSolarResource-LeapYear"]
)
# fmt: on
def test_solar_resource_h2i_download_leap_year(
plant_simulation,
site_config,
subtests,
model,
which,
lat,
lon,
resource_year,
model_name,
):
plant_config = {
"site": site_config,
"plant": plant_simulation,
}

prob = om.Problem()
comp = supported_models[model](
plant_config=plant_config,
resource_config=plant_config["site"]["resources"]["solar_resource"]["resource_parameters"],
driver_config={},
)
prob.model.add_subsystem("resource", comp)
prob.setup()
prob.run_model()
solar_data = prob.get_val("resource.solar_resource_data")
name_expected = f"{lat}_{lon}_{resource_year}_{model_name}_60min_local_tz.csv"
with subtests.test("filepath for data was found where expected"):
assert Path(solar_data["filepath"]).exists()
assert Path(solar_data["filepath"]).name == name_expected

with subtests.test("Data timezone"):
assert pytest.approx(solar_data["data_tz"], rel=1e-6) == 8
with subtests.test("Site Elevation"):
assert pytest.approx(solar_data["elevation"], rel=1e-6) == 71

data_keys = [k for k, v in solar_data.items() if not isinstance(v, float | int | str)]
for k in data_keys:
with subtests.test(f"{k} resource data is 8760"):
assert len(solar_data[k]) == 8760

with subtests.test("There are 16 timeseries data keys"):
assert len(data_keys) == 16
with subtests.test("Start time"):
assert solar_data["start_time"] == f"{resource_year}/01/01 00:00:00 (+0800)"
with subtests.test("Time step"):
assert solar_data["dt"] == plant_simulation["simulation"]["dt"]
41 changes: 39 additions & 2 deletions h2integrate/resource/wind/openmeteo_wind.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class OpenMeteoHistoricalWindAPIConfig(ResourceBaseAPIConfig):
Args:
resource_year (int): Year to use for resource data.
Must been between 1940 the year before the current calendar year. (inclusive).
include_leap_day (bool, optional): If False, remove data from leap day if the
resource_year is a leap year. Otherwise, leave leap day data in. Defaults to False.
verify_download (bool, optional): Whether to verify the API download from the url.
If an `openmeteo_requests.Client.OpenMeteoRequestsError` error is thrown,
try setting to True. Defaults to False.
Expand All @@ -37,6 +39,7 @@ class OpenMeteoHistoricalWindAPIConfig(ResourceBaseAPIConfig):
"""

resource_year: int = field(converter=int, validator=range_val(1940, datetime.now().year - 1))
include_leap_day: bool = field(default=False)
dataset_desc: str = "openmeteo_archive"
resource_type: str = "wind"
valid_intervals: list[int] = field(factory=lambda: [60])
Expand Down Expand Up @@ -279,7 +282,41 @@ def load_data(self, fpath):
data["Minute"] = time.minute

data = data[data["Year"] == self.config.resource_year]
# TODO: throw error if data isn't proper length

data = data.reset_index(drop=True)

# Check if data includes leap day
data_has_leap_day = int(data[data["Month"] == 2]["Day"].max()) == 29

# Remove leap day if needed
if not self.config.include_leap_day and data_has_leap_day:
# Get index of dataframe that includes leap day
leap_day_index = (
data.reset_index(drop=False)
.set_index(keys=["Month", "Day"], drop=True)
.loc[(2, 29)]["index"]
.to_list()
)
# Drop the leap day data from the dataframe
data = data.drop(index=leap_day_index)

# Check if data is the same length as the number of timesteps
if len(data) != self.n_timesteps:
leap_day_msg = ""
if data_has_leap_day and len(data) > self.n_timesteps:
# Add extra detail to error message if error may be due to leap day
leap_day_msg = (
"This may be because the resource data includes a leap day. ",
"To remove data from a leap day from resource data, please set "
"`include_leap_day` to False.",
)

msg = (
f"{self.__class__.__name__}: Resource data is not the same length as n_timesteps. "
f"Resource data has length {len(data)}, n_timesteps is {self.n_timesteps}. "
f"{leap_day_msg}"
)
raise ValueError(msg)

data, data_units = self.format_timeseries_data(data)
# make units for data in openmdao-compatible units
Expand Down Expand Up @@ -325,7 +362,7 @@ def format_timeseries_data(self, data):

if "is_day" in c:
data_rename_mapper.update({c: "is_day"})
data_units.update({"is_day": "percent"})
data_units.update({"is_day": "unitless"})

if "surface" in c:
new_c += "_0m"
Expand Down
60 changes: 60 additions & 0 deletions h2integrate/resource/wind/test/test_openmeteo_wind_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,63 @@ def test_wind_resource_h2i_download(
assert wind_data["start_time"] == f"{resource_year}/01/01 00:00:00 (-0600)"
with subtests.test("Time step"):
assert wind_data["dt"] == plant_simulation["simulation"]["dt"]



# fmt: off
@pytest.mark.unit
@pytest.mark.parametrize(
"model,which,lat,lon,resource_year,model_name,timezone,elevation",
[("OpenMeteoHistoricalWindResource", "wind", -28.454864, 114.551749, 2024, "openmeteo_archive", 8, 71.0)], # noqa: E501
ids=["Non-UTC Leap Year"],
)
# fmt: on
def test_wind_resource_h2i_download_leap_year(
plant_simulation,
site_config,
subtests,
model,
lat,
lon,
resource_year,
model_name,
timezone,
elevation,
):
plant_config = {
"site": site_config,
"plant": plant_simulation,
}

prob = om.Problem()
comp = supported_models[model](
plant_config=plant_config,
resource_config=plant_config["site"]["resources"]["wind_resource"]["resource_parameters"],
driver_config={},
)
prob.model.add_subsystem("resource", comp)
prob.setup()
prob.run_model()
wind_data = prob.get_val("resource.wind_resource_data")

expected_fn = f"{lat}_{lon}_{resource_year}_{model_name}_60min_local_tz.csv"
with subtests.test("filepath for data was found where expected"):
assert Path(wind_data["filepath"]).exists()
assert Path(wind_data["filepath"]).name == expected_fn

with subtests.test("Data timezone"):
assert pytest.approx(wind_data["data_tz"], rel=1e-6) == timezone
with subtests.test("Site Elevation"):
assert pytest.approx(wind_data["elevation"], rel=1e-6) == elevation

data_keys = [k for k, v in wind_data.items() if not isinstance(v, float | int | str)]
for k in data_keys:
with subtests.test(f"{k} resource data is 8760"):
assert len(wind_data[k]) == 8760

with subtests.test("theres 14 timeseries data keys"):
assert len(data_keys) == 14
with subtests.test("Start time"):
assert wind_data["start_time"] == f"{resource_year}/01/01 00:00:00 (+0800)"
with subtests.test("Time step"):
assert wind_data["dt"] == plant_simulation["simulation"]["dt"]
Loading
Loading