Skip to content

Commit ad2e6ac

Browse files
authored
Fix silently ignoring file path in pvsystem.retrieve_sam when name is provided (#2020)
* My approach to the issue * Deprecate previous parameters * No reason to over-engineer, right? * Update v0.10.5.rst * Update pvsystem.py * Improve error handling * Add ppl involved * kevin's suggestions
1 parent 8668a61 commit ad2e6ac

File tree

3 files changed

+99
-111
lines changed

3 files changed

+99
-111
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Enhancements
1515

1616
Bug fixes
1717
~~~~~~~~~
18+
* Fixed :py:func:`pvlib.pvsystem.retrieve_sam` silently ignoring the `path` parameter
19+
when `name` was provided. Now an exception is raised requesting to only provide one
20+
of the two parameters. (:issue:`2018`, :pull:`2020`)
1821

1922

2023
Testing
@@ -36,3 +39,5 @@ Requirements
3639
Contributors
3740
~~~~~~~~~~~~
3841
* Cliff Hansen (:ghuser:`cwhanse`)
42+
* :ghuser:`apct69`
43+
* Mark Mikofski (:ghuser:`mikofski`)

pvlib/pvsystem.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import functools
88
import io
99
import itertools
10-
import os
10+
from pathlib import Path
1111
import inspect
1212
from urllib.request import urlopen
1313
import numpy as np
@@ -1958,9 +1958,9 @@ def calcparams_pvsyst(effective_irradiance, temp_cell,
19581958

19591959

19601960
def retrieve_sam(name=None, path=None):
1961-
'''
1962-
Retrieve latest module and inverter info from a local file or the
1963-
SAM website.
1961+
"""
1962+
Retrieve latest module and inverter info from a file bundled with pvlib,
1963+
a path or an URL (like SAM's website).
19641964
19651965
This function will retrieve either:
19661966
@@ -1971,10 +1971,14 @@ def retrieve_sam(name=None, path=None):
19711971
19721972
and return it as a pandas DataFrame.
19731973
1974+
.. note::
1975+
Only provide one of ``name`` or ``path``.
1976+
19741977
Parameters
19751978
----------
19761979
name : string, optional
1977-
Name can be one of:
1980+
Use one of the following strings to retrieve a database bundled with
1981+
pvlib:
19781982
19791983
* 'CECMod' - returns the CEC module database
19801984
* 'CECInverter' - returns the CEC Inverter database
@@ -1985,7 +1989,7 @@ def retrieve_sam(name=None, path=None):
19851989
* 'ADRInverter' - returns the ADR Inverter database
19861990
19871991
path : string, optional
1988-
Path to the SAM file. May also be a URL.
1992+
Path to a CSV file or a URL.
19891993
19901994
Returns
19911995
-------
@@ -1997,7 +2001,11 @@ def retrieve_sam(name=None, path=None):
19972001
Raises
19982002
------
19992003
ValueError
2000-
If no name or path is provided.
2004+
If no ``name`` or ``path`` is provided.
2005+
ValueError
2006+
If both ``name`` and ``path`` are provided.
2007+
KeyError
2008+
If the provided ``name`` is not a valid database name.
20012009
20022010
Notes
20032011
-----
@@ -2030,38 +2038,38 @@ def retrieve_sam(name=None, path=None):
20302038
CEC_Date NaN
20312039
CEC_Type Utility Interactive
20322040
Name: AE_Solar_Energy__AE6_0__277V_, dtype: object
2033-
'''
2034-
2035-
if name is not None:
2036-
name = name.lower()
2037-
data_path = os.path.join(
2038-
os.path.dirname(os.path.abspath(__file__)), 'data')
2039-
if name == 'cecmod':
2040-
csvdata = os.path.join(
2041-
data_path, 'sam-library-cec-modules-2019-03-05.csv')
2042-
elif name == 'sandiamod':
2043-
csvdata = os.path.join(
2044-
data_path, 'sam-library-sandia-modules-2015-6-30.csv')
2045-
elif name == 'adrinverter':
2046-
csvdata = os.path.join(
2047-
data_path, 'adr-library-cec-inverters-2019-03-05.csv')
2048-
elif name in ['cecinverter', 'sandiainverter']:
2049-
# Allowing either, to provide for old code,
2050-
# while aligning with current expectations
2051-
csvdata = os.path.join(
2052-
data_path, 'sam-library-cec-inverters-2019-03-05.csv')
2053-
else:
2054-
raise ValueError(f'invalid name {name}')
2055-
elif path is not None:
2056-
if path.startswith('http'):
2057-
response = urlopen(path)
2058-
csvdata = io.StringIO(response.read().decode(errors='ignore'))
2059-
else:
2060-
csvdata = path
2041+
"""
2042+
# error: path was previously silently ignored if name was given GH#2018
2043+
if name is not None and path is not None:
2044+
raise ValueError("Please provide either 'name' or 'path', not both.")
20612045
elif name is None and path is None:
2062-
raise ValueError("A name or path must be provided!")
2063-
2064-
return _parse_raw_sam_df(csvdata)
2046+
raise ValueError("Please provide either 'name' or 'path'.")
2047+
elif name is not None:
2048+
internal_dbs = {
2049+
"cecmod": "sam-library-cec-modules-2019-03-05.csv",
2050+
"sandiamod": "sam-library-sandia-modules-2015-6-30.csv",
2051+
"adrinverter": "adr-library-cec-inverters-2019-03-05.csv",
2052+
# Both 'cecinverter' and 'sandiainverter', point to same database
2053+
# to provide for old code, while aligning with current expectations
2054+
"cecinverter": "sam-library-cec-inverters-2019-03-05.csv",
2055+
"sandiainverter": "sam-library-cec-inverters-2019-03-05.csv",
2056+
}
2057+
try:
2058+
csvdata_path = Path(__file__).parent.joinpath(
2059+
"data", internal_dbs[name.lower()]
2060+
)
2061+
except KeyError:
2062+
raise KeyError(
2063+
f"Invalid name {name}. "
2064+
+ f"Provide one of {list(internal_dbs.keys())}."
2065+
) from None
2066+
else: # path is not None
2067+
if path.lower().startswith("http"): # URL check is not case-sensitive
2068+
response = urlopen(path) # URL is case-sensitive
2069+
csvdata_path = io.StringIO(response.read().decode(errors="ignore"))
2070+
else:
2071+
csvdata_path = path
2072+
return _parse_raw_sam_df(csvdata_path)
20652073

20662074

20672075
def _normalize_sam_product_names(names):

pvlib/tests/test_pvsystem.py

Lines changed: 48 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -103,82 +103,57 @@ def test_PVSystem_get_iam_invalid(sapm_module_params, mocker):
103103
system.get_iam(45, iam_model='not_a_model')
104104

105105

106-
def test_retrieve_sam_raise_no_parameters():
106+
def test_retrieve_sam_raises_exceptions():
107107
"""
108-
Raise an exception if no parameters are provided to `retrieve_sam()`.
108+
Raise an exception if an invalid parameter is provided to `retrieve_sam()`.
109109
"""
110-
with pytest.raises(ValueError) as error:
110+
with pytest.raises(ValueError, match="Please provide either"):
111111
pvsystem.retrieve_sam()
112-
assert 'A name or path must be provided!' == str(error.value)
113-
114-
115-
def test_retrieve_sam_cecmod():
116-
"""
117-
Test the expected data is retrieved from the CEC module database. In
118-
particular, check for a known module in the database and check for the
119-
expected keys for that module.
120-
"""
121-
data = pvsystem.retrieve_sam('cecmod')
122-
keys = [
123-
'BIPV',
124-
'Date',
125-
'T_NOCT',
126-
'A_c',
127-
'N_s',
128-
'I_sc_ref',
129-
'V_oc_ref',
130-
'I_mp_ref',
131-
'V_mp_ref',
132-
'alpha_sc',
133-
'beta_oc',
134-
'a_ref',
135-
'I_L_ref',
136-
'I_o_ref',
137-
'R_s',
138-
'R_sh_ref',
139-
'Adjust',
140-
'gamma_r',
141-
'Version',
142-
'STC',
143-
'PTC',
144-
'Technology',
145-
'Bifacial',
146-
'Length',
147-
'Width',
148-
]
149-
module = 'Itek_Energy_LLC_iT_300_HE'
150-
assert module in data
151-
assert set(data[module].keys()) == set(keys)
152-
153-
154-
def test_retrieve_sam_cecinverter():
155-
"""
156-
Test the expected data is retrieved from the CEC inverter database. In
157-
particular, check for a known inverter in the database and check for the
158-
expected keys for that inverter.
159-
"""
160-
data = pvsystem.retrieve_sam('cecinverter')
161-
keys = [
162-
'Vac',
163-
'Paco',
164-
'Pdco',
165-
'Vdco',
166-
'Pso',
167-
'C0',
168-
'C1',
169-
'C2',
170-
'C3',
171-
'Pnt',
172-
'Vdcmax',
173-
'Idcmax',
174-
'Mppt_low',
175-
'Mppt_high',
176-
'CEC_Date',
177-
'CEC_Type',
178-
]
179-
inverter = 'Yaskawa_Solectria_Solar__PVI_5300_208__208V_'
180-
assert inverter in data
181-
assert set(data[inverter].keys()) == set(keys)
112+
with pytest.raises(ValueError, match="Please provide either.*, not both."):
113+
pvsystem.retrieve_sam(name="this_surely_wont_work", path="wont_work")
114+
with pytest.raises(KeyError, match="Invalid name"):
115+
pvsystem.retrieve_sam(name="this_surely_wont_work")
116+
with pytest.raises(FileNotFoundError):
117+
pvsystem.retrieve_sam(path="this_surely_wont_work.csv")
118+
119+
120+
def test_retrieve_sam_databases():
121+
"""Test the expected keys are retrieved from each database."""
122+
keys_per_database = {
123+
"cecmod": {'Technology', 'Bifacial', 'STC', 'PTC', 'A_c', 'Length',
124+
'Width', 'N_s', 'I_sc_ref', 'V_oc_ref', 'I_mp_ref',
125+
'V_mp_ref', 'alpha_sc', 'beta_oc', 'T_NOCT', 'a_ref',
126+
'I_L_ref', 'I_o_ref', 'R_s', 'R_sh_ref', 'Adjust',
127+
'gamma_r', 'BIPV', 'Version', 'Date'},
128+
"sandiamod": {'Vintage', 'Area', 'Material', 'Cells_in_Series',
129+
'Parallel_Strings', 'Isco', 'Voco', 'Impo', 'Vmpo',
130+
'Aisc', 'Aimp', 'C0', 'C1', 'Bvoco', 'Mbvoc', 'Bvmpo',
131+
'Mbvmp', 'N', 'C2', 'C3', 'A0', 'A1', 'A2', 'A3', 'A4',
132+
'B0', 'B1', 'B2', 'B3', 'B4', 'B5', 'DTC', 'FD', 'A',
133+
'B', 'C4', 'C5', 'IXO', 'IXXO', 'C6', 'C7', 'Notes'},
134+
"adrinverter": {'Manufacturer', 'Model', 'Source', 'Vac', 'Vintage',
135+
'Pacmax', 'Pnom', 'Vnom', 'Vmin', 'Vmax',
136+
'ADRCoefficients', 'Pnt', 'Vdcmax', 'Idcmax',
137+
'MPPTLow', 'MPPTHi', 'TambLow', 'TambHi', 'Weight',
138+
'PacFitErrMax', 'YearOfData'},
139+
"cecinverter": {'Vac', 'Paco', 'Pdco', 'Vdco', 'Pso', 'C0', 'C1', 'C2',
140+
'C3', 'Pnt', 'Vdcmax', 'Idcmax', 'Mppt_low',
141+
'Mppt_high', 'CEC_Date', 'CEC_Type'}
142+
} # fmt: skip
143+
item_per_database = {
144+
"cecmod": "Itek_Energy_LLC_iT_300_HE",
145+
"sandiamod": "Canadian_Solar_CS6X_300M__2013_",
146+
"adrinverter": "Sainty_Solar__SSI_4K4U_240V__CEC_2011_",
147+
"cecinverter": "ABB__PVI_3_0_OUTD_S_US__208V_",
148+
}
149+
# duplicate the cecinverter items for sandiainverter, for backwards compat
150+
keys_per_database["sandiainverter"] = keys_per_database["cecinverter"]
151+
item_per_database["sandiainverter"] = item_per_database["cecinverter"]
152+
153+
for database in keys_per_database.keys():
154+
data = pvsystem.retrieve_sam(database)
155+
assert set(data.index) == keys_per_database[database]
156+
assert item_per_database[database] in data.columns
182157

183158

184159
def test_sapm(sapm_module_params):

0 commit comments

Comments
 (0)