Skip to content

Commit b5e89be

Browse files
mikofskikandersolarAdamRJensen
authored
coerce and rotate pvgis TMY data to desired tz and year (#2138)
* coerce and rotate pvgis TMY data to desired tz and year - add private function `_coerce_and_rotate_pvgis()` - add `utc_offset` and `coerce_year` params to docstring for `get_pvgis_tmy` - call private function if `utc_offset` is not zero * test get_pvgis_tmy_coerce_year check if utc_offset and coerce_year work as expected * fix flake8 in test_pvgis_coerce_year - remove whitespace - shorter lines * remove iloc for index in test pvgis coerce - incorrect syntax for indices * deal with leap year in pvgis when coercing - if february is a leap year, when shifting tz, causes issues - so replace february year with non-leap year * fix space around operator in coerce pvgis - also fix use ts for timestamp when fixing feb leap year * fix pd.Timestamp in pvgis coerce year - lower case "s" not TimeStamp * Update v0.11.1 what's new for coerce pvgis tmy - add description and links to issue/pr * replace year and tzinfo in pvgis_tmy coerce year - also use np.roll - also make new index and dataframe instead of altering original - removes need to sanitize original index February for leap year - remove calendar import but add numpy and pytz - code much simpler shorter, easier to read * remove unused imports from pvgis.py for flake8 * change private function name to _coerce_and_roll_pvgis_tmy * spot check rolled pvgis TMY values after converting tz - fix Turin is actually CET which is UTC+1 - be DRY so use variables for test year and tz constants, versus WET and hardcoded - check tz unchanged if default zero utc_offset - use _ output args instead of indexing data[0] - add comments * Update utc_offset description - explain setting utc_offset will roll data to start at Jan 1 midnight * change coerce_year and utc_offset defaults to None in pvgis TMY - update arg docstrings - allow user to coerce year even if utc_offset is None or zero - use 1990 as default if utc_offset is not None or zero, but coerce_year was unspecified - add warning comment to be explicit and test identity to avoid unexpected implicit booleaness * rename roll_utc_offset in get_pvgis_tmy - refactor utc_offset everywhere including comments and docstring - add additional test to coerce year even if utc offset is zero or none - change tzname to 'UTC' (versus Etc/GMT or Etc/GMT+0) if replacing with zero utc offset * Update pvlib/iotools/pvgis.py with suggestions use "optional" vs. "default None" per #1574 Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> * Update docs/sphinx/source/whatsnew/v0.11.1.rst rename argument "roll_utc_offset" in whatsnew Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> * rename _coerce_and_roll_tmy, remove 'pvgis' * rename index with new tz in coerce pvgis tmy * allow tz of None in _coerce_and_roll_tmy - treat tz=None as UTC - allows get_pvgis_tmy to be simpler - remove unnecessary comments * clarify input tmy_data is UTC... - ... in docstring of private function pvgis._coerce_and_roll_tmy() - rename tmy_data - name new_index explicitly using pd.DatetimeIndex() * fix flake8 in _coerce_and_roll_tmy --------- Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com>
1 parent 0428fbe commit b5e89be

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

docs/sphinx/source/whatsnew/v0.11.1.rst

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Enhancements
1919
* Add new parameters for min/max absolute air mass to
2020
:py:func:`pvlib.spectrum.spectral_factor_firstsolar`.
2121
(:issue:`2086`, :pull:`2100`)
22+
* Add ``roll_utc_offset`` and ``coerce_year`` arguments to
23+
:py:func:`pvlib.iotools.get_pvgis_tmy` to allow user to specify time zone,
24+
rotate indices of TMY to begin at midnight, and force indices to desired
25+
year. (:issue:`2139`, :pull:`2138`)
2226
* Restructured the pvlib/spectrum folder by breaking up the contents of
2327
pvlib/spectrum/mismatch.py into pvlib/spectrum/mismatch.py,
2428
pvlib/spectrum/irradiance.py, and
@@ -62,5 +66,6 @@ Contributors
6266
* Leonardo Micheli (:ghuser:`lmicheli`)
6367
* Echedey Luis (:ghuser:`echedey-ls`)
6468
* Rajiv Daxini (:ghuser:`RDaxini`)
69+
* Mark A. Mikofski (:ghuser:`mikofski`)
6570
* Ben Pierce (:ghuser:`bgpierc`)
6671
* Jose Meza (:ghuser:`JoseMezaMendieta`)

pvlib/iotools/pvgis.py

+39-3
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
import json
1919
from pathlib import Path
2020
import requests
21+
import numpy as np
2122
import pandas as pd
23+
import pytz
2224
from pvlib.iotools import read_epw, parse_epw
23-
import warnings
24-
from pvlib._deprecation import pvlibDeprecationWarning
2525

2626
URL = 'https://re.jrc.ec.europa.eu/api/'
2727

@@ -390,9 +390,33 @@ def read_pvgis_hourly(filename, pvgis_format=None, map_variables=True):
390390
raise ValueError(err_msg)
391391

392392

393+
def _coerce_and_roll_tmy(tmy_data, tz, year):
394+
"""
395+
Assumes ``tmy_data`` input is UTC, converts from UTC to ``tz``, rolls
396+
dataframe so timeseries starts at midnight, and forces all indices to
397+
``year``. Only works for integer ``tz``, but ``None`` and ``False`` are
398+
re-interpreted as zero / UTC.
399+
"""
400+
if tz:
401+
tzname = pytz.timezone(f'Etc/GMT{-tz:+d}')
402+
else:
403+
tz = 0
404+
tzname = pytz.timezone('UTC')
405+
new_index = pd.DatetimeIndex([
406+
timestamp.replace(year=year, tzinfo=tzname)
407+
for timestamp in tmy_data.index],
408+
name=f'time({tzname})')
409+
new_tmy_data = pd.DataFrame(
410+
np.roll(tmy_data, tz, axis=0),
411+
columns=tmy_data.columns,
412+
index=new_index)
413+
return new_tmy_data
414+
415+
393416
def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True,
394417
userhorizon=None, startyear=None, endyear=None,
395-
map_variables=True, url=URL, timeout=30):
418+
map_variables=True, url=URL, timeout=30,
419+
roll_utc_offset=None, coerce_year=None):
396420
"""
397421
Get TMY data from PVGIS.
398422
@@ -424,6 +448,13 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True,
424448
base url of PVGIS API, append ``tmy`` to get TMY endpoint
425449
timeout : int, default 30
426450
time in seconds to wait for server response before timeout
451+
roll_utc_offset: int, optional
452+
Use to specify a time zone other than the default UTC zero and roll
453+
dataframe by ``roll_utc_offset`` so it starts at midnight on January
454+
1st. Ignored if ``None``, otherwise will force year to ``coerce_year``.
455+
coerce_year: int, optional
456+
Use to force indices to desired year. Will default to 1990 if
457+
``coerce_year`` is not specified, but ``roll_utc_offset`` is specified.
427458
428459
Returns
429460
-------
@@ -510,6 +541,11 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True,
510541
if map_variables:
511542
data = data.rename(columns=VARIABLE_MAP)
512543

544+
if not (roll_utc_offset is None and coerce_year is None):
545+
# roll_utc_offset is specified, but coerce_year isn't
546+
coerce_year = coerce_year or 1990
547+
data = _coerce_and_roll_tmy(data, roll_utc_offset, coerce_year)
548+
513549
return data, months_selected, inputs, meta
514550

515551

pvlib/tests/iotools/test_pvgis.py

+47
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,53 @@ def _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data):
435435
assert np.allclose(data[outvar], expected[outvar])
436436

437437

438+
@pytest.mark.remote_data
439+
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
440+
def test_get_pvgis_tmy_coerce_year():
441+
"""test utc_offset and coerce_year work as expected"""
442+
base_case, _, _, _ = get_pvgis_tmy(45, 8) # Turin
443+
assert str(base_case.index.tz) == 'UTC'
444+
assert base_case.index.name == 'time(UTC)'
445+
noon_test_data = [
446+
base_case[base_case.index.month == m].iloc[12]
447+
for m in range(1, 13)]
448+
cet_tz = 1 # Turin time is CET
449+
cet_name = 'Etc/GMT-1'
450+
# check indices of rolled data after converting timezone
451+
pvgis_data, _, _, _ = get_pvgis_tmy(45, 8, roll_utc_offset=cet_tz)
452+
jan1_midnight = pd.Timestamp('1990-01-01 00:00:00', tz=cet_name)
453+
dec31_midnight = pd.Timestamp('1990-12-31 23:00:00', tz=cet_name)
454+
assert pvgis_data.index[0] == jan1_midnight
455+
assert pvgis_data.index[-1] == dec31_midnight
456+
assert pvgis_data.index.name == f'time({cet_name})'
457+
# spot check rolled data matches original
458+
for m, test_case in enumerate(noon_test_data):
459+
expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz]
460+
assert all(test_case == expected)
461+
# repeat tests with year coerced
462+
test_yr = 2021
463+
pvgis_data, _, _, _ = get_pvgis_tmy(
464+
45, 8, roll_utc_offset=cet_tz, coerce_year=test_yr)
465+
jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00:00:00', tz=cet_name)
466+
dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23:00:00', tz=cet_name)
467+
assert pvgis_data.index[0] == jan1_midnight
468+
assert pvgis_data.index[-1] == dec31_midnight
469+
assert pvgis_data.index.name == f'time({cet_name})'
470+
for m, test_case in enumerate(noon_test_data):
471+
expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz]
472+
assert all(test_case == expected)
473+
# repeat tests with year coerced but utc offset none or zero
474+
pvgis_data, _, _, _ = get_pvgis_tmy(45, 8, coerce_year=test_yr)
475+
jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00:00:00', tz='UTC')
476+
dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23:00:00', tz='UTC')
477+
assert pvgis_data.index[0] == jan1_midnight
478+
assert pvgis_data.index[-1] == dec31_midnight
479+
assert pvgis_data.index.name == 'time(UTC)'
480+
for m, test_case in enumerate(noon_test_data):
481+
expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12]
482+
assert all(test_case == expected)
483+
484+
438485
@pytest.mark.remote_data
439486
@pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
440487
def test_get_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,

0 commit comments

Comments
 (0)