Skip to content

Commit d635c09

Browse files
mikofskiwholmgren
authored andcommitted
add pvgis to iotools (#845)
* add get_pvgis_tmy to iotools * stickler fixes maybe? * add pvgis tests - upload test data - check the JSON output format - add get_pvgis_tmy to api - add timeout and url args as suggested - add raises to explain error messages - correct user horizon docstring decription * fix stickler * STY: fix indents and long lines in test meta JSON * check each column of output separately * add test for basic output format * check error raised if bad output format * must use NamedTemporaryFile with delete=True * add test for csv output format * add test for epw output format * add parse_epw to use buffer v. filepath * add test data for pvgis tmy userhorizon * add parse_epw to iotools api * add tests for usehorizon & userhorizon * check meta from epw * in test_epw, meta not used so replace with _ * pvgis-tmy-epw don't use temp files anymore * fix use lowercase url in request * test startyear and endyear * test a bad url, returns 404 * use assert cond is False instead of ==, thx stickler * update api docs and what's new * fix typo in Parameters * use with context manager for IO * instead of try:finally * fix defaults notation * add reference links and improve docstring, * use fixtures for test_pvgis * also move META dict to a json file in pvlib/data * fix reference links * fix open for py35 * also add references header in pvgis.py * remove whitespace on blank line * initialize data to None in case API fails to respond to bad outputformat
1 parent 5a6b65c commit d635c09

File tree

10 files changed

+9245
-9
lines changed

10 files changed

+9245
-9
lines changed

docs/sphinx/source/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ relevant to solar energy modeling.
359359
iotools.read_tmy2
360360
iotools.read_tmy3
361361
iotools.read_epw
362+
iotools.parse_epw
362363
iotools.read_srml
363364
iotools.read_srml_month_from_solardat
364365
iotools.read_surfrad
@@ -371,6 +372,7 @@ relevant to solar energy modeling.
371372
iotools.get_psm3
372373
iotools.read_psm3
373374
iotools.parse_psm3
375+
iotools.get_pvgis_tmy
374376

375377
A :py:class:`~pvlib.location.Location` object may be created from metadata
376378
in some files.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Enhancements
1414
objects. (:issue:`841`)
1515
* Added `leap_day` parameter to `iotools.get_psm3` instead of hardcoding it as
1616
False.
17-
17+
* Added :py:func:`~pvlib.iotools.get_pvgis_tmy` to get PVGIS TMY datasets.
18+
* Added :py:func:`~pvlib.iotools.parse_epw` to parse a file-like buffer
19+
containing weather data in the EPW format.
1820

1921
Bug fixes
2022
~~~~~~~~~
@@ -33,3 +35,4 @@ Documentation
3335
Contributors
3436
~~~~~~~~~~~~
3537
* Kevin Anderson (:ghuser:`kanderso-nrel`)
38+
* Mark Mikofski (:ghuser:`mikofski`)

pvlib/data/pvgis_tmy_meta.json

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"inputs": {
3+
"location": {
4+
"description": "Selected location",
5+
"variables": {
6+
"latitude": {
7+
"description": "Latitude",
8+
"units": "decimal degree"
9+
},
10+
"longitude": {
11+
"description": "Longitude",
12+
"units": "decimal degree"
13+
},
14+
"elevation": {
15+
"description": "Elevation",
16+
"units": "m"
17+
}
18+
}
19+
},
20+
"meteo_data": {
21+
"description": "Sources of meteorological data",
22+
"variables": {
23+
"radiation_db": {
24+
"description": "Solar radiation database"
25+
},
26+
"meteo_db": {
27+
"description": "Database used for meteorological variables other than solar radiation"
28+
},
29+
"year_min": {
30+
"description": "First year of the calculations"
31+
},
32+
"year_max": {
33+
"description": "Last year of the calculations"
34+
},
35+
"use_horizon": {
36+
"description": "Include horizon shadows"
37+
},
38+
"horizon_db": {
39+
"description": "Source of horizon data"
40+
}
41+
}
42+
}
43+
},
44+
"outputs": {
45+
"months_selected": {
46+
"type": "time series",
47+
"timestamp": "monthly",
48+
"description": "months selected for the TMY"
49+
},
50+
"tmy_hourly": {
51+
"type": "time series",
52+
"timestamp": "hourly",
53+
"variables": {
54+
"T2m": {
55+
"description": "2-m air temperature",
56+
"units": "degree Celsius"
57+
},
58+
"RH": {
59+
"description": "relative humidity",
60+
"units": "%"
61+
},
62+
"G(h)": {
63+
"description": "Global irradiance on the horizontal plane",
64+
"units": "W/m2"
65+
},
66+
"Gb(n)": {
67+
"description": "Beam/direct irradiance on a plane always normal to sun rays",
68+
"units": "W/m2"
69+
},
70+
"Gd(h)": {
71+
"description": "Diffuse irradiance on the horizontal plane",
72+
"units": "W/m2"
73+
},
74+
"IR(h)": {
75+
"description": "Surface infrared (thermal) irradiance on a horizontal plane",
76+
"units": "W/m2"
77+
},
78+
"WS10m": {
79+
"description": "10-m total wind speed",
80+
"units": "m/s"
81+
},
82+
"WD10m": {
83+
"description": "10-m wind direction (0 = N, 90 = E)",
84+
"units": "degree"
85+
},
86+
"SP": {
87+
"description": "Surface (air) pressure",
88+
"units": "Pa"
89+
}
90+
}
91+
}
92+
}
93+
}

