Skip to content

Commit d6b85c1

Browse files
mikofskicwhanse
andauthored
add read pvgis tmy (#907)
* add read_pvgis_tmy * for files downloaded from pvgis tool * update tests, api, and docs * allow commit .csv to pvlib/data * update what's new with read_pvgis_tmy * closes #880 * test import from api vs. full namespace path * py35 no likey fstring in read_pvgis_tmy * don't use is for string comparison, ditto * state returns, add see also * use str in json.load in read_pvgis * test read_pvgis_tmy_json, don't use binary buffer * try parse_epw in read_pvgis_tmy first * since attribute error only occurs for str/path * more comments, space between pvgis outputformats * raise ValueError if unknown output format * test if ValueError raised for bad output format * trailing new line in pvgis.py * use `pvgis_format` instead of confusing arg - infer parser format from file extension for csv, epw, and json - much more detailed docstring for `pvgis_format` - update tests for inferring format from extensions as well as failing if basic output format or file buffer isn't explicit * Apply pvgis suggestions from code review to docstring thanks Cliff! Co-Authored-By: Cliff Hansen <[email protected]> * wrap long lines in read pvgis docstring * wordsmithing and formatting, hope it's okay * don't need to index filename to get pvgis_format, just use rsplit(',', 1)[-1] to file extension * use pathlib instead of rsplit in read_pvgis - TypeError raised if filename is buffer, not ValueError * python 3.5 has differnet messae for pathlib - py>3.5: expected str, bytes or os.PathLike object - py=3.5: argument should be a path or str object Co-authored-by: Cliff Hansen <[email protected]>
1 parent 113917b commit d6b85c1

10 files changed

+26530
-12
lines changed

.gitignore

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,17 @@ coverage.xml
7878
*.h5
7979

8080

81-
#Datafiles
81+
# Datafiles
8282
*.csv
83+
# Ignore some csv
84+
!pvlib/data/*.csv
8385

8486
# vi
8587
*.swp
8688

87-
#Ignore some notebooks
89+
# Ignore some notebooks
8890
*.ipynb
8991
!docs/tutorials/*.ipynb
9092

91-
#Ignore Mac DS_store files
93+
# Ignore Mac DS_store files
9294
*.DS_Store

docs/sphinx/source/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ relevant to solar energy modeling.
384384
iotools.read_psm3
385385
iotools.parse_psm3
386386
iotools.get_pvgis_tmy
387+
iotools.read_pvgis_tmy
387388

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

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Enhancements
2020
``temperature_model='faiman'`` option to :py:class:`~pvlib.modelchain.ModelChain`
2121
(:pull:`897`) (:issue:`836`).
2222
* Add Kimber soiling model :py:func:`pvlib.losses.soiling_kimber`. (:pull:`860`)
23+
* Add :func:`~pvlib.iotools.read_pvgis_tmy` for files downloaded using the
24+
PVGIS tool. (:issue:`880`)
2325

2426
Bug fixes
2527
~~~~~~~~~

pvlib/data/tmy_45.000_8.000_2005_2016.csv

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

pvlib/data/tmy_45.000_8.000_2005_2016.epw

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

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

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

pvlib/data/tmy_45.000_8.000_2005_2016.txt

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

pvlib/iotools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
from pvlib.iotools.psm3 import get_psm3 # noqa: F401
1313
from pvlib.iotools.psm3 import read_psm3 # noqa: F401
1414
from pvlib.iotools.psm3 import parse_psm3 # noqa: F401
15-
from pvlib.iotools.pvgis import get_pvgis_tmy # noqa: F401
15+
from pvlib.iotools.pvgis import get_pvgis_tmy, read_pvgis_tmy # noqa: F401

pvlib/iotools/pvgis.py

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
<https://ec.europa.eu/jrc/en/PVGIS/tools/monthly-radiation>`_
1616
"""
1717
import io
18+
import json
19+
from pathlib import Path
1820
import requests
1921
import pandas as pd
20-
from pvlib.iotools import parse_epw
22+
from pvlib.iotools import read_epw, parse_epw
2123

2224
URL = 'https://re.jrc.ec.europa.eu/api/'
2325

@@ -72,6 +74,10 @@ def get_pvgis_tmy(lat, lon, outputformat='json', usehorizon=True,
7274
the error message in the response will be raised as an exception,
7375
otherwise raise whatever ``HTTP/1.1`` error occurred
7476
77+
See also
78+
--------
79+
read_pvgis_tmy
80+
7581
References
7682
----------
7783
@@ -174,3 +180,99 @@ def _parse_pvgis_tmy_basic(src):
174180
data['time(UTC)'], format='%Y%m%d:%H%M', utc=True)
175181
data = data.drop('time(UTC)', axis=1)
176182
return data, None, None, None
183+
184+
185+
def read_pvgis_tmy(filename, pvgis_format=None):
186+
"""
187+
Read a file downloaded from PVGIS.
188+
189+
Parameters
190+
----------
191+
filename : str, pathlib.Path, or file-like buffer
192+
Name, path, or buffer of file downloaded from PVGIS.
193+
pvgis_format : str, default None
194+
Format of PVGIS file or buffer. Equivalent to the ``outputformat``
195+
parameter in the PVGIS TMY API. If `filename` is a file and
196+
`pvgis_format` is ``None`` then the file extension will be used to
197+
determine the PVGIS format to parse. For PVGIS files from the API with
198+
``outputformat='basic'``, please set `pvgis_format` to ``'basic'``. If
199+
`filename` is a buffer, then `pvgis_format` is required and must be in
200+
``['csv', 'epw', 'json', 'basic']``.
201+
202+
Returns
203+
-------
204+
data : pandas.DataFrame
205+
the weather data
206+
months_selected : list
207+
TMY year for each month, ``None`` for basic and EPW
208+
inputs : dict
209+
the inputs, ``None`` for basic and EPW
210+
meta : list or dict
211+
meta data, ``None`` for basic
212+
213+
Raises
214+
------
215+
ValueError
216+
if `pvgis_format` is ``None`` and the file extension is neither
217+
``.csv``, ``.json``, nor ``.epw``, or if `pvgis_format` is provided as
218+
input but isn't in ``['csv', 'epw', 'json', 'basic']``
219+
TypeError
220+
if `pvgis_format` is ``None`` and `filename` is a buffer
221+
222+
See also
223+
--------
224+
get_pvgis_tmy
225+
"""
226+
# get the PVGIS outputformat
227+
if pvgis_format is None:
228+
# get the file extension from suffix, but remove the dot and make sure
229+
# it's lower case to compare with epw, csv, or json
230+
# NOTE: raises TypeError if filename is a buffer
231+
outputformat = Path(filename).suffix[1:].lower()
232+
else:
233+
outputformat = pvgis_format
234+
235+
# parse the pvgis file based on the output format, either 'epw', 'json',
236+
# 'csv', or 'basic'
237+
238+
# EPW: use the EPW parser from the pvlib.iotools epw.py module
239+
if outputformat == 'epw':
240+
try:
241+
data, meta = parse_epw(filename)
242+
except AttributeError: # str/path has no .read() attribute
243+
data, meta = read_epw(filename)
244+
return data, None, None, meta
245+
246+
# NOTE: json, csv, and basic output formats have parsers defined as private
247+
# functions in this module
248+
249+
# JSON: use Python built-in json module to convert file contents to a
250+
# Python dictionary, and pass the dictionary to the _parse_pvgis_tmy_json()
251+
# function from this module
252+
if outputformat == 'json':
253+
try:
254+
src = json.load(filename)
255+
except AttributeError: # str/path has no .read() attribute
256+
with open(str(filename), 'r') as fbuf:
257+
src = json.load(fbuf)
258+
return _parse_pvgis_tmy_json(src)
259+
260+
# CSV or basic: use the correct parser from this module
261+
# eg: _parse_pvgis_tmy_csv() or _parse_pvgist_tmy_basic()
262+
if outputformat in ['csv', 'basic']:
263+
# get the correct parser function for this output format from globals()
264+
pvgis_parser = globals()['_parse_pvgis_tmy_{:s}'.format(outputformat)]
265+
# NOTE: pvgis_parse() is a pvgis parser function from this module,
266+
# either _parse_pvgis_tmy_csv() or _parse_pvgist_tmy_basic()
267+
try:
268+
pvgis_data = pvgis_parser(filename)
269+
except AttributeError: # str/path has no .read() attribute
270+
with open(str(filename), 'rb') as fbuf:
271+
pvgis_data = pvgis_parser(fbuf)
272+
return pvgis_data
273+
274+
# raise exception if pvgis format isn't in ['csv', 'basic', 'epw', 'json']
275+
err_msg = (
276+
"pvgis format '{:s}' was unknown, must be either 'epw', 'json', 'csv'"
277+
", or 'basic'").format(outputformat)
278+
raise ValueError(err_msg)

pvlib/tests/iotools/test_pvgis.py

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pandas as pd
77
import pytest
88
import requests
9-
from pvlib.iotools import get_pvgis_tmy
9+
from pvlib.iotools import get_pvgis_tmy, read_pvgis_tmy
1010
from conftest import DATA_DIR
1111

1212

@@ -70,7 +70,14 @@ def csv_meta(meta_expected):
7070
@pytest.mark.remote_data
7171
def test_get_pvgis_tmy(expected, month_year_expected, inputs_expected,
7272
meta_expected):
73-
data, months_selected, inputs, meta = get_pvgis_tmy(45, 8)
73+
pvgis_data = get_pvgis_tmy(45, 8)
74+
_compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
75+
meta_expected, pvgis_data)
76+
77+
78+
def _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
79+
meta_expected, pvgis_data):
80+
data, months_selected, inputs, meta = pvgis_data
7481
# check each column of output separately
7582
for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys():
7683
assert np.allclose(data[outvar], expected[outvar])
@@ -113,7 +120,12 @@ def test_get_pvgis_tmy_kwargs(userhorizon_expected):
113120

114121
@pytest.mark.remote_data
115122
def test_get_pvgis_tmy_basic(expected, meta_expected):
116-
data, _, _, _ = get_pvgis_tmy(45, 8, outputformat='basic')
123+
pvgis_data = get_pvgis_tmy(45, 8, outputformat='basic')
124+
_compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data)
125+
126+
127+
def _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data):
128+
data, _, _, _ = pvgis_data
117129
# check each column of output separately
118130
for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys():
119131
assert np.allclose(data[outvar], expected[outvar])
@@ -122,8 +134,14 @@ def test_get_pvgis_tmy_basic(expected, meta_expected):
122134
@pytest.mark.remote_data
123135
def test_get_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
124136
meta_expected, csv_meta):
125-
data, months_selected, inputs, meta = get_pvgis_tmy(
126-
45, 8, outputformat='csv')
137+
pvgis_data = get_pvgis_tmy(45, 8, outputformat='csv')
138+
_compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
139+
meta_expected, csv_meta, pvgis_data)
140+
141+
142+
def _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
143+
meta_expected, csv_meta, pvgis_data):
144+
data, months_selected, inputs, meta = pvgis_data
127145
# check each column of output separately
128146
for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys():
129147
assert np.allclose(data[outvar], expected[outvar])
@@ -144,8 +162,12 @@ def test_get_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
144162

145163
@pytest.mark.remote_data
146164
def test_get_pvgis_tmy_epw(expected, epw_meta):
147-
data, _, _, meta = get_pvgis_tmy(
148-
45, 8, outputformat='epw')
165+
pvgis_data = get_pvgis_tmy(45, 8, outputformat='epw')
166+
_compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
167+
168+
169+
def _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data):
170+
data, _, _, meta = pvgis_data
149171
assert np.allclose(data.ghi, expected['G(h)'])
150172
assert np.allclose(data.dni, expected['Gb(n)'])
151173
assert np.allclose(data.dhi, expected['Gd(h)'])
@@ -160,3 +182,73 @@ def test_get_pvgis_tmy_error():
160182
get_pvgis_tmy(45, 8, outputformat='bad')
161183
with pytest.raises(requests.HTTPError, match='404 Client Error'):
162184
get_pvgis_tmy(45, 8, url='https://re.jrc.ec.europa.eu/')
185+
186+
187+
def test_read_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
188+
meta_expected):
189+
fn = DATA_DIR / 'tmy_45.000_8.000_2005_2016.json'
190+
# infer outputformat from file extensions
191+
pvgis_data = read_pvgis_tmy(fn)
192+
_compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
193+
meta_expected, pvgis_data)
194+
# explicit pvgis outputformat
195+
pvgis_data = read_pvgis_tmy(fn, pvgis_format='json')
196+
_compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
197+
meta_expected, pvgis_data)
198+
with fn.open('r') as fbuf:
199+
pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='json')
200+
_compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected,
201+
meta_expected, pvgis_data)
202+
203+
204+
def test_read_pvgis_tmy_epw(expected, epw_meta):
205+
fn = DATA_DIR / 'tmy_45.000_8.000_2005_2016.epw'
206+
# infer outputformat from file extensions
207+
pvgis_data = read_pvgis_tmy(fn)
208+
_compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
209+
# explicit pvgis outputformat
210+
pvgis_data = read_pvgis_tmy(fn, pvgis_format='epw')
211+
_compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
212+
with fn.open('r') as fbuf:
213+
pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='epw')
214+
_compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data)
215+
216+
217+
def test_read_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
218+
meta_expected, csv_meta):
219+
fn = DATA_DIR / 'tmy_45.000_8.000_2005_2016.csv'
220+
# infer outputformat from file extensions
221+
pvgis_data = read_pvgis_tmy(fn)
222+
_compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
223+
meta_expected, csv_meta, pvgis_data)
224+
# explicit pvgis outputformat
225+
pvgis_data = read_pvgis_tmy(fn, pvgis_format='csv')
226+
_compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
227+
meta_expected, csv_meta, pvgis_data)
228+
with fn.open('rb') as fbuf:
229+
pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='csv')
230+
_compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected,
231+
meta_expected, csv_meta, pvgis_data)
232+
233+
234+
def test_read_pvgis_tmy_basic(expected, meta_expected):
235+
fn = DATA_DIR / 'tmy_45.000_8.000_2005_2016.txt'
236+
# XXX: can't infer outputformat from file extensions for basic
237+
with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"):
238+
read_pvgis_tmy(fn)
239+
# explicit pvgis outputformat
240+
pvgis_data = read_pvgis_tmy(fn, pvgis_format='basic')
241+
_compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data)
242+
with fn.open('rb') as fbuf:
243+
pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='basic')
244+
_compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data)
245+
# file buffer raises TypeError if passed to pathlib.Path()
246+
with pytest.raises(TypeError):
247+
read_pvgis_tmy(fbuf)
248+
249+
250+
def test_read_pvgis_tmy_exception():
251+
bad_outputformat = 'bad'
252+
err_msg = "pvgis format '{:s}' was unknown".format(bad_outputformat)
253+
with pytest.raises(ValueError, match=err_msg):
254+
read_pvgis_tmy('filename', pvgis_format=bad_outputformat)

0 commit comments

Comments
 (0)