pvlib/data/pvgis_tmy_test.dat

Lines changed: 8761 additions & 0 deletions
Large diffs are not rendered by default.

pvlib/data/tmy_45.000_8.000_userhorizon.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

pvlib/iotools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pvlib.iotools.tmy import read_tmy2 # noqa: F401
22
from pvlib.iotools.tmy import read_tmy3 # noqa: F401
3-
from pvlib.iotools.epw import read_epw # noqa: F401
3+
from pvlib.iotools.epw import read_epw, parse_epw # noqa: F401
44
from pvlib.iotools.srml import read_srml # noqa: F401
55
from pvlib.iotools.srml import read_srml_month_from_solardat # noqa: F401
66
from pvlib.iotools.surfrad import read_surfrad # noqa: F401
@@ -13,3 +13,4 @@
1313
from pvlib.iotools.psm3 import get_psm3 # noqa: F401
1414
from pvlib.iotools.psm3 import read_psm3 # noqa: F401
1515
from pvlib.iotools.psm3 import parse_psm3 # noqa: F401
16+
from pvlib.iotools.pvgis import get_pvgis_tmy # noqa: F401

pvlib/iotools/epw.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,18 @@ def read_epw(filename, coerce_year=None):
3535
3636
Returns
3737
-------
38-
Tuple of the form (data, metadata).
39-
4038
data : DataFrame
4139
A pandas dataframe with the columns described in the table
4240
below. For more detailed descriptions of each component, please
43-
consult the EnergyPlus Auxiliary Programs documentation
44-
available at: https://energyplus.net/documentation.
41+
consult the EnergyPlus Auxiliary Programs documentation [1]_
4542
4643
metadata : dict
4744
The site metadata available in the file.
4845
46+
See Also
47+
--------
48+
pvlib.iotools.parse_epw
49+
4950
Notes
5051
-----
5152
@@ -111,8 +112,8 @@ def read_epw(filename, coerce_year=None):
111112
References
112113
----------
113114
114-
.. [1] EnergyPlus documentation, Auxiliary Programs
115-
https://energyplus.net/documentation.
115+
.. [1] `EnergyPlus documentation, Auxiliary Programs
116+
<https://energyplus.net/documentation>`_
116117
'''
117118

118119
if filename.startswith('http'):
@@ -127,7 +128,45 @@ def read_epw(filename, coerce_year=None):
127128
else:
128129
# Assume it's accessible via the file system
129130
csvdata = open(filename, 'r')
131+
try:
132+
data, meta = parse_epw(csvdata, coerce_year)
133+
finally:
134+
csvdata.close()
135+
return data, meta
136+
137+
138+
def parse_epw(csvdata, coerce_year=None):
139+
"""
140+
Given a file-like buffer with data in Energy Plus Weather (EPW) format,
141+
parse the data into a dataframe.
142+
143+
Parameters
144+
----------
145+
csvdata : file-like buffer
146+
a file-like buffer containing data in the EPW format
147+
148+
coerce_year : None or int, default None
149+
If supplied, the year of the data will be set to this value. This can
150+
be a useful feature because EPW data is composed of data from
151+
different years.
152+
Warning: EPW files always have 365*24 = 8760 data rows;
153+
be careful with the use of leap years.
154+
155+
Returns
156+
-------
157+
data : DataFrame
158+
A pandas dataframe with the columns described in the table
159+
below. For more detailed descriptions of each component, please
160+
consult the EnergyPlus Auxiliary Programs documentation
161+
available at: https://energyplus.net/documentation.
162+
163+
metadata : dict
164+
The site metadata available in the file.
130165
166+
See Also
167+
--------
168+
pvlib.iotools.read_epw
169+
"""
131170
# Read line with metadata
132171
firstline = csvdata.readline()
133172

pvlib/iotools/pvgis.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""
2+
Get, read, and parse data from `PVGIS <https://ec.europa.eu/jrc/en/pvgis>`_.
3+
4+
For more information, see the following links:
5+
* `Interactive Tools <https://re.jrc.ec.europa.eu/pvg_tools/en/tools.html>`_
6+
* `Data downloads <https://ec.europa.eu/jrc/en/PVGIS/downloads/data>`_
7+
* `User manual docs <https://ec.europa.eu/jrc/en/PVGIS/docs/usermanual>`_
8+
9+
More detailed information about the API for TMY and hourly radiation are here:
10+
* `TMY <https://ec.europa.eu/jrc/en/PVGIS/tools/tmy>`_
11+
* `hourly radiation
12+
<https://ec.europa.eu/jrc/en/PVGIS/tools/hourly-radiation>`_
13+
* `daily radiation <https://ec.europa.eu/jrc/en/PVGIS/tools/daily-radiation>`_
14+
* `monthly radiation
15+
<https://ec.europa.eu/jrc/en/PVGIS/tools/monthly-radiation>`_
16+
"""
17+
import io
18+
import requests
19+
import pandas as pd
20+
from pvlib.iotools import parse_epw
21+
22+
URL = 'https://re.jrc.ec.europa.eu/api/'
23+
24+
25+
def get_pvgis_tmy(lat, lon, outputformat='json', usehorizon=True,
26+
userhorizon=None, startyear=None, endyear=None, url=URL,
27+
timeout=30):
28+
"""
29+
Get TMY data from PVGIS [1]_. For more information see the PVGIS TMY tool
30+
documentation [2]_.
31+
32+
Parameters
33+
----------
34+
lat : float
35+
Latitude in degrees north
36+
lon : float
37+
Longitude in dgrees east
38+
outputformat : str, default 'json'
39+
Must be in ``['csv', 'basic', 'epw', 'json']``. See PVGIS TMY tool
40+
documentation [2]_ for more info.
41+
usehorizon : bool, default True
42+
include effects of horizon
43+
userhorizon : list of float, default None
44+
optional user specified elevation of horizon in degrees, at equally
45+
spaced azimuth clockwise from north, only valid if `usehorizon` is
46+
true, if `usehorizon` is true but `userhorizon` is `None` then PVGIS
47+
will calculate the horizon [3]_
48+
startyear : int, default None
49+
first year to calculate TMY
50+
endyear : int, default None
51+
last year to calculate TMY, must be at least 10 years from first year
52+
url : str, default :const:`pvlib.iotools.pvgis.URL`
53+
base url of PVGIS API, append ``tmy`` to get TMY endpoint
54+
timeout : int, default 30
55+
time in seconds to wait for server response before timeout
56+
57+
Returns
58+
-------
59+
data : pandas.DataFrame
60+
the weather data
61+
months_selected : list
62+
TMY year for each month, ``None`` for basic and EPW
63+
inputs : dict
64+
the inputs, ``None`` for basic and EPW
65+
meta : list or dict
66+
meta data, ``None`` for basic
67+
68+
Raises
69+
------
70+
requests.HTTPError
71+
if the request response status is ``HTTP/1.1 400 BAD REQUEST``, then
72+
the error message in the response will be raised as an exception,
73+
otherwise raise whatever ``HTTP/1.1`` error occurred
74+
75+
References
76+
----------
77+
78+
.. [1] `PVGIS <https://ec.europa.eu/jrc/en/pvgis>`_
79+
.. [2] `PVGIS TMY tool <https://ec.europa.eu/jrc/en/PVGIS/tools/tmy>`_
80+
.. [3] `PVGIS horizon profile tool
81+
<https://ec.europa.eu/jrc/en/PVGIS/tools/horizon>`_
82+
"""
83+
# use requests to format the query string by passing params dictionary
84+
params = {'lat': lat, 'lon': lon, 'outputformat': outputformat}
85+
# pvgis only likes 0 for False, and 1 for True, not strings, also the
86+
# default for usehorizon is already 1 (ie: True), so only set if False
87+
if not usehorizon:
88+
params['usehorizon'] = 0
89+
if userhorizon is not None:
90+
params['userhorizon'] = ','.join(str(x) for x in userhorizon)
91+
if startyear is not None:
92+
params['startyear'] = startyear
93+
if endyear is not None:
94+
params['endyear'] = endyear
95+
res = requests.get(url + 'tmy', params=params, timeout=timeout)
96+
# PVGIS returns really well formatted error messages in JSON for HTTP/1.1
97+
# 400 BAD REQUEST so try to return that if possible, otherwise raise the
98+
# HTTP/1.1 error caught by requests
99+
if not res.ok:
100+
try:
101+
err_msg = res.json()
102+
except Exception:
103+
res.raise_for_status()
104+
else:
105+
raise requests.HTTPError(err_msg['message'])
106+
# initialize data to None in case API fails to respond to bad outputformat
107+
data = None, None, None, None
108+
if outputformat == 'json':
109+
src = res.json()
110+
return _parse_pvgis_tmy_json(src)
111+
elif outputformat == 'csv':
112+
with io.BytesIO(res.content) as src:
113+
data = _parse_pvgis_tmy_csv(src)
114+
elif outputformat == 'basic':
115+
with io.BytesIO(res.content) as src:
116+
data = _parse_pvgis_tmy_basic(src)
117+
elif outputformat == 'epw':
118+
with io.StringIO(res.content.decode('utf-8')) as src:
119+
data, meta = parse_epw(src)
120+
data = (data, None, None, meta)
121+
else:
122+
# this line is never reached because if outputformat is not valid then
123+
# the response is HTTP/1.1 400 BAD REQUEST which is handled earlier
124+
pass
125+
return data
126+
127+
128+
def _parse_pvgis_tmy_json(src):
129+
inputs = src['inputs']
130+
meta = src['meta']
131+
months_selected = src['outputs']['months_selected']
132+
data = pd.DataFrame(src['outputs']['tmy_hourly'])
133+
data.index = pd.to_datetime(
134+
data['time(UTC)'], format='%Y%m%d:%H%M', utc=True)
135+
data = data.drop('time(UTC)', axis=1)
136+
return data, months_selected, inputs, meta
137+
138+
139+
def _parse_pvgis_tmy_csv(src):
140+
# the first 3 rows are latitude, longitude, elevation
141+
inputs = {}
142+
# 'Latitude (decimal degrees): 45.000\r\n'
143+
inputs['latitude'] = float(src.readline().split(b':')[1])
144+
# 'Longitude (decimal degrees): 8.000\r\n'
145+
inputs['longitude'] = float(src.readline().split(b':')[1])
146+
# Elevation (m): 1389.0\r\n
147+
inputs['elevation'] = float(src.readline().split(b':')[1])
148+
# then there's a 13 row comma separated table with two columns: month, year
149+
# which contains the year used for that month in the
150+
src.readline() # get "month,year\r\n"
151+
months_selected = []
152+
for month in range(12):
153+
months_selected.append(
154+
{'month': month+1, 'year': int(src.readline().split(b',')[1])})
155+
# then there's the TMY (typical meteorological year) data
156+
# first there's a header row:
157+
# time(UTC),T2m,RH,G(h),Gb(n),Gd(h),IR(h),WS10m,WD10m,SP
158+
headers = [h.decode('utf-8').strip() for h in src.readline().split(b',')]
159+
data = pd.DataFrame(
160+
[src.readline().split(b',') for _ in range(8760)], columns=headers)
161+
dtidx = data['time(UTC)'].apply(lambda dt: dt.decode('utf-8'))
162+
dtidx = pd.to_datetime(dtidx, format='%Y%m%d:%H%M', utc=True)
163+
data = data.drop('time(UTC)', axis=1)
164+
data = pd.DataFrame(data, dtype=float)
165+
data.index = dtidx
166+
# finally there's some meta data
167+
meta = [line.decode('utf-8').strip() for line in src.readlines()]
168+
return data, months_selected, inputs, meta
169+
170+
171+
def _parse_pvgis_tmy_basic(src):
172+
data = pd.read_csv(src)
173+
data.index = pd.to_datetime(
174+
data['time(UTC)'], format='%Y%m%d:%H%M', utc=True)
175+
data = data.drop('time(UTC)', axis=1)
176+
return data, None, None, None

0 commit comments

Comments
 (0)