diff --git a/.github/workflows/pytest-remote-data.yml b/.github/workflows/pytest-remote-data.yml index f74fd38ad8..812cad9346 100644 --- a/.github/workflows/pytest-remote-data.yml +++ b/.github/workflows/pytest-remote-data.yml @@ -56,7 +56,7 @@ jobs: strategy: fail-fast: false # don't cancel other matrix jobs when one fails matrix: - python-version: [3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, 3.10, 3.11, 3.12, 3.13] suffix: [''] # the alternative to "-min" include: - python-version: 3.9 diff --git a/benchmarks/benchmarks/detect_clearsky.py b/benchmarks/benchmarks/detect_clearsky.py index 7f2039f13c..7356fac056 100644 --- a/benchmarks/benchmarks/detect_clearsky.py +++ b/benchmarks/benchmarks/detect_clearsky.py @@ -9,22 +9,26 @@ class DetectClear: params = [1, 10, 100] # number of days - param_names = ['ndays'] + param_names = ["ndays"] def setup(self, ndays): - self.times = pd.date_range(start='20180601', freq='1min', - periods=1440*ndays) + self.times = pd.date_range( + start="20180601", freq="1min", periods=1440 * ndays + ) self.lat = 35.1 self.lon = -106.6 self.solar_position = solarposition.get_solarposition( - self.times, self.lat, self.lon) + self.times, self.lat, self.lon + ) clearsky_df = clearsky.simplified_solis( - self.solar_position['apparent_elevation']) - self.clearsky = clearsky_df['ghi'] - measured_dni = clearsky_df['dni'].where( - (self.times.hour % 2).astype(bool), 0) - cos_zen = np.cos(np.deg2rad(self.solar_position['apparent_zenith'])) - self.measured = measured_dni * cos_zen + clearsky_df['dhi'] + self.solar_position["apparent_elevation"] + ) + self.clearsky = clearsky_df["ghi"] + measured_dni = clearsky_df["dni"].where( + (self.times.hour % 2).astype(bool), 0 + ) + cos_zen = np.cos(np.deg2rad(self.solar_position["apparent_zenith"])) + self.measured = measured_dni * cos_zen + clearsky_df["dhi"] self.measured *= 0.98 self.window_length = 10 diff --git a/benchmarks/benchmarks/infinite_sheds.py b/benchmarks/benchmarks/infinite_sheds.py index 755c2ec3ef..884f81a808 100644 --- a/benchmarks/benchmarks/infinite_sheds.py +++ b/benchmarks/benchmarks/infinite_sheds.py @@ -9,14 +9,12 @@ class InfiniteSheds: - # benchmark variant parameters (run both vectorize=True and False) params = [True, False] - param_names = ['vectorize'] + param_names = ["vectorize"] def setup(self, vectorize): - self.times = pd.date_range(start='20180601', freq='1min', - periods=1440) + self.times = pd.date_range(start="20180601", freq="1min", periods=1440) self.location = location.Location(40, -80) self.solar_position = self.location.get_solarposition(self.times) self.clearsky_irradiance = self.location.get_clearsky( @@ -27,33 +25,33 @@ def setup(self, vectorize): self.surface_azimuth = 180 self.gcr = 0.35 self.height = 2.5 - self.pitch = 5. + self.pitch = 5.0 self.albedo = 0.2 self.npoints = 100 - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): self.tracking = tracking.singleaxis( - self.solar_position['apparent_zenith'], - self.solar_position['azimuth'], + self.solar_position["apparent_zenith"], + self.solar_position["azimuth"], axis_tilt=0, axis_azimuth=0, max_angle=60, backtrack=True, - gcr=self.gcr + gcr=self.gcr, ) def time_get_irradiance_poa_fixed(self, vectorize): infinite_sheds.get_irradiance_poa( surface_tilt=self.surface_tilt, surface_azimuth=self.surface_azimuth, - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth'], + solar_zenith=self.solar_position["apparent_zenith"], + solar_azimuth=self.solar_position["azimuth"], gcr=self.gcr, height=self.height, pitch=self.pitch, - ghi=self.clearsky_irradiance['ghi'], - dhi=self.clearsky_irradiance['dhi'], - dni=self.clearsky_irradiance['dni'], + ghi=self.clearsky_irradiance["ghi"], + dhi=self.clearsky_irradiance["dhi"], + dni=self.clearsky_irradiance["dni"], albedo=self.albedo, npoints=self.npoints, vectorize=vectorize, @@ -61,16 +59,16 @@ def time_get_irradiance_poa_fixed(self, vectorize): def time_get_irradiance_poa_tracking(self, vectorize): infinite_sheds.get_irradiance_poa( - surface_tilt=self.tracking['surface_tilt'], - surface_azimuth=self.tracking['surface_azimuth'], - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth'], + surface_tilt=self.tracking["surface_tilt"], + surface_azimuth=self.tracking["surface_azimuth"], + solar_zenith=self.solar_position["apparent_zenith"], + solar_azimuth=self.solar_position["azimuth"], gcr=self.gcr, height=self.height, pitch=self.pitch, - ghi=self.clearsky_irradiance['ghi'], - dhi=self.clearsky_irradiance['dhi'], - dni=self.clearsky_irradiance['dni'], + ghi=self.clearsky_irradiance["ghi"], + dhi=self.clearsky_irradiance["dhi"], + dni=self.clearsky_irradiance["dni"], albedo=self.albedo, npoints=self.npoints, vectorize=vectorize, @@ -80,14 +78,14 @@ def time_get_irradiance_fixed(self, vectorize): infinite_sheds.get_irradiance( surface_tilt=self.surface_tilt, surface_azimuth=self.surface_azimuth, - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth'], + solar_zenith=self.solar_position["apparent_zenith"], + solar_azimuth=self.solar_position["azimuth"], gcr=self.gcr, height=self.height, pitch=self.pitch, - ghi=self.clearsky_irradiance['ghi'], - dhi=self.clearsky_irradiance['dhi'], - dni=self.clearsky_irradiance['dni'], + ghi=self.clearsky_irradiance["ghi"], + dhi=self.clearsky_irradiance["dhi"], + dni=self.clearsky_irradiance["dni"], albedo=self.albedo, npoints=self.npoints, vectorize=vectorize, @@ -95,16 +93,16 @@ def time_get_irradiance_fixed(self, vectorize): def time_get_irradiance_tracking(self, vectorize): infinite_sheds.get_irradiance( - surface_tilt=self.tracking['surface_tilt'], - surface_azimuth=self.tracking['surface_azimuth'], - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth'], + surface_tilt=self.tracking["surface_tilt"], + surface_azimuth=self.tracking["surface_azimuth"], + solar_zenith=self.solar_position["apparent_zenith"], + solar_azimuth=self.solar_position["azimuth"], gcr=self.gcr, height=self.height, pitch=self.pitch, - ghi=self.clearsky_irradiance['ghi'], - dhi=self.clearsky_irradiance['dhi'], - dni=self.clearsky_irradiance['dni'], + ghi=self.clearsky_irradiance["ghi"], + dhi=self.clearsky_irradiance["dhi"], + dni=self.clearsky_irradiance["dni"], albedo=self.albedo, npoints=self.npoints, vectorize=vectorize, diff --git a/benchmarks/benchmarks/irradiance.py b/benchmarks/benchmarks/irradiance.py index 75cf6e965f..6e6be273f4 100644 --- a/benchmarks/benchmarks/irradiance.py +++ b/benchmarks/benchmarks/irradiance.py @@ -7,62 +7,82 @@ class Irradiance: - def setup(self): - self.times = pd.date_range(start='20180601', freq='1min', - periods=14400) - self.days = pd.date_range(start='20180601', freq='d', periods=30) + self.times = pd.date_range( + start="20180601", freq="1min", periods=14400 + ) + self.days = pd.date_range(start="20180601", freq="d", periods=30) self.location = location.Location(40, -80) self.solar_position = self.location.get_solarposition(self.times) self.clearsky_irradiance = self.location.get_clearsky(self.times) self.tilt = 20 self.azimuth = 180 - self.aoi = irradiance.aoi(self.tilt, self.azimuth, - self.solar_position.apparent_zenith, - self.solar_position.azimuth) + self.aoi = irradiance.aoi( + self.tilt, + self.azimuth, + self.solar_position.apparent_zenith, + self.solar_position.azimuth, + ) def time_get_extra_radiation(self): irradiance.get_extra_radiation(self.days) def time_aoi(self): - irradiance.aoi(self.tilt, self.azimuth, - self.solar_position.apparent_zenith, - self.solar_position.azimuth) + irradiance.aoi( + self.tilt, + self.azimuth, + self.solar_position.apparent_zenith, + self.solar_position.azimuth, + ) def time_aoi_projection(self): - irradiance.aoi_projection(self.tilt, self.azimuth, - self.solar_position.apparent_zenith, - self.solar_position.azimuth) + irradiance.aoi_projection( + self.tilt, + self.azimuth, + self.solar_position.apparent_zenith, + self.solar_position.azimuth, + ) def time_get_ground_diffuse(self): irradiance.get_ground_diffuse(self.tilt, self.clearsky_irradiance.ghi) def time_get_total_irradiance(self): - irradiance.get_total_irradiance(self.tilt, self.azimuth, - self.solar_position.apparent_zenith, - self.solar_position.azimuth, - self.clearsky_irradiance.dni, - self.clearsky_irradiance.ghi, - self.clearsky_irradiance.dhi) + irradiance.get_total_irradiance( + self.tilt, + self.azimuth, + self.solar_position.apparent_zenith, + self.solar_position.azimuth, + self.clearsky_irradiance.dni, + self.clearsky_irradiance.ghi, + self.clearsky_irradiance.dhi, + ) def time_disc(self): - irradiance.disc(self.clearsky_irradiance.ghi, - self.solar_position.apparent_zenith, - self.times) + irradiance.disc( + self.clearsky_irradiance.ghi, + self.solar_position.apparent_zenith, + self.times, + ) def time_dirint(self): - irradiance.dirint(self.clearsky_irradiance.ghi, - self.solar_position.apparent_zenith, - self.times) + irradiance.dirint( + self.clearsky_irradiance.ghi, + self.solar_position.apparent_zenith, + self.times, + ) def time_dirindex(self): - irradiance.dirindex(self.clearsky_irradiance.ghi, - self.clearsky_irradiance.ghi, - self.clearsky_irradiance.dni, - self.solar_position.apparent_zenith, - self.times) + irradiance.dirindex( + self.clearsky_irradiance.ghi, + self.clearsky_irradiance.ghi, + self.clearsky_irradiance.dni, + self.solar_position.apparent_zenith, + self.times, + ) def time_erbs(self): - irradiance.erbs(self.clearsky_irradiance.ghi, - self.solar_position.apparent_zenith, - self.times) + irradiance.erbs( + self.clearsky_irradiance.ghi, + self.solar_position.apparent_zenith, + self.times, + ) diff --git a/benchmarks/benchmarks/location.py b/benchmarks/benchmarks/location.py index 57ce125846..f0fad96a19 100644 --- a/benchmarks/benchmarks/location.py +++ b/benchmarks/benchmarks/location.py @@ -8,17 +8,17 @@ def set_solar_position(obj): - obj.location = pvlib.location.Location(32, -110, altitude=700, - tz='Etc/GMT+7') - obj.times = pd.date_range(start='20180601', freq='3min', - periods=1440) - obj.days = pd.date_range(start='20180101', freq='d', periods=365, - tz=obj.location.tz) + obj.location = pvlib.location.Location( + 32, -110, altitude=700, tz="Etc/GMT+7" + ) + obj.times = pd.date_range(start="20180601", freq="3min", periods=1440) + obj.days = pd.date_range( + start="20180101", freq="d", periods=365, tz=obj.location.tz + ) obj.solar_position = obj.location.get_solarposition(obj.times) class Location: - def setup(self): set_solar_position(self) @@ -30,22 +30,22 @@ def time_location_get_solarposition(self): self.location.get_solarposition(times=self.times) def time_location_get_clearsky(self): - self.location.get_clearsky(times=self.times, - solar_position=self.solar_position) + self.location.get_clearsky( + times=self.times, solar_position=self.solar_position + ) class Location_0_6_1: - def setup(self): - if Version(pvlib.__version__) < Version('0.6.1'): + if Version(pvlib.__version__) < Version("0.6.1"): raise NotImplementedError set_solar_position(self) def time_location_get_sun_rise_set_transit_pyephem(self): - self.location.get_sun_rise_set_transit(times=self.days, - method='pyephem') + self.location.get_sun_rise_set_transit( + times=self.days, method="pyephem" + ) def time_location_get_sun_rise_set_transit_spa(self): - self.location.get_sun_rise_set_transit(times=self.days, - method='spa') + self.location.get_sun_rise_set_transit(times=self.days, method="spa") diff --git a/benchmarks/benchmarks/scaling.py b/benchmarks/benchmarks/scaling.py index 8c2ed1471b..112be79f0d 100644 --- a/benchmarks/benchmarks/scaling.py +++ b/benchmarks/benchmarks/scaling.py @@ -8,20 +8,22 @@ class Scaling: - def setup(self): self.n = 1000 lat = np.array((9.99, 10, 10.01)) lon = np.array((4.99, 5, 5.01)) - self.coordinates = np.array([(lati, loni) for - (lati, loni) in zip(lat, lon)]) - self.times = pd.date_range('2019-01-01', freq='1T', periods=self.n) + self.coordinates = np.array( + [(lati, loni) for (lati, loni) in zip(lat, lon)] + ) + self.times = pd.date_range("2019-01-01", freq="1T", periods=self.n) self.positions = np.array([[0, 0], [100, 0], [100, 100], [0, 100]]) - self.clearsky_index = pd.Series(np.random.rand(self.n), - index=self.times) + self.clearsky_index = pd.Series( + np.random.rand(self.n), index=self.times + ) self.cloud_speed = 5 - self.tmscales = np.array((1, 2, 4, 8, 16, 32, 64, - 128, 256, 512, 1024, 2048, 4096)) + self.tmscales = np.array( + (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096) + ) def time_latlon_to_xy(self): scaling.latlon_to_xy(self.coordinates) @@ -33,5 +35,6 @@ def time__compute_vr(self): scaling._compute_vr(self.positions, self.cloud_speed, self.tmscales) def time_wvm(self): - scaling.wvm(self.clearsky_index, self.positions, - self.cloud_speed, dt=1) + scaling.wvm( + self.clearsky_index, self.positions, self.cloud_speed, dt=1 + ) diff --git a/benchmarks/benchmarks/solarposition.py b/benchmarks/benchmarks/solarposition.py index 0ccb379d8c..f074cb9398 100644 --- a/benchmarks/benchmarks/solarposition.py +++ b/benchmarks/benchmarks/solarposition.py @@ -10,7 +10,7 @@ from packaging.version import Version -if Version(pvlib.__version__) >= Version('0.6.1'): +if Version(pvlib.__version__) >= Version("0.6.1"): sun_rise_set_transit_spa = solarposition.sun_rise_set_transit_spa else: sun_rise_set_transit_spa = solarposition.get_sun_rise_set_transit @@ -18,16 +18,18 @@ class SolarPosition: params = [1, 10, 100] # number of days - param_names = ['ndays'] + param_names = ["ndays"] def setup(self, ndays): - self.times = pd.date_range(start='20180601', freq='1min', - periods=1440*ndays) - self.times_localized = self.times.tz_localize('Etc/GMT+7') + self.times = pd.date_range( + start="20180601", freq="1min", periods=1440 * ndays + ) + self.times_localized = self.times.tz_localize("Etc/GMT+7") self.lat = 35.1 self.lon = -106.6 self.times_daily = pd.date_range( - start='20180601', freq='24h', periods=ndays, tz='Etc/GMT+7') + start="20180601", freq="24h", periods=ndays, tz="Etc/GMT+7" + ) # GH 512 def time_ephemeris(self, ndays): @@ -48,22 +50,22 @@ def time_sun_rise_set_transit_spa(self, ndays): def time_sun_rise_set_transit_ephem(self, ndays): solarposition.sun_rise_set_transit_ephem( - self.times_daily, self.lat, self.lon) + self.times_daily, self.lat, self.lon + ) def time_sun_rise_set_transit_geometric_full_comparison(self, ndays): dayofyear = self.times_daily.dayofyear declination = solarposition.declination_spencer71(dayofyear) equation_of_time = solarposition.equation_of_time_spencer71(dayofyear) solarposition.sun_rise_set_transit_geometric( - self.times_daily, self.lat, self.lon, declination, - equation_of_time) + self.times_daily, self.lat, self.lon, declination, equation_of_time + ) def time_nrel_earthsun_distance(self, ndays): solarposition.nrel_earthsun_distance(self.times_localized) class SolarPositionCalcTime: - def setup(self): # test calc_time for finding times at which sun is 3 degrees # above the horizon. @@ -74,11 +76,15 @@ def setup(self): self.value = 0.05235987755982988 self.lat = 32.2 self.lon = -110.9 - self.attribute = 'alt' + self.attribute = "alt" def time_calc_time(self): # datetime.datetime(2020, 9, 14, 13, 24, 13, 861913, tzinfo=) solarposition.calc_time( - self.start, self.end, self.lat, self.lon, self.attribute, - self.value + self.start, + self.end, + self.lat, + self.lon, + self.attribute, + self.value, ) diff --git a/benchmarks/benchmarks/solarposition_numba.py b/benchmarks/benchmarks/solarposition_numba.py index 5d4c277eb1..e268eb9b1b 100644 --- a/benchmarks/benchmarks/solarposition_numba.py +++ b/benchmarks/benchmarks/solarposition_numba.py @@ -11,14 +11,15 @@ import pandas as pd import os -os.environ['PVLIB_USE_NUMBA'] = '1' + +os.environ["PVLIB_USE_NUMBA"] = "1" import pvlib # NOQA: E402 from pvlib import solarposition # NOQA: E402 -if Version(pvlib.__version__) >= Version('0.6.1'): +if Version(pvlib.__version__) >= Version("0.6.1"): sun_rise_set_transit_spa = solarposition.sun_rise_set_transit_spa else: sun_rise_set_transit_spa = solarposition.get_sun_rise_set_transit @@ -26,21 +27,25 @@ class SolarPositionNumba: params = [1, 10, 100] # number of days - param_names = ['ndays'] + param_names = ["ndays"] def setup(self, ndays): - self.times = pd.date_range(start='20180601', freq='1min', - periods=1440*ndays) - self.times_localized = self.times.tz_localize('Etc/GMT+7') + self.times = pd.date_range( + start="20180601", freq="1min", periods=1440 * ndays + ) + self.times_localized = self.times.tz_localize("Etc/GMT+7") self.lat = 35.1 self.lon = -106.6 self.times_daily = pd.date_range( - start='20180601', freq='24h', periods=ndays, tz='Etc/GMT+7') + start="20180601", freq="24h", periods=ndays, tz="Etc/GMT+7" + ) def time_spa_python(self, ndays): solarposition.spa_python( - self.times_localized, self.lat, self.lon, how='numba') + self.times_localized, self.lat, self.lon, how="numba" + ) def time_sun_rise_set_transit_spa(self, ndays): sun_rise_set_transit_spa( - self.times_daily, self.lat, self.lon, how='numba') + self.times_daily, self.lat, self.lon, how="numba" + ) diff --git a/benchmarks/benchmarks/temperature.py b/benchmarks/benchmarks/temperature.py index a2ffe331f5..b06a13a643 100644 --- a/benchmarks/benchmarks/temperature.py +++ b/benchmarks/benchmarks/temperature.py @@ -9,28 +9,28 @@ def set_weather_data(obj): - obj.times = pd.date_range(start='20180601', freq='1min', - periods=14400) + obj.times = pd.date_range(start="20180601", freq="1min", periods=14400) obj.poa = pd.Series(1000, index=obj.times) obj.tamb = pd.Series(20, index=obj.times) obj.wind_speed = pd.Series(2, index=obj.times) class SAPM: - def setup(self): set_weather_data(self) - if Version(pvlib.__version__) >= Version('0.7.0'): - kwargs = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'] - kwargs = kwargs['open_rack_glass_glass'] - self.sapm_cell_wrapper = partial(pvlib.temperature.sapm_cell, - **kwargs) + if Version(pvlib.__version__) >= Version("0.7.0"): + kwargs = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"] + kwargs = kwargs["open_rack_glass_glass"] + self.sapm_cell_wrapper = partial( + pvlib.temperature.sapm_cell, **kwargs + ) else: sapm_celltemp = pvlib.pvsystem.sapm_celltemp def sapm_cell_wrapper(poa_global, temp_air, wind_speed): # just swap order; model params are provided by default return sapm_celltemp(poa_global, wind_speed, temp_air) + self.sapm_cell_wrapper = sapm_cell_wrapper def time_sapm_cell(self): @@ -39,13 +39,13 @@ def time_sapm_cell(self): class Fuentes: - def setup(self): - if Version(pvlib.__version__) < Version('0.8.0'): + if Version(pvlib.__version__) < Version("0.8.0"): raise NotImplementedError set_weather_data(self) def time_fuentes(self): - pvlib.temperature.fuentes(self.poa, self.tamb, self.wind_speed, - noct_installed=45) + pvlib.temperature.fuentes( + self.poa, self.tamb, self.wind_speed, noct_installed=45 + ) diff --git a/benchmarks/benchmarks/tracking.py b/benchmarks/benchmarks/tracking.py index 29f5ac4c6e..0627582b61 100644 --- a/benchmarks/benchmarks/tracking.py +++ b/benchmarks/benchmarks/tracking.py @@ -8,22 +8,24 @@ class SingleAxis: - def setup(self): - self.times = pd.date_range(start='20180601', freq='1min', - periods=14400) + self.times = pd.date_range( + start="20180601", freq="1min", periods=14400 + ) self.lat = 35.1 self.lon = -106.6 - self.solar_position = solarposition.get_solarposition(self.times, - self.lat, - self.lon) + self.solar_position = solarposition.get_solarposition( + self.times, self.lat, self.lon + ) def time_singleaxis(self): - with np.errstate(invalid='ignore'): - tracking.singleaxis(self.solar_position.apparent_zenith, - self.solar_position.azimuth, - axis_tilt=0, - axis_azimuth=0, - max_angle=60, - backtrack=True, - gcr=0.45) + with np.errstate(invalid="ignore"): + tracking.singleaxis( + self.solar_position.apparent_zenith, + self.solar_position.azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=60, + backtrack=True, + gcr=0.45, + ) diff --git a/ci/requirements-py3.13.yml b/ci/requirements-py3.13.yml new file mode 100644 index 0000000000..f3d8fc2d0c --- /dev/null +++ b/ci/requirements-py3.13.yml @@ -0,0 +1,28 @@ +name: test_env +channels: + - defaults + - conda-forge +dependencies: + - coveralls + - cython + - ephem + - h5py + - numba + - numpy >= 1.17.3 + - pandas >= 1.3.0 + - pip + - pytest + - pytest-cov + - pytest-mock + - requests-mock + - pytest-timeout + - pytest-rerunfailures + - conda-forge::pytest-remotedata # version in default channel is old + - python=3.12 + - pytz + - requests + - scipy >= 1.6.0 + - statsmodels + - pip: + - nrel-pysam>=2.0 + # - solarfactors # required shapely<2 isn't available for 3.12 diff --git a/docs/examples/adr-pvarray/plot_fit_to_matrix.py b/docs/examples/adr-pvarray/plot_fit_to_matrix.py index b256262664..900cc9e896 100644 --- a/docs/examples/adr-pvarray/plot_fit_to_matrix.py +++ b/docs/examples/adr-pvarray/plot_fit_to_matrix.py @@ -26,7 +26,7 @@ # Here are some matrix measurements: # -iec61853data = ''' +iec61853data = """ irradiance temperature p_mp 0 100 15.0 30.159 1 200 15.0 63.057 @@ -50,7 +50,7 @@ 24 800 75.0 218.585 25 1000 75.0 273.651 26 1100 75.0 301.013 -''' +""" df = pd.read_csv(StringIO(iec61853data), delim_whitespace=True) # %% @@ -61,16 +61,17 @@ # simulate the module operating in a PV system. # -P_REF = 322.305 # (W) STC value from the table above -G_REF = 1000. # (W/m2) +P_REF = 322.305 # (W) STC value from the table above +G_REF = 1000.0 # (W/m2) -df['eta_rel'] = (df['p_mp'] / P_REF) / (df['irradiance'] / G_REF) +df["eta_rel"] = (df["p_mp"] / P_REF) / (df["irradiance"] / G_REF) -adr_params = fit_pvefficiency_adr(df['irradiance'], df['temperature'], - df['eta_rel']) +adr_params = fit_pvefficiency_adr( + df["irradiance"], df["temperature"], df["eta_rel"] +) for k, v in adr_params.items(): - print('%-5s = %8.5f' % (k, v)) + print("%-5s = %8.5f" % (k, v)) # %% # @@ -79,15 +80,16 @@ # they are most likely evidence of the limitations of measurement accuracy. # -eta_rel_adr = pvefficiency_adr(df['irradiance'], - df['temperature'], **adr_params) +eta_rel_adr = pvefficiency_adr( + df["irradiance"], df["temperature"], **adr_params +) plt.figure() -plt.plot(df['irradiance'], df['eta_rel'], 'oc', ms=8) -plt.plot(df['irradiance'], eta_rel_adr, '.k') -plt.legend(['Lab measurements', 'ADR model fit'], loc='lower right') -plt.xlabel('Irradiance [W/m²]') -plt.ylabel('Relative efficiency [-]') +plt.plot(df["irradiance"], df["eta_rel"], "oc", ms=8) +plt.plot(df["irradiance"], eta_rel_adr, ".k") +plt.legend(["Lab measurements", "ADR model fit"], loc="lower right") +plt.xlabel("Irradiance [W/m²]") +plt.ylabel("Relative efficiency [-]") plt.grid(alpha=0.5) plt.xlim(0, 1200) plt.ylim(0.7, 1.1) diff --git a/docs/examples/adr-pvarray/plot_simulate_fast.py b/docs/examples/adr-pvarray/plot_simulate_fast.py index 380744f626..db8fd19f58 100644 --- a/docs/examples/adr-pvarray/plot_simulate_fast.py +++ b/docs/examples/adr-pvarray/plot_simulate_fast.py @@ -29,18 +29,19 @@ # Generate a matrix of power values # -pvsyst_params = {'alpha_sc': 0.0015, - 'gamma_ref': 1.20585, - 'mu_gamma': -9.41066e-05, - 'I_L_ref': 5.9301, - 'I_o_ref': 2.9691e-10, - 'R_sh_ref': 1144, - 'R_sh_0': 3850, - 'R_s': 0.6, - 'cells_in_series': 96, - 'R_sh_exp': 5.5, - 'EgRef': 1.12, - } +pvsyst_params = { + "alpha_sc": 0.0015, + "gamma_ref": 1.20585, + "mu_gamma": -9.41066e-05, + "I_L_ref": 5.9301, + "I_o_ref": 2.9691e-10, + "R_sh_ref": 1144, + "R_sh_0": 3850, + "R_s": 0.6, + "cells_in_series": 96, + "R_sh_exp": 5.5, + "EgRef": 1.12, +} G_REF = 1000 T_REF = 25 @@ -48,19 +49,18 @@ params_stc = calcparams_pvsyst(G_REF, T_REF, **pvsyst_params) mpp_stc = max_power_point(*params_stc) -P_REF = mpp_stc['p_mp'] +P_REF = mpp_stc["p_mp"] -g, t = np.meshgrid(np.linspace(100, 1100, 11), - np.linspace(0, 75, 4)) +g, t = np.meshgrid(np.linspace(100, 1100, 11), np.linspace(0, 75, 4)) adjusted_params = calcparams_pvsyst(g, t, **pvsyst_params) mpp = max_power_point(*adjusted_params) -p_mp = mpp['p_mp'] +p_mp = mpp["p_mp"] -print('irradiance') +print("irradiance") print(g[:1].round(0)) -print('maximum power') +print("maximum power") print(p_mp.round(1)) # %% @@ -73,7 +73,7 @@ adr_params = fit_pvefficiency_adr(g, t, eta_rel_pvs, dict_output=True) for k, v in adr_params.items(): - print('%-5s = %8.5f' % (k, v)) + print("%-5s = %8.5f" % (k, v)) # %% # @@ -85,16 +85,16 @@ rmse = np.sqrt(np.mean(np.square(eta_rel_adr - eta_rel_pvs))) plt.figure() -plt.plot(g.flat, eta_rel_pvs.flat, 'oc', ms=8) -plt.plot(g.flat, eta_rel_adr.flat, '.k') +plt.plot(g.flat, eta_rel_pvs.flat, "oc", ms=8) +plt.plot(g.flat, eta_rel_adr.flat, ".k") plt.grid(alpha=0.5) plt.xlim(0, 1200) plt.ylim(0.7, 1.1) -plt.xlabel('Irradiance [W/m²]') -plt.ylabel('Relative efficiency [-]') -plt.legend(['PVsyst model output', 'ADR model fit'], loc='lower right') -plt.title('Differences: mean %.5f, RMS %.5f' % (mbe, rmse)) +plt.xlabel("Irradiance [W/m²]") +plt.ylabel("Relative efficiency [-]") +plt.legend(["PVsyst model output", "ADR model fit"], loc="lower right") +plt.title("Differences: mean %.5f, RMS %.5f" % (mbe, rmse)) plt.show() # %% @@ -103,7 +103,7 @@ # g = np.random.uniform(0, 1200, 8760) -t = np.random.uniform(20, 80, 8760) +t = np.random.uniform(20, 80, 8760) def run_adr(): @@ -115,16 +115,18 @@ def run_adr(): def run_pvsyst(): adjusted_params = calcparams_pvsyst(g, t, **pvsyst_params) mpp = max_power_point(*adjusted_params) - p_pvs = mpp['p_mp'] + p_pvs = mpp["p_mp"] return p_pvs -elapsed_adr = timeit('run_adr()', number=1, globals=globals()) -elapsed_pvs = timeit('run_pvsyst()', number=1, globals=globals()) +elapsed_adr = timeit("run_adr()", number=1, globals=globals()) +elapsed_pvs = timeit("run_pvsyst()", number=1, globals=globals()) -print('Elapsed time for the PVsyst model: %9.6f s' % elapsed_pvs) -print('Elapsed time for the ADR model: %9.6f s' % elapsed_adr) -print('ADR acceleration ratio: %9.0f x' % (elapsed_pvs/elapsed_adr)) +print("Elapsed time for the PVsyst model: %9.6f s" % elapsed_pvs) +print("Elapsed time for the ADR model: %9.6f s" % elapsed_adr) +print( + "ADR acceleration ratio: %9.0f x" % (elapsed_pvs / elapsed_adr) +) # %% # @@ -140,15 +142,15 @@ def run_pvsyst(): # sphinx_gallery_thumbnail_number = 2 plt.figure() -pc = plt.scatter(p_pvs, p_adr-p_pvs, c=t, cmap='jet') +pc = plt.scatter(p_pvs, p_adr - p_pvs, c=t, cmap="jet") plt.colorbar() pc.set_alpha(0.25) plt.ylim(-1.4, 1.4) plt.grid(alpha=0.5) -plt.xlabel('Power calculated using the PVsyst model [W]') -plt.ylabel('ADR model power - PVsyst model power [W]') -plt.title('Differences: mean %.2f W, RMS %.2f W' % (mbe, rmse)) +plt.xlabel("Power calculated using the PVsyst model [W]") +plt.ylabel("ADR model power - PVsyst model power [W]") +plt.title("Differences: mean %.2f W, RMS %.2f W" % (mbe, rmse)) plt.show() # %% diff --git a/docs/examples/adr-pvarray/plot_simulate_system.py b/docs/examples/adr-pvarray/plot_simulate_system.py index b0abede934..ce0e70f03a 100644 --- a/docs/examples/adr-pvarray/plot_simulate_system.py +++ b/docs/examples/adr-pvarray/plot_simulate_system.py @@ -27,15 +27,21 @@ # PVLIB_DIR = pvlib.__path__[0] -DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV') - -tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990, - map_variables=True) - -df = pd.DataFrame({'ghi': tmy['ghi'], 'dhi': tmy['dhi'], 'dni': tmy['dni'], - 'temp_air': tmy['temp_air'], - 'wind_speed': tmy['wind_speed'], - }) +DATA_FILE = os.path.join(PVLIB_DIR, "data", "723170TYA.CSV") + +tmy, metadata = iotools.read_tmy3( + DATA_FILE, coerce_year=1990, map_variables=True +) + +df = pd.DataFrame( + { + "ghi": tmy["ghi"], + "dhi": tmy["dhi"], + "dni": tmy["dni"], + "temp_air": tmy["temp_air"], + "wind_speed": tmy["wind_speed"], + } +) # %% # @@ -52,22 +58,29 @@ # Determine total irradiance on a fixed-tilt array # -TILT = metadata['latitude'] +TILT = metadata["latitude"] ORIENT = 180 -total_irrad = get_total_irradiance(TILT, ORIENT, - solpos.apparent_zenith, solpos.azimuth, - df.dni, df.ghi, df.dhi) +total_irrad = get_total_irradiance( + TILT, + ORIENT, + solpos.apparent_zenith, + solpos.azimuth, + df.dni, + df.ghi, + df.dhi, +) -df['poa_global'] = total_irrad.poa_global +df["poa_global"] = total_irrad.poa_global # %% # # Estimate the expected operating temperature of the PV modules # -df['temp_pv'] = pvlib.temperature.faiman(df.poa_global, df.temp_air, - df.wind_speed) +df["temp_pv"] = pvlib.temperature.faiman( + df.poa_global, df.temp_air, df.wind_speed +) # %% # @@ -89,22 +102,23 @@ # Borrow the ADR model parameters from the other example: -adr_params = {'k_a': 0.99924, - 'k_d': -5.49097, - 'tc_d': 0.01918, - 'k_rs': 0.06999, - 'k_rsh': 0.26144 - } +adr_params = { + "k_a": 0.99924, + "k_d": -5.49097, + "tc_d": 0.01918, + "k_rs": 0.06999, + "k_rsh": 0.26144, +} -df['eta_rel'] = pvefficiency_adr(df['poa_global'], df['temp_pv'], **adr_params) +df["eta_rel"] = pvefficiency_adr(df["poa_global"], df["temp_pv"], **adr_params) # Set the desired array size: -P_STC = 5000. # (W) +P_STC = 5000.0 # (W) # and the irradiance level needed to achieve this output: -G_STC = 1000. # (W/m2) +G_STC = 1000.0 # (W/m2) -df['p_mp'] = P_STC * df['eta_rel'] * (df['poa_global'] / G_STC) +df["p_mp"] = P_STC * df["eta_rel"] * (df["poa_global"] / G_STC) # %% # @@ -112,22 +126,22 @@ # plt.figure() -pc = plt.scatter(df['poa_global'], df['eta_rel'], c=df['temp_pv'], cmap='jet') -plt.colorbar(label='Temperature [C]', ax=plt.gca()) +pc = plt.scatter(df["poa_global"], df["eta_rel"], c=df["temp_pv"], cmap="jet") +plt.colorbar(label="Temperature [C]", ax=plt.gca()) pc.set_alpha(0.25) plt.grid(alpha=0.5) plt.ylim(0.48) -plt.xlabel('Irradiance [W/m²]') -plt.ylabel('Relative efficiency [-]') +plt.xlabel("Irradiance [W/m²]") +plt.ylabel("Relative efficiency [-]") plt.show() plt.figure() -pc = plt.scatter(df['poa_global'], df['p_mp'], c=df['temp_pv'], cmap='jet') -plt.colorbar(label='Temperature [C]', ax=plt.gca()) +pc = plt.scatter(df["poa_global"], df["p_mp"], c=df["temp_pv"], cmap="jet") +plt.colorbar(label="Temperature [C]", ax=plt.gca()) pc.set_alpha(0.25) plt.grid(alpha=0.5) -plt.xlabel('Irradiance [W/m²]') -plt.ylabel('Array power [W]') +plt.xlabel("Irradiance [W/m²]") +plt.ylabel("Array power [W]") plt.show() # %% @@ -135,12 +149,12 @@ # One day: # -DEMO_DAY = '1990-08-05' +DEMO_DAY = "1990-08-05" plt.figure() -plt.plot(df['p_mp'][DEMO_DAY]) +plt.plot(df["p_mp"][DEMO_DAY]) plt.xticks(rotation=30) -plt.ylabel('Power [W]') +plt.ylabel("Power [W]") plt.show() # %% diff --git a/docs/examples/bifacial/plot_bifi_model_mc.py b/docs/examples/bifacial/plot_bifi_model_mc.py index 679ff22921..79082c2e0e 100644 --- a/docs/examples/bifacial/plot_bifi_model_mc.py +++ b/docs/examples/bifacial/plot_bifi_model_mc.py @@ -25,7 +25,6 @@ # either ``pip install solarfactors`` or ``pip install pvlib[optional]``, # which installs all of pvlib's optional dependencies. - import pandas as pd from pvlib import pvsystem from pvlib import location @@ -35,12 +34,12 @@ import warnings # supressing shapely warnings that occur on import of pvfactors -warnings.filterwarnings(action='ignore', module='pvfactors') +warnings.filterwarnings(action="ignore", module="pvfactors") # create site location and times characteristics lat, lon = 36.084, -79.817 -tz = 'Etc/GMT+5' -times = pd.date_range('2021-06-21', '2021-6-22', freq='1T', tz=tz) +tz = "Etc/GMT+5" +times = pd.date_range("2021-06-21", "2021-6-22", freq="1T", tz=tz) # create site system characteristics axis_tilt = 0 @@ -53,53 +52,57 @@ bifaciality = 0.75 # load temperature parameters and module/inverter specifications -temp_model_parameters = PARAMS['sapm']['open_rack_glass_glass'] -cec_modules = pvsystem.retrieve_sam('CECMod') -cec_module = cec_modules['Trina_Solar_TSM_300DEG5C_07_II_'] -cec_inverters = pvsystem.retrieve_sam('cecinverter') -cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] +temp_model_parameters = PARAMS["sapm"]["open_rack_glass_glass"] +cec_modules = pvsystem.retrieve_sam("CECMod") +cec_module = cec_modules["Trina_Solar_TSM_300DEG5C_07_II_"] +cec_inverters = pvsystem.retrieve_sam("cecinverter") +cec_inverter = cec_inverters["ABB__MICRO_0_25_I_OUTD_US_208__208V_"] # create a location for site, and get solar position and clearsky data -site_location = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +site_location = location.Location(lat, lon, tz=tz, name="Greensboro, NC") solar_position = site_location.get_solarposition(times) cs = site_location.get_clearsky(times) # load solar position and tracker orientation for use in pvsystem object -sat_mount = pvsystem.SingleAxisTrackerMount(axis_tilt=axis_tilt, - axis_azimuth=axis_azimuth, - max_angle=max_angle, - backtrack=True, - gcr=gcr) +sat_mount = pvsystem.SingleAxisTrackerMount( + axis_tilt=axis_tilt, + axis_azimuth=axis_azimuth, + max_angle=max_angle, + backtrack=True, + gcr=gcr, +) # created for use in pvfactors timeseries -orientation = sat_mount.get_orientation(solar_position['apparent_zenith'], - solar_position['azimuth']) +orientation = sat_mount.get_orientation( + solar_position["apparent_zenith"], solar_position["azimuth"] +) # get rear and front side irradiance from pvfactors transposition engine # explicity simulate on pvarray with 3 rows, with sensor placed in middle row # users may select different values depending on needs -irrad = pvfactors_timeseries(solar_position['azimuth'], - solar_position['apparent_zenith'], - orientation['surface_azimuth'], - orientation['surface_tilt'], - axis_azimuth, - times, - cs['dni'], - cs['dhi'], - gcr, - pvrow_height, - pvrow_width, - albedo, - n_pvrows=3, - index_observed_pvrow=1 - ) +irrad = pvfactors_timeseries( + solar_position["azimuth"], + solar_position["apparent_zenith"], + orientation["surface_azimuth"], + orientation["surface_tilt"], + axis_azimuth, + times, + cs["dni"], + cs["dhi"], + gcr, + pvrow_height, + pvrow_width, + albedo, + n_pvrows=3, + index_observed_pvrow=1, +) # turn into pandas DataFrame irrad = pd.concat(irrad, axis=1) # create bifacial effective irradiance using aoi-corrected timeseries values -irrad['effective_irradiance'] = ( - irrad['total_abs_front'] + (irrad['total_abs_back'] * bifaciality) +irrad["effective_irradiance"] = irrad["total_abs_front"] + ( + irrad["total_abs_back"] * bifaciality ) # %% @@ -107,21 +110,23 @@ # bifacial simulation. # dc arrays -array = pvsystem.Array(mount=sat_mount, - module_parameters=cec_module, - temperature_model_parameters=temp_model_parameters) +array = pvsystem.Array( + mount=sat_mount, + module_parameters=cec_module, + temperature_model_parameters=temp_model_parameters, +) # create system object -system = pvsystem.PVSystem(arrays=[array], - inverter_parameters=cec_inverter) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters=cec_inverter) # ModelChain requires the parameter aoi_loss to have a value. pvfactors # applies surface reflection models in the calculation of front and back # irradiance, so assign aoi_model='no_loss' to avoid double counting # reflections. -mc_bifi = modelchain.ModelChain(system, site_location, aoi_model='no_loss') +mc_bifi = modelchain.ModelChain(system, site_location, aoi_model="no_loss") mc_bifi.run_model_from_effective_irradiance(irrad) # plot results -mc_bifi.results.ac.plot(title='Bifacial Simulation on June Solstice', - ylabel='AC Power') +mc_bifi.results.ac.plot( + title="Bifacial Simulation on June Solstice", ylabel="AC Power" +) diff --git a/docs/examples/bifacial/plot_bifi_model_pvwatts.py b/docs/examples/bifacial/plot_bifi_model_pvwatts.py index 76a813fd4c..3c7cd6a156 100644 --- a/docs/examples/bifacial/plot_bifi_model_pvwatts.py +++ b/docs/examples/bifacial/plot_bifi_model_pvwatts.py @@ -27,15 +27,15 @@ import warnings # supressing shapely warnings that occur on import of pvfactors -warnings.filterwarnings(action='ignore', module='pvfactors') +warnings.filterwarnings(action="ignore", module="pvfactors") # using Greensboro, NC for this example lat, lon = 36.084, -79.817 -tz = 'Etc/GMT+5' -times = pd.date_range('2021-06-21', '2021-06-22', freq='1T', tz=tz) +tz = "Etc/GMT+5" +times = pd.date_range("2021-06-21", "2021-06-22", freq="1T", tz=tz) # create location object and get clearsky data -site_location = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +site_location = location.Location(lat, lon, tz=tz, name="Greensboro, NC") cs = site_location.get_clearsky(times) # get solar position data @@ -45,12 +45,13 @@ # pull orientation data for a single-axis tracker gcr = 0.35 max_phi = 60 -orientation = tracking.singleaxis(solar_position['apparent_zenith'], - solar_position['azimuth'], - max_angle=max_phi, - backtrack=True, - gcr=gcr - ) +orientation = tracking.singleaxis( + solar_position["apparent_zenith"], + solar_position["azimuth"], + max_angle=max_phi, + backtrack=True, + gcr=gcr, +) # set axis_azimuth, albedo, pvrow width and height, and use # the pvfactors engine for both front and rear-side absorbed irradiance @@ -61,61 +62,58 @@ # explicity simulate on pvarray with 3 rows, with sensor placed in middle row # users may select different values depending on needs -irrad = pvfactors_timeseries(solar_position['azimuth'], - solar_position['apparent_zenith'], - orientation['surface_azimuth'], - orientation['surface_tilt'], - axis_azimuth, - cs.index, - cs['dni'], - cs['dhi'], - gcr, - pvrow_height, - pvrow_width, - albedo, - n_pvrows=3, - index_observed_pvrow=1 - ) +irrad = pvfactors_timeseries( + solar_position["azimuth"], + solar_position["apparent_zenith"], + orientation["surface_azimuth"], + orientation["surface_tilt"], + axis_azimuth, + cs.index, + cs["dni"], + cs["dhi"], + gcr, + pvrow_height, + pvrow_width, + albedo, + n_pvrows=3, + index_observed_pvrow=1, +) # turn into pandas DataFrame irrad = pd.concat(irrad, axis=1) # using bifaciality factor and pvfactors results, create effective irradiance bifaciality = 0.75 -effective_irrad_bifi = irrad['total_abs_front'] + (irrad['total_abs_back'] - * bifaciality) +effective_irrad_bifi = irrad["total_abs_front"] + ( + irrad["total_abs_back"] * bifaciality +) # get cell temperature using the Faiman model -temp_cell = temperature.faiman(effective_irrad_bifi, temp_air=25, - wind_speed=1) +temp_cell = temperature.faiman(effective_irrad_bifi, temp_air=25, wind_speed=1) # using the pvwatts_dc model and parameters detailed above, # set pdc0 and return DC power for both bifacial and monofacial pdc0 = 1 gamma_pdc = -0.0043 -pdc_bifi = pvsystem.pvwatts_dc(effective_irrad_bifi, - temp_cell, - pdc0, - gamma_pdc=gamma_pdc - ).fillna(0) -pdc_bifi.plot(title='Bifacial Simulation on June Solstice', ylabel='DC Power') +pdc_bifi = pvsystem.pvwatts_dc( + effective_irrad_bifi, temp_cell, pdc0, gamma_pdc=gamma_pdc +).fillna(0) +pdc_bifi.plot(title="Bifacial Simulation on June Solstice", ylabel="DC Power") # %% # For illustration, perform monofacial simulation using pvfactors front-side # irradiance (AOI-corrected), and plot along with bifacial results. -effective_irrad_mono = irrad['total_abs_front'] -pdc_mono = pvsystem.pvwatts_dc(effective_irrad_mono, - temp_cell, - pdc0, - gamma_pdc=gamma_pdc - ).fillna(0) +effective_irrad_mono = irrad["total_abs_front"] +pdc_mono = pvsystem.pvwatts_dc( + effective_irrad_mono, temp_cell, pdc0, gamma_pdc=gamma_pdc +).fillna(0) # plot monofacial results plt.figure() -plt.title('Bifacial vs Monofacial Simulation - June Solstice') -pdc_bifi.plot(label='Bifacial') -pdc_mono.plot(label='Monofacial') -plt.ylabel('DC Power') +plt.title("Bifacial vs Monofacial Simulation - June Solstice") +pdc_bifi.plot(label="Bifacial") +pdc_mono.plot(label="Monofacial") +plt.ylabel("DC Power") plt.legend() # sphinx_gallery_thumbnail_number = 2 diff --git a/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py b/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py index 63d7db902a..09ce7abe52 100644 --- a/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py +++ b/docs/examples/bifacial/plot_pvfactors_fixed_tilt.py @@ -25,12 +25,12 @@ import warnings # supressing shapely warnings that occur on import of pvfactors -warnings.filterwarnings(action='ignore', module='pvfactors') +warnings.filterwarnings(action="ignore", module="pvfactors") # %% # First, generate the usual modeling inputs: -times = pd.date_range('2021-06-21', '2021-06-22', freq='1T', tz='Etc/GMT+5') +times = pd.date_range("2021-06-21", "2021-06-22", freq="1T", tz="Etc/GMT+5") loc = location.Location(latitude=40, longitude=-80, tz=times.tz) sp = loc.get_solarposition(times) cs = loc.get_clearsky(times) @@ -51,24 +51,24 @@ # fixed ``surface_azimuth``. irrad = pvfactors_timeseries( - solar_azimuth=sp['azimuth'], - solar_zenith=sp['apparent_zenith'], + solar_azimuth=sp["azimuth"], + solar_zenith=sp["apparent_zenith"], surface_azimuth=180, # south-facing array surface_tilt=20, axis_azimuth=90, # 90 degrees off from surface_azimuth. 270 is ok too timestamps=times, - dni=cs['dni'], - dhi=cs['dhi'], + dni=cs["dni"], + dhi=cs["dhi"], gcr=gcr, pvrow_height=pvrow_height, pvrow_width=pvrow_width, albedo=albedo, n_pvrows=3, - index_observed_pvrow=1 + index_observed_pvrow=1, ) # turn into pandas DataFrame irrad = pd.concat(irrad, axis=1) -irrad[['total_inc_back', 'total_abs_back']].plot() -plt.ylabel('Irradiance [W m$^{-2}$]') +irrad[["total_inc_back", "total_abs_back"]].plot() +plt.ylabel("Irradiance [W m$^{-2}$]") diff --git a/docs/examples/floating-pv/plot_floating_pv_cell_temperature.py b/docs/examples/floating-pv/plot_floating_pv_cell_temperature.py index c1b7c93f97..2597cc852f 100644 --- a/docs/examples/floating-pv/plot_floating_pv_cell_temperature.py +++ b/docs/examples/floating-pv/plot_floating_pv_cell_temperature.py @@ -128,45 +128,45 @@ azimuth = 180 # south-facing # Datafile found in the pvlib distribution -data_file = Path(pvlib.__path__[0]).joinpath('data', '723170TYA.CSV') +data_file = Path(pvlib.__path__[0]).joinpath("data", "723170TYA.CSV") tmy, metadata = pvlib.iotools.read_tmy3( data_file, coerce_year=2002, map_variables=True ) tmy = tmy.filter( - ['ghi', 'dni', 'dni_extra', 'dhi', 'temp_air', 'wind_speed', 'pressure'] + ["ghi", "dni", "dni_extra", "dhi", "temp_air", "wind_speed", "pressure"] ) # remaining columns are not needed -tmy = tmy['2002-06-06 00:00':'2002-06-06 23:59'] # select period +tmy = tmy["2002-06-06 00:00":"2002-06-06 23:59"] # select period solar_position = pvlib.solarposition.get_solarposition( # TMY timestamp is at end of hour, so shift to center of interval - tmy.index.shift(freq='-30T'), - latitude=metadata['latitude'], - longitude=metadata['longitude'], - altitude=metadata['altitude'], - pressure=tmy['pressure'] * 100, # convert from millibar to Pa - temperature=tmy['temp_air'], + tmy.index.shift(freq="-30T"), + latitude=metadata["latitude"], + longitude=metadata["longitude"], + altitude=metadata["altitude"], + pressure=tmy["pressure"] * 100, # convert from millibar to Pa + temperature=tmy["temp_air"], ) solar_position.index = tmy.index # reset index to end of the hour # Albedo calculation for inland water bodies albedo = pvlib.albedo.inland_water_dvoracek( - solar_elevation=solar_position['elevation'], - surface_condition='clear_water_no_waves' + solar_elevation=solar_position["elevation"], + surface_condition="clear_water_no_waves", ) # Use transposition model to find plane-of-array irradiance irradiance = pvlib.irradiance.get_total_irradiance( surface_tilt=tilt, surface_azimuth=azimuth, - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth'], - dni=tmy['dni'], - dni_extra=tmy['dni_extra'], - ghi=tmy['ghi'], - dhi=tmy['dhi'], + solar_zenith=solar_position["apparent_zenith"], + solar_azimuth=solar_position["azimuth"], + dni=tmy["dni"], + dni_extra=tmy["dni_extra"], + ghi=tmy["ghi"], + dhi=tmy["dhi"], albedo=albedo, - model='haydavies' + model="haydavies", ) # %% @@ -178,43 +178,53 @@ # Make a dictionary containing all the sets of coefficients presented in the # above table. heat_loss_coeffs = { - 'open_structure_small_footprint_tracking_NL': [24.4, 6.5, 'C0', 'solid'], - 'open_structure_small_footprint_tracking_NL_2': [57, 0, 'C0', 'dashed'], - 'closed_structure_large_footprint_NL': [25.2, 3.7, 'C1', 'solid'], - 'closed_structure_large_footprint_NL_2': [37, 0, 'C1', 'dashed'], - 'closed_structure_large_footprint_SG': [34.8, 0.8, 'C2', 'solid'], - 'closed_structure_large_footprint_SG_2': [36, 0, 'C2', 'dashed'], - 'closed_structure_medium_footprint_SG': [18.9, 8.9, 'C3', 'solid'], - 'closed_structure_medium_footprint_SG_2': [41, 0, 'C3', 'dashed'], - 'open_structure_free_standing_SG': [35.3, 8.9, 'C4', 'solid'], - 'open_structure_free_standing_SG_2': [55, 0, 'C4', 'dashed'], - 'in_contact_with_water_NO': [71, 0, 'C5', 'solid'], - 'open_structure_free_standing_IT': [31.9, 1.5, 'C6', 'solid'], - 'open_structure_free_standing_bifacial_IT': [35.2, 1.5, 'C7', 'solid'], - 'default_PVSyst_coeffs_for_land_systems': [29.0, 0, 'C8', 'solid'] + "open_structure_small_footprint_tracking_NL": [24.4, 6.5, "C0", "solid"], + "open_structure_small_footprint_tracking_NL_2": [57, 0, "C0", "dashed"], + "closed_structure_large_footprint_NL": [25.2, 3.7, "C1", "solid"], + "closed_structure_large_footprint_NL_2": [37, 0, "C1", "dashed"], + "closed_structure_large_footprint_SG": [34.8, 0.8, "C2", "solid"], + "closed_structure_large_footprint_SG_2": [36, 0, "C2", "dashed"], + "closed_structure_medium_footprint_SG": [18.9, 8.9, "C3", "solid"], + "closed_structure_medium_footprint_SG_2": [41, 0, "C3", "dashed"], + "open_structure_free_standing_SG": [35.3, 8.9, "C4", "solid"], + "open_structure_free_standing_SG_2": [55, 0, "C4", "dashed"], + "in_contact_with_water_NO": [71, 0, "C5", "solid"], + "open_structure_free_standing_IT": [31.9, 1.5, "C6", "solid"], + "open_structure_free_standing_bifacial_IT": [35.2, 1.5, "C7", "solid"], + "default_PVSyst_coeffs_for_land_systems": [29.0, 0, "C8", "solid"], } # Plot the cell temperature for each set of the above heat loss coefficients for coeffs in heat_loss_coeffs: T_cell = pvlib.temperature.pvsyst_cell( - poa_global=irradiance['poa_global'], - temp_air=tmy['temp_air'], - wind_speed=tmy['wind_speed'], + poa_global=irradiance["poa_global"], + temp_air=tmy["temp_air"], + wind_speed=tmy["wind_speed"], u_c=heat_loss_coeffs[coeffs][0], - u_v=heat_loss_coeffs[coeffs][1] + u_v=heat_loss_coeffs[coeffs][1], ) # Convert Dataframe Indexes to Hour format to make plotting easier T_cell.index = T_cell.index.strftime("%H") - plt.plot(T_cell, label=coeffs, c=heat_loss_coeffs[coeffs][2], - ls=heat_loss_coeffs[coeffs][3], alpha=0.8) + plt.plot( + T_cell, + label=coeffs, + c=heat_loss_coeffs[coeffs][2], + ls=heat_loss_coeffs[coeffs][3], + alpha=0.8, + ) -plt.xlabel('Hour') -plt.ylabel('PV cell temperature [°C]') +plt.xlabel("Hour") +plt.ylabel("PV cell temperature [°C]") plt.ylim(10, 45) -plt.xlim('06', '20') +plt.xlim("06", "20") plt.grid() -plt.legend(loc='upper left', frameon=False, ncols=2, fontsize='x-small', - bbox_to_anchor=(0, -0.2)) +plt.legend( + loc="upper left", + frameon=False, + ncols=2, + fontsize="x-small", + bbox_to_anchor=(0, -0.2), +) plt.tight_layout() plt.show() diff --git a/docs/examples/irradiance-decomposition/plot_diffuse_fraction.py b/docs/examples/irradiance-decomposition/plot_diffuse_fraction.py index 1c33824356..adae4d8ed7 100644 --- a/docs/examples/irradiance-decomposition/plot_diffuse_fraction.py +++ b/docs/examples/irradiance-decomposition/plot_diffuse_fraction.py @@ -26,19 +26,23 @@ # in the pvlib data directory. TMY3 are made from the median months from years # of data measured from 1990 to 2010. Therefore we change the timestamps to a # common year, 1990. -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' -greensboro, metadata = read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990, - map_variables=True) +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" +greensboro, metadata = read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=1990, map_variables=True +) # Many of the diffuse fraction estimation methods require the "true" zenith, so # we calculate the solar positions for the 1990 at Greensboro, NC. # NOTE: TMY3 files timestamps indicate the end of the hour, so shift indices # back 30-minutes to calculate solar position at center of the interval solpos = get_solarposition( - greensboro.index.shift(freq="-30T"), latitude=metadata['latitude'], - longitude=metadata['longitude'], altitude=metadata['altitude'], - pressure=greensboro.pressure*100, # convert from millibar to Pa - temperature=greensboro.temp_air) + greensboro.index.shift(freq="-30T"), + latitude=metadata["latitude"], + longitude=metadata["longitude"], + altitude=metadata["altitude"], + pressure=greensboro.pressure * 100, # convert from millibar to Pa + temperature=greensboro.temp_air, +) solpos.index = greensboro.index # reset index to end of the hour # %% @@ -57,13 +61,17 @@ # an exponential relation with airmass. out_disc = irradiance.disc( - greensboro.ghi, solpos.zenith, greensboro.index, greensboro.pressure*100) + greensboro.ghi, solpos.zenith, greensboro.index, greensboro.pressure * 100 +) # use "complete sum" AKA "closure" equations: DHI = GHI - DNI * cos(zenith) df_disc = irradiance.complete_irradiance( - solar_zenith=solpos.apparent_zenith, ghi=greensboro.ghi, dni=out_disc.dni, - dhi=None) -out_disc = out_disc.rename(columns={'dni': 'dni_disc'}) -out_disc['dhi_disc'] = df_disc.dhi + solar_zenith=solpos.apparent_zenith, + ghi=greensboro.ghi, + dni=out_disc.dni, + dhi=None, +) +out_disc = out_disc.rename(columns={"dni": "dni_disc"}) +out_disc["dhi_disc"] = df_disc.dhi # %% # DIRINT @@ -73,15 +81,23 @@ # developed by Richard Perez and Pierre Ineichen in 1992. dni_dirint = irradiance.dirint( - greensboro.ghi, solpos.zenith, greensboro.index, greensboro.pressure*100, - temp_dew=greensboro.temp_dew) + greensboro.ghi, + solpos.zenith, + greensboro.index, + greensboro.pressure * 100, + temp_dew=greensboro.temp_dew, +) # use "complete sum" AKA "closure" equation: DHI = GHI - DNI * cos(zenith) df_dirint = irradiance.complete_irradiance( - solar_zenith=solpos.apparent_zenith, ghi=greensboro.ghi, dni=dni_dirint, - dhi=None) + solar_zenith=solpos.apparent_zenith, + ghi=greensboro.ghi, + dni=dni_dirint, + dhi=None, +) out_dirint = pd.DataFrame( - {'dni_dirint': dni_dirint, 'dhi_dirint': df_dirint.dhi}, - index=greensboro.index) + {"dni_dirint": dni_dirint, "dhi_dirint": df_dirint.dhi}, + index=greensboro.index, +) # %% # Erbs @@ -93,7 +109,7 @@ # between 0.22 < kt <= 0.8, and a horizontal line for kt > 0.8. out_erbs = irradiance.erbs(greensboro.ghi, solpos.zenith, greensboro.index) -out_erbs = out_erbs.rename(columns={'dni': 'dni_erbs', 'dhi': 'dhi_erbs'}) +out_erbs = out_erbs.rename(columns={"dni": "dni_erbs", "dhi": "dhi_erbs"}) # %% # Boland @@ -105,7 +121,8 @@ out_boland = irradiance.boland(greensboro.ghi, solpos.zenith, greensboro.index) out_boland = out_boland.rename( - columns={'dni': 'dni_boland', 'dhi': 'dhi_boland'}) + columns={"dni": "dni_boland", "dhi": "dhi_boland"} +) # %% # Comparison Plots @@ -119,38 +136,54 @@ # file together to make plotting easier. dni_renames = { - 'dni': 'TMY3', 'dni_disc': 'DISC', 'dni_dirint': 'DIRINT', - 'dni_erbs': 'Erbs', 'dni_boland': 'Boland'} + "dni": "TMY3", + "dni_disc": "DISC", + "dni_dirint": "DIRINT", + "dni_erbs": "Erbs", + "dni_boland": "Boland", +} dni = [ - greensboro.dni, out_disc.dni_disc, out_dirint.dni_dirint, - out_erbs.dni_erbs, out_boland.dni_boland] + greensboro.dni, + out_disc.dni_disc, + out_dirint.dni_dirint, + out_erbs.dni_erbs, + out_boland.dni_boland, +] dni = pd.concat(dni, axis=1).rename(columns=dni_renames) dhi_renames = { - 'dhi': 'TMY3', 'dhi_disc': 'DISC', 'dhi_dirint': 'DIRINT', - 'dhi_erbs': 'Erbs', 'dhi_boland': 'Boland'} + "dhi": "TMY3", + "dhi_disc": "DISC", + "dhi_dirint": "DIRINT", + "dhi_erbs": "Erbs", + "dhi_boland": "Boland", +} dhi = [ - greensboro.dhi, out_disc.dhi_disc, out_dirint.dhi_dirint, - out_erbs.dhi_erbs, out_boland.dhi_boland] + greensboro.dhi, + out_disc.dhi_disc, + out_dirint.dhi_dirint, + out_erbs.dhi_erbs, + out_boland.dhi_boland, +] dhi = pd.concat(dhi, axis=1).rename(columns=dhi_renames) -ghi_kt = pd.concat([greensboro.ghi/1000.0, out_erbs.kt], axis=1) +ghi_kt = pd.concat([greensboro.ghi / 1000.0, out_erbs.kt], axis=1) # %% # Winter # ++++++ # Finally, let's plot them for a few winter days and compare -JAN04, JAN07 = '1990-01-04 00:00:00-05:00', '1990-01-07 23:59:59-05:00' +JAN04, JAN07 = "1990-01-04 00:00:00-05:00", "1990-01-07 23:59:59-05:00" f, ax = plt.subplots(3, 1, figsize=(8, 10), sharex=True) dni[JAN04:JAN07].plot(ax=ax[0]) ax[0].grid(which="both") -ax[0].set_ylabel('DNI $[W/m^2]$') -ax[0].set_title('Comparison of Diffuse Fraction Estimation Methods') +ax[0].set_ylabel("DNI $[W/m^2]$") +ax[0].set_title("Comparison of Diffuse Fraction Estimation Methods") dhi[JAN04:JAN07].plot(ax=ax[1]) ax[1].grid(which="both") -ax[1].set_ylabel('DHI $[W/m^2]$') +ax[1].set_ylabel("DHI $[W/m^2]$") ghi_kt[JAN04:JAN07].plot(ax=ax[2]) -ax[2].grid(which='both') -ax[2].set_ylabel(r'$\frac{GHI}{E0}, k_t$') +ax[2].grid(which="both") +ax[2].set_ylabel(r"$\frac{GHI}{E0}, k_t$") f.tight_layout() # %% @@ -158,18 +191,18 @@ # ++++++ # And a few spring days ... -APR04, APR07 = '1990-04-04 00:00:00-05:00', '1990-04-07 23:59:59-05:00' +APR04, APR07 = "1990-04-04 00:00:00-05:00", "1990-04-07 23:59:59-05:00" f, ax = plt.subplots(3, 1, figsize=(8, 10), sharex=True) dni[APR04:APR07].plot(ax=ax[0]) ax[0].grid(which="both") -ax[0].set_ylabel('DNI $[W/m^2]$') -ax[0].set_title('Comparison of Diffuse Fraction Estimation Methods') +ax[0].set_ylabel("DNI $[W/m^2]$") +ax[0].set_title("Comparison of Diffuse Fraction Estimation Methods") dhi[APR04:APR07].plot(ax=ax[1]) ax[1].grid(which="both") -ax[1].set_ylabel('DHI $[W/m^2]$') +ax[1].set_ylabel("DHI $[W/m^2]$") ghi_kt[APR04:APR07].plot(ax=ax[2]) -ax[2].grid(which='both') -ax[2].set_ylabel(r'$\frac{GHI}{E0}, k_t$') +ax[2].grid(which="both") +ax[2].set_ylabel(r"$\frac{GHI}{E0}, k_t$") f.tight_layout() # %% @@ -177,18 +210,18 @@ # ++++++ # And few summer days to finish off the seasons. -JUL04, JUL07 = '1990-07-04 00:00:00-05:00', '1990-07-07 23:59:59-05:00' +JUL04, JUL07 = "1990-07-04 00:00:00-05:00", "1990-07-07 23:59:59-05:00" f, ax = plt.subplots(3, 1, figsize=(8, 10), sharex=True) dni[JUL04:JUL07].plot(ax=ax[0]) ax[0].grid(which="both") -ax[0].set_ylabel('DNI $[W/m^2]$') -ax[0].set_title('Comparison of Diffuse Fraction Estimation Methods') +ax[0].set_ylabel("DNI $[W/m^2]$") +ax[0].set_title("Comparison of Diffuse Fraction Estimation Methods") dhi[JUL04:JUL07].plot(ax=ax[1]) ax[1].grid(which="both") -ax[1].set_ylabel('DHI $[W/m^2]$') +ax[1].set_ylabel("DHI $[W/m^2]$") ghi_kt[JUL04:JUL07].plot(ax=ax[2]) -ax[2].grid(which='both') -ax[2].set_ylabel(r'$\frac{GHI}{E0}, k_t$') +ax[2].grid(which="both") +ax[2].set_ylabel(r"$\frac{GHI}{E0}, k_t$") f.tight_layout() # %% diff --git a/docs/examples/irradiance-transposition/plot_ghi_transposition.py b/docs/examples/irradiance-transposition/plot_ghi_transposition.py index 5eeb27ca36..a73a1f19fc 100644 --- a/docs/examples/irradiance-transposition/plot_ghi_transposition.py +++ b/docs/examples/irradiance-transposition/plot_ghi_transposition.py @@ -18,7 +18,7 @@ from matplotlib import pyplot as plt # For this example, we will be using Golden, Colorado -tz = 'MST' +tz = "MST" lat, lon = 39.755, -105.221 # Create location object to store lat, lon, timezone @@ -30,8 +30,9 @@ # different locations def get_irradiance(site_location, date, tilt, surface_azimuth): # Creates one day's worth of 10 min intervals - times = pd.date_range(date, freq='10min', periods=6*24, - tz=site_location.tz) + times = pd.date_range( + date, freq="10min", periods=6 * 24, tz=site_location.tz + ) # Generate clearsky data using the Ineichen model, which is the default # The get_clearsky method returns a dataframe with values for GHI, DNI, # and DHI @@ -42,20 +43,22 @@ def get_irradiance(site_location, date, tilt, surface_azimuth): POA_irradiance = irradiance.get_total_irradiance( surface_tilt=tilt, surface_azimuth=surface_azimuth, - dni=clearsky['dni'], - ghi=clearsky['ghi'], - dhi=clearsky['dhi'], - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth']) + dni=clearsky["dni"], + ghi=clearsky["ghi"], + dhi=clearsky["dhi"], + solar_zenith=solar_position["apparent_zenith"], + solar_azimuth=solar_position["azimuth"], + ) # Return DataFrame with only GHI and POA - return pd.DataFrame({'GHI': clearsky['ghi'], - 'POA': POA_irradiance['poa_global']}) + return pd.DataFrame( + {"GHI": clearsky["ghi"], "POA": POA_irradiance["poa_global"]} + ) # Get irradiance data for summer and winter solstice, assuming 25 degree tilt # and a south facing array -summer_irradiance = get_irradiance(site, '06-20-2020', 25, 180) -winter_irradiance = get_irradiance(site, '12-21-2020', 25, 180) +summer_irradiance = get_irradiance(site, "06-20-2020", 25, 180) +winter_irradiance = get_irradiance(site, "12-21-2020", 25, 180) # Convert Dataframe Indexes to Hour:Minute format to make plotting easier summer_irradiance.index = summer_irradiance.index.strftime("%H:%M") @@ -63,13 +66,13 @@ def get_irradiance(site_location, date, tilt, surface_azimuth): # Plot GHI vs. POA for winter and summer fig, (ax1, ax2) = plt.subplots(1, 2, sharey=True) -summer_irradiance['GHI'].plot(ax=ax1, label='GHI') -summer_irradiance['POA'].plot(ax=ax1, label='POA') -winter_irradiance['GHI'].plot(ax=ax2, label='GHI') -winter_irradiance['POA'].plot(ax=ax2, label='POA') -ax1.set_xlabel('Time of day (Summer)') -ax2.set_xlabel('Time of day (Winter)') -ax1.set_ylabel('Irradiance ($W/m^2$)') +summer_irradiance["GHI"].plot(ax=ax1, label="GHI") +summer_irradiance["POA"].plot(ax=ax1, label="POA") +winter_irradiance["GHI"].plot(ax=ax2, label="GHI") +winter_irradiance["POA"].plot(ax=ax2, label="POA") +ax1.set_xlabel("Time of day (Summer)") +ax2.set_xlabel("Time of day (Winter)") +ax1.set_ylabel("Irradiance ($W/m^2$)") ax1.legend() ax2.legend() plt.show() diff --git a/docs/examples/irradiance-transposition/plot_interval_transposition_error.py b/docs/examples/irradiance-transposition/plot_interval_transposition_error.py index 76adfed932..50310d428b 100644 --- a/docs/examples/irradiance-transposition/plot_interval_transposition_error.py +++ b/docs/examples/irradiance-transposition/plot_interval_transposition_error.py @@ -67,7 +67,7 @@ def transpose(irradiance, timeshift): """ idx = irradiance.index # calculate solar position for shifted timestamps: - idx = idx + pd.Timedelta(timeshift, unit='min') + idx = idx + pd.Timedelta(timeshift, unit="min") solpos = location.get_solarposition(idx) # but still report the values with the original timestamps: solpos.index = irradiance.index @@ -75,14 +75,14 @@ def transpose(irradiance, timeshift): poa_components = pvlib.irradiance.get_total_irradiance( surface_tilt=20, surface_azimuth=180, - solar_zenith=solpos['apparent_zenith'], - solar_azimuth=solpos['azimuth'], - dni=irradiance['dni'], - ghi=irradiance['ghi'], - dhi=irradiance['dhi'], - model='isotropic', + solar_zenith=solpos["apparent_zenith"], + solar_azimuth=solpos["azimuth"], + dni=irradiance["dni"], + ghi=irradiance["ghi"], + dhi=irradiance["dhi"], + model="isotropic", ) - return poa_components['poa_global'] + return poa_components["poa_global"] # %% @@ -93,9 +93,10 @@ def transpose(irradiance, timeshift): # is negligible. # baseline: all calculations done at 1-second scale -location = pvlib.location.Location(40, -80, tz='Etc/GMT+5') -times = pd.date_range('2019-06-01 05:00', '2019-06-01 19:00', - freq='1s', tz='Etc/GMT+5') +location = pvlib.location.Location(40, -80, tz="Etc/GMT+5") +times = pd.date_range( + "2019-06-01 05:00", "2019-06-01 19:00", freq="1s", tz="Etc/GMT+5" +) solpos = location.get_solarposition(times) clearsky = location.get_clearsky(times, solar_position=solpos) poa_1s = transpose(clearsky, timeshift=0) # no shift needed for 1s data @@ -110,8 +111,7 @@ def transpose(irradiance, timeshift): results = [] for timescale_minutes in [1, 5, 10, 15, 30, 60]: - - timescale_str = f'{timescale_minutes}min' + timescale_str = f"{timescale_minutes}min" # get the "true" interval average of poa as the baseline for comparison poa_avg = poa_1s.resample(timescale_str).mean() # get interval averages of irradiance components to use for transposition @@ -121,26 +121,30 @@ def transpose(irradiance, timeshift): poa_avg_noshift = transpose(clearsky_avg, timeshift=0) # low-res interval averages of 1-second data, with half-interval shift - poa_avg_halfshift = transpose(clearsky_avg, timeshift=timescale_minutes/2) - - df = pd.DataFrame({ - 'ground truth': poa_avg, - 'modeled, half shift': poa_avg_halfshift, - 'modeled, no shift': poa_avg_noshift, - }) - error = df.subtract(df['ground truth'], axis=0) + poa_avg_halfshift = transpose( + clearsky_avg, timeshift=timescale_minutes / 2 + ) + + df = pd.DataFrame( + { + "ground truth": poa_avg, + "modeled, half shift": poa_avg_halfshift, + "modeled, no shift": poa_avg_noshift, + } + ) + error = df.subtract(df["ground truth"], axis=0) # add another trace to the error plot - error['modeled, no shift'].plot(ax=ax, label=timescale_str) + error["modeled, no shift"].plot(ax=ax, label=timescale_str) # calculate error statistics and save for later stats = error.abs().mean() # average absolute error across daylight hours - stats['timescale_minutes'] = timescale_minutes + stats["timescale_minutes"] = timescale_minutes results.append(stats) ax.legend(ncol=2) -ax.set_ylabel('Transposition Error [W/m$^2$]') +ax.set_ylabel("Transposition Error [W/m$^2$]") fig.tight_layout() -df_results = pd.DataFrame(results).set_index('timescale_minutes') +df_results = pd.DataFrame(results).set_index("timescale_minutes") print(df_results) # %% @@ -152,9 +156,9 @@ def transpose(irradiance, timeshift): # instead of the edge reduces the error by one or two orders of magnitude: fig, ax = plt.subplots(figsize=(5, 3)) -df_results[['modeled, no shift', 'modeled, half shift']].plot.bar(rot=0, ax=ax) -ax.set_ylabel('Mean Absolute Error [W/m$^2$]') -ax.set_xlabel('Transposition Timescale [minutes]') +df_results[["modeled, no shift", "modeled, half shift"]].plot.bar(rot=0, ax=ax) +ax.set_ylabel("Mean Absolute Error [W/m$^2$]") +ax.set_xlabel("Transposition Timescale [minutes]") fig.tight_layout() # %% @@ -165,6 +169,6 @@ def transpose(irradiance, timeshift): # truth irradiance. fig, ax = plt.subplots(figsize=(5, 3)) -ax = df.plot(ax=ax, style=[None, ':', None], lw=3) -ax.set_ylabel('Irradiance [W/m$^2$]') +ax = df.plot(ax=ax, style=[None, ":", None], lw=3) +ax.set_ylabel("Irradiance [W/m$^2$]") fig.tight_layout() diff --git a/docs/examples/irradiance-transposition/plot_mixed_orientation.py b/docs/examples/irradiance-transposition/plot_mixed_orientation.py index 987b4b4994..bf1c8db610 100644 --- a/docs/examples/irradiance-transposition/plot_mixed_orientation.py +++ b/docs/examples/irradiance-transposition/plot_mixed_orientation.py @@ -14,36 +14,39 @@ # This particular example has one east-facing array (azimuth=90) and one # west-facing array (azimuth=270), which aside from orientation are identical. - from pvlib import pvsystem, modelchain, location import pandas as pd import matplotlib.pyplot as plt array_kwargs = dict( module_parameters=dict(pdc0=1, gamma_pdc=-0.004), - temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3) + temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3), ) arrays = [ - pvsystem.Array(pvsystem.FixedMount(30, 270), name='West-Facing Array', - **array_kwargs), - pvsystem.Array(pvsystem.FixedMount(30, 90), name='East-Facing Array', - **array_kwargs), + pvsystem.Array( + pvsystem.FixedMount(30, 270), name="West-Facing Array", **array_kwargs + ), + pvsystem.Array( + pvsystem.FixedMount(30, 90), name="East-Facing Array", **array_kwargs + ), ] loc = location.Location(40, -80) system = pvsystem.PVSystem(arrays=arrays, inverter_parameters=dict(pdc0=3)) -mc = modelchain.ModelChain(system, loc, aoi_model='physical', - spectral_model='no_loss') +mc = modelchain.ModelChain( + system, loc, aoi_model="physical", spectral_model="no_loss" +) -times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min', - tz='Etc/GMT+5') +times = pd.date_range( + "2019-01-01 06:00", "2019-01-01 18:00", freq="5min", tz="Etc/GMT+5" +) weather = loc.get_clearsky(times) mc.run_model(weather) fig, ax = plt.subplots() for array, pdc in zip(system.arrays, mc.results.dc): - pdc.plot(label=f'{array.name}') -mc.results.ac.plot(label='Inverter') -plt.ylabel('System Output') + pdc.plot(label=f"{array.name}") +mc.results.ac.plot(label="Inverter") +plt.ylabel("System Output") plt.legend() plt.show() diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py index 8df8339ad4..0909961ac5 100644 --- a/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py +++ b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py @@ -43,12 +43,13 @@ import matplotlib import matplotlib.pyplot as plt -from pvlib.irradiance import (erbs_driesse, - get_total_irradiance, - ghi_from_poa_driesse_2023, - ) +from pvlib.irradiance import ( + erbs_driesse, + get_total_irradiance, + ghi_from_poa_driesse_2023, +) -matplotlib.rcParams['axes.grid'] = True +matplotlib.rcParams["axes.grid"] = True # %% # @@ -70,20 +71,26 @@ # transpose using the Perez-Driesse model. # -ghi = np.linspace(0, 500, 100+1) +ghi = np.linspace(0, 500, 100 + 1) erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra) -dni = erbsout['dni'] -dhi = erbsout['dhi'] +dni = erbsout["dni"] +dhi = erbsout["dhi"] -irrads = get_total_irradiance(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, - model='perez-driesse') +irrads = get_total_irradiance( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra, + model="perez-driesse", +) -poa_global = irrads['poa_global'] +poa_global = irrads["poa_global"] # %% # @@ -92,13 +99,17 @@ poa_test = 200 -ghi_hat = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - poa_test, - dni_extra, - full_output=False) +ghi_hat = ghi_from_poa_driesse_2023( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + poa_test, + dni_extra, + full_output=False, +) -print('Estimated GHI: %.2f W/m².' % ghi_hat) +print("Estimated GHI: %.2f W/m²." % ghi_hat) # %% # @@ -106,19 +117,21 @@ # plt.figure() -plt.plot(ghi, poa_global, 'k-') -plt.axvline(ghi_hat, color='g', lw=1) -plt.axhline(poa_test, color='g', lw=1) -plt.plot(ghi_hat, poa_test, 'gs') -plt.annotate('GHI=%.2f' % (ghi_hat), - xy=(ghi_hat-2, 200+2), - xytext=(ghi_hat-20, 200+20), - ha='right', - arrowprops={'arrowstyle': 'simple'}) +plt.plot(ghi, poa_global, "k-") +plt.axvline(ghi_hat, color="g", lw=1) +plt.axhline(poa_test, color="g", lw=1) +plt.plot(ghi_hat, poa_test, "gs") +plt.annotate( + "GHI=%.2f" % (ghi_hat), + xy=(ghi_hat - 2, 200 + 2), + xytext=(ghi_hat - 20, 200 + 20), + ha="right", + arrowprops={"arrowstyle": "simple"}, +) plt.xlim(0, 500) plt.ylim(0, 250) -plt.xlabel('GHI [W/m²]') -plt.ylabel('POA [W/m²]') +plt.xlabel("GHI [W/m²]") +plt.ylabel("POA [W/m²]") plt.show() # %% @@ -136,16 +149,22 @@ erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra) -dni = erbsout['dni'] -dhi = erbsout['dhi'] +dni = erbsout["dni"] +dhi = erbsout["dhi"] -irrads = get_total_irradiance(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, - model='perez-driesse') +irrads = get_total_irradiance( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra, + model="perez-driesse", +) -poa_global = irrads['poa_global'] +poa_global = irrads["poa_global"] # %% # @@ -156,26 +175,33 @@ # out, other times not. # -result = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - poa_global, - dni_extra, - full_output=True, - ) +result = ghi_from_poa_driesse_2023( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + poa_global, + dni_extra, + full_output=True, +) ghi_hat, conv, niter = result correct = np.isclose(ghi, ghi_hat, atol=0.01) plt.figure() -plt.plot(np.where(correct, ghi, np.nan), np.where(correct, poa_global, np.nan), - 'g.', label='correct GHI found') -plt.plot(ghi[~correct], poa_global[~correct], 'r.', label='unreachable GHI') -plt.plot(ghi[~conv], poa_global[~conv], 'm.', label='out of range (kt > 1.25)') -plt.axhspan(88, 103, color='y', alpha=0.25, label='problem region') +plt.plot( + np.where(correct, ghi, np.nan), + np.where(correct, poa_global, np.nan), + "g.", + label="correct GHI found", +) +plt.plot(ghi[~correct], poa_global[~correct], "r.", label="unreachable GHI") +plt.plot(ghi[~conv], poa_global[~conv], "m.", label="out of range (kt > 1.25)") +plt.axhspan(88, 103, color="y", alpha=0.25, label="problem region") plt.xlim(0, 500) plt.ylim(0, 250) -plt.xlabel('GHI [W/m²]') -plt.ylabel('POA [W/m²]') +plt.xlabel("GHI [W/m²]") +plt.ylabel("POA [W/m²]") plt.legend() plt.show() diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_year.py b/docs/examples/irradiance-transposition/plot_rtranpose_year.py index c0b9860bba..ff9d7630c5 100644 --- a/docs/examples/irradiance-transposition/plot_rtranpose_year.py +++ b/docs/examples/irradiance-transposition/plot_rtranpose_year.py @@ -47,11 +47,12 @@ import pvlib from pvlib import iotools, location -from pvlib.irradiance import (get_extra_radiation, - get_total_irradiance, - ghi_from_poa_driesse_2023, - aoi, - ) +from pvlib.irradiance import ( + get_extra_radiation, + get_total_irradiance, + ghi_from_poa_driesse_2023, + aoi, +) # %% # @@ -59,15 +60,21 @@ # PVLIB_DIR = pvlib.__path__[0] -DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV') - -tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990, - map_variables=True) - -df = pd.DataFrame({'ghi': tmy['ghi'], 'dhi': tmy['dhi'], 'dni': tmy['dni'], - 'temp_air': tmy['temp_air'], - 'wind_speed': tmy['wind_speed'], - }) +DATA_FILE = os.path.join(PVLIB_DIR, "data", "723170TYA.CSV") + +tmy, metadata = iotools.read_tmy3( + DATA_FILE, coerce_year=1990, map_variables=True +) + +df = pd.DataFrame( + { + "ghi": tmy["ghi"], + "dhi": tmy["dhi"], + "dni": tmy["dni"], + "temp_air": tmy["temp_air"], + "wind_speed": tmy["wind_speed"], + } +) # %% # @@ -88,17 +95,22 @@ TILT = 30 ORIENT = 150 -df['dni_extra'] = get_extra_radiation(df.index) +df["dni_extra"] = get_extra_radiation(df.index) -total_irrad = get_total_irradiance(TILT, ORIENT, - solpos.apparent_zenith, - solpos.azimuth, - df.dni, df.ghi, df.dhi, - dni_extra=df.dni_extra, - model='perez-driesse') +total_irrad = get_total_irradiance( + TILT, + ORIENT, + solpos.apparent_zenith, + solpos.azimuth, + df.dni, + df.ghi, + df.dhi, + dni_extra=df.dni_extra, + model="perez-driesse", +) -df['poa_global'] = total_irrad.poa_global -df['aoi'] = aoi(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth) +df["poa_global"] = total_irrad.poa_global +df["aoi"] = aoi(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth) # %% # @@ -114,14 +126,17 @@ start = time.process_time() -df['ghi_rev'] = ghi_from_poa_driesse_2023(TILT, ORIENT, - solpos.apparent_zenith, - solpos.azimuth, - df.poa_global, - dni_extra=df.dni_extra) +df["ghi_rev"] = ghi_from_poa_driesse_2023( + TILT, + ORIENT, + solpos.apparent_zenith, + solpos.azimuth, + df.poa_global, + dni_extra=df.dni_extra, +) finish = time.process_time() -print('Elapsed time for reverse transposition: %.1f s' % (finish - start)) +print("Elapsed time for reverse transposition: %.1f s" % (finish - start)) # %% # @@ -137,16 +152,17 @@ # because errors from forward and reverse transposition will both be present. # -df = df.sort_values('aoi') +df = df.sort_values("aoi") plt.figure() -plt.gca().grid(True, alpha=.5) -pc = plt.scatter(df['ghi'], df['ghi_rev'], c=df['aoi'], s=15, - cmap='jet', vmin=60, vmax=120) -plt.colorbar(label='AOI [°]') +plt.gca().grid(True, alpha=0.5) +pc = plt.scatter( + df["ghi"], df["ghi_rev"], c=df["aoi"], s=15, cmap="jet", vmin=60, vmax=120 +) +plt.colorbar(label="AOI [°]") pc.set_alpha(0.5) -plt.xlabel('GHI original [W/m²]') -plt.ylabel('GHI from POA [W/m²]') +plt.xlabel("GHI original [W/m²]") +plt.ylabel("GHI from POA [W/m²]") plt.show() diff --git a/docs/examples/irradiance-transposition/plot_seasonal_tilt.py b/docs/examples/irradiance-transposition/plot_seasonal_tilt.py index dc4b433412..6613ea8044 100644 --- a/docs/examples/irradiance-transposition/plot_seasonal_tilt.py +++ b/docs/examples/irradiance-transposition/plot_seasonal_tilt.py @@ -34,57 +34,71 @@ class SeasonalTiltMount(pvsystem.AbstractMount): def get_orientation(self, solar_zenith, solar_azimuth): # note: determining tilt based on month may produce different # results depending on the time zone of the timestamps - tilts = [self.monthly_tilts[m-1] for m in solar_zenith.index.month] - return pd.DataFrame({ - 'surface_tilt': tilts, - 'surface_azimuth': self.surface_azimuth, - }, index=solar_zenith.index) + tilts = [self.monthly_tilts[m - 1] for m in solar_zenith.index.month] + return pd.DataFrame( + { + "surface_tilt": tilts, + "surface_azimuth": self.surface_azimuth, + }, + index=solar_zenith.index, + ) # %% # First let's grab some weather data and make sure our mount produces tilts # like we expect: -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' -tmy, metadata = iotools.read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990, - map_variables=True) +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" +tmy, metadata = iotools.read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=1990, map_variables=True +) # shift from TMY3 right-labeled index to left-labeled index: tmy.index = tmy.index - pd.Timedelta(hours=1) -weather = pd.DataFrame({ - 'ghi': tmy['ghi'], 'dhi': tmy['dhi'], 'dni': tmy['dni'], - 'temp_air': tmy['temp_air'], 'wind_speed': tmy['wind_speed'], -}) +weather = pd.DataFrame( + { + "ghi": tmy["ghi"], + "dhi": tmy["dhi"], + "dni": tmy["dni"], + "temp_air": tmy["temp_air"], + "wind_speed": tmy["wind_speed"], + } +) loc = location.Location.from_tmy(metadata) solpos = loc.get_solarposition(weather.index) # same default monthly tilts as SAM: tilts = [40, 40, 40, 20, 20, 20, 20, 20, 20, 40, 40, 40] mount = SeasonalTiltMount(monthly_tilts=tilts) orientation = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth) -orientation['surface_tilt'].plot() -plt.ylabel('Surface Tilt [degrees]') +orientation["surface_tilt"].plot() +plt.ylabel("Surface Tilt [degrees]") plt.show() # %% # With our custom tilt strategy defined, we can create the corresponding # Array and PVSystem, and then run a ModelChain as usual: -module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05} -temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'] -array = pvsystem.Array(mount=mount, module_parameters=module_parameters, - temperature_model_parameters=temp_params) -system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1}) -mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') +module_parameters = {"pdc0": 1, "gamma_pdc": -0.004, "b": 0.05} +temp_params = TEMPERATURE_MODEL_PARAMETERS["sapm"]["open_rack_glass_polymer"] +array = pvsystem.Array( + mount=mount, + module_parameters=module_parameters, + temperature_model_parameters=temp_params, +) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters={"pdc0": 1}) +mc = modelchain.ModelChain(system, loc, spectral_model="no_loss") _ = mc.run_model(weather) # %% # Now let's re-run the simulation assuming tilt=30 for the entire year: -array2 = pvsystem.Array(mount=pvsystem.FixedMount(30, 180), - module_parameters=module_parameters, - temperature_model_parameters=temp_params) -system2 = pvsystem.PVSystem(arrays=[array2], inverter_parameters={'pdc0': 1}) -mc2 = modelchain.ModelChain(system2, loc, spectral_model='no_loss') +array2 = pvsystem.Array( + mount=pvsystem.FixedMount(30, 180), + module_parameters=module_parameters, + temperature_model_parameters=temp_params, +) +system2 = pvsystem.PVSystem(arrays=[array2], inverter_parameters={"pdc0": 1}) +mc2 = modelchain.ModelChain(system2, loc, spectral_model="no_loss") _ = mc2.run_model(weather) # %% @@ -92,10 +106,12 @@ def get_orientation(self, solar_zenith, solar_azimuth): # strategies: # sphinx_gallery_thumbnail_number = 2 -results = pd.DataFrame({ - 'Seasonal 20/40 Production': mc.results.ac, - 'Fixed 30 Production': mc2.results.ac, -}) -results.resample('m').sum().plot() -plt.ylabel('Monthly Production') +results = pd.DataFrame( + { + "Seasonal 20/40 Production": mc.results.ac, + "Fixed 30 Production": mc2.results.ac, + } +) +results.resample("m").sum().plot() +plt.ylabel("Monthly Production") plt.show() diff --git a/docs/examples/irradiance-transposition/plot_transposition_gain.py b/docs/examples/irradiance-transposition/plot_transposition_gain.py index e0b7031f0b..6b6bd87b7c 100644 --- a/docs/examples/irradiance-transposition/plot_transposition_gain.py +++ b/docs/examples/irradiance-transposition/plot_transposition_gain.py @@ -29,11 +29,12 @@ import pathlib # get full path to the data directory -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" # get TMY3 dataset -tmy, metadata = read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990, - map_variables=True) +tmy, metadata = read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=1990, map_variables=True +) # TMY3 datasets are right-labeled (AKA "end of interval") which means the last # interval of Dec 31, 23:00 to Jan 1 00:00 is labeled Jan 1 00:00. When rolling # up hourly irradiance to monthly insolation, a spurious January value is @@ -48,10 +49,10 @@ # Note also that TMY datasets are right-labeled hourly intervals, e.g. the # 10AM to 11AM interval is labeled 11. We should calculate solar position in # the middle of the interval (10:30), so we subtract 30 minutes: -times = tmy.index - pd.Timedelta('30min') +times = tmy.index - pd.Timedelta("30min") solar_position = location.get_solarposition(times) # but remember to shift the index back to line up with the TMY data: -solar_position.index += pd.Timedelta('30min') +solar_position.index += pd.Timedelta("30min") # create a helper function to do the transposition for us @@ -61,13 +62,14 @@ def calculate_poa(tmy, solar_position, surface_tilt, surface_azimuth): poa = irradiance.get_total_irradiance( surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, - dni=tmy['dni'], - ghi=tmy['ghi'], - dhi=tmy['dhi'], - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth'], - model='isotropic') - return poa['poa_global'] # just return the total in-plane irradiance + dni=tmy["dni"], + ghi=tmy["ghi"], + dhi=tmy["dhi"], + solar_zenith=solar_position["apparent_zenith"], + solar_azimuth=solar_position["azimuth"], + model="isotropic", + ) + return poa["poa_global"] # just return the total in-plane irradiance # create a dataframe to keep track of our monthly insolations @@ -80,30 +82,34 @@ def calculate_poa(tmy, solar_position, surface_tilt, surface_azimuth): column_name = f"FT-{tilt}" # TMYs are hourly, so we can just sum up irradiance [W/m^2] to get # insolation [Wh/m^2]: - df_monthly[column_name] = poa_irradiance.resample('m').sum() + df_monthly[column_name] = poa_irradiance.resample("m").sum() # single-axis tracking: -orientation = tracking.singleaxis(solar_position['apparent_zenith'], - solar_position['azimuth'], - axis_tilt=0, # flat array - axis_azimuth=180, # south-facing azimuth - max_angle=60, # a common maximum rotation - backtrack=True, # backtrack for a c-Si array - gcr=0.4) # a common ground coverage ratio - -poa_irradiance = calculate_poa(tmy, - solar_position, - orientation['surface_tilt'], - orientation['surface_azimuth']) -df_monthly['SAT-0.4'] = poa_irradiance.resample('m').sum() +orientation = tracking.singleaxis( + solar_position["apparent_zenith"], + solar_position["azimuth"], + axis_tilt=0, # flat array + axis_azimuth=180, # south-facing azimuth + max_angle=60, # a common maximum rotation + backtrack=True, # backtrack for a c-Si array + gcr=0.4, +) # a common ground coverage ratio + +poa_irradiance = calculate_poa( + tmy, + solar_position, + orientation["surface_tilt"], + orientation["surface_azimuth"], +) +df_monthly["SAT-0.4"] = poa_irradiance.resample("m").sum() # calculate the percent difference from GHI -ghi_monthly = tmy['ghi'].resample('m').sum() +ghi_monthly = tmy["ghi"].resample("m").sum() df_monthly = 100 * (df_monthly.divide(ghi_monthly, axis=0) - 1) df_monthly.plot() -plt.xlabel('Month of Year') -plt.ylabel('Monthly Transposition Gain [%]') +plt.xlabel("Month of Year") +plt.ylabel("Monthly Transposition Gain [%]") plt.tight_layout() plt.show() diff --git a/docs/examples/irradiance-transposition/use_perez_modelchain.py b/docs/examples/irradiance-transposition/use_perez_modelchain.py index dd0d12f2a5..871a701478 100644 --- a/docs/examples/irradiance-transposition/use_perez_modelchain.py +++ b/docs/examples/irradiance-transposition/use_perez_modelchain.py @@ -22,7 +22,6 @@ # This example shows how the :py:class:`~pvlib.modelchain.ModelChain` can # be adjusted to use a different set of Perez coefficients. -import pandas as pd from pvlib.pvsystem import PVSystem from pvlib.modelchain import ModelChain from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS @@ -33,36 +32,39 @@ # load in TMY weather data from North Carolina included with pvlib PVLIB_DIR = pvlib.__path__[0] -DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV') +DATA_FILE = os.path.join(PVLIB_DIR, "data", "723170TYA.CSV") -tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990, - map_variables=True) +tmy, metadata = iotools.read_tmy3( + DATA_FILE, coerce_year=1990, map_variables=True +) -weather_data = tmy[['ghi', 'dhi', 'dni', 'temp_air', 'wind_speed']] +weather_data = tmy[["ghi", "dhi", "dni", "temp_air", "wind_speed"]] loc = location.Location.from_tmy(metadata) -#%% +# %% # Now, let's set up a standard PV model using the ``ModelChain`` -surface_tilt = metadata['latitude'] +surface_tilt = metadata["latitude"] surface_azimuth = 180 # define an example module and inverter -sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod') -cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter') -sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] -cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] +sandia_modules = pvlib.pvsystem.retrieve_sam("SandiaMod") +cec_inverters = pvlib.pvsystem.retrieve_sam("cecinverter") +sandia_module = sandia_modules["Canadian_Solar_CS5P_220M___2009_"] +cec_inverter = cec_inverters["ABB__MICRO_0_25_I_OUTD_US_208__208V_"] -temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] +temp_params = TEMPERATURE_MODEL_PARAMETERS["sapm"]["open_rack_glass_glass"] # define the system and ModelChain -system = PVSystem(arrays=None, - surface_tilt=surface_tilt, - surface_azimuth=surface_azimuth, - module_parameters=sandia_module, - inverter_parameters=cec_inverter, - temperature_model_parameters=temp_params) +system = PVSystem( + arrays=None, + surface_tilt=surface_tilt, + surface_azimuth=surface_azimuth, + module_parameters=sandia_module, + inverter_parameters=cec_inverter, + temperature_model_parameters=temp_params, +) mc = ModelChain(system, location=loc) @@ -72,7 +74,7 @@ # alternative Perez coefficients. This enables comparison at the end. # Cape Canaveral seems like the most likely match for climate -model_perez = 'capecanaveral1988' +model_perez = "capecanaveral1988" solar_position = loc.get_solarposition(times=weather_data.index) dni_extra = irradiance.get_extra_radiation(weather_data.index) @@ -80,25 +82,27 @@ POA_irradiance = irradiance.get_total_irradiance( surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, - dni=weather_data['dni'], - ghi=weather_data['ghi'], - dhi=weather_data['dhi'], - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth'], - model='perez', - dni_extra=dni_extra) + dni=weather_data["dni"], + ghi=weather_data["ghi"], + dhi=weather_data["dhi"], + solar_zenith=solar_position["apparent_zenith"], + solar_azimuth=solar_position["azimuth"], + model="perez", + dni_extra=dni_extra, +) POA_irradiance_new_perez = irradiance.get_total_irradiance( surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, - dni=weather_data['dni'], - ghi=weather_data['ghi'], - dhi=weather_data['dhi'], - solar_zenith=solar_position['apparent_zenith'], - solar_azimuth=solar_position['azimuth'], - model='perez', + dni=weather_data["dni"], + ghi=weather_data["ghi"], + dhi=weather_data["dhi"], + solar_zenith=solar_position["apparent_zenith"], + solar_azimuth=solar_position["azimuth"], + model="perez", model_perez=model_perez, - dni_extra=dni_extra) + dni_extra=dni_extra, +) # %% # Now, run the ``ModelChain`` with both sets of irradiance data and compare @@ -111,11 +115,13 @@ mc.run_model_from_poa(POA_irradiance_new_perez) ac_power_new_perez = mc.results.ac -start, stop = '1990-05-05 06:00:00', '1990-05-05 19:00:00' -plt.plot(ac_power_default.loc[start:stop], - label="Default Composite Perez Model") -plt.plot(ac_power_new_perez.loc[start:stop], - label="Cape Canaveral Perez Model") +start, stop = "1990-05-05 06:00:00", "1990-05-05 19:00:00" +plt.plot( + ac_power_default.loc[start:stop], label="Default Composite Perez Model" +) +plt.plot( + ac_power_new_perez.loc[start:stop], label="Cape Canaveral Perez Model" +) plt.xticks(rotation=90) plt.ylabel("AC Power ($W$)") plt.legend() diff --git a/docs/examples/iv-modeling/plot_singlediode.py b/docs/examples/iv-modeling/plot_singlediode.py index 69c0079a20..1d4f732c52 100644 --- a/docs/examples/iv-modeling/plot_singlediode.py +++ b/docs/examples/iv-modeling/plot_singlediode.py @@ -39,67 +39,60 @@ # Example module parameters for the Canadian Solar CS5P-220M: parameters = { - 'Name': 'Canadian Solar CS5P-220M', - 'BIPV': 'N', - 'Date': '10/5/2009', - 'T_NOCT': 42.4, - 'A_c': 1.7, - 'N_s': 96, - 'I_sc_ref': 5.1, - 'V_oc_ref': 59.4, - 'I_mp_ref': 4.69, - 'V_mp_ref': 46.9, - 'alpha_sc': 0.004539, - 'beta_oc': -0.22216, - 'a_ref': 2.6373, - 'I_L_ref': 5.114, - 'I_o_ref': 8.196e-10, - 'R_s': 1.065, - 'R_sh_ref': 381.68, - 'Adjust': 8.7, - 'gamma_r': -0.476, - 'Version': 'MM106', - 'PTC': 200.1, - 'Technology': 'Mono-c-Si', + "Name": "Canadian Solar CS5P-220M", + "BIPV": "N", + "Date": "10/5/2009", + "T_NOCT": 42.4, + "A_c": 1.7, + "N_s": 96, + "I_sc_ref": 5.1, + "V_oc_ref": 59.4, + "I_mp_ref": 4.69, + "V_mp_ref": 46.9, + "alpha_sc": 0.004539, + "beta_oc": -0.22216, + "a_ref": 2.6373, + "I_L_ref": 5.114, + "I_o_ref": 8.196e-10, + "R_s": 1.065, + "R_sh_ref": 381.68, + "Adjust": 8.7, + "gamma_r": -0.476, + "Version": "MM106", + "PTC": 200.1, + "Technology": "Mono-c-Si", } -cases = [ - (1000, 55), - (800, 55), - (600, 55), - (400, 25), - (400, 40), - (400, 55) -] +cases = [(1000, 55), (800, 55), (600, 55), (400, 25), (400, 40), (400, 55)] -conditions = pd.DataFrame(cases, columns=['Geff', 'Tcell']) +conditions = pd.DataFrame(cases, columns=["Geff", "Tcell"]) # adjust the reference parameters according to the operating # conditions using the De Soto model: IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - conditions['Geff'], - conditions['Tcell'], - alpha_sc=parameters['alpha_sc'], - a_ref=parameters['a_ref'], - I_L_ref=parameters['I_L_ref'], - I_o_ref=parameters['I_o_ref'], - R_sh_ref=parameters['R_sh_ref'], - R_s=parameters['R_s'], + conditions["Geff"], + conditions["Tcell"], + alpha_sc=parameters["alpha_sc"], + a_ref=parameters["a_ref"], + I_L_ref=parameters["I_L_ref"], + I_o_ref=parameters["I_o_ref"], + R_sh_ref=parameters["R_sh_ref"], + R_s=parameters["R_s"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) # plug the parameters into the SDE and solve for IV curves: SDE_params = { - 'photocurrent': IL, - 'saturation_current': I0, - 'resistance_series': Rs, - 'resistance_shunt': Rsh, - 'nNsVth': nNsVth + "photocurrent": IL, + "saturation_current": I0, + "resistance_series": Rs, + "resistance_shunt": Rsh, + "nNsVth": nNsVth, } -curve_info = pvsystem.singlediode(method='lambertw', **SDE_params) -v = pd.DataFrame(np.linspace(0., curve_info['v_oc'], 100)) -i = pd.DataFrame(pvsystem.i_from_v(voltage=v, method='lambertw', **SDE_params)) +curve_info = pvsystem.singlediode(method="lambertw", **SDE_params) +v = pd.DataFrame(np.linspace(0.0, curve_info["v_oc"], 100)) +i = pd.DataFrame(pvsystem.i_from_v(voltage=v, method="lambertw", **SDE_params)) # plot the calculated curves: plt.figure() @@ -109,40 +102,53 @@ "$T_{cell}$ " + f"{case['Tcell']} $\\degree C$" ) plt.plot(v[idx], i[idx], label=label) - v_mp = curve_info['v_mp'][idx] - i_mp = curve_info['i_mp'][idx] + v_mp = curve_info["v_mp"][idx] + i_mp = curve_info["i_mp"][idx] # mark the MPP - plt.plot([v_mp], [i_mp], ls='', marker='o', c='k') + plt.plot([v_mp], [i_mp], ls="", marker="o", c="k") plt.xlim(left=0) plt.ylim(bottom=0) plt.legend(loc=(1.0, 0)) -plt.xlabel('Module voltage [V]') -plt.ylabel('Module current [A]') -plt.title(parameters['Name']) +plt.xlabel("Module voltage [V]") +plt.ylabel("Module current [A]") +plt.title(parameters["Name"]) plt.gcf().set_tight_layout(True) # draw trend arrows def draw_arrow(ax, label, x0, y0, rotation, size, direction): - style = direction + 'arrow' + style = direction + "arrow" bbox_props = dict(boxstyle=style, fc=(0.8, 0.9, 0.9), ec="b", lw=1) - t = ax.text(x0, y0, label, ha="left", va="bottom", rotation=rotation, - size=size, bbox=bbox_props, zorder=-1) + t = ax.text( + x0, + y0, + label, + ha="left", + va="bottom", + rotation=rotation, + size=size, + bbox=bbox_props, + zorder=-1, + ) bb = t.get_bbox_patch() bb.set_boxstyle(style, pad=0.6) ax = plt.gca() -draw_arrow(ax, 'Irradiance', 20, 2.5, 90, 15, 'r') -draw_arrow(ax, 'Temperature', 35, 1, 0, 15, 'l') +draw_arrow(ax, "Irradiance", 20, 2.5, 90, 15, "r") +draw_arrow(ax, "Temperature", 35, 1, 0, 15, "l") plt.show() -print(pd.DataFrame({ - 'i_sc': curve_info['i_sc'], - 'v_oc': curve_info['v_oc'], - 'i_mp': curve_info['i_mp'], - 'v_mp': curve_info['v_mp'], - 'p_mp': curve_info['p_mp'], -})) +print( + pd.DataFrame( + { + "i_sc": curve_info["i_sc"], + "v_oc": curve_info["v_oc"], + "i_mp": curve_info["i_mp"], + "v_mp": curve_info["v_mp"], + "p_mp": curve_info["p_mp"], + } + ) +) diff --git a/docs/examples/reflections/plot_convert_iam_models.py b/docs/examples/reflections/plot_convert_iam_models.py index 6b0ec78ab3..75b964b277 100644 --- a/docs/examples/reflections/plot_convert_iam_models.py +++ b/docs/examples/reflections/plot_convert_iam_models.py @@ -1,4 +1,3 @@ - """ IAM Model Conversion ==================== @@ -26,7 +25,7 @@ import matplotlib.pyplot as plt from pvlib.tools import cosd -from pvlib.iam import (ashrae, martin_ruiz, physical, convert) +from pvlib.iam import ashrae, martin_ruiz, physical, convert # %% # Converting from one IAM model to another model @@ -37,33 +36,33 @@ # Compute IAM values using the martin_ruiz model. aoi = np.linspace(0, 90, 100) -martin_ruiz_params = {'a_r': 0.16} +martin_ruiz_params = {"a_r": 0.16} martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params) # Get parameters for the physical model and compute IAM using these parameters. -physical_params = convert('martin_ruiz', martin_ruiz_params, 'physical') +physical_params = convert("martin_ruiz", martin_ruiz_params, "physical") physical_iam = physical(aoi, **physical_params) # Get parameters for the ASHRAE model and compute IAM using these parameters. -ashrae_params = convert('martin_ruiz', martin_ruiz_params, 'ashrae') +ashrae_params = convert("martin_ruiz", martin_ruiz_params, "ashrae") ashrae_iam = ashrae(aoi, **ashrae_params) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5), sharey=True) # Plot each model's IAM vs. angle-of-incidence (AOI). -ax1.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') -ax1.plot(aoi, physical_iam, label='physical') -ax1.set_xlabel('AOI (degrees)') -ax1.set_title('Convert from Martin-Ruiz to physical') +ax1.plot(aoi, martin_ruiz_iam, label="Martin-Ruiz") +ax1.plot(aoi, physical_iam, label="physical") +ax1.set_xlabel("AOI (degrees)") +ax1.set_title("Convert from Martin-Ruiz to physical") ax1.legend() -ax2.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') -ax2.plot(aoi, ashrae_iam, label='ASHRAE') -ax2.set_xlabel('AOI (degrees)') -ax2.set_title('Convert from Martin-Ruiz to ASHRAE') +ax2.plot(aoi, martin_ruiz_iam, label="Martin-Ruiz") +ax2.plot(aoi, ashrae_iam, label="ASHRAE") +ax2.set_xlabel("AOI (degrees)") +ax2.set_title("Convert from Martin-Ruiz to ASHRAE") ax2.legend() -ax1.set_ylabel('IAM') +ax1.set_ylabel("IAM") plt.show() @@ -88,14 +87,15 @@ # Compute IAM using the Martin-Ruiz model. aoi = np.linspace(0, 90, 100) -martin_ruiz_params = {'a_r': 0.16} +martin_ruiz_params = {"a_r": 0.16} martin_ruiz_iam = martin_ruiz(aoi, **martin_ruiz_params) # Get parameters for the physical model ... # ... using the default weight function. -physical_params_default = convert('martin_ruiz', martin_ruiz_params, - 'physical') +physical_params_default = convert( + "martin_ruiz", martin_ruiz_params, "physical" +) physical_iam_default = physical(aoi, **physical_params_default) @@ -105,17 +105,18 @@ def weight_function(aoi): return cosd(aoi) -physical_params_custom = convert('martin_ruiz', martin_ruiz_params, 'physical', - weight=weight_function) +physical_params_custom = convert( + "martin_ruiz", martin_ruiz_params, "physical", weight=weight_function +) physical_iam_custom = physical(aoi, **physical_params_custom) # Plot IAM vs AOI. -plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') -plt.plot(aoi, physical_iam_default, label='Default weight function') -plt.plot(aoi, physical_iam_custom, label='Custom weight function') -plt.xlabel('AOI (degrees)') -plt.ylabel('IAM') -plt.title('Martin-Ruiz to physical') +plt.plot(aoi, martin_ruiz_iam, label="Martin-Ruiz") +plt.plot(aoi, physical_iam_default, label="Default weight function") +plt.plot(aoi, physical_iam_custom, label="Custom weight function") +plt.xlabel("AOI (degrees)") +plt.ylabel("IAM") +plt.title("Martin-Ruiz to physical") plt.legend() plt.show() @@ -129,21 +130,22 @@ def weight_function(aoi): # Get parameters for the ASHRAE model ... # ... using the default weight function. -ashrae_params_default = convert('martin_ruiz', martin_ruiz_params, 'ashrae') +ashrae_params_default = convert("martin_ruiz", martin_ruiz_params, "ashrae") ashrae_iam_default = ashrae(aoi, **ashrae_params_default) # ... using the custom weight function -ashrae_params_custom = convert('martin_ruiz', martin_ruiz_params, 'ashrae', - weight=weight_function) +ashrae_params_custom = convert( + "martin_ruiz", martin_ruiz_params, "ashrae", weight=weight_function +) ashrae_iam_custom = ashrae(aoi, **ashrae_params_custom) # Plot IAM vs AOI. -plt.plot(aoi, martin_ruiz_iam, label='Martin-Ruiz') -plt.plot(aoi, ashrae_iam_default, label='Default weight function') -plt.plot(aoi, ashrae_iam_custom, label='Custom weight function') -plt.xlabel('AOI (degrees)') -plt.ylabel('IAM') -plt.title('Martin-Ruiz to ASHRAE') +plt.plot(aoi, martin_ruiz_iam, label="Martin-Ruiz") +plt.plot(aoi, ashrae_iam_default, label="Default weight function") +plt.plot(aoi, ashrae_iam_custom, label="Custom weight function") +plt.xlabel("AOI (degrees)") +plt.ylabel("IAM") +plt.title("Martin-Ruiz to ASHRAE") plt.legend() plt.show() diff --git a/docs/examples/reflections/plot_diffuse_aoi_correction.py b/docs/examples/reflections/plot_diffuse_aoi_correction.py index 5cd2f5cb41..a29b1507d0 100644 --- a/docs/examples/reflections/plot_diffuse_aoi_correction.py +++ b/docs/examples/reflections/plot_diffuse_aoi_correction.py @@ -31,7 +31,6 @@ # .. [2] Duffie, John A. & Beckman, William A. (2013). Solar Engineering # of Thermal Processes. DOI: 10.1002/9781118671603 - from pvlib.iam import marion_diffuse, physical import numpy as np import matplotlib.pyplot as plt @@ -59,10 +58,10 @@ iam_no_coating = physical(aoi, n=1.526, K=0) iam_ar_coating = physical(aoi, n=1.3, K=0) -plt.plot(aoi, iam_ar_coating, c='b', label='$F_b$, AR coated, n=1.3') -plt.plot(aoi, iam_no_coating, c='r', label='$F_b$, uncoated, n=1.526') -plt.xlabel(r'Angle-of-Incidence, AOI $(\degree)$') -plt.ylabel('Diffuse Incidence Angle Modifier') +plt.plot(aoi, iam_ar_coating, c="b", label="$F_b$, AR coated, n=1.3") +plt.plot(aoi, iam_no_coating, c="r", label="$F_b$, uncoated, n=1.526") +plt.xlabel(r"Angle-of-Incidence, AOI $(\degree)$") +plt.ylabel("Diffuse Incidence Angle Modifier") plt.legend() plt.ylim([0, 1.2]) plt.grid() @@ -80,20 +79,30 @@ tilts = np.arange(0, 91, 2.5) # marion_diffuse calculates all three IAM values (sky, horizon, ground) -iam_no_coating = marion_diffuse('physical', tilts, n=1.526, K=0) -iam_ar_coating = marion_diffuse('physical', tilts, n=1.3, K=0) +iam_no_coating = marion_diffuse("physical", tilts, n=1.526, K=0) +iam_ar_coating = marion_diffuse("physical", tilts, n=1.3, K=0) # %% # First we recreate Figure 4 in [1]_, showing the dependence of the sky diffuse # incidence angle modifier on module tilt. -plt.plot(tilts, iam_ar_coating['sky'], c='b', marker='^', - label='$F_{sky}$, AR coated, n=1.3') -plt.plot(tilts, iam_no_coating['sky'], c='r', marker='x', - label='$F_{sky}$, uncoated, n=1.526') +plt.plot( + tilts, + iam_ar_coating["sky"], + c="b", + marker="^", + label="$F_{sky}$, AR coated, n=1.3", +) +plt.plot( + tilts, + iam_no_coating["sky"], + c="r", + marker="x", + label="$F_{sky}$, uncoated, n=1.526", +) plt.ylim([0.9, 1.0]) -plt.xlabel(r'PV Module Tilt, $\beta (\degree)$') -plt.ylabel('Diffuse Incidence Angle Modifier') +plt.xlabel(r"PV Module Tilt, $\beta (\degree)$") +plt.ylabel("Diffuse Incidence Angle Modifier") plt.grid() plt.legend() plt.show() @@ -104,16 +113,36 @@ # :py:func:`pvlib.iam.marion_diffuse` defaults to using 1800 points for the # horizon case (instead of 180 like the others) to match [1]_. -plt.plot(tilts, iam_ar_coating['horizon'], c='b', marker='^', - label='$F_{hor}$, AR coated, n=1.3') -plt.plot(tilts, iam_no_coating['horizon'], c='r', marker='x', - label='$F_{hor}$, uncoated, n=1.526') -plt.plot(tilts, iam_ar_coating['ground'], c='b', marker='s', - label='$F_{grd}$, AR coated, n=1.3') -plt.plot(tilts, iam_no_coating['ground'], c='r', marker='+', - label='$F_{grd}$, uncoated, n=1.526') -plt.xlabel(r'PV Module Tilt, $\beta (\degree)$') -plt.ylabel('Diffuse Incidence Angle Modifier') +plt.plot( + tilts, + iam_ar_coating["horizon"], + c="b", + marker="^", + label="$F_{hor}$, AR coated, n=1.3", +) +plt.plot( + tilts, + iam_no_coating["horizon"], + c="r", + marker="x", + label="$F_{hor}$, uncoated, n=1.526", +) +plt.plot( + tilts, + iam_ar_coating["ground"], + c="b", + marker="s", + label="$F_{grd}$, AR coated, n=1.3", +) +plt.plot( + tilts, + iam_no_coating["ground"], + c="r", + marker="+", + label="$F_{grd}$, uncoated, n=1.526", +) +plt.xlabel(r"PV Module Tilt, $\beta (\degree)$") +plt.ylabel("Diffuse Incidence Angle Modifier") plt.grid() plt.legend() plt.show() diff --git a/docs/examples/reflections/plot_fit_iam_models.py b/docs/examples/reflections/plot_fit_iam_models.py index 6feb84423a..4c670906ce 100644 --- a/docs/examples/reflections/plot_fit_iam_models.py +++ b/docs/examples/reflections/plot_fit_iam_models.py @@ -1,4 +1,3 @@ - """ IAM Model Fitting ================================ @@ -26,7 +25,7 @@ import matplotlib.pyplot as plt from pvlib.tools import cosd -from pvlib.iam import (martin_ruiz, physical, fit) +from pvlib.iam import martin_ruiz, physical, fit # %% @@ -40,22 +39,22 @@ # Create some IAM data. aoi = np.linspace(0, 85, 10) -params = {'a_r': 0.16} +params = {"a_r": 0.16} iam = martin_ruiz(aoi, **params) data = iam * np.array([uniform(0.98, 1.02) for _ in range(len(iam))]) # Get parameters for the physical model by fitting to the perturbed data. -physical_params = fit(aoi, data, 'physical') +physical_params = fit(aoi, data, "physical") # Compute IAM with the fitted physical model parameters. physical_iam = physical(aoi, **physical_params) # Plot IAM vs. AOI -plt.scatter(aoi, data, c='darkorange', label='Data') -plt.plot(aoi, physical_iam, label='physical') -plt.xlabel('AOI (degrees)') -plt.ylabel('IAM') -plt.title('Fitting the physical model to data') +plt.scatter(aoi, data, c="darkorange", label="Data") +plt.plot(aoi, physical_iam, label="physical") +plt.xlabel("AOI (degrees)") +plt.ylabel("IAM") +plt.title("Fitting the physical model to data") plt.legend() plt.show() @@ -69,28 +68,29 @@ # function to :py:func:`pvlib.iam.fit`. # + # Define a custom weight function. The weight function must take ``aoi`` # as its argument and return a vector of the same length as ``aoi``. def weight_function(aoi): return cosd(aoi) -physical_params_custom = fit(aoi, data, 'physical', weight=weight_function) +physical_params_custom = fit(aoi, data, "physical", weight=weight_function) physical_iam_custom = physical(aoi, **physical_params_custom) # Plot IAM vs AOI. fig, ax = plt.subplots(2, 1, figsize=(5, 8)) -ax[0].plot(aoi, data, '.', label='Data (from Martin-Ruiz model)') -ax[0].plot(aoi, physical_iam, label='With default weight function') -ax[0].plot(aoi, physical_iam_custom, label='With custom weight function') -ax[0].set_xlabel('AOI (degrees)') -ax[0].set_ylabel('IAM') +ax[0].plot(aoi, data, ".", label="Data (from Martin-Ruiz model)") +ax[0].plot(aoi, physical_iam, label="With default weight function") +ax[0].plot(aoi, physical_iam_custom, label="With custom weight function") +ax[0].set_xlabel("AOI (degrees)") +ax[0].set_ylabel("IAM") ax[0].legend() -ax[1].plot(aoi, physical_iam_custom - physical_iam, label='Custom - default') -ax[1].set_xlabel('AOI (degrees)') -ax[1].set_ylabel('Diff. in IAM') +ax[1].plot(aoi, physical_iam_custom - physical_iam, label="Custom - default") +ax[1].set_xlabel("AOI (degrees)") +ax[1].set_ylabel("Diff. in IAM") ax[1].legend() plt.tight_layout() plt.show() diff --git a/docs/examples/shading/plot_partial_module_shading_simple.py b/docs/examples/shading/plot_partial_module_shading_simple.py index ce031eff10..a9b39310a4 100644 --- a/docs/examples/shading/plot_partial_module_shading_simple.py +++ b/docs/examples/shading/plot_partial_module_shading_simple.py @@ -46,18 +46,18 @@ # For simplicity, use cell temperature of 25C for all calculations. # kB is J/K, qe is C=J/V # kB * T / qe -> V -Vth = kB * (273.15+25) / qe +Vth = kB * (273.15 + 25) / qe cell_parameters = { - 'I_L_ref': 8.24, - 'I_o_ref': 2.36e-9, - 'a_ref': 1.3*Vth, - 'R_sh_ref': 1000, - 'R_s': 0.00181, - 'alpha_sc': 0.0042, - 'breakdown_factor': 2e-3, - 'breakdown_exp': 3, - 'breakdown_voltage': -15, + "I_L_ref": 8.24, + "I_o_ref": 2.36e-9, + "a_ref": 1.3 * Vth, + "R_sh_ref": 1000, + "R_s": 0.00181, + "alpha_sc": 0.0042, + "breakdown_factor": 2e-3, + "breakdown_exp": 3, + "breakdown_voltage": -15, } # %% @@ -91,12 +91,12 @@ def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000): sde_args = pvsystem.calcparams_desoto( Geff, Tcell, - alpha_sc=parameters['alpha_sc'], - a_ref=parameters['a_ref'], - I_L_ref=parameters['I_L_ref'], - I_o_ref=parameters['I_o_ref'], - R_sh_ref=parameters['R_sh_ref'], - R_s=parameters['R_s'], + alpha_sc=parameters["alpha_sc"], + a_ref=parameters["a_ref"], + I_L_ref=parameters["I_L_ref"], + I_o_ref=parameters["I_o_ref"], + R_sh_ref=parameters["R_sh_ref"], + R_s=parameters["R_s"], ) # sde_args has values: # (photocurrent, saturation_current, resistance_series, @@ -105,22 +105,22 @@ def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000): # Use Bishop's method to calculate points on the IV curve with V ranging # from the reverse breakdown voltage to open circuit kwargs = { - 'breakdown_factor': parameters['breakdown_factor'], - 'breakdown_exp': parameters['breakdown_exp'], - 'breakdown_voltage': parameters['breakdown_voltage'], + "breakdown_factor": parameters["breakdown_factor"], + "breakdown_exp": parameters["breakdown_exp"], + "breakdown_voltage": parameters["breakdown_voltage"], } - v_oc = singlediode.bishop88_v_from_i( - 0.0, *sde_args, **kwargs - ) + v_oc = singlediode.bishop88_v_from_i(0.0, *sde_args, **kwargs) # ideally would use some intelligent log-spacing to concentrate points # around the forward- and reverse-bias knees, but this is good enough: - vd = np.linspace(0.99*kwargs['breakdown_voltage'], v_oc, ivcurve_pnts) + vd = np.linspace(0.99 * kwargs["breakdown_voltage"], v_oc, ivcurve_pnts) ivcurve_i, ivcurve_v, _ = singlediode.bishop88(vd, *sde_args, **kwargs) - return pd.DataFrame({ - 'i': ivcurve_i, - 'v': ivcurve_v, - }) + return pd.DataFrame( + { + "i": ivcurve_i, + "v": ivcurve_v, + } + ) # %% @@ -131,18 +131,19 @@ def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000): # portion largely intact. In this example plot, we choose :math:`200 W/m^2` # as the amount of irradiance received by a shaded cell. + def plot_curves(dfs, labels, title): """plot the forward- and reverse-bias portions of an IV curve""" fig, axes = plt.subplots(1, 2, sharey=True, figsize=(5, 3)) for df, label in zip(dfs, labels): - df.plot('v', 'i', label=label, ax=axes[0]) - df.plot('v', 'i', label=label, ax=axes[1]) + df.plot("v", "i", label=label, ax=axes[0]) + df.plot("v", "i", label=label, ax=axes[1]) axes[0].set_xlim(right=0) axes[0].set_ylim([0, 25]) - axes[1].set_xlim([0, df['v'].max()*1.5]) - axes[0].set_ylabel('current [A]') - axes[0].set_xlabel('voltage [V]') - axes[1].set_xlabel('voltage [V]') + axes[1].set_xlim([0, df["v"].max() * 1.5]) + axes[0].set_ylabel("current [A]") + axes[0].set_xlabel("voltage [V]") + axes[1].set_xlabel("voltage [V]") fig.suptitle(title) fig.tight_layout() return axes @@ -150,9 +151,11 @@ def plot_curves(dfs, labels, title): cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=25) cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=25) -ax = plot_curves([cell_curve_full_sun, cell_curve_shaded], - labels=['Full Sun', 'Shaded'], - title='Cell-level reverse- and forward-biased IV curves') +ax = plot_curves( + [cell_curve_full_sun, cell_curve_shaded], + labels=["Full Sun", "Shaded"], + title="Cell-level reverse- and forward-biased IV curves", +) # %% # This figure shows how a cell's current decreases roughly in proportion to @@ -179,8 +182,12 @@ def plot_curves(dfs, labels, title): def interpolate(df, i): """convenience wrapper around scipy.interpolate.interp1d""" - f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear', - fill_value='extrapolate') + f_interp = interp1d( + np.flipud(df["i"]), + np.flipud(df["v"]), + kind="linear", + fill_value="extrapolate", + ) return f_interp(i) @@ -190,14 +197,14 @@ def combine_series(dfs): The current range is based on the first curve's current range. """ df1 = dfs[0] - imin = df1['i'].min() - imax = df1['i'].max() + imin = df1["i"].min() + imax = df1["i"].max() i = np.linspace(imin, imax, 1000) v = 0 for df2 in dfs: v_cell = interpolate(df2, i) v += v_cell - return pd.DataFrame({'i': i, 'v': v}) + return pd.DataFrame({"i": i, "v": v}) # %% @@ -214,8 +221,16 @@ def combine_series(dfs): # substring's voltage is clamped to the diode's trigger voltage (assumed to # be 0.5V here). -def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, - shaded_fraction, cells_per_string=24, strings=3): + +def simulate_module( + cell_parameters, + poa_direct, + poa_diffuse, + Tcell, + shaded_fraction, + cells_per_string=24, + strings=3, +): """ Simulate the IV curve for a partially shaded module. The shade is assumed to be coming up from the bottom of the module when in @@ -232,32 +247,30 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, partial_shade_fraction = 1 - (shaded_fraction * nrow - nrow_full_shade) df_lit = simulate_full_curve( - cell_parameters, - poa_diffuse + poa_direct, - Tcell) + cell_parameters, poa_diffuse + poa_direct, Tcell + ) df_partial = simulate_full_curve( cell_parameters, poa_diffuse + partial_shade_fraction * poa_direct, - Tcell) - df_shaded = simulate_full_curve( - cell_parameters, - poa_diffuse, - Tcell) + Tcell, + ) + df_shaded = simulate_full_curve(cell_parameters, poa_diffuse, Tcell) # build a list of IV curves for a single column of cells (half a substring) - include_partial_cell = (shaded_fraction < 1) + include_partial_cell = shaded_fraction < 1 half_substring_curves = ( [df_lit] * (nrow - nrow_full_shade - 1) + ([df_partial] if include_partial_cell else []) # noqa: W503 + [df_shaded] * nrow_full_shade # noqa: W503 ) substring_curve = combine_series(half_substring_curves) - substring_curve['v'] *= 2 # turn half strings into whole strings + substring_curve["v"] *= 2 # turn half strings into whole strings # bypass diode: - substring_curve['v'] = substring_curve['v'].clip(lower=-0.5) + substring_curve["v"] = substring_curve["v"].clip(lower=-0.5) # no need to interpolate since we're just scaling voltage directly: - substring_curve['v'] *= strings + substring_curve["v"] *= strings return substring_curve + # %% # Now let's see how shade affects the IV curves at the module level. For this # example, the bottom 10% of the module is shaded. Assuming 12 cells per @@ -272,16 +285,18 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, kwargs = { - 'cell_parameters': cell_parameters, - 'poa_direct': 800, - 'poa_diffuse': 200, - 'Tcell': 25 + "cell_parameters": cell_parameters, + "poa_direct": 800, + "poa_diffuse": 200, + "Tcell": 25, } module_curve_full_sun = simulate_module(shaded_fraction=0, **kwargs) module_curve_shaded = simulate_module(shaded_fraction=0.1, **kwargs) -ax = plot_curves([module_curve_full_sun, module_curve_shaded], - labels=['Full Sun', 'Shaded'], - title='Module-level reverse- and forward-biased IV curves') +ax = plot_curves( + [module_curve_full_sun, module_curve_shaded], + labels=["Full Sun", "Shaded"], + title="Module-level reverse- and forward-biased IV curves", +) # %% # Calculating shading loss across shading scenarios @@ -303,30 +318,33 @@ def find_pmp(df): data = [] for diffuse_fraction in np.linspace(0, 1, 11): for shaded_fraction in np.linspace(0, 1, 51): - - df = simulate_module(cell_parameters, - poa_direct=(1-diffuse_fraction)*1000, - poa_diffuse=diffuse_fraction*1000, - Tcell=25, - shaded_fraction=shaded_fraction) - data.append({ - 'fd': diffuse_fraction, - 'fs': shaded_fraction, - 'pmp': find_pmp(df) - }) + df = simulate_module( + cell_parameters, + poa_direct=(1 - diffuse_fraction) * 1000, + poa_diffuse=diffuse_fraction * 1000, + Tcell=25, + shaded_fraction=shaded_fraction, + ) + data.append( + { + "fd": diffuse_fraction, + "fs": shaded_fraction, + "pmp": find_pmp(df), + } + ) results = pd.DataFrame(data) -results['pmp'] /= results['pmp'].max() # normalize power to 0-1 -results_pivot = results.pivot(index='fd', columns='fs', values='pmp') +results["pmp"] /= results["pmp"].max() # normalize power to 0-1 +results_pivot = results.pivot(index="fd", columns="fs", values="pmp") plt.figure() -plt.imshow(results_pivot, origin='lower', aspect='auto') -plt.xlabel('shaded fraction') -plt.ylabel('diffuse fraction') +plt.imshow(results_pivot, origin="lower", aspect="auto") +plt.xlabel("shaded fraction") +plt.ylabel("diffuse fraction") xlabels = [f"{fs:0.02f}" for fs in results_pivot.columns[::5]] ylabels = [f"{fd:0.02f}" for fd in results_pivot.index] -plt.xticks(range(0, 5*len(xlabels), 5), xlabels) +plt.xticks(range(0, 5 * len(xlabels), 5), xlabels) plt.yticks(range(0, len(ylabels)), ylabels) -plt.title('Module P_mp across shading conditions') +plt.title("Module P_mp across shading conditions") plt.colorbar() plt.show() # use this figure as the thumbnail: diff --git a/docs/examples/shading/plot_passias_diffuse_shading.py b/docs/examples/shading/plot_passias_diffuse_shading.py index 828cf141a1..e5d54f2e32 100644 --- a/docs/examples/shading/plot_passias_diffuse_shading.py +++ b/docs/examples/shading/plot_passias_diffuse_shading.py @@ -43,12 +43,12 @@ plt.figure() for k in [1, 1.5, 2, 2.5, 3, 4, 5, 7, 10]: - gcr = 1/k + gcr = 1 / k psi = shading.masking_angle_passias(surface_tilt, gcr) - plt.plot(surface_tilt, psi, label=f'k={k}') + plt.plot(surface_tilt, psi, label=f"k={k}") -plt.xlabel('Inclination angle [degrees]') -plt.ylabel('Average masking angle [degrees]') +plt.xlabel("Inclination angle [degrees]") +plt.ylabel("Average masking angle [degrees]") plt.legend() plt.show() @@ -66,15 +66,15 @@ plt.figure() for k in [1, 1.5, 2, 10]: - gcr = 1/k + gcr = 1 / k psi = shading.masking_angle_passias(surface_tilt, gcr) shading_loss = shading.sky_diffuse_passias(psi) transposition_ratio = irradiance.isotropic(surface_tilt, dhi=1.0) - relative_diffuse = transposition_ratio * (1-shading_loss) * 100 # % - plt.plot(surface_tilt, relative_diffuse, label=f'k={k}') + relative_diffuse = transposition_ratio * (1 - shading_loss) * 100 # % + plt.plot(surface_tilt, relative_diffuse, label=f"k={k}") -plt.xlabel('Inclination angle [degrees]') -plt.ylabel('Relative diffuse irradiance [%]') +plt.xlabel("Inclination angle [degrees]") +plt.ylabel("Relative diffuse irradiance [%]") plt.ylim(0, 105) plt.legend() plt.show() diff --git a/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py b/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py index bdeb414adb..c2048a83d9 100644 --- a/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py +++ b/docs/examples/shading/plot_simple_irradiance_adjustment_for_horizon_shading.py @@ -23,12 +23,10 @@ # Golden, CO latitude, longitude = 39.76, -105.22 -tz = 'MST' +tz = "MST" # Set times in the morning of the December solstice. -times = pd.date_range( - '2020-12-20 6:30', '2020-12-20 9:00', freq='1T', tz=tz -) +times = pd.date_range("2020-12-20 6:30", "2020-12-20 9:00", freq="1T", tz=tz) # Create location object, and get solar position and clearsky irradiance data. location = pvlib.location.Location(latitude, longitude, tz) @@ -49,18 +47,65 @@ # With basic inputs in place, let's perform the adjustment for horizon shading: # Use hard-coded horizon profile data from location object above. -horizon_profile = pd.Series([ - 10.7, 11.8, 11.5, 10.3, 8.0, 6.5, 3.8, 2.3, 2.3, 2.3, 4.6, 8.0, 10.3, 11.1, - 10.7, 10.3, 9.2, 6.1, 5.3, 2.3, 3.1, 1.9, 1.9, 2.7, 3.8, 5.3, 6.5, 8.4, - 8.8, 8.4, 8.4, 8.4, 6.5, 6.1, 6.5, 6.1, 7.3, 9.2, 8.4, 8.0, 5.7, 5.3, 5.3, - 4.2, 4.2, 4.2, 7.3, 9.5 -], index=np.arange(0, 360, 7.5)) +horizon_profile = pd.Series( + [ + 10.7, + 11.8, + 11.5, + 10.3, + 8.0, + 6.5, + 3.8, + 2.3, + 2.3, + 2.3, + 4.6, + 8.0, + 10.3, + 11.1, + 10.7, + 10.3, + 9.2, + 6.1, + 5.3, + 2.3, + 3.1, + 1.9, + 1.9, + 2.7, + 3.8, + 5.3, + 6.5, + 8.4, + 8.8, + 8.4, + 8.4, + 8.4, + 6.5, + 6.1, + 6.5, + 6.1, + 7.3, + 9.2, + 8.4, + 8.0, + 5.7, + 5.3, + 5.3, + 4.2, + 4.2, + 4.2, + 7.3, + 9.5, + ], + index=np.arange(0, 360, 7.5), +) ax = horizon_profile.plot(xlim=(0, 360), ylim=(0, None), figsize=(6, 2.5)) -ax.set_title('Horizon profile') +ax.set_title("Horizon profile") ax.set_xticks([0, 90, 180, 270, 360]) -ax.set_xlabel('Azimuth [°]') -ax.set_ylabel('Horizon angle [°]') +ax.set_xlabel("Azimuth [°]") +ax.set_ylabel("Horizon angle [°]") # %% # .. admonition:: Horizon data from PVGIS @@ -93,26 +138,32 @@ ) irrad_post_adj = pvlib.irradiance.get_total_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni_adjusted, - ghi_adjusted, dhi + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni_adjusted, + ghi_adjusted, + dhi, ) # Create and plot result DataFrames. -poa_global_comparison = pd.DataFrame({ - 'poa_global_pre-adjustment': irrad_pre_adj.poa_global, - 'poa_global_post-adjustment': irrad_post_adj.poa_global -}) +poa_global_comparison = pd.DataFrame( + { + "poa_global_pre-adjustment": irrad_pre_adj.poa_global, + "poa_global_post-adjustment": irrad_post_adj.poa_global, + } +) -dni_comparison = pd.DataFrame({ - 'dni_pre-adjustment': dni, - 'dni_post-adjustment': dni_adjusted -}) +dni_comparison = pd.DataFrame( + {"dni_pre-adjustment": dni, "dni_post-adjustment": dni_adjusted} +) # Plot results poa_global_comparison.plot( - title='POA-Global: Before and after Horizon Adjustment', - ylabel='Irradiance' + title="POA-Global: Before and after Horizon Adjustment", + ylabel="Irradiance", ) dni_comparison.plot( - title='DNI: Before and after Horizon Adjustment', ylabel='Irradiance' + title="DNI: Before and after Horizon Adjustment", ylabel="Irradiance" ) diff --git a/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py b/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py index 9103fa0207..571f76f2b5 100644 --- a/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py +++ b/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py @@ -28,43 +28,56 @@ import pandas as pd # get full path to the data directory -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" # read rainfall, PM2.5, and PM10 data from file -imperial_county = pd.read_csv(DATA_DIR / 'soiling_hsu_example_inputs.csv', - index_col=0, parse_dates=True) -rainfall = imperial_county['rain'] -depo_veloc = {'2_5': 0.0009, '10': 0.004} # default values from [1] (m/s) -rain_accum_period = pd.Timedelta('1h') # default +imperial_county = pd.read_csv( + DATA_DIR / "soiling_hsu_example_inputs.csv", index_col=0, parse_dates=True +) +rainfall = imperial_county["rain"] +depo_veloc = {"2_5": 0.0009, "10": 0.004} # default values from [1] (m/s) +rain_accum_period = pd.Timedelta("1h") # default cleaning_threshold = 0.5 tilt = 30 -pm2_5 = imperial_county['PM2_5'].values -pm10 = imperial_county['PM10'].values +pm2_5 = imperial_county["PM2_5"].values +pm10 = imperial_county["PM10"].values # run the hsu soiling model -soiling_ratio = soiling.hsu(rainfall, cleaning_threshold, tilt, pm2_5, pm10, - depo_veloc=depo_veloc, - rain_accum_period=rain_accum_period) +soiling_ratio = soiling.hsu( + rainfall, + cleaning_threshold, + tilt, + pm2_5, + pm10, + depo_veloc=depo_veloc, + rain_accum_period=rain_accum_period, +) # %% # And now we'll plot the modeled daily soiling ratios and compare # with Coello and Boyle Fig 3A: -daily_soiling_ratio = soiling_ratio.resample('d').mean() +daily_soiling_ratio = soiling_ratio.resample("d").mean() fig, ax1 = plt.subplots(figsize=(8, 2)) -ax1.plot(daily_soiling_ratio.index, daily_soiling_ratio, marker='.', - c='r', label='hsu function output') -ax1.set_ylabel('Daily Soiling Ratio') +ax1.plot( + daily_soiling_ratio.index, + daily_soiling_ratio, + marker=".", + c="r", + label="hsu function output", +) +ax1.set_ylabel("Daily Soiling Ratio") ax1.set_ylim(0.79, 1.01) -ax1.set_title('Imperial County TMY') -ax1.legend(loc='center left') +ax1.set_title("Imperial County TMY") +ax1.legend(loc="center left") -daily_rain = rainfall.resample('d').sum() +daily_rain = rainfall.resample("d").sum() ax2 = ax1.twinx() -ax2.plot(daily_rain.index, daily_rain, marker='.', - c='c', label='daily rainfall') -ax2.set_ylabel('Daily Rain (mm)') +ax2.plot( + daily_rain.index, daily_rain, marker=".", c="c", label="daily rainfall" +) +ax2.set_ylabel("Daily Rain (mm)") ax2.set_ylim(-10, 210) -ax2.legend(loc='center right') +ax2.legend(loc="center right") fig.tight_layout() fig.show() diff --git a/docs/examples/soiling/plot_greensboro_kimber_soiling.py b/docs/examples/soiling/plot_greensboro_kimber_soiling.py index 6565679f6d..8231e234be 100644 --- a/docs/examples/soiling/plot_greensboro_kimber_soiling.py +++ b/docs/examples/soiling/plot_greensboro_kimber_soiling.py @@ -37,31 +37,39 @@ import pvlib # get full path to the data directory -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" # get TMY3 data with rain -greensboro, _ = read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990, - map_variables=True) +greensboro, _ = read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=1990, map_variables=True +) # get the rain data -greensboro_rain = greensboro['Lprecip depth (mm)'] +greensboro_rain = greensboro["Lprecip depth (mm)"] # calculate soiling with no wash dates and cleaning threshold of 25-mm of rain THRESHOLD = 25.0 soiling_no_wash = kimber(greensboro_rain, cleaning_threshold=THRESHOLD) -soiling_no_wash.name = 'soiling' +soiling_no_wash.name = "soiling" # daily rain totals -daily_rain = greensboro_rain.iloc[:-1].resample('D').sum() +daily_rain = greensboro_rain.iloc[:-1].resample("D").sum() plt.plot( - daily_rain.index.to_pydatetime(), daily_rain.values/25.4, - soiling_no_wash.index.to_pydatetime(), soiling_no_wash.values*100.0) + daily_rain.index.to_pydatetime(), + daily_rain.values / 25.4, + soiling_no_wash.index.to_pydatetime(), + soiling_no_wash.values * 100.0, +) plt.hlines( - THRESHOLD/25.4, xmin=datetime(1990, 1, 1), xmax=datetime(1990, 12, 31), - linestyles='--') + THRESHOLD / 25.4, + xmin=datetime(1990, 1, 1), + xmax=datetime(1990, 12, 31), + linestyles="--", +) plt.grid() plt.title( - f'Kimber Soiling Model, dashed line shows threshold ({THRESHOLD}[mm])') -plt.xlabel('timestamp') -plt.ylabel('soiling build-up fraction [%] and daily rainfall [inches]') -plt.legend(['daily rainfall [in]', 'soiling [%]']) + f"Kimber Soiling Model, dashed line shows threshold ({THRESHOLD}[mm])" +) +plt.xlabel("timestamp") +plt.ylabel("soiling build-up fraction [%] and daily rainfall [inches]") +plt.legend(["daily rainfall [in]", "soiling [%]"]) plt.tight_layout() plt.show() diff --git a/docs/examples/solar-position/plot_sunpath_diagrams.py b/docs/examples/solar-position/plot_sunpath_diagrams.py index f6f237c706..71617dfcb5 100644 --- a/docs/examples/solar-position/plot_sunpath_diagrams.py +++ b/docs/examples/solar-position/plot_sunpath_diagrams.py @@ -5,7 +5,7 @@ Examples of generating sunpath diagrams. """ -#%% +# %% # This example shows basic usage of pvlib's solar position calculations with # :py:meth:`pvlib.solarposition.get_solarposition`. The examples shown here # will generate sunpath diagrams that shows solar position over a year. @@ -21,27 +21,34 @@ import numpy as np import matplotlib.pyplot as plt -tz = 'Asia/Calcutta' +tz = "Asia/Calcutta" lat, lon = 28.6, 77.2 -times = pd.date_range('2019-01-01 00:00:00', '2020-01-01', freq='H', tz=tz) +times = pd.date_range("2019-01-01 00:00:00", "2020-01-01", freq="H", tz=tz) solpos = solarposition.get_solarposition(times, lat, lon) # remove nighttime -solpos = solpos.loc[solpos['apparent_elevation'] > 0, :] +solpos = solpos.loc[solpos["apparent_elevation"] > 0, :] -ax = plt.subplot(1, 1, 1, projection='polar') +ax = plt.subplot(1, 1, 1, projection="polar") # draw the analemma loops -points = ax.scatter(np.radians(solpos.azimuth), solpos.apparent_zenith, - s=2, label=None, c=solpos.index.dayofyear, - cmap='twilight_shifted_r') +points = ax.scatter( + np.radians(solpos.azimuth), + solpos.apparent_zenith, + s=2, + label=None, + c=solpos.index.dayofyear, + cmap="twilight_shifted_r", +) # add and format colorbar cbar = ax.figure.colorbar(points) -times_ticks = pd.date_range('2019-01-01', '2020-01-01', freq='MS', tz=tz) +times_ticks = pd.date_range("2019-01-01", "2020-01-01", freq="MS", tz=tz) cbar.set_ticks(ticks=times_ticks.dayofyear, labels=[], minor=False) -cbar.set_ticks(ticks=times_ticks.dayofyear+15, - labels=times_ticks.strftime('%b'), - minor=True) -cbar.ax.tick_params(which='minor', width=0) +cbar.set_ticks( + ticks=times_ticks.dayofyear + 15, + labels=times_ticks.strftime("%b"), + minor=True, +) +cbar.ax.tick_params(which="minor", width=0) # draw hour labels for hour in np.unique(solpos.index.hour): @@ -49,27 +56,32 @@ subset = solpos.loc[solpos.index.hour == hour, :] r = subset.apparent_zenith pos = solpos.loc[r.idxmin(), :] - ax.text(np.radians(pos['azimuth']), pos['apparent_zenith'], - str(hour).zfill(2), ha='center', va='bottom') + ax.text( + np.radians(pos["azimuth"]), + pos["apparent_zenith"], + str(hour).zfill(2), + ha="center", + va="bottom", + ) # draw individual days -for date in pd.to_datetime(['2019-03-21', '2019-06-21', '2019-12-21']): - times = pd.date_range(date, date+pd.Timedelta('24h'), freq='5min', tz=tz) +for date in pd.to_datetime(["2019-03-21", "2019-06-21", "2019-12-21"]): + times = pd.date_range(date, date + pd.Timedelta("24h"), freq="5min", tz=tz) solpos = solarposition.get_solarposition(times, lat, lon) - solpos = solpos.loc[solpos['apparent_elevation'] > 0, :] - label = date.strftime('%b %d') + solpos = solpos.loc[solpos["apparent_elevation"] > 0, :] + label = date.strftime("%b %d") ax.plot(np.radians(solpos.azimuth), solpos.apparent_zenith, label=label) -ax.figure.legend(loc='upper left') +ax.figure.legend(loc="upper left") # change coordinates to be like a compass -ax.set_theta_zero_location('N') +ax.set_theta_zero_location("N") ax.set_theta_direction(-1) ax.set_rmax(90) plt.show() -#%% +# %% # This is a polar plot of hourly solar zenith and azimuth. The figure-8 # patterns are called `analemmas `_ and # show how the sun's path slowly shifts over the course of the year . The @@ -98,7 +110,7 @@ # - after about 5:30 PM on the spring equinox # - after about 4:30 PM on the winter solstice -#%% +# %% # PVSyst Plot # ----------- # @@ -110,46 +122,58 @@ import numpy as np import matplotlib.pyplot as plt -tz = 'Asia/Calcutta' +tz = "Asia/Calcutta" lat, lon = 28.6, 77.2 -times = pd.date_range('2019-01-01 00:00:00', '2020-01-01', freq='H', tz=tz) +times = pd.date_range("2019-01-01 00:00:00", "2020-01-01", freq="H", tz=tz) solpos = solarposition.get_solarposition(times, lat, lon) # remove nighttime -solpos = solpos.loc[solpos['apparent_elevation'] > 0, :] +solpos = solpos.loc[solpos["apparent_elevation"] > 0, :] fig, ax = plt.subplots() -points = ax.scatter(solpos.azimuth, solpos.apparent_elevation, s=2, - c=solpos.index.dayofyear, label=None, - cmap='twilight_shifted_r') +points = ax.scatter( + solpos.azimuth, + solpos.apparent_elevation, + s=2, + c=solpos.index.dayofyear, + label=None, + cmap="twilight_shifted_r", +) # add and format colorbar cbar = fig.colorbar(points) -times_ticks = pd.date_range('2019-01-01', '2020-01-01', freq='MS', tz=tz) +times_ticks = pd.date_range("2019-01-01", "2020-01-01", freq="MS", tz=tz) cbar.set_ticks(ticks=times_ticks.dayofyear, labels=[], minor=False) -cbar.set_ticks(ticks=times_ticks.dayofyear+15, - labels=times_ticks.strftime('%b'), - minor=True) -cbar.ax.tick_params(which='minor', width=0) +cbar.set_ticks( + ticks=times_ticks.dayofyear + 15, + labels=times_ticks.strftime("%b"), + minor=True, +) +cbar.ax.tick_params(which="minor", width=0) for hour in np.unique(solpos.index.hour): # choose label position by the largest elevation for each hour subset = solpos.loc[solpos.index.hour == hour, :] height = subset.apparent_elevation pos = solpos.loc[height.idxmax(), :] - azimuth_offset = -10 if pos['azimuth'] < 180 else 10 - ax.text(pos['azimuth']+azimuth_offset, pos['apparent_elevation'], - str(hour).zfill(2), ha='center', va='bottom') - -for date in pd.to_datetime(['2019-03-21', '2019-06-21', '2019-12-21']): - times = pd.date_range(date, date+pd.Timedelta('24h'), freq='5min', tz=tz) + azimuth_offset = -10 if pos["azimuth"] < 180 else 10 + ax.text( + pos["azimuth"] + azimuth_offset, + pos["apparent_elevation"], + str(hour).zfill(2), + ha="center", + va="bottom", + ) + +for date in pd.to_datetime(["2019-03-21", "2019-06-21", "2019-12-21"]): + times = pd.date_range(date, date + pd.Timedelta("24h"), freq="5min", tz=tz) solpos = solarposition.get_solarposition(times, lat, lon) - solpos = solpos.loc[solpos['apparent_elevation'] > 0, :] - label = date.strftime('%d %b') + solpos = solpos.loc[solpos["apparent_elevation"] > 0, :] + label = date.strftime("%d %b") ax.plot(solpos.azimuth, solpos.apparent_elevation, label=label) -ax.figure.legend(loc='upper center', bbox_to_anchor=[0.45, 1], ncols=3) -ax.set_xlabel('Solar Azimuth (degrees)') -ax.set_ylabel('Solar Elevation (degrees)') +ax.figure.legend(loc="upper center", bbox_to_anchor=[0.45, 1], ncols=3) +ax.set_xlabel("Solar Azimuth (degrees)") +ax.set_ylabel("Solar Elevation (degrees)") ax.set_xticks([0, 90, 180, 270, 360]) ax.set_ylim(0, None) diff --git a/docs/examples/solar-tracking/plot_discontinuous_tracking.py b/docs/examples/solar-tracking/plot_discontinuous_tracking.py index f385675d57..88c7fe6c16 100644 --- a/docs/examples/solar-tracking/plot_discontinuous_tracking.py +++ b/docs/examples/solar-tracking/plot_discontinuous_tracking.py @@ -32,55 +32,63 @@ def get_orientation(self, solar_zenith, solar_azimuth): # Different trackers update at different rates; in this example we'll # assume a relatively slow update interval of 15 minutes to make the # effect more visually apparent. - zenith_subset = solar_zenith.resample('15min').first() - azimuth_subset = solar_azimuth.resample('15min').first() + zenith_subset = solar_zenith.resample("15min").first() + azimuth_subset = solar_azimuth.resample("15min").first() tracking_data_15min = tracking.singleaxis( - zenith_subset, azimuth_subset, - self.axis_tilt, self.axis_azimuth, - self.max_angle, self.backtrack, - self.gcr, self.cross_axis_tilt + zenith_subset, + azimuth_subset, + self.axis_tilt, + self.axis_azimuth, + self.max_angle, + self.backtrack, + self.gcr, + self.cross_axis_tilt, ) # propagate the 15-minute positions to 1-minute stair-stepped values: - tracking_data_1min = tracking_data_15min.reindex(solar_zenith.index, - method='ffill') + tracking_data_1min = tracking_data_15min.reindex( + solar_zenith.index, method="ffill" + ) return tracking_data_1min # %% # Let's take a look at the tracker rotation curve it produces: -times = pd.date_range('2019-06-01', '2019-06-02', freq='1min', tz='US/Eastern') +times = pd.date_range("2019-06-01", "2019-06-02", freq="1min", tz="US/Eastern") loc = location.Location(40, -80) solpos = loc.get_solarposition(times) mount = DiscontinuousTrackerMount(axis_azimuth=180, gcr=0.4) tracker_data = mount.get_orientation(solpos.apparent_zenith, solpos.azimuth) -tracker_data['tracker_theta'].plot() -plt.ylabel('Tracker Rotation [degree]') +tracker_data["tracker_theta"].plot() +plt.ylabel("Tracker Rotation [degree]") plt.show() # %% # With our custom tracking logic defined, we can create the corresponding # Array and PVSystem, and then run a ModelChain as usual: -module_parameters = {'pdc0': 1, 'gamma_pdc': -0.004, 'b': 0.05} -temp_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'] -array = pvsystem.Array(mount=mount, module_parameters=module_parameters, - temperature_model_parameters=temp_params) -system = pvsystem.PVSystem(arrays=[array], inverter_parameters={'pdc0': 1}) -mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') +module_parameters = {"pdc0": 1, "gamma_pdc": -0.004, "b": 0.05} +temp_params = TEMPERATURE_MODEL_PARAMETERS["sapm"]["open_rack_glass_polymer"] +array = pvsystem.Array( + mount=mount, + module_parameters=module_parameters, + temperature_model_parameters=temp_params, +) +system = pvsystem.PVSystem(arrays=[array], inverter_parameters={"pdc0": 1}) +mc = modelchain.ModelChain(system, loc, spectral_model="no_loss") # simple simulated weather, just to show the effect of discrete tracking weather = loc.get_clearsky(times) -weather['temp_air'] = 25 -weather['wind_speed'] = 1 +weather["temp_air"] = 25 +weather["wind_speed"] = 1 mc.run_model(weather) fig, axes = plt.subplots(2, 1, sharex=True) mc.results.effective_irradiance.plot(ax=axes[0]) -axes[0].set_ylabel('Effective Irradiance [W/m^2]') +axes[0].set_ylabel("Effective Irradiance [W/m^2]") mc.results.ac.plot(ax=axes[1]) -axes[1].set_ylabel('AC Power') +axes[1].set_ylabel("AC Power") fig.show() # %% diff --git a/docs/examples/solar-tracking/plot_dual_axis_tracking.py b/docs/examples/solar-tracking/plot_dual_axis_tracking.py index 8f51512726..863923468a 100644 --- a/docs/examples/solar-tracking/plot_dual_axis_tracking.py +++ b/docs/examples/solar-tracking/plot_dual_axis_tracking.py @@ -23,22 +23,24 @@ class DualAxisTrackerMount(pvsystem.AbstractMount): def get_orientation(self, solar_zenith, solar_azimuth): # no rotation limits, no backtracking - return {'surface_tilt': solar_zenith, 'surface_azimuth': solar_azimuth} + return {"surface_tilt": solar_zenith, "surface_azimuth": solar_azimuth} loc = location.Location(40, -80) array = pvsystem.Array( mount=DualAxisTrackerMount(), module_parameters=dict(pdc0=1, gamma_pdc=-0.004, b=0.05), - temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3)) + temperature_model_parameters=dict(a=-3.56, b=-0.075, deltaT=3), +) system = pvsystem.PVSystem(arrays=[array], inverter_parameters=dict(pdc0=3)) -mc = modelchain.ModelChain(system, loc, spectral_model='no_loss') +mc = modelchain.ModelChain(system, loc, spectral_model="no_loss") -times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='5min', - tz='Etc/GMT+5') +times = pd.date_range( + "2019-01-01 06:00", "2019-01-01 18:00", freq="5min", tz="Etc/GMT+5" +) weather = loc.get_clearsky(times) mc.run_model(weather) mc.results.ac.plot() -plt.ylabel('Output Power') +plt.ylabel("Output Power") plt.show() diff --git a/docs/examples/solar-tracking/plot_single_axis_tracking.py b/docs/examples/solar-tracking/plot_single_axis_tracking.py index 80ed800be6..7b4cc878d8 100644 --- a/docs/examples/solar-tracking/plot_single_axis_tracking.py +++ b/docs/examples/solar-tracking/plot_single_axis_tracking.py @@ -5,7 +5,7 @@ Examples of modeling tilt angles for single-axis tracker arrays. """ -#%% +# %% # This example shows basic usage of pvlib's tracker position calculations with # :py:meth:`pvlib.tracking.singleaxis`. The examples shown here demonstrate # how the tracker parameters affect the generated tilt angles. @@ -24,28 +24,28 @@ import pandas as pd import matplotlib.pyplot as plt -tz = 'US/Eastern' +tz = "US/Eastern" lat, lon = 40, -80 -times = pd.date_range('2019-01-01', '2019-01-02', freq='5min', - tz=tz) +times = pd.date_range("2019-01-01", "2019-01-02", freq="5min", tz=tz) solpos = solarposition.get_solarposition(times, lat, lon) truetracking_angles = tracking.singleaxis( - apparent_zenith=solpos['apparent_zenith'], - apparent_azimuth=solpos['azimuth'], + apparent_zenith=solpos["apparent_zenith"], + apparent_azimuth=solpos["azimuth"], axis_tilt=0, axis_azimuth=180, max_angle=90, backtrack=False, # for true-tracking - gcr=0.5) # irrelevant for true-tracking + gcr=0.5, +) # irrelevant for true-tracking -truetracking_position = truetracking_angles['tracker_theta'].fillna(0) -truetracking_position.plot(title='Truetracking Curve') +truetracking_position = truetracking_angles["tracker_theta"].fillna(0) +truetracking_position.plot(title="Truetracking Curve") plt.show() -#%% +# %% # Backtracking # ------------- # @@ -60,18 +60,19 @@ for gcr in [0.2, 0.4, 0.6]: backtracking_angles = tracking.singleaxis( - apparent_zenith=solpos['apparent_zenith'], - apparent_azimuth=solpos['azimuth'], + apparent_zenith=solpos["apparent_zenith"], + apparent_azimuth=solpos["azimuth"], axis_tilt=0, axis_azimuth=180, max_angle=90, backtrack=True, - gcr=gcr) + gcr=gcr, + ) - backtracking_position = backtracking_angles['tracker_theta'].fillna(0) - backtracking_position.plot(title='Backtracking Curve', - label=f'GCR:{gcr:0.01f}', - ax=ax) + backtracking_position = backtracking_angles["tracker_theta"].fillna(0) + backtracking_position.plot( + title="Backtracking Curve", label=f"GCR:{gcr:0.01f}", ax=ax + ) plt.legend() plt.show() diff --git a/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py b/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py index 979007efa1..63308ed574 100644 --- a/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py +++ b/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py @@ -86,35 +86,37 @@ import matplotlib.pyplot as plt # PV system parameters -tz = 'US/Eastern' +tz = "US/Eastern" lat, lon = 40, -80 gcr = 0.4 # calculate the solar position -times = pd.date_range('2019-01-01 06:00', '2019-01-01 18:00', freq='1min', - tz=tz) +times = pd.date_range( + "2019-01-01 06:00", "2019-01-01 18:00", freq="1min", tz=tz +) solpos = solarposition.get_solarposition(times, lat, lon) # compare the backtracking angle at various terrain slopes fig, ax = plt.subplots() for cross_axis_tilt in [0, 5, 10]: tracker_data = tracking.singleaxis( - apparent_zenith=solpos['apparent_zenith'], - apparent_azimuth=solpos['azimuth'], + apparent_zenith=solpos["apparent_zenith"], + apparent_azimuth=solpos["azimuth"], axis_tilt=0, # flat because the axis is perpendicular to the slope axis_azimuth=180, # N-S axis, azimuth facing south max_angle=90, backtrack=True, gcr=gcr, - cross_axis_tilt=cross_axis_tilt) + cross_axis_tilt=cross_axis_tilt, + ) # tracker rotation is undefined at night - backtracking_position = tracker_data['tracker_theta'].fillna(0) - label = 'cross-axis tilt: {}°'.format(cross_axis_tilt) + backtracking_position = tracker_data["tracker_theta"].fillna(0) + label = "cross-axis tilt: {}°".format(cross_axis_tilt) backtracking_position.plot(label=label, ax=ax) plt.legend() -plt.title('Backtracking Curves') +plt.title("Backtracking Curves") plt.show() # %% @@ -144,30 +146,32 @@ axis_tilt = tracking.calc_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth) # calculate the cross-axis tilt: -cross_axis_tilt = tracking.calc_cross_axis_tilt(slope_azimuth, slope_tilt, - axis_azimuth, axis_tilt) +cross_axis_tilt = tracking.calc_cross_axis_tilt( + slope_azimuth, slope_tilt, axis_azimuth, axis_tilt +) -print('Axis tilt:', '{:0.01f}°'.format(axis_tilt)) -print('Cross-axis tilt:', '{:0.01f}°'.format(cross_axis_tilt)) +print("Axis tilt:", "{:0.01f}°".format(axis_tilt)) +print("Cross-axis tilt:", "{:0.01f}°".format(cross_axis_tilt)) # %% # And now we can pass use these values to generate the tracker curve as # before: tracker_data = tracking.singleaxis( - apparent_zenith=solpos['apparent_zenith'], - apparent_azimuth=solpos['azimuth'], + apparent_zenith=solpos["apparent_zenith"], + apparent_azimuth=solpos["azimuth"], axis_tilt=axis_tilt, # no longer flat because the terrain imparts a tilt axis_azimuth=axis_azimuth, max_angle=90, backtrack=True, gcr=gcr, - cross_axis_tilt=cross_axis_tilt) + cross_axis_tilt=cross_axis_tilt, +) -backtracking_position = tracker_data['tracker_theta'].fillna(0) +backtracking_position = tracker_data["tracker_theta"].fillna(0) backtracking_position.plot() -title_template = 'Axis tilt: {:0.01f}° Cross-axis tilt: {:0.01f}°' +title_template = "Axis tilt: {:0.01f}° Cross-axis tilt: {:0.01f}°" plt.title(title_template.format(axis_tilt, cross_axis_tilt)) plt.show() diff --git a/docs/examples/spectrum/average_photon_energy.py b/docs/examples/spectrum/average_photon_energy.py index a883f929ea..9388e2df81 100644 --- a/docs/examples/spectrum/average_photon_energy.py +++ b/docs/examples/spectrum/average_photon_energy.py @@ -43,13 +43,15 @@ ozone = 0.31 # atm-cm albedo = 0.2 -times = pd.date_range('2023-01-01 08:00', freq='h', periods=9, - tz='America/Denver') +times = pd.date_range( + "2023-01-01 08:00", freq="h", periods=9, tz="America/Denver" +) solpos = solarposition.get_solarposition(times, lat, lon) aoi = irradiance.aoi(tilt, azimuth, solpos.apparent_zenith, solpos.azimuth) -relative_airmass = atmosphere.get_relative_airmass(solpos.apparent_zenith, - model='kastenyoung1989') +relative_airmass = atmosphere.get_relative_airmass( + solpos.apparent_zenith, model="kastenyoung1989" +) # %% # Spectral simulation @@ -80,16 +82,13 @@ # hours on the first day of 2023. plt.figure() -plt.plot(spectra_components['wavelength'], spectra_components['poa_global']) +plt.plot(spectra_components["wavelength"], spectra_components["poa_global"]) plt.xlim(200, 2700) plt.ylim(0, 1.8) plt.ylabel(r"Spectral irradiance (Wm⁻²nm⁻¹)") plt.xlabel(r"Wavelength (nm)") time_labels = times.strftime("%H%M") -labels = [ - f"{t}, {am_:0.02f}" - for t, am_ in zip(time_labels, relative_airmass) -] +labels = [f"{t}, {am_:0.02f}" for t, am_ in zip(time_labels, relative_airmass)] plt.legend(labels, title="Time, AM") plt.show() @@ -102,8 +101,8 @@ # total broadband irradiance, which we calculate by integrating the entire # spectral irradiance distribution with respect to wavelength. -spectral_poa = spectra_components['poa_global'] -wavelength = spectra_components['wavelength'] +spectral_poa = spectra_components["poa_global"] +wavelength = spectra_components["wavelength"] broadband_irradiance = trapezoid(spectral_poa, wavelength, axis=0) @@ -117,10 +116,7 @@ plt.ylabel(r"Normalised Irradiance (nm⁻¹)") plt.xlabel(r"Wavelength (nm)") time_labels = times.strftime("%H%M") -labels = [ - f"{t}, {am_:0.02f}" - for t, am_ in zip(time_labels, relative_airmass) -] +labels = [f"{t}, {am_:0.02f}" for t, am_ in zip(time_labels, relative_airmass)] plt.legend(labels, title="Time, AM") plt.show() @@ -166,11 +162,8 @@ plt.ylabel(r"Normalised Irradiance (nm⁻¹)") plt.xlabel(r"Wavelength (nm)") time_labels = times.strftime("%H%M") -labels = [ - f"{t}, {ape_:0.02f}" - for t, ape_ in zip(time_labels, ape) -] -plt.legend(labels, title="Time, APE (eV)") +labels = [f"{t}, {ape_:0.02f}" for t, ape_ in zip(time_labels, ape)] +plt.legend(labels, title="Time, APE (eV)") plt.show() # %% diff --git a/docs/examples/spectrum/plot_spectrl2_fig51A.py b/docs/examples/spectrum/plot_spectrl2_fig51A.py index 0a921aaf79..768e4564a4 100644 --- a/docs/examples/spectrum/plot_spectrl2_fig51A.py +++ b/docs/examples/spectrum/plot_spectrl2_fig51A.py @@ -34,15 +34,16 @@ ozone = 0.31 # atm-cm albedo = 0.2 -times = pd.date_range('1984-03-20 06:17', freq='h', periods=6, tz='Etc/GMT+7') +times = pd.date_range("1984-03-20 06:17", freq="h", periods=6, tz="Etc/GMT+7") solpos = solarposition.get_solarposition(times, lat, lon) aoi = irradiance.aoi(tilt, azimuth, solpos.apparent_zenith, solpos.azimuth) # The technical report uses the 'kasten1966' airmass model, but later # versions of SPECTRL2 use 'kastenyoung1989'. Here we use 'kasten1966' # for consistency with the technical report. -relative_airmass = atmosphere.get_relative_airmass(solpos.apparent_zenith, - model='kasten1966') +relative_airmass = atmosphere.get_relative_airmass( + solpos.apparent_zenith, model="kasten1966" +) # %% # With all the necessary inputs in hand we can model spectral irradiance using @@ -69,7 +70,7 @@ # Figure 5-1A: plt.figure() -plt.plot(spectra['wavelength'], spectra['poa_global']) +plt.plot(spectra["wavelength"], spectra["poa_global"]) plt.xlim(200, 2700) plt.ylim(0, 1.8) plt.title(r"Day 80 1984, $\tau=0.1$, Wv=0.5 cm") diff --git a/docs/examples/spectrum/spectral_factor.py b/docs/examples/spectrum/spectral_factor.py index 83ee488fb4..3f828855b8 100644 --- a/docs/examples/spectrum/spectral_factor.py +++ b/docs/examples/spectrum/spectral_factor.py @@ -34,10 +34,11 @@ import pvlib from pvlib import location -DATA_DIR = pathlib.Path(pvlib.__file__).parent / 'data' -meteo, metadata = pvlib.iotools.read_tmy3(DATA_DIR / '723170TYA.CSV', - coerce_year=2001, map_variables=True) -meteo = meteo.loc['2001-08-01':'2001-08-07'] +DATA_DIR = pathlib.Path(pvlib.__file__).parent / "data" +meteo, metadata = pvlib.iotools.read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=2001, map_variables=True +) +meteo = meteo.loc["2001-08-01":"2001-08-07"] # %% # Spectral Factor Functions @@ -62,22 +63,25 @@ # of the interval. # Create a location object -lat, lon = metadata['latitude'], metadata['longitude'] -alt = altitude = metadata['altitude'] -tz = 'Etc/GMT+5' -loc = location.Location(lat, lon, tz=tz, name='Greensboro, NC') +lat, lon = metadata["latitude"], metadata["longitude"] +alt = altitude = metadata["altitude"] +tz = "Etc/GMT+5" +loc = location.Location(lat, lon, tz=tz, name="Greensboro, NC") # Calculate solar position parameters solpos = loc.get_solarposition( meteo.index.shift(freq="-30min"), - pressure=meteo.pressure*100, # convert from millibar to Pa - temperature=meteo.temp_air) + pressure=meteo.pressure * 100, # convert from millibar to Pa + temperature=meteo.temp_air, +) solpos.index = meteo.index # reset index to end of the hour airmass_relative = pvlib.atmosphere.get_relative_airmass( - solpos.apparent_zenith).dropna() -airmass_absolute = pvlib.atmosphere.get_absolute_airmass(airmass_relative, - meteo.pressure*100) + solpos.apparent_zenith +).dropna() +airmass_absolute = pvlib.atmosphere.get_absolute_airmass( + airmass_relative, meteo.pressure * 100 +) # %% # Now we calculate the clearsky index, :math:`k_c`, which is the ratio of GHI # to clearsky GHI. @@ -101,17 +105,19 @@ # built-in coefficients. # Import some for a mc-Si module from the SAPM module database. -module = pvlib.pvsystem.retrieve_sam('SandiaMod')['LG_LG290N1C_G3__2013_'] +module = pvlib.pvsystem.retrieve_sam("SandiaMod")["LG_LG290N1C_G3__2013_"] # # Calculate M using the three models for an mc-Si PV module. m_sapm = pvlib.spectrum.spectral_factor_sapm(airmass_absolute, module) -m_pvspec = pvlib.spectrum.spectral_factor_pvspec(airmass_absolute, kc, - 'multisi') -m_fs = pvlib.spectrum.spectral_factor_firstsolar(w, airmass_absolute, - 'multisi') +m_pvspec = pvlib.spectrum.spectral_factor_pvspec( + airmass_absolute, kc, "multisi" +) +m_fs = pvlib.spectrum.spectral_factor_firstsolar( + w, airmass_absolute, "multisi" +) df_results = pd.concat([m_sapm, m_pvspec, m_fs], axis=1) -df_results.columns = ['SAPM', 'PVSPEC', 'FS'] +df_results.columns = ["SAPM", "PVSPEC", "FS"] # %% # Comparison Plots # ---------------- @@ -122,16 +128,17 @@ # Plot M fig1, (ax1, ax2) = plt.subplots(2, 1) df_results.plot(ax=ax1, legend=False) -ax1.set_xlabel('Day') -ax1.set_ylabel('Spectral mismatch (-)') +ax1.set_xlabel("Day") +ax1.set_ylabel("Spectral mismatch (-)") ax1.set_ylim(0.85, 1.15) -ax1.legend(loc='upper center', frameon=False, ncols=3, - bbox_to_anchor=(0.5, 1.3)) +ax1.legend( + loc="upper center", frameon=False, ncols=3, bbox_to_anchor=(0.5, 1.3) +) # We can also zoom in one one day, for example August 2nd. -df_results.loc['2001-08-02'].plot(ax=ax2, legend=False) -ax2.set_xlabel('Time') -ax2.set_ylabel('Spectral mismatch (-)') +df_results.loc["2001-08-02"].plot(ax=ax2, legend=False) +ax2.set_xlabel("Time") +ax2.set_ylabel("Spectral mismatch (-)") ax2.set_ylim(0.85, 1.15) plt.tight_layout() diff --git a/docs/examples/system-models/plot_oedi_9068.py b/docs/examples/system-models/plot_oedi_9068.py index 02487eff77..bf7386c057 100644 --- a/docs/examples/system-models/plot_oedi_9068.py +++ b/docs/examples/system-models/plot_oedi_9068.py @@ -49,10 +49,10 @@ # We know the system uses 117.5 W CdTe modules. Based on the system vintage # (data begins in 2017), it seems likely that the array uses First Solar # Series 4 modules (FS-4117). -cec_module_db = pvlib.pvsystem.retrieve_sam('cecmod') -module_parameters = cec_module_db['First_Solar__Inc__FS_4117_3'] +cec_module_db = pvlib.pvsystem.retrieve_sam("cecmod") +module_parameters = cec_module_db["First_Solar__Inc__FS_4117_3"] # ensure that correct spectral correction is applied -module_parameters['Technology'] = 'CdTe' +module_parameters["Technology"] = "CdTe" # default Faiman model parameters: temperature_model_parameters = dict(u0=25.0, u1=6.84) @@ -63,8 +63,8 @@ # It's not clear what specific model is installed, so let's just assume # this inverter, which the CEC database lists as having a nominal AC # capacity of 1833 kW: -cec_inverter_db = pvlib.pvsystem.retrieve_sam('cecinverter') -inverter_parameters = cec_inverter_db['TMEIC__PVL_L1833GRM'] +cec_inverter_db = pvlib.pvsystem.retrieve_sam("cecinverter") +inverter_parameters = cec_inverter_db["TMEIC__PVL_L1833GRM"] # We'll use the PVWatts v5 losses model. Set shading to zero as it is # accounted for elsewhere in the model, and disable availability loss since @@ -106,27 +106,27 @@ gcr=gcr, backtrack=backtrack, max_angle=max_angle, - axis_azimuth=axis_azimuth + axis_azimuth=axis_azimuth, ) array = pvlib.pvsystem.Array( mount, module_parameters=module_parameters, modules_per_string=modules_per_string, temperature_model_parameters=temperature_model_parameters, - strings=strings_per_inverter + strings=strings_per_inverter, ) system = pvlib.pvsystem.PVSystem( array, inverter_parameters=inverter_parameters, - losses_parameters=losses_parameters + losses_parameters=losses_parameters, ) model = pvlib.modelchain.ModelChain( system, location, - spectral_model='first_solar', - aoi_model='physical', - losses_model='pvwatts' + spectral_model="first_solar", + aoi_model="physical", + losses_model="pvwatts", ) # %% @@ -138,15 +138,29 @@ # example, we will use weather data taken from the NSRDB PSM3 for the year # 2019. -api_key = 'DEMO_KEY' -email = 'your_email@domain.com' - -keys = ['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed', - 'albedo', 'precipitable_water'] -psm3, psm3_metadata = pvlib.iotools.get_psm3(latitude, longitude, api_key, - email, interval=5, names=2019, - map_variables=True, leap_day=True, - attributes=keys) +api_key = "DEMO_KEY" +email = "your_email@domain.com" + +keys = [ + "ghi", + "dni", + "dhi", + "temp_air", + "wind_speed", + "albedo", + "precipitable_water", +] +psm3, psm3_metadata = pvlib.iotools.get_psm3( + latitude, + longitude, + api_key, + email, + interval=5, + names=2019, + map_variables=True, + leap_day=True, + attributes=keys, +) # %% # Pre-generate some model inputs @@ -167,19 +181,26 @@ solar_position = location.get_solarposition(psm3.index, latitude, longitude) tracker_angles = mount.get_orientation( - solar_position['apparent_zenith'], - solar_position['azimuth'] + solar_position["apparent_zenith"], solar_position["azimuth"] ) dni_extra = pvlib.irradiance.get_extra_radiation(psm3.index) # note: this system is monofacial, so only calculate irradiance for the # front side: averaged_irradiance = pvlib.bifacial.infinite_sheds.get_irradiance_poa( - tracker_angles['surface_tilt'], tracker_angles['surface_azimuth'], - solar_position['apparent_zenith'], solar_position['azimuth'], - gcr, axis_height, pitch, - psm3['ghi'], psm3['dhi'], psm3['dni'], psm3['albedo'], - model='haydavies', dni_extra=dni_extra, + tracker_angles["surface_tilt"], + tracker_angles["surface_azimuth"], + solar_position["apparent_zenith"], + solar_position["azimuth"], + gcr, + axis_height, + pitch, + psm3["ghi"], + psm3["dhi"], + psm3["dni"], + psm3["albedo"], + model="haydavies", + dni_extra=dni_extra, ) # %% @@ -188,16 +209,16 @@ # temperature as well: cell_temperature_steady_state = pvlib.temperature.faiman( - poa_global=averaged_irradiance['poa_global'], - temp_air=psm3['temp_air'], - wind_speed=psm3['wind_speed'], + poa_global=averaged_irradiance["poa_global"], + temp_air=psm3["temp_air"], + wind_speed=psm3["wind_speed"], **temperature_model_parameters, ) cell_temperature = pvlib.temperature.prilliman( cell_temperature_steady_state, - psm3['wind_speed'], - unit_mass=module_unit_mass + psm3["wind_speed"], + unit_mass=module_unit_mass, ) # %% @@ -208,13 +229,17 @@ # to use pre-calculated plane-of-array irradiance, we will use # :py:meth:`~pvlib.modelchain.ModelChain.run_model_from_poa`: -weather_inputs = pd.DataFrame({ - 'poa_global': averaged_irradiance['poa_global'], - 'poa_direct': averaged_irradiance['poa_direct'], - 'poa_diffuse': averaged_irradiance['poa_diffuse'], - 'cell_temperature': cell_temperature, - 'precipitable_water': psm3['precipitable_water'], # for the spectral model -}) +weather_inputs = pd.DataFrame( + { + "poa_global": averaged_irradiance["poa_global"], + "poa_direct": averaged_irradiance["poa_direct"], + "poa_diffuse": averaged_irradiance["poa_diffuse"], + "cell_temperature": cell_temperature, + "precipitable_water": psm3[ + "precipitable_water" + ], # for the spectral model + } +) model.run_model_from_poa(weather_inputs) @@ -227,41 +252,41 @@ fn = r"path/to/9068_ac_power_data.csv" df_inverter_measured = pd.read_csv(fn, index_col=0, parse_dates=True) -df_inverter_measured = df_inverter_measured.tz_localize('US/Mountain', - ambiguous='NaT', - nonexistent='NaT') +df_inverter_measured = df_inverter_measured.tz_localize( + "US/Mountain", ambiguous="NaT", nonexistent="NaT" +) # convert to standard time to match the NSRDB-based simulation -df_inverter_measured = df_inverter_measured.tz_convert('Etc/GMT+7') +df_inverter_measured = df_inverter_measured.tz_convert("Etc/GMT+7") # %% inverter_ac_powers = [ - 'inverter_1_ac_power_(kw)_inv_150143', - 'inverter_2_ac_power_(kw)_inv_150144' + "inverter_1_ac_power_(kw)_inv_150143", + "inverter_2_ac_power_(kw)_inv_150144", ] -df = df_inverter_measured.loc['2019', inverter_ac_powers] -df['model'] = model.results.ac / 1000 # convert W to kW +df = df_inverter_measured.loc["2019", inverter_ac_powers] +df["model"] = model.results.ac / 1000 # convert W to kW # %% for column_name in inverter_ac_powers: fig, axes = plt.subplots(1, 3, figsize=(12, 4)) - df.plot.scatter('model', column_name, ax=axes[0], s=1, alpha=0.1) - axes[0].axline((0, 0), slope=1, c='k') - axes[0].set_ylabel('Measured 5-min power [kW]') - axes[0].set_xlabel('Modeled 5-min power [kW]') - - hourly_average = df.resample('h').mean() - hourly_average.plot.scatter('model', column_name, ax=axes[1], s=2) - axes[1].axline((0, 0), slope=1, c='k') - axes[1].set_ylabel('Measured hourly energy [kWh]') - axes[1].set_xlabel('Modeled hourly energy [kWh]') - - daily_total = hourly_average.resample('d').sum() - daily_total.plot.scatter('model', column_name, ax=axes[2], s=5) - axes[2].axline((0, 0), slope=1, c='k') - axes[2].set_ylabel('Measured daily energy [kWh]') - axes[2].set_xlabel('Modeled daily energy [kWh]') + df.plot.scatter("model", column_name, ax=axes[0], s=1, alpha=0.1) + axes[0].axline((0, 0), slope=1, c="k") + axes[0].set_ylabel("Measured 5-min power [kW]") + axes[0].set_xlabel("Modeled 5-min power [kW]") + + hourly_average = df.resample("h").mean() + hourly_average.plot.scatter("model", column_name, ax=axes[1], s=2) + axes[1].axline((0, 0), slope=1, c="k") + axes[1].set_ylabel("Measured hourly energy [kWh]") + axes[1].set_xlabel("Modeled hourly energy [kWh]") + + daily_total = hourly_average.resample("d").sum() + daily_total.plot.scatter("model", column_name, ax=axes[2], s=5) + axes[2].axline((0, 0), slope=1, c="k") + axes[2].set_ylabel("Measured daily energy [kWh]") + axes[2].set_xlabel("Modeled daily energy [kWh]") fig.suptitle(column_name) fig.tight_layout() @@ -273,10 +298,10 @@ # .. image:: ../../_images/OEDI_9068_inverter2_comparison.png fig, ax = plt.subplots(figsize=(12, 4)) -daily_energy = df.clip(lower=0).resample('h').mean().resample('d').sum() +daily_energy = df.clip(lower=0).resample("h").mean().resample("d").sum() daily_energy.plot(ax=ax) plt.ylim(bottom=0) -plt.ylabel('Daily Production [kWh]') +plt.ylabel("Daily Production [kWh]") plt.tight_layout() # %% diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index a08507c0b3..501a4f2592 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -30,8 +30,8 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../sphinxext')) -sys.path.insert(0, os.path.abspath('../../../')) +sys.path.insert(0, os.path.abspath("../sphinxext")) +sys.path.insert(0, os.path.abspath("../../../")) # -- General configuration ------------------------------------------------ @@ -44,37 +44,43 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.extlinks', - 'sphinx.ext.napoleon', - 'sphinx.ext.autosummary', - 'IPython.sphinxext.ipython_directive', - 'IPython.sphinxext.ipython_console_highlighting', - 'sphinx_gallery.gen_gallery', - 'sphinx_toggleprompt', - 'sphinx_favicon', - 'hoverxref.extension', + "sphinx.ext.autodoc", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.extlinks", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "IPython.sphinxext.ipython_directive", + "IPython.sphinxext.ipython_console_highlighting", + "sphinx_gallery.gen_gallery", + "sphinx_toggleprompt", + "sphinx_favicon", + "hoverxref.extension", ] -mathjax3_config = {'chtml': {'displayAlign': 'left', - 'displayIndent': '2em'}} +mathjax3_config = {"chtml": {"displayAlign": "left", "displayIndent": "2em"}} # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable', None), - 'matplotlib': ('https://matplotlib.org/stable', None), + "python": ("https://docs.python.org/3/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), + "matplotlib": ("https://matplotlib.org/stable", None), } # Enable hover tooltips hoverxref_auto_ref = True hoverxref_roles = [ - "class", "meth", "func", "ref", "term", "obj", "mod", "data" + "class", + "meth", + "func", + "ref", + "term", + "obj", + "mod", + "data", ] hoverxref_role_types = dict.fromkeys(hoverxref_roles, "tooltip") hoverxref_domains = ["py"] @@ -83,22 +89,22 @@ napoleon_use_rtype = False # group rtype on same line together with return # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'pvlib python' -copyright = '''pvlib python Contributors (2023); +project = "pvlib python" +copyright = """pvlib python Contributors (2023); PVLIB python Development Team (2014); - Sandia National Laboratories (2013)''' + Sandia National Laboratories (2013)""" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -107,7 +113,7 @@ import pvlib # noqa: E402 # The short X.Y version. -version = '%s' % (pvlib.__version__) +version = "%s" % (pvlib.__version__) # The full version, including alpha/beta/rc tags. release = version @@ -123,7 +129,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['whatsnew/*', '**.ipynb_checkpoints'] +exclude_patterns = ["whatsnew/*", "**.ipynb_checkpoints"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -141,7 +147,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -209,7 +215,7 @@ # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_images/pvlib_logo_horiz.png' +html_logo = "_images/pvlib_logo_horiz.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -219,7 +225,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -270,7 +276,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'pvlib_pythondoc' +htmlhelp_basename = "pvlib_pythondoc" # custom CSS workarounds @@ -283,16 +289,15 @@ def setup(app): # Add a warning banner at the top of the page if viewing the "latest" docs app.add_js_file("version-alert.js") + # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -301,9 +306,13 @@ def setup(app): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'pvlib_python.tex', 'pvlib\\_python Documentation', - 'Sandia National Laboratoraties and pvlib python Development Team', - 'manual'), + ( + "index", + "pvlib_python.tex", + "pvlib\\_python Documentation", + "Sandia National Laboratoraties and pvlib python Development Team", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -344,8 +353,13 @@ def setup(app): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'pvlib_python', 'pvlib_python Documentation', - ['Sandia National Laboratoraties and pvlib python Development Team'], 1) + ( + "index", + "pvlib_python", + "pvlib_python Documentation", + ["Sandia National Laboratoraties and pvlib python Development Team"], + 1, + ) ] # If true, show URL addresses after external links. @@ -358,10 +372,15 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'pvlib python', 'pvlib python Documentation', - 'Sandia National Laboratoraties and pvlib python Development Team', - 'pvlib python', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "pvlib python", + "pvlib python Documentation", + "Sandia National Laboratoraties and pvlib python Development Team", + "pvlib python", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. @@ -380,30 +399,30 @@ def setup(app): # suppress "WARNING: Footnote [1] is not referenced." messages # https://github.com/pvlib/pvlib-python/issues/837 -suppress_warnings = ['ref.footnote'] +suppress_warnings = ["ref.footnote"] # settings for sphinx-gallery sphinx_gallery_conf = { - 'examples_dirs': ['../../examples'], # location of gallery scripts - 'gallery_dirs': ['gallery'], # location of generated output + "examples_dirs": ["../../examples"], # location of gallery scripts + "gallery_dirs": ["gallery"], # location of generated output # execute all scripts except for ones in the "system-models" directory: - 'filename_pattern': '^((?!system-models).)*$', - + "filename_pattern": "^((?!system-models).)*$", # directory where function/class granular galleries are stored - 'backreferences_dir': 'reference/generated/gallery_backreferences', - + "backreferences_dir": "reference/generated/gallery_backreferences", # Modules for which function/class level galleries are created. In # this case only pvlib, could include others though. Must be tuple of str - 'doc_module': ('pvlib',), - + "doc_module": ("pvlib",), # https://sphinx-gallery.github.io/dev/configuration.html#removing-config-comments # noqa: E501 - 'remove_config_comments': True, + "remove_config_comments": True, } # supress warnings in gallery output # https://sphinx-gallery.github.io/stable/configuration.html -warnings.filterwarnings("ignore", category=UserWarning, - message='Matplotlib is currently using agg, which is a' - ' non-GUI backend, so cannot show the figure.') +warnings.filterwarnings( + "ignore", + category=UserWarning, + message="Matplotlib is currently using agg, which is a" + " non-GUI backend, so cannot show the figure.", +) # %% helper functions for intelligent "View on Github" linking # based on @@ -427,7 +446,7 @@ def get_obj_module(qualname): attrname = None while modname not in sys.modules: attrname = classname - modname, classname = modname.rsplit('.', 1) + modname, classname = modname.rsplit(".", 1) # retrieve object and find original module name if classname: @@ -467,29 +486,35 @@ def make_github_url(file_name): URL_BASE = "https://github.com/pvlib/pvlib-python/blob/main/" # is it a gallery page? - if any(d in file_name for d in sphinx_gallery_conf['gallery_dirs']): + if any(d in file_name for d in sphinx_gallery_conf["gallery_dirs"]): example_folder = file_name.split("/")[-2] if file_name.split("/")[-1] == "index.rst": example_file = "README.rst" else: - example_file = file_name.split("/")[-1].replace('.rst', '.py') + example_file = file_name.split("/")[-1].replace(".rst", ".py") - if example_folder == 'gallery': + if example_folder == "gallery": target_url = URL_BASE + "docs/examples/" + example_file # noqa: E501 else: - target_url = URL_BASE + "docs/examples/" + example_folder + "/" + example_file # noqa: E501 + target_url = ( + URL_BASE + + "docs/examples/" + + example_folder + + "/" + + example_file + ) # noqa: E501 # is it an API autogen page? elif "generated" in file_name: # pagename looks like "generated/pvlib.atmosphere.alt2pres.rst" - qualname = file_name.split("/")[-1].replace('.rst', '') + qualname = file_name.split("/")[-1].replace(".rst", "") obj, module = get_obj_module(qualname) path = module.__name__.replace(".", "/") + ".py" target_url = URL_BASE + path # add line numbers if possible: start, end = get_linenos(obj) if start and end: - target_url += f'#L{start}-L{end}' + target_url += f"#L{start}-L{end}" # Just a normal source RST page else: @@ -501,6 +526,6 @@ def make_github_url(file_name): # variables to pass into the HTML templating engine; these are accessible from # _templates/breadcrumbs.html html_context = { - 'make_github_url': make_github_url, - 'edit_page_url_template': '{{ make_github_url(file_name) }}', + "make_github_url": make_github_url, + "edit_page_url_template": "{{ make_github_url(file_name) }}", } diff --git a/docs/tutorials/atmosphere.ipynb b/docs/tutorials/atmosphere.ipynb index df574d747d..0207b8663b 100644 --- a/docs/tutorials/atmosphere.ipynb +++ b/docs/tutorials/atmosphere.ipynb @@ -25,12 +25,8 @@ "\n", "# built in python modules\n", "import datetime\n", - "import logging\n", - "import os\n", - "import inspect\n", "\n", "# python add-ons\n", - "import numpy as np\n", "import pandas as pd" ] }, @@ -50,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')" + "tus = Location(32.2, -111, \"US/Arizona\", 700, \"Tucson\")" ] }, { @@ -113,9 +109,16 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,24), end=datetime.datetime(2014,6,25), freq='1Min', tz=tus.tz)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 24),\n", + " end=datetime.datetime(2014, 6, 25),\n", + " freq=\"1Min\",\n", + " tz=tus.tz,\n", + ")\n", "\n", - "solpos = pvlib.solarposition.get_solarposition(times, tus.latitude, tus.longitude)\n", + "solpos = pvlib.solarposition.get_solarposition(\n", + " times, tus.latitude, tus.longitude\n", + ")\n", "print(solpos.head())\n", "solpos.plot();" ] @@ -139,13 +142,21 @@ } ], "source": [ - "pvlib.atmosphere.get_relative_airmass(solpos['zenith']).plot(label='kastenyoung1989, zenith')\n", - "pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith']).plot(label='kastenyoung1989, app. zenith')\n", - "pvlib.atmosphere.get_relative_airmass(solpos['zenith'], model='young1994').plot(label='young1994, zenith')\n", - "pvlib.atmosphere.get_relative_airmass(solpos['zenith'], model='simple').plot(label='simple, zenith')\n", + "pvlib.atmosphere.get_relative_airmass(solpos[\"zenith\"]).plot(\n", + " label=\"kastenyoung1989, zenith\"\n", + ")\n", + "pvlib.atmosphere.get_relative_airmass(solpos[\"apparent_zenith\"]).plot(\n", + " label=\"kastenyoung1989, app. zenith\"\n", + ")\n", + "pvlib.atmosphere.get_relative_airmass(\n", + " solpos[\"zenith\"], model=\"young1994\"\n", + ").plot(label=\"young1994, zenith\")\n", + "pvlib.atmosphere.get_relative_airmass(solpos[\"zenith\"], model=\"simple\").plot(\n", + " label=\"simple, zenith\"\n", + ")\n", "plt.legend()\n", - "plt.ylabel('Airmass')\n", - "plt.ylim(0,100);" + "plt.ylabel(\"Airmass\")\n", + "plt.ylim(0, 100);" ] }, { @@ -167,12 +178,20 @@ } ], "source": [ - "plt.plot(solpos['zenith'], pvlib.atmosphere.get_relative_airmass(solpos['zenith'], model='simple'), label='simple')\n", - "plt.plot(solpos['zenith'], pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith']), label='default')\n", - "plt.xlim(0,100)\n", - "plt.ylim(0,100)\n", - "plt.xlabel('Zenith angle (deg)')\n", - "plt.ylabel('Airmass')\n", + "plt.plot(\n", + " solpos[\"zenith\"],\n", + " pvlib.atmosphere.get_relative_airmass(solpos[\"zenith\"], model=\"simple\"),\n", + " label=\"simple\",\n", + ")\n", + "plt.plot(\n", + " solpos[\"zenith\"],\n", + " pvlib.atmosphere.get_relative_airmass(solpos[\"apparent_zenith\"]),\n", + " label=\"default\",\n", + ")\n", + "plt.xlim(0, 100)\n", + "plt.ylim(0, 100)\n", + "plt.xlabel(\"Zenith angle (deg)\")\n", + "plt.ylabel(\"Airmass\")\n", "plt.legend();" ] }, diff --git a/docs/tutorials/irradiance.ipynb b/docs/tutorials/irradiance.ipynb index 251d8b5eb8..e9ed6a257a 100644 --- a/docs/tutorials/irradiance.ipynb +++ b/docs/tutorials/irradiance.ipynb @@ -28,7 +28,7 @@ "source": [ "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", - " \n", + "\n", "# built in python modules\n", "import datetime\n", "\n", @@ -79,12 +79,12 @@ "outputs": [], "source": [ "# DatetimeIndex in yields a TimeSeries out\n", - "times = pd.date_range('2014-01-01', '2015-01-01', freq='1h')\n", + "times = pd.date_range(\"2014-01-01\", \"2015-01-01\", freq=\"1h\")\n", "\n", - "spencer = pvlib.irradiance.get_extra_radiation(times, method='spencer')\n", - "asce = pvlib.irradiance.get_extra_radiation(times, method='asce')\n", - "ephem = pvlib.irradiance.get_extra_radiation(times, method='pyephem') \n", - "nrel = pvlib.irradiance.get_extra_radiation(times, method='nrel')" + "spencer = pvlib.irradiance.get_extra_radiation(times, method=\"spencer\")\n", + "asce = pvlib.irradiance.get_extra_radiation(times, method=\"asce\")\n", + "ephem = pvlib.irradiance.get_extra_radiation(times, method=\"pyephem\")\n", + "nrel = pvlib.irradiance.get_extra_radiation(times, method=\"nrel\")" ] }, { @@ -106,12 +106,12 @@ } ], "source": [ - "spencer.plot(label='spencer')\n", - "asce.plot(label='asce')\n", - "ephem.plot(label='pyephem')\n", - "nrel.plot(label='nrel')\n", + "spencer.plot(label=\"spencer\")\n", + "asce.plot(label=\"asce\")\n", + "ephem.plot(label=\"pyephem\")\n", + "nrel.plot(label=\"nrel\")\n", "plt.legend()\n", - "plt.ylabel('Extraterrestrial radiation (W/m^2)');" + "plt.ylabel(\"Extraterrestrial radiation (W/m^2)\");" ] }, { @@ -142,7 +142,7 @@ "source": [ "et_diff = spencer - ephem\n", "et_diff.plot()\n", - "plt.ylabel('spencer-ephem (W/m**2)');" + "plt.ylabel(\"spencer-ephem (W/m**2)\");" ] }, { @@ -180,7 +180,7 @@ "source": [ "et_diff = nrel - ephem\n", "et_diff.plot()\n", - "plt.ylabel('nrel-ephem (W/m**2)');" + "plt.ylabel(\"nrel-ephem (W/m**2)\");" ] }, { @@ -209,13 +209,15 @@ } ], "source": [ - "spencer_1361 = pvlib.irradiance.get_extra_radiation(times, method='spencer', solar_constant=1361)\n", + "spencer_1361 = pvlib.irradiance.get_extra_radiation(\n", + " times, method=\"spencer\", solar_constant=1361\n", + ")\n", "\n", - "spencer.plot(label='default 1366.7')\n", - "spencer_1361.plot(label='1361')\n", + "spencer.plot(label=\"default 1366.7\")\n", + "spencer_1361.plot(label=\"1361\")\n", "plt.legend()\n", - "plt.title('Impact of solar constant')\n", - "plt.ylabel('ET Irradiance (W/m^2)');" + "plt.title(\"Impact of solar constant\")\n", + "plt.ylabel(\"ET Irradiance (W/m^2)\");" ] }, { @@ -231,7 +233,7 @@ "metadata": {}, "outputs": [], "source": [ - "times = pd.date_range(start='2015', end='2016', freq='1min')" + "times = pd.date_range(start=\"2015\", end=\"2016\", freq=\"1min\")" ] }, { @@ -318,41 +320,41 @@ } ], "source": [ - "methods = ['spencer', 'asce', 'pyephem', 'nrel']\n", + "methods = [\"spencer\", \"asce\", \"pyephem\", \"nrel\"]\n", "\n", "# pandas timestamp input\n", - "times = pd.Timestamp('20161026')\n", + "times = pd.Timestamp(\"20161026\")\n", "for method in methods:\n", " dni_extra = pvlib.irradiance.get_extra_radiation(times, method=method)\n", " assert isinstance(dni_extra, float)\n", " print(times, method, dni_extra)\n", - " \n", - " \n", + "\n", + "\n", "# date input\n", "times = datetime.date(2016, 10, 26)\n", "for method in methods:\n", " dni_extra = pvlib.irradiance.get_extra_radiation(times, method=method)\n", " assert isinstance(dni_extra, float)\n", " print(times, method, dni_extra)\n", - " \n", - " \n", + "\n", + "\n", "# integer doy input\n", "times = 300\n", "for method in methods:\n", " dni_extra = pvlib.irradiance.get_extra_radiation(times, method=method)\n", " assert isinstance(dni_extra, float)\n", " print(times, method, dni_extra)\n", - " \n", - " \n", + "\n", + "\n", "# array doy input\n", "times = np.arange(1, 366)\n", "for method in methods:\n", " dni_extra = pvlib.irradiance.get_extra_radiation(times, method=method)\n", " assert isinstance(dni_extra, np.ndarray)\n", " plt.plot(times, dni_extra, label=method)\n", - " \n", + "\n", "plt.legend()\n", - "plt.ylabel('Extraterrestrial radiation (W/m^2)');" + "plt.ylabel(\"Extraterrestrial radiation (W/m^2)\");" ] }, { @@ -392,14 +394,16 @@ } ], "source": [ - "tus = pvlib.location.Location(32.2, -111, 'US/Arizona', 700, 'Tucson')\n", - "times = pd.date_range(start='2016-01-01', end='2016-01-02', freq='1min', tz=tus.tz)\n", - "times_utc = times.tz_convert('UTC')\n", + "tus = pvlib.location.Location(32.2, -111, \"US/Arizona\", 700, \"Tucson\")\n", + "times = pd.date_range(\n", + " start=\"2016-01-01\", end=\"2016-01-02\", freq=\"1min\", tz=tus.tz\n", + ")\n", + "times_utc = times.tz_convert(\"UTC\")\n", "ephem_data = tus.get_solarposition(times)\n", "irrad_data = tus.get_clearsky(times)\n", "irrad_data.plot()\n", - "plt.ylabel('Irradiance $W/m^2$')\n", - "plt.title('Ineichen, climatological turbidity');" + "plt.ylabel(\"Irradiance $W/m^2$\")\n", + "plt.title(\"Ineichen, climatological turbidity\");" ] }, { @@ -442,9 +446,11 @@ } ], "source": [ - "ground_irrad = pvlib.irradiance.get_ground_diffuse(40, irrad_data['ghi'], albedo=.25)\n", + "ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " 40, irrad_data[\"ghi\"], albedo=0.25\n", + ")\n", "ground_irrad.plot()\n", - "plt.ylabel('Diffuse ground irradiance (W/m^2)');" + "plt.ylabel(\"Diffuse ground irradiance (W/m^2)\");" ] }, { @@ -473,13 +479,17 @@ } ], "source": [ - "for surface, albedo in sorted(pvlib.irradiance.SURFACE_ALBEDOS.items(), key=lambda x: x[1], reverse=True):\n", - " ground_irrad = pvlib.irradiance.get_ground_diffuse(40, irrad_data['ghi'], surface_type=surface)\n", - " ground_irrad.plot(label='{}: {}'.format(surface, albedo))\n", + "for surface, albedo in sorted(\n", + " pvlib.irradiance.SURFACE_ALBEDOS.items(), key=lambda x: x[1], reverse=True\n", + "):\n", + " ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " 40, irrad_data[\"ghi\"], surface_type=surface\n", + " )\n", + " ground_irrad.plot(label=\"{}: {}\".format(surface, albedo))\n", "\n", "plt.legend()\n", - "plt.ylabel('Diffuse ground irradiance (W/m^2)')\n", - "plt.title('Surface types');" + "plt.ylabel(\"Diffuse ground irradiance (W/m^2)\")\n", + "plt.title(\"Surface types\");" ] }, { @@ -509,12 +519,14 @@ ], "source": [ "for surf_tilt in np.linspace(0, 90, 5):\n", - " ground_irrad = pvlib.irradiance.get_ground_diffuse(surf_tilt, irrad_data['ghi'])\n", + " ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " surf_tilt, irrad_data[\"ghi\"]\n", + " )\n", " ground_irrad.plot(label=surf_tilt)\n", "\n", "plt.legend()\n", - "plt.ylabel('Diffuse ground irradiance (W/m^2)')\n", - "plt.title('Ground diffuse as a function of tilt');" + "plt.ylabel(\"Diffuse ground irradiance (W/m^2)\")\n", + "plt.title(\"Ground diffuse as a function of tilt\");" ] }, { @@ -572,12 +584,12 @@ } ], "source": [ - "sky_diffuse = pvlib.irradiance.isotropic(40, irrad_data['dhi'])\n", - "sky_diffuse.plot(label='isotropic diffuse')\n", - "irrad_data['dhi'].plot()\n", - "irrad_data['ghi'].plot()\n", + "sky_diffuse = pvlib.irradiance.isotropic(40, irrad_data[\"dhi\"])\n", + "sky_diffuse.plot(label=\"isotropic diffuse\")\n", + "irrad_data[\"dhi\"].plot()\n", + "irrad_data[\"ghi\"].plot()\n", "plt.legend()\n", - "plt.ylabel('Irradiance (W/m^2)');" + "plt.ylabel(\"Irradiance (W/m^2)\");" ] }, { @@ -606,11 +618,11 @@ } ], "source": [ - "sky_diffuse = pvlib.irradiance.isotropic(40, irrad_data['dhi'])\n", - "sky_diffuse.plot(label='isotropic diffuse')\n", - "irrad_data['dhi'].plot()\n", + "sky_diffuse = pvlib.irradiance.isotropic(40, irrad_data[\"dhi\"])\n", + "sky_diffuse.plot(label=\"isotropic diffuse\")\n", + "irrad_data[\"dhi\"].plot()\n", "plt.legend()\n", - "plt.ylabel('Irradiance (W/m^2)');" + "plt.ylabel(\"Irradiance (W/m^2)\");" ] }, { @@ -642,13 +654,19 @@ "surf_tilt = 40\n", "surf_az = 180\n", "\n", - "sky_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "sky_diffuse.plot(label='klucher diffuse')\n", - "irrad_data['dhi'].plot()\n", - "#irrad_data['ghi'].plot()\n", + "sky_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "sky_diffuse.plot(label=\"klucher diffuse\")\n", + "irrad_data[\"dhi\"].plot()\n", + "# irrad_data['ghi'].plot()\n", "plt.legend()\n", - "plt.ylabel('Irradiance (W/m^2)');" + "plt.ylabel(\"Irradiance (W/m^2)\");" ] }, { @@ -671,19 +689,25 @@ ], "source": [ "surf_tilt = 40\n", - "surf_az = 180 # south facing\n", + "surf_az = 180 # south facing\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", - "irrad_data['dhi'].plot()\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend()\n", - "plt.ylabel('Irradiance (W/m^2)');" + "plt.ylabel(\"Irradiance (W/m^2)\");" ] }, { @@ -714,15 +738,21 @@ "source": [ "surf_tilt = 40\n", "\n", - "irrad_data['dhi'].plot()\n", + "irrad_data[\"dhi\"].plot()\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic\")\n", "\n", "for surf_az in np.linspace(0, 270, 4):\n", - " klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - " klucher_diffuse.plot(label='klucher: {}'.format(surf_az))\n", + " klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " )\n", + " klucher_diffuse.plot(label=\"klucher: {}\".format(surf_az))\n", "\n", "plt.legend();" ] @@ -755,15 +785,21 @@ "source": [ "surf_tilt = 0\n", "\n", - "irrad_data['dhi'].plot()\n", + "irrad_data[\"dhi\"].plot()\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic\")\n", "\n", "for surf_az in np.linspace(0, 270, 4):\n", - " klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - " klucher_diffuse.plot(label='klucher: {}'.format(surf_az))\n", + " klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " )\n", + " klucher_diffuse.plot(label=\"klucher: {}\".format(surf_az))\n", "\n", "plt.legend();" ] @@ -802,23 +838,35 @@ ], "source": [ "surf_tilt = 32\n", - "surf_az = 180 # south facing\n", + "surf_az = 180 # south facing\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", - "reindl_diffuse = pvlib.irradiance.reindl(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "reindl_diffuse.plot(label='reindl diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "reindl_diffuse = pvlib.irradiance.reindl(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " irrad_data[\"ghi\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "reindl_diffuse.plot(label=\"reindl diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -850,23 +898,35 @@ ], "source": [ "surf_tilt = 32\n", - "surf_az = 90 \n", + "surf_az = 90\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", - "reindl_diffuse = pvlib.irradiance.reindl(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "reindl_diffuse.plot(label='reindl diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "reindl_diffuse = pvlib.irradiance.reindl(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " irrad_data[\"ghi\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "reindl_diffuse.plot(label=\"reindl diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -905,29 +965,47 @@ ], "source": [ "surf_tilt = 32\n", - "surf_az = 180 \n", + "surf_az = 180\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "reindl_diffuse = pvlib.irradiance.reindl(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "reindl_diffuse.plot(label='reindl diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "reindl_diffuse = pvlib.irradiance.reindl(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " irrad_data[\"ghi\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "reindl_diffuse.plot(label=\"reindl diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -959,29 +1037,47 @@ ], "source": [ "surf_tilt = 32\n", - "surf_az = 90 \n", + "surf_az = 90\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "reindl_diffuse = pvlib.irradiance.reindl(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "reindl_diffuse.plot(label='reindl diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "reindl_diffuse = pvlib.irradiance.reindl(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " irrad_data[\"ghi\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "reindl_diffuse.plot(label=\"reindl diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -1020,27 +1116,40 @@ ], "source": [ "surf_tilt = 32\n", - "surf_az = 90 \n", + "surf_az = 90\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", "\n", - "king_diffuse = pvlib.irradiance.king(surf_tilt,irrad_data['dhi'], irrad_data['ghi'], ephem_data['azimuth'])\n", - "king_diffuse.plot(label='king diffuse')\n", + "king_diffuse = pvlib.irradiance.king(\n", + " surf_tilt, irrad_data[\"dhi\"], irrad_data[\"ghi\"], ephem_data[\"azimuth\"]\n", + ")\n", + "king_diffuse.plot(label=\"king diffuse\")\n", "\n", - "irrad_data['dhi'].plot()\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -1065,23 +1174,23 @@ "metadata": {}, "outputs": [], "source": [ - "sun_zen = ephem_data['apparent_zenith']\n", - "sun_az = ephem_data['azimuth']\n", - "DNI = irrad_data['dni']\n", - "DHI = irrad_data['dhi']\n", + "sun_zen = ephem_data[\"apparent_zenith\"]\n", + "sun_az = ephem_data[\"azimuth\"]\n", + "DNI = irrad_data[\"dni\"]\n", + "DHI = irrad_data[\"dhi\"]\n", "DNI_ET = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "AM = pvlib.atmosphere.get_relative_airmass(sun_zen)\n", "\n", "surf_tilt = 32\n", "surf_az = 180\n", "\n", - "kappa = 1.041 #for sun_zen in radians\n", - "z = np.radians(sun_zen) # convert to radians\n", + "kappa = 1.041 # for sun_zen in radians\n", + "z = np.radians(sun_zen) # convert to radians\n", "\n", - "#Dhfilter = DHI > 0\n", + "# Dhfilter = DHI > 0\n", "\n", "# epsilon is the sky's clearness\n", - "eps = ( (DHI + DNI)/DHI + kappa*(z**3) ) / ( 1 + kappa*(z**3) )" + "eps = ((DHI + DNI) / DHI + kappa * (z**3)) / (1 + kappa * (z**3))" ] }, { @@ -1126,17 +1235,17 @@ ], "source": [ "ebin = eps.copy()\n", - "ebin[(eps<1.065)] = 1\n", - "ebin[(eps>=1.065) & (eps<1.23)] = 2\n", - "ebin[(eps>=1.23) & (eps<1.5)] = 3\n", - "ebin[(eps>=1.5) & (eps<1.95)] = 4\n", - "ebin[(eps>=1.95) & (eps<2.8)] = 5\n", - "ebin[(eps>=2.8) & (eps<4.5)] = 6\n", - "ebin[(eps>=4.5) & (eps<6.2)] = 7\n", - "ebin[eps>=6.2] = 8\n", + "ebin[(eps < 1.065)] = 1\n", + "ebin[(eps >= 1.065) & (eps < 1.23)] = 2\n", + "ebin[(eps >= 1.23) & (eps < 1.5)] = 3\n", + "ebin[(eps >= 1.5) & (eps < 1.95)] = 4\n", + "ebin[(eps >= 1.95) & (eps < 2.8)] = 5\n", + "ebin[(eps >= 2.8) & (eps < 4.5)] = 6\n", + "ebin[(eps >= 4.5) & (eps < 6.2)] = 7\n", + "ebin[eps >= 6.2] = 8\n", "\n", "ebin.plot()\n", - "plt.ylim(0,9);" + "plt.ylim(0, 9);" ] }, { @@ -1205,21 +1314,29 @@ } ], "source": [ - "modelt = 'allsitescomposite1990'\n", + "modelt = \"allsitescomposite1990\"\n", "\n", "F1c, F2c = pvlib.irradiance._get_perez_coefficients(modelt)\n", "\n", - "F1 = F1c[ebin,0] + F1c[ebin,1]*delta[ebin.index] + F1c[ebin,2]*z[ebin.index]\n", - "F1[F1<0]=0;\n", - "F1=F1.astype(float)\n", - "\n", - "#F2= F2c[ebin,0] + F2c[ebin,1]*delta[ebinfilter] + F2c[ebin,2]*z[ebinfilter]\n", - "F2= F2c[ebin,0] + F2c[ebin,1]*delta[ebin.index] + F2c[ebin,2]*z[ebin.index]\n", - "F2[F2<0]=0\n", - "F2=F2.astype(float)\n", - "\n", - "F1.plot(label='F1')\n", - "F2.plot(label='F2')\n", + "F1 = (\n", + " F1c[ebin, 0]\n", + " + F1c[ebin, 1] * delta[ebin.index]\n", + " + F1c[ebin, 2] * z[ebin.index]\n", + ")\n", + "F1[F1 < 0] = 0\n", + "F1 = F1.astype(float)\n", + "\n", + "# F2= F2c[ebin,0] + F2c[ebin,1]*delta[ebinfilter] + F2c[ebin,2]*z[ebinfilter]\n", + "F2 = (\n", + " F2c[ebin, 0]\n", + " + F2c[ebin, 1] * delta[ebin.index]\n", + " + F2c[ebin, 2] * z[ebin.index]\n", + ")\n", + "F2[F2 < 0] = 0\n", + "F2 = F2.astype(float)\n", + "\n", + "F1.plot(label=\"F1\")\n", + "F2.plot(label=\"F2\")\n", "plt.legend();" ] }, @@ -1251,14 +1368,18 @@ } ], "source": [ - "A = tools.cosd(surf_tilt)*tools.cosd(sun_zen) + tools.sind(surf_tilt)*tools.sind(sun_zen)*tools.cosd(sun_az-surf_az) #removed +180 from azimuth modifier: Rob Andrews October 19th 2012\n", - "#A[A < 0] = 0\n", + "A = tools.cosd(surf_tilt) * tools.cosd(sun_zen) + tools.sind(\n", + " surf_tilt\n", + ") * tools.sind(sun_zen) * tools.cosd(\n", + " sun_az - surf_az\n", + ") # removed +180 from azimuth modifier: Rob Andrews October 19th 2012\n", + "# A[A < 0] = 0\n", "\n", - "B = tools.cosd(sun_zen);\n", - "#B[B < pvl_tools.cosd(85)] = pvl_tools.cosd(85)\n", + "B = tools.cosd(sun_zen)\n", + "# B[B < pvl_tools.cosd(85)] = pvl_tools.cosd(85)\n", "\n", - "A.plot(label='A')\n", - "B.plot(label='B')\n", + "A.plot(label=\"A\")\n", + "B.plot(label=\"B\")\n", "plt.legend();" ] }, @@ -1281,7 +1402,11 @@ } ], "source": [ - "sky_diffuse = DHI*( 0.5* (1-F1)*(1+tools.cosd(surf_tilt))+F1 * A[ebin.index]/ B[ebin.index] + F2*tools.sind(surf_tilt))\n", + "sky_diffuse = DHI * (\n", + " 0.5 * (1 - F1) * (1 + tools.cosd(surf_tilt))\n", + " + F1 * A[ebin.index] / B[ebin.index]\n", + " + F2 * tools.sind(surf_tilt)\n", + ")\n", "sky_diffuse[sky_diffuse < 0] = 0\n", "sky_diffuse[AM.isnull()] = 0\n", "\n", @@ -1314,38 +1439,55 @@ } ], "source": [ - "sun_zen = ephem_data['apparent_zenith']\n", - "sun_az = ephem_data['azimuth']\n", - "DNI = irrad_data['dni']\n", - "DHI = irrad_data['dhi']\n", + "sun_zen = ephem_data[\"apparent_zenith\"]\n", + "sun_az = ephem_data[\"azimuth\"]\n", + "DNI = irrad_data[\"dni\"]\n", + "DHI = irrad_data[\"dhi\"]\n", "DNI_ET = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "AM = pvlib.atmosphere.get_relative_airmass(sun_zen)\n", "\n", "surf_tilt = 32\n", "surf_az = 180\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "perez_diffuse = pvlib.irradiance.perez(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'],\n", - " AM)\n", - "perez_diffuse.plot(label='perez diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "perez_diffuse = pvlib.irradiance.perez(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " AM,\n", + ")\n", + "perez_diffuse.plot(label=\"perez diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -1369,38 +1511,55 @@ } ], "source": [ - "sun_zen = ephem_data['apparent_zenith']\n", - "sun_az = ephem_data['azimuth']\n", - "DNI = irrad_data['dni']\n", - "DHI = irrad_data['dhi']\n", + "sun_zen = ephem_data[\"apparent_zenith\"]\n", + "sun_az = ephem_data[\"azimuth\"]\n", + "DNI = irrad_data[\"dni\"]\n", + "DHI = irrad_data[\"dhi\"]\n", "DNI_ET = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "AM = pvlib.atmosphere.get_relative_airmass(sun_zen)\n", "\n", "surf_tilt = 32\n", "surf_az = 90\n", "\n", - "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data['dhi'])\n", - "iso_diffuse.plot(label='isotropic diffuse')\n", + "iso_diffuse = pvlib.irradiance.isotropic(surf_tilt, irrad_data[\"dhi\"])\n", + "iso_diffuse.plot(label=\"isotropic diffuse\")\n", "\n", - "klucher_diffuse = pvlib.irradiance.klucher(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['ghi'], \n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "klucher_diffuse.plot(label='klucher diffuse')\n", + "klucher_diffuse = pvlib.irradiance.klucher(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"ghi\"],\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "klucher_diffuse.plot(label=\"klucher diffuse\")\n", "\n", "dni_et = pvlib.irradiance.get_extra_radiation(times_utc.dayofyear)\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "perez_diffuse = pvlib.irradiance.perez(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'],\n", - " AM)\n", - "perez_diffuse.plot(label='perez diffuse')\n", - "\n", - "irrad_data['dhi'].plot()\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "perez_diffuse = pvlib.irradiance.perez(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " AM,\n", + ")\n", + "perez_diffuse.plot(label=\"perez diffuse\")\n", + "\n", + "irrad_data[\"dhi\"].plot()\n", "\n", "plt.legend();" ] @@ -1431,17 +1590,31 @@ } ], "source": [ - "perez_diffuse = pvlib.irradiance.perez(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'],\n", - " AM, model='allsitescomposite1990')\n", - "perez_diffuse.plot(label='allsitescomposite1990')\n", - "\n", - "perez_diffuse = pvlib.irradiance.perez(surf_tilt, surf_az, \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'],\n", - " AM, model='phoenix1988')\n", - "perez_diffuse.plot(label='phoenix1988')\n", + "perez_diffuse = pvlib.irradiance.perez(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " AM,\n", + " model=\"allsitescomposite1990\",\n", + ")\n", + "perez_diffuse.plot(label=\"allsitescomposite1990\")\n", + "\n", + "perez_diffuse = pvlib.irradiance.perez(\n", + " surf_tilt,\n", + " surf_az,\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " AM,\n", + " model=\"phoenix1988\",\n", + ")\n", + "perez_diffuse.plot(label=\"phoenix1988\")\n", "\n", "plt.legend();" ] @@ -1486,10 +1659,12 @@ } ], "source": [ - "proj = pvlib.irradiance.aoi(32, 180, ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", + "proj = pvlib.irradiance.aoi(\n", + " 32, 180, ephem_data[\"apparent_zenith\"], ephem_data[\"azimuth\"]\n", + ")\n", "proj.plot()\n", "\n", - "#plt.ylim(-1.1,1.1)\n", + "# plt.ylim(-1.1,1.1)\n", "plt.legend();" ] }, @@ -1519,10 +1694,12 @@ } ], "source": [ - "proj = pvlib.irradiance.aoi_projection(32, 180, ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", + "proj = pvlib.irradiance.aoi_projection(\n", + " 32, 180, ephem_data[\"apparent_zenith\"], ephem_data[\"azimuth\"]\n", + ")\n", "proj.plot()\n", "\n", - "plt.ylim(-1.1,1.1)\n", + "plt.ylim(-1.1, 1.1)\n", "plt.legend();" ] }, @@ -1558,16 +1735,20 @@ "sen_alt_rad = np.radians(90 - surf_tilt)\n", "sen_azi_rad = np.radians(surf_az)\n", "\n", - "alts = np.radians(90 - ephem_data['apparent_zenith'])\n", - "azis = np.radians(ephem_data['azimuth'])\n", + "alts = np.radians(90 - ephem_data[\"apparent_zenith\"])\n", + "azis = np.radians(ephem_data[\"azimuth\"])\n", "\n", - "dotprod = np.cos(sen_alt_rad)*np.cos(alts)*np.cos(sen_azi_rad-azis) + np.sin(sen_alt_rad)*np.sin(alts)\n", - "dotprod.plot(label='dotprod')\n", + "dotprod = np.cos(sen_alt_rad) * np.cos(alts) * np.cos(\n", + " sen_azi_rad - azis\n", + ") + np.sin(sen_alt_rad) * np.sin(alts)\n", + "dotprod.plot(label=\"dotprod\")\n", "\n", - "proj = pvlib.irradiance.aoi_projection(surf_tilt, surf_az, ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", + "proj = pvlib.irradiance.aoi_projection(\n", + " surf_tilt, surf_az, ephem_data[\"apparent_zenith\"], ephem_data[\"azimuth\"]\n", + ")\n", "proj.plot()\n", "\n", - "plt.ylim(-1.1,1.1)\n", + "plt.ylim(-1.1, 1.1)\n", "plt.legend();" ] }, @@ -1599,29 +1780,35 @@ "outputs": [], "source": [ "def get_total_irradiance_per_model(surface_tilt, surface_azimuth):\n", - " models = ['isotropic', 'klucher', 'haydavies', 'reindl', 'king', 'perez']\n", + " models = [\"isotropic\", \"klucher\", \"haydavies\", \"reindl\", \"king\", \"perez\"]\n", " totals = {}\n", "\n", " for model in models:\n", " total = pvlib.irradiance.get_total_irradiance(\n", - " surface_tilt, surface_azimuth,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'],\n", - " dni=irrad_data['dni'], ghi=irrad_data['ghi'], dhi=irrad_data['dhi'],\n", - " dni_extra=dni_et, airmass=AM,\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + " dni=irrad_data[\"dni\"],\n", + " ghi=irrad_data[\"ghi\"],\n", + " dhi=irrad_data[\"dhi\"],\n", + " dni_extra=dni_et,\n", + " airmass=AM,\n", " model=model,\n", - " surface_type='urban')\n", + " surface_type=\"urban\",\n", + " )\n", " totals[model] = total\n", " total.plot()\n", " plt.title(model)\n", " plt.ylim(-50, 1100)\n", - " plt.ylabel('Irradiance (W/m^2)')\n", + " plt.ylabel(\"Irradiance (W/m^2)\")\n", "\n", " plt.figure()\n", " for model, total in totals.items():\n", - " total['poa_global'].plot(lw=.5, label=model)\n", + " total[\"poa_global\"].plot(lw=0.5, label=model)\n", "\n", " plt.legend()\n", - " plt.ylabel('Irradiance (W/m^2)')" + " plt.ylabel(\"Irradiance (W/m^2)\")" ] }, { diff --git a/docs/tutorials/pvsystem.ipynb b/docs/tutorials/pvsystem.ipynb index 89065ef2a9..f905b6f517 100644 --- a/docs/tutorials/pvsystem.ipynb +++ b/docs/tutorials/pvsystem.ipynb @@ -35,15 +35,13 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": "We suggest you install seaborn using conda or pip and rerun this cell\n" } ], "source": [ "# built-in python modules\n", - "import os\n", - "import inspect\n", "import datetime\n", "\n", "# scientific python add-ons\n", @@ -54,12 +52,16 @@ "# first line makes the plots appear in the notebook\n", "%matplotlib inline \n", "import matplotlib.pyplot as plt\n", + "\n", "# seaborn makes your plots look better\n", "try:\n", " import seaborn as sns\n", + "\n", " sns.set(rc={\"figure.figsize\": (12, 6)})\n", "except ImportError:\n", - " print('We suggest you install seaborn using conda or pip and rerun this cell')\n", + " print(\n", + " \"We suggest you install seaborn using conda or pip and rerun this cell\"\n", + " )\n", "\n", "# finally, we import the pvlib library\n", "import pvlib" @@ -73,8 +75,8 @@ }, "outputs": [ { - "output_type": "stream", "name": "stdout", + "output_type": "stream", "text": "\nINSTALLED VERSIONS\n------------------\ncommit : None\npython : 3.8.2.final.0\npython-bits : 64\nOS : Darwin\nOS-release : 19.5.0\nmachine : x86_64\nprocessor : i386\nbyteorder : little\nLC_ALL : None\nLANG : None\nLOCALE : None.UTF-8\n\npandas : 1.0.3\nnumpy : 1.18.2\npytz : 2019.3\ndateutil : 2.8.1\npip : 20.0.2\nsetuptools : 46.1.3.post20200330\nCython : 0.29.16\npytest : 5.4.1\nhypothesis : None\nsphinx : 1.8.5\nblosc : None\nfeather : None\nxlsxwriter : None\nlxml.etree : None\nhtml5lib : None\npymysql : None\npsycopg2 : None\njinja2 : 2.11.2\nIPython : 7.16.1\npandas_datareader: None\nbs4 : 4.9.0\nbottleneck : None\nfastparquet : None\ngcsfs : None\nlxml.etree : None\nmatplotlib : 3.2.1\nnumexpr : 2.7.1\nodfpy : None\nopenpyxl : None\npandas_gbq : None\npyarrow : None\npytables : None\npytest : 5.4.1\npyxlsb : None\ns3fs : None\nscipy : 1.4.1\nsqlalchemy : None\ntables : 3.6.1\ntabulate : None\nxarray : None\nxlrd : None\nxlwt : None\nxlsxwriter : None\nnumba : 0.49.0\n" } ], @@ -88,7 +90,6 @@ "metadata": {}, "outputs": [], "source": [ - "import pvlib\n", "from pvlib import pvsystem, inverter" ] }, @@ -105,24 +106,24 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "angles = np.linspace(-180,180,3601)\n", - "ashraeiam = pd.Series(pvsystem.iam.ashrae(angles, .05), index=angles)\n", + "angles = np.linspace(-180, 180, 3601)\n", + "ashraeiam = pd.Series(pvsystem.iam.ashrae(angles, 0.05), index=angles)\n", "\n", "ashraeiam.plot()\n", - "plt.ylabel('ASHRAE modifier')\n", - "plt.xlabel('input angle (deg)');" + "plt.ylabel(\"ASHRAE modifier\")\n", + "plt.xlabel(\"input angle (deg)\");" ] }, { @@ -131,24 +132,24 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "angles = np.linspace(-180,180,3601)\n", + "angles = np.linspace(-180, 180, 3601)\n", "physicaliam = pd.Series(pvsystem.iam.ashrae(angles), index=angles)\n", "\n", "physicaliam.plot()\n", - "plt.ylabel('physical modifier')\n", - "plt.xlabel('input index');" + "plt.ylabel(\"physical modifier\")\n", + "plt.xlabel(\"input index\");" ] }, { @@ -157,23 +158,23 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEGCAYAAABo25JHAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deZhcdZ3v8fe3lt6XLN1k6yQdSEgCWUjSArIoiGJkEAaDAhrZVLyOzLiMXMKD1wXH65aZuaMEFZRRHAyLimZYjGCCwMjWgZCQTUI2OglJZ0/vtfzuH3U6NE06Vd1d1edU+vN6nn666tSpU9/uVOdTv+X8jjnnEBGRwSvkdwEiIuIvBYGIyCCnIBARGeQUBCIig5yCQERkkIv4XUBvVVVVudraWr/LEBHJKytWrNjjnKs+2mN5FwS1tbXU19f7XYaISF4xs609PaauIRGRQU5BICIyyCkIREQGubwbIxCRwSUWi9HQ0EBbW5vfpeSFoqIiampqiEajGT9HQSAigdbQ0EB5eTm1tbWYmd/lBJpzjr1799LQ0MCECRMyfl7OuobM7G4z221mr/bwuJnZD81so5mtMrPZuapFRPJXW1sbw4cPVwhkwMwYPnx4r1tPuRwj+AUw9xiPfwiY5H3dAPw4h7WISB5TCGSuL7+rnHUNOeeeMrPaY+xyKXCPS62D/ZyZDTGzUc65nbmqSSSdeKyDwwf20tp8iPbmA7Q3HyLWephY6yESbU24eAcuEcMl45CI4ZIJSHRAMpE6QCgMoTBmYe92BAuFIVxAuLCUcFEZkaJyosWlFJaUU1BcQWnFUCqGVhMKh/394WXQ8nOMYAzwRpf7Dd62dwSBmd1AqtXAuHHjBqQ4Ob60t7XQ2LCJAztfp7VxC/H92wg37yLato+i2H7K4geocAeppJmhwNABri/hjP1WzqFQBS3hStqiQ4gVDSNRPobI0LEUV49n6KgTqRo9gcKikgGuTgB+//vfc9lll7Fu3TqmTJlCMpnki1/8IsuWLcPMKCoq4oEHHmDChAlHTnytqqoC4Mknn2ThwoU8/PDD/OIXv+Cmm25izJgxtLW18dnPfpYvfelLR15n5cqVzJo1i8cee4y5c9/qVAmHw0yfPv3I/SuvvJIFCxZk5WfLi8Fi59ydwJ0AdXV1upKO9Gjf7u3seO0lmhvWQOMGyg69zgkd26hmPzVAjbdf0hn7rYJDoSG0RIawu/RkdhQNI1lShRUPIVRUTqSonEhxBQXF5RSUVlJYUkG0sIhQOEI0WkAoEiUaLSAciRKJpGZoJBJx4vEYyUScRCJBMpEgEe8gHuugveUQ7S2H6WhtIt7aRLy9iURbE4mWA9CyD2vdS7R9P4UdBxjStp2KllcZvu8gdDsfdDfD2FU4npaKk6B6MmVjTmXM5DkMqRo5oL/rwWbx4sWcc845LF68mG9+85vcf//97Nixg1WrVhEKhWhoaKC0tDSjY11xxRXcfvvt7N27l8mTJ3P55ZczduzYd7xO1yAoLi5m5cqVOfnZ/AyC7cDYLvdrvG0iGWlva+H1lU9xaOOzFLz5MqOa1jKKRoZ5jx92xeyIjmfLkDPZWDGOyLBxlFRPYOjok6gaXcvwwiKGZ7mmSKiASLQga8dra21mz/bNHHhzEy2NW0ns30bk4BaGNG/mxN0PU9r4G1gLPA4NNpI3y04lPmoWQya9m5Nmnku0oDBrtQxmTU1NPPPMMyxfvpwPf/jDfPOb32Tnzp2MGjWKUCg11FpTU5PmKO80fPhwJk6cyM6dOxk7dizOOR588EEef/xxzj33XNra2igqKsr2j/MOfgbBEuBGM7sPOAM4qPEBSWfruhXsfOlhSt54momtqzjF2gHYYSewo3waW0ecRum4WYycOJOqkeOYHMrvcyaLikupmTiNmonT3vGYSyZ5c/smGjetonnryxTsWknN4VcYefjP8DdofriINSWn0TbuPYypu5ixk2b68BNk1zf/ew1rdxzK6jFPGV3B1z986jH3+cMf/sDcuXM5+eSTGT58OCtWrOBjH/sY55xzDk8//TQXXHAB8+fPZ9asWUeec/755xP2xn2ampqYMmXKO467bds22tramDFjBgB//etfmTBhAieddBLnnXcejzzyCPPmzQOgtbWV00477chzb7nlFq644op+//yQwyAws8XAeUCVmTUAXweiAM65nwCPAhcBG4EW4Lpc1SL5bcu6enb+dTGjty9lfPINxgPbQmNYXX0xBSe/j3Ezz2P0iBpG+13oALNQiJFjJzJy7ETgI0e279mxlW2rlhN7bTlj9j1HzYbnYMP32Ryq5c1xH6LmnE8wduL0ng8s77B48WK+8IUvAKm++cWLF7Nw4UI2bNjAsmXLWLZsGRdccAEPPvggF1xwAQDLly9/xxhBp/vvv5+nnnqK9evXc/vttx/51L948WKuvPLKI69zzz33HAmCXHYNWb5dvL6urs5p9dHjX1tLE6v/9AsqXv0Vk+PrSThjfeF0mk66mPFnzfP+85NM7Ni8nm3P/ZbKTQ8zNbYWgHXRU2k+7TpmvP+TFBTmvuuhP9atW8fUqVN9e/19+/ZRU1NDdXU1ZkYikcDM2Lp169umai5cuJCtW7fyox/9KO1gcX19Pbfffjv19fVceOGFrF27lurqampqaohEIoTD4SMnh+3cuZPy8nLKyspoamrKqOaj/c7MbIVzru5o++fFYLEMHocP7uPVh77P1C2/4l00sTVUw3Mnf4WJ77uGU0dqxlhfjJ4whdETbgVuZVfD62xe/ktqNt3P1Be/wp4X/4WNJ17NjI98hZKySr9LDaTf/OY3fPKTn+SnP/3pkW3vfe97efrpp5k4cSKjR48mmUyyatWqI108maqrq+OTn/wk//Ef/8H555/PjBkzWLp06ZHHr7nmGh566CGuvvrqrP08R6MgkEBob2vhpQe+wymbfs67aeaV4jNoOOcfOfXdf8f4PO/nD5IRNScx4pO3kUx8nVVPPQTP38GZm37I3oW/ZNXJn2HO5f9bA8zdLF68mJtvvvlt2+bNm8c111zDsGHDaG9PjVOdfvrp3Hjjjb0+/s0338zs2bN58803ueyyy97xOj/+8Y+5+uqr3zFGMHfuXL773e/24Sd6J3UNie9W/+V3DHnyVsa6HawsPpPSD36VSaed63dZg8b6Fx4n/ud/YVr7SjaHxtP2wR8w9YwP+l3WEX53DeWj3nYN6aOW+Ka1+TDP/+gapi9PzRNYdd7dnHbzUoXAAJty+geYdstfePmsRRQnW5j62Md47o4baG9r8bs0GSAKAvHFG6+9wu5/fTdn7P09z438BCfcvIIZ583zu6xBbdaF86n4ygqer5rHmbvvZ9sPzmXH5vV+lyUDQEEgA27NXx+l4t4PUZE8yOr33cOZ/+sOLZsQECVllZxx4928fNYiTkjspPCXF/K3l/7id1mSYwoCGVAr/3wfk5bO50BoKC1X/4np77nU75LkKGZdOJ+DH3+Udiuk5g8fZfVTD/ldkuSQgkAGzCvLH+SUpz7P1uiJDLnxScacqAHAIBt38mkUfHYZb4ZHMfHPN7Du+aXpnyR5SUEgA2LjK89w8pP/wLbIeE74h8eoHFbtd0mSgaqRY6n87CM0hqsZ++g1bF77ot8lSQ4oCCTn9ry5jfKHruagVTLkM39QCOSZ4SNqKLz+v2mzQqIPzufgvka/SwqM2tpa9uzZ069j1NfX80//9E99eu55551HNqbTKwgkp5KJBLvunk+5a6L5I/dQNXJs+idJ4IyoOYk9F/2ME5KNbLnrE7hk0u+Sjht1dXX88Ic/9LUGBYHk1Av3fZtTO15hzcxbOWnGWX6XI/0w5fQP8NLkLzOz9Xnqf/8jv8sZUFu2bGHKlCl84hOfYOrUqVx++eW0tKTOs/jRj37E7NmzmT59OuvXryeZTDJp0iQaG1Mtp2QyycSJE2lsbOTBBx9k2rRpzJw5k/e85z1Aah2iiy++GEitUnrdddcxffp0ZsyYwW9/+1sAPve5z1FXV8epp57K17/+9az/fFpiQnLmjY2rmfW3H/Jy6VnU/f0/+l2OZMHpV9zCmu8tZeor3+HNd/3dwC/+99gCeHN1do85cjp8KP1SDRs2bODnP/85Z599Ntdffz133HEHAFVVVbz00kvccccdLFy4kJ/97GfMnz+fe++9ly9+8Ys88cQTzJw5k+rqam677TaWLl3KmDFjOHDgwDte41vf+haVlZWsXp36Gffv3w/At7/9bYYNG0YikeCCCy7o07pGx6IWgeTM3t/dRJwwY+f/BNN6QceFUDjM0KvuIkKchgdvTv+E48jYsWM5++yzAZg/fz7PPPMMAB/5SGoJ8Dlz5rBlyxYArr/+eu655x4A7r77bq67LnX2/Nlnn821117LXXfdRSKReMdrPPHEE3z+858/cn/o0NRFUx944AFmz57NrFmzWLNmDWvXrs3qz6YWgeTEqid/y2ktz/LcSf/EmaPH+12OZNHoCVN4tmY+797+n2yoX8bkuvcN3Itn8Mk9V7ouOd31fmFhapG+cDhMPB4HUqExYsQIli1bxgsvvMC9994LwE9+8hOef/55HnnkEebMmcOKFSvSvu7mzZtZuHAhL774IkOHDuXaa6+lra0tmz+aWgSSfS6ZpPiZ77DdRjDrY7f4XY7kwIwrv8EehhD/09f8LmXAbNu2jWeffRaAX//615xzzjnH3P/Tn/408+fP56Mf/eiRK5W9/vrrnHHGGdx2221UV1fzxhtvvO05H/jAB1i0aNGR+/v37+fQoUOUlpZSWVnJrl27eOyxx7L8kykIJAdWP/UQk+KvsX3aP2jpiONUafkQNk6+gVM7VrP+hcf9LmdATJ48mUWLFjF16lT279/P5z73uWPuf8kllxwZ/O100003MX36dKZNm8ZZZ53FzJlvv3zoV7/6Vfbv339kQHn58uXMnDmTWbNmMWXKFD7+8Y8f6Z7KJi1DLVm37ttnMTS2i2G3rAn81a+k71qaDtK+8FS2lExn1v/O/qfUTkFYhnrLli1cfPHFvPrqqxk/p76+ni996Us8/fTTOazs6LQMtfjq9dXPMTW2hi2TrlEIHOdKyipZP+4qZrX8lTdee8XvcgLlu9/9LvPmzeM73/mO36VkREEgWbXnyR/T5qJMnftZv0uRATBp7o3EXYiGZT/zu5Scqq2t7VVrYMGCBWzdujXtOEJQKAgka5oPH+DUPUtZPeR9VA4f4Xc5MgCqRo9ndemZTNq5hFhHe85eJ9+6sP3Ul9+VgkCyZt2T91NmrZSeeV36neW4YbOvpooDrPnLb3Ny/KKiIvbu3aswyIBzjr1791JU1LtuWZ1HIFkTXvcHdjOMKadf6HcpMoCmvXceB575CvHVv4MPfDzrx6+pqaGhoeHIkg1ybEVFRdTU1PTqOQoCyYqmQ/s5pfkFVp5wKSd4c6ZlcIhEC3htyLlMPvAXOtrbsj5JIBqNMmHChKweU95OXUOSFeuf/g2FFqN8zkf9LkV8EJ12KRW0sP7ZR/wuRfpAQSBZ4f72OPspZ3Ld+/0uRXww5exLaHZFtK5e4ncp0gcKAuk3l0xSe/AFNpXNIRxRb+NgVFRcysaSmYze94LfpUgfKAik37ZueIlq9pOYcJ7fpYiPWseey1i3g51bN/hdivSSgkD67c2XHgVgbN1FPlcifhoxMzVbrGHFH32uRHpLQSD9Vrj9WRpsJKPGT/a7FPFR7dR3sYch2Jan/C5FeklBIP3ikknGtqxlZ8XM9DvLcc1CId4oncaow1m+gpjkXE6DwMzmmtkGM9toZguO8vg4M1tuZi+b2SozU99Cntm57TWqOEBy9By/S5EAaB8xmzFuF/t2b/e7FOmFnAWBmYWBRcCHgFOAq8zslG67fRV4wDk3C7gSuCNX9Uhu7FiT6gYYPiX7a6RL/imf+G4A3nh14Jdelr7LZYvgdGCjc26Tc64DuA+4tNs+DqjwblcCO3JYj+RAfOsLtLoCxk99l9+lSADUTj+LhDNaN2kaaT7JZRCMAbpeh63B29bVN4D5ZtYAPAr849EOZGY3mFm9mdVrvZFgqdz/KlsKJhItKPS7FAmA0vIhbA2Pp3jPKr9LkV7we7D4KuAXzrka4CLgV2b2jpqcc3c65+qcc3XV1dUDXqQcnUsmGRPbwqGKk/0uRQJkX9kkRrZt8rsM6YVcBsF2YGyX+zXetq4+BTwA4Jx7FigCqnJYk2TRrobXqaAFRpzqdykSILGqqYxgLwf3qfWeL3IZBC8Ck8xsgpkVkBoM7r4QyTbgAgAzm0oqCPTuyRO7Nr4MQMV4TR2Vt5TUTAdgx2sv+VyJZCpnQeCciwM3AkuBdaRmB60xs9vM7BJvt38GPmNmrwCLgWudrj6RN1oaUvPFR0+a7XMlEiQjvPfDoa26jnG+yOkKYc65R0kNAnfd9rUut9cCmneYp6J71rKL4YwYpnEbecuIMSdyiBLYtcbvUiRDfg8WSx6rbN7CrqJav8uQgLFQiB3RWsoOa8A4XygIpE9cMsmIxA5ay8b7XYoE0OGSGoZ36LSgfKEgkD45uG83FbTghtb6XYoEULxyAie4vbS1NvtdimRAQSB9snvbegAKq0/yuRIJomj1SYTMsUvXJsgLCgLpk0M7/gbA0LFTfK5Egqh81CQA9jcoCPKBgkD6JNb4OgAjxysI5J1G1KbWl2zb9ZrPlUgmFATSJ5EDW9jNMIpKyvwuRQKoctgJHKIE27/Z71IkAwoC6ZPSlgb2REf7XYYElIVCNIZHUNismUP5QEEgfTIk3khL8Ui/y5AAayo4gfKO3X6XIRlQEEivuWSS4cl9xEtG+F2KBFhbySiGJfb4XYZkQEEgvXZg7y4KLQaV3S8vIfKWZPkohnJI5xLkAQWB9Nq+N7cCUDBUQSA9iwypAWDPdg0YB52CQHqtqXELACVVY4+9owxqxVXjADi4a6vPlUg6CgLptba9qesLDR1Z628hEmiVI1LrULXu3eZzJZKOgkB6LXloBwlnDB+hFoH0bPioWgBi+xv8LUTSUhBIr4UP72CfDSESLfC7FAmwkrJKml0R1qKZQ0GnIJBeK2xr5EB4uN9lSB44EKok0qogCDoFgfRaSfwArdGhfpcheaApPJTC9n1+lyFpKAik18riB2gvVBBIei0FwyiJ7/e7DElDQSC9VukOkShS15CkFyscRkXigN9lSBoKAumV1ubDlFg7rkRBIOklSqoY4g6RTCT8LkWOQUEgvXJgT2o1yXBZtc+VSD6w0mqiluDwAQ0YB5mCQHqlad8uAAoqTvC5EskHEe99cqBxu8+VyLEoCKRXWg6kgqBoiIJA0iuqTK1Q27TvTZ8rkWNREEivdBxMrS9fOlRLUEt6JUNT16xoP7jL50rkWBQE0iuJ5lRfb8XwUT5XIvmgpDI1qSDerCmkQaYgkF5xzXuIuTAVlcP8LkXyQPmQKgCSLQqCIFMQSK+E2g9x2EqxkN46kl5JaQUxF8a1HfS7FDkG/TVLr0Q6DtFsZX6XIXnCQiGarJRQu4IgyBQE0iuR2GHawqV+lyF5pMnKiHQoCIIsp0FgZnPNbIOZbTSzBT3s8zEzW2tma8zs17msR/qvMN5EW1gtAslca7iMaOyQ32XIMURydWAzCwOLgA8ADcCLZrbEObe2yz6TgFuAs51z+81Mk9MDrijZREuhziqWzLVFKiiKKwiCLJctgtOBjc65Tc65DuA+4NJu+3wGWOSc2w/gnNudw3okC0qSzcQLyv0uQ/JILFpBSeKw32XIMeQyCMYAb3S53+Bt6+pk4GQz+x8ze87M5h7tQGZ2g5nVm1l9Y2NjjsqVTJS5ZpIFFX6XIXkkXlBBqWvyuww5Br8HiyPAJOA84CrgLjMb0n0n59ydzrk651xddbW6JfwS62hPrTxaVOl3KZJHkoWVlLtmXDLpdynSg7RBYGZhM1vYh2NvB7pe3bzG29ZVA7DEORdzzm0G/kYqGCSAmg+lTgoyBYH0ghUPIWJJmps0cyio0gaBcy4BnNOHY78ITDKzCWZWAFwJLOm2z+9JtQYwsypSXUWb+vBaMgCaD+0FIFysIJDMdX5waDmsC9QEVaazhl42syXAg0Bz50bn3O96eoJzLm5mNwJLgTBwt3NujZndBtQ755Z4j11oZmuBBHCTc25vH38WybGWQ6lrz0ZLdZlKyVy4KDXduFUtgsDKNAiKgL3A+7psc0CPQQDgnHsUeLTbtq91ue2AL3tfEnDtTamuoWjpO4ZxRHoUKU5NLmhvVhAEVUZB4Jy7LteFSPDFvBUki8q14JxkLuoFQUeLgiCoMpo1ZGYnm9mfzexV7/4MM/tqbkuToIm1pE4KKi5X15BkrqA0FQSd7x8Jnkynj95F6gzgGIBzbhWpwV8ZRFx7ai54canOI5DMFXnvl0SbziUIqkyDoMQ590K3bfFsFyPBljwSBDqzWDJXVJqaNZRo09nFQZVpEOwxs5NIDRBjZpcDO3NWlQRTrIWEMwqLSvyuRPJIcVkqCFy7giCoMp019HngTmCKmW0HNgPzc1aVBJLFWmiliDJdlEZ6oaSzK7FdXUNBlemsoU3A+82sFAg55xTtg1Ao1kybFaJFqKU3QuEwza4IOhQEQXXMIDCz+c65/zKzL3fbDoBz7t9yWJsETCjeSpsV+V2G5KEWKyYUa06/o/giXYugszNYo4NCON5Ce6jY7zIkD7VZMeG4giCo0gXBSd73tc65B3NdjARbNNFCTEEgfdAeKiaiIAisdKN+F1mqH+iWgShGgi2aaCMWUteQ9F57uIRovMXvMqQH6VoEfwT2A2Vm1vW0QCO1VJDOLBpECpJttBRW+V2G5KF4qIiSuFYfDapjtgicczc554YAjzjnKrp8lSsEBp8C10oirK4h6b1EuIhost3vMqQHGU0Id851v9awDEJFro1EVCeTSe8lw0UUOAVBUB0zCMzsGe/7YTM71P37wJQoQVHs2nARBYH0XjKiIAiyY44ROOfO8b5r+uggl0wkKKIDV1DqdymSh5KRYgoVBIGV7oSyYy4875zbl91yJKjaWpsoMYepa0j6wEWKKKTD7zKkB+lmDa0gtdCcAeNIzSAyYAiwDZiQ0+okMFqbD1MCWKEWmJA+iJZQYAnisQ4i0QK/q5Fu0s0amuCcOxF4Aviwc67KOTccuBj400AUKMHQ3pJaJyZUqK4h6T2Lps4/aWvVSWVBlOkykmd61x8GwDn3GHBWbkqSIIq1pf6AQwWaPiq919ml2NaiheeCKNNlqHd4l6b8L+/+J4AduSlJgijW0QpAOKogkN4z7wNER5vOLg6iTFsEVwHVwEPe1wneNhkk4u1eEBRoiQnpvXBBqkUQa1WLIIgyvR7BPuALZlaeuuv0rznIxDtbBOoakj7oDIKOdrUIgiijFoGZTTezl4FXgTVmtsLMpuW2NAmSREcbAJFCBYH0XtibZNA51iTBkmnX0E+BLzvnxjvnxgP/TOrSlTJIJGOpFkFELQLpg6j3ASKhFkEgZRoEpc655Z13nHNPAppHOIh0tgiihRojkN6LFKX+u4grCAIp01lDm8zs/wC/8u7PBzblpiQJIhfrDAKdWSy9F/WCINGhIAiiTFsE15OaNfRb76sKuC5XRUnwJI8EgbqGpPcKi1NBkFSLIJAyDYKTgLHe/gXABcBTuSpKgsfFU0FQUKQWgfRe5/vGeWNNEiyZdg3dC3yF1KyhZO7KkcCKpVaOLFQQSB90tiRdXCuQBlGmQdDonPvvnFYiwRZvI+GMqBYMkz4o6JxkENcKpEGUadfQ183sZ2Z2lZl9pPMr3ZPMbK6ZbTCzjWa24Bj7zTMzZ2Z1GVcuAyveRjsFWCjTt4zIWwq8M9JdQi2CIMq0RXAdMAWI8lbXkAN+19MTzCwMLAI+ADQAL5rZEufc2m77lQNfAJ7vXekykCzeRodFUceQ9IWFQnS4iFoEAZVpELzLOTe5l8c+HdjonNsEYGb3AZcCa7vt9y3ge8BNvTy+DCBLtNOBuoWk7zqIYmoRBFKm7fy/mtkpvTz2GOCNLvcbvG1HmNlsYKxz7pFjHcjMbjCzejOrb2xs7GUZkg2hRDsxi/pdhuSxmCkIgirTFsGZwEoz2wy0k7pKmXPOzejrC5tZCPg34Np0+zrn7sRb0qKurs719TWl70LJdmKmFoH0XYwIllDXUBBlGgRz+3Ds7aTOPehU423rVA5MA540M4CRwBIzu8Q5V9+H15McCiU6iCsIpB/iFiWUjPldhhxFpstQb+3DsV8EJpnZBFIBcCXw8S7HPEjqDGUAzOxJ4CsKgWAKJ9uJhwr9LkPyWMyihJJqEQRRzuYCOufiwI3AUmAd8IBzbo2Z3WZml+TqdSU3IskO4iG1CKTv4hbF1CIIpEy7hvrEu87xo922fa2Hfc/LZS3SPxHXQUdIC85K3yUsSlgtgkDS2UGSkUiyg6RmDUk/xK1AQRBQCgLJSIgELpTTBqQc5xKhKGF1DQWSgkAyEnZxkiG1CKTvkqECIk4tgiBSEEhGwi6Bs7DfZUgeS4QKiDi1CIJIQSAZCatrSPrJhaIKgoBSEEhGFATSX6muobjfZchRKAgkI2ESYAoC6btkuIAIahEEkYJAMhJxahFIP4ULKFAQBJKCQDISJgEKAukHFy4kqjGCQFIQSEYiGiOQfnJqEQSWgkDScskkUUtAWOcRSD9ECgmbIx7TuQRBoyCQtBIJb6aHWgTSH977R0EQPAoCSSse95rzCgLpB/NalDEFQeAoCCStzk9wFlYQSD94HySScY0TBI2CQNJKxNU1JP2nFkFwKQgkrXgsdcFx02Cx9EPn+ycRVxAEjYJA0kpqsFiyoTMIYuoaChoFgaTV2SIIqUUg/dD5/kkkFARBoyCQtNQikGzonGyQ1BhB4CgIJK2415S3iFoE0nch7/0T16yhwFEQSFpJrylvukKZ9IOFCwBIarA4cBQEklbnvO+QWgTSDxojCC4FgaSV8IJAJ5RJf3ROH9UJZcGjIJC0OruGNGtI+qOzRamuoeBREEhaR7qG1CKQfghH1CIIKgWBpHWkRRAp8LkSyWed7x+XVBAEjYJA0lKLQLKh8/2TjOsC9kGjIJC0Oj/BqUUg/RGOdsFdqVkAAA1zSURBVE4fVYsgaBQEklbnJzi1CKQ/OscIXEKDxUGjIJC0nDdGEI4oCKTvwpFCAFxCXUNBk9MgMLO5ZrbBzDaa2YKjPP5lM1trZqvM7M9mNj6X9UjfvDVYXOhzJZLPOj9IOJ1QFjg5CwIzCwOLgA8BpwBXmdkp3XZ7Gahzzs0AfgN8P1f1SN91foILq2tI+iHSOWtIQRA4uWwRnA5sdM5tcs51APcBl3bdwTm33DnX4t19DqjJYT3SV51dQ1GdUCZ91zlYrCAInlwGwRjgjS73G7xtPfkU8NjRHjCzG8ys3szqGxsbs1iiZKJzGeqwZg1JP3QGATqPIHACMVhsZvOBOuAHR3vcOXenc67OOVdXXV09sMXJkT/csBadk36IdE420GBx4OSy03c7MLbL/Rpv29uY2fuBW4H3Oufac1iP9FHnGEFEQSD9EIl6s4bUIgicXLYIXgQmmdkEMysArgSWdN3BzGYBPwUucc7tzmEt0h9Jr2soqq4h6bto5/tHLYLAyVkQOOfiwI3AUmAd8IBzbo2Z3WZml3i7/QAoAx40s5VmtqSHw4mPOgf31CKQ/rBQiLgLaYwggHI6H9A59yjwaLdtX+ty+/25fH3JEq9FEFGLQPopQfjI+0mCIxCDxRJwSZ1HINkRJ4wpCAJHQSDpJePEXBgL6e0i/RO3MKbzCAJHf9mSliViJPRWkSyIEwGnFkHQ6K9b0nOJVN+uSD8lUIsgiBQEkpYlYsRNQSD9lyCMuaTfZUg3CgJJTy0CyZKEhTF1DQWOgkDSsmQs1bcr0k9J06yhIFIQSFqWTJBUi0CyIEEEcwm/y5BuFASSlrm4xggkK5IWVhAEkIJA0rJknKSCQLIgaWFCWmIicBQEkpa5BAmNEUgWJIgQUosgcBQEklbIqUUg2ZG0sIIggBQEkpYl4yRMLQLpP6cxgkBSEEhaIRfXrCHJiqSFCes8gsBREEhaIZcgGVKLQPovGYoQQi2CoFEQSFoaI5BsSZoGi4NIQSBphVxCQSBZ4dQ1FEgKAkkr7OI4DRZLFjgLq2sogBQEkpZaBJItLqSuoSBSEEhaYRI4DRZLFjiLEFYQBI6CQNIKuYS6hiQrXChMWF1DgaMgkLTCxDV9VLLChSIKggBSEEhaYbUIJFtMQRBECgJJS2MEki0upDGCIFIQSFphEqAgkGwIRYioRRA4CgJJSy0CyRYNFgeTgkDSirgE6DwCyQILRYmQ9LsM6UZBIGlFSODCUb/LkOOAC0UImSOZUKsgSBQEklZEYwSSJea9j2Kxdp8rka4UBHJMyUSCkDkFgWRHKNXFmIjrusVBoiCQY+r85GYhdQ1JFnhdjPG4ViANkpwGgZnNNbMNZrbRzBYc5fFCM7vfe/x5M6vNZT3Se0c+uYU0WCxZ4LUsE7EOnwuRrnIWBGYWBhYBHwJOAa4ys1O67fYpYL9zbiLw78D3clWP9E0s5gWBBoslCyzsBUFCXUNBksuO39OBjc65TQBmdh9wKbC2yz6XAt/wbv8GuN3MzDnnsl3Mi7/7D6pfvSvbhz3uhV2CStAYgWSFeR8oWn/6QbZoSnKv7Z3zReb83aezftxc/nWPAd7ocr8BOKOnfZxzcTM7CAwH9nTdycxuAG4AGDduXJ+KiZQNZ1/JhD49d7DbZVOpedclfpchx4GxdRdRv/UZQkl1DfVFQdmwnBw3Lz7mOefuBO4EqKur61NrYdaF8+HC+VmtS0R6Z9T4yYz68m/9LkO6yeVg8XZgbJf7Nd62o+5jZhGgEtibw5pERKSbXAbBi8AkM5tgZgXAlcCSbvssAa7xbl8OLMvF+ICIiPQsZ11DXp//jcBSIAzc7ZxbY2a3AfXOuSXAz4FfmdlGYB+psBARkQGU0zEC59yjwKPdtn2ty+024KO5rEFERI5NZxaLiAxyCgIRkUFOQSAiMsgpCEREBjnLt9maZtYIbPW7jjSq6HZ2dECpzuzKlzohf2pVndkz3jlXfbQH8i4I8oGZ1Tvn6vyuIx3VmV35UifkT62qc2Coa0hEZJBTEIiIDHIKgty40+8CMqQ6sytf6oT8qVV1DgCNEYiIDHJqEYiIDHIKAhGRQU5B0A9m9lEzW2NmSTOr67K91sxazWyl9/WTLo/NMbPVZrbRzH5oZuZXnd5jt3i1bDCzD3bZPtfbttHMFuS6xqMxs2+Y2fYuv8eL0tXtlyD8vnpiZlu899xKM6v3tg0zs8fN7DXv+1CfarvbzHab2atdth21Nkv5ofc7XmVms32uM2/en2k55/TVxy9gKjAZeBKo67K9Fni1h+e8AJwJGPAY8CEf6zwFeAUoBCYAr5NaMjzs3T4RKPD2OcWH3+83gK8cZftR6/bxfRCI39cx6tsCVHXb9n1ggXd7AfA9n2p7DzC7699LT7UBF3l/M+b9DT3vc5158f7M5Estgn5wzq1zzm3IdH8zGwVUOOeec6l3zD3A3+esQM8x6rwUuM851+6c2wxsBE73vjY65zY55zqA+7x9g6Knuv0S9N/X0VwK/NK7/UsG4H14NM65p0hdi6Srnmq7FLjHpTwHDPH+pvyqsydBe3+mpSDInQlm9rKZ/cXMzvW2jQEauuzT4G3zyxjgjS73O+vpabsfbvS6Ae7u0n0RpPogePV054A/mdkKM7vB2zbCObfTu/0mMMKf0o6qp9qC+HvOh/dnWnlx8Xo/mdkTwMijPHSrc+4PPTxtJzDOObfXzOYAvzezU3NWJH2u03fHqhv4MfAtUv+RfQv4V+D6gavuuHGOc267mZ0APG5m67s+6JxzZhbIeeRBro3j6P2pIEjDOff+PjynHWj3bq8ws9eBk4HtQE2XXWu8bb7U6b322B7q6Wl7VmVat5ndBTzs3T1W3X4IWj1v45zb7n3fbWYPkeqm2GVmo5xzO73uld2+Fvl2PdUWqN+zc25X5+2Avz/TUtdQDphZtZmFvdsnApOATV5z95CZnenNFroa8PPT+hLgSjMrNLMJXp0vAC8Ck8xsgpkVkLqW9JKBLq5b/+9lQOeMjZ7q9ksgfl9HY2alZlbeeRu4kNTvcQlwjbfbNfj7Puyup9qWAFd7s4fOBA526UIacHn0/kzP79HqfP4i9Y/fQOrT/y5gqbd9HrAGWAm8BHy4y3PqSL1hXgduxzu72486vcdu9WrZQJcZTKRmaPzNe+xWn36/vwJWA6tI/XGNSle3j+8F339fPdR1IqkZLK9478lbve3DgT8DrwFPAMN8qm8xqa7UmPce/VRPtZGaLbTI+x2vpssMOJ/qzJv3Z7ovLTEhIjLIqWtIRGSQUxCIiAxyCgIRkUFOQSAiMsgpCEREBjkFgQSemf01B8esNbOPZ/u4R3md88zs4fR7vu05o3p6jpk92X0F2V4c92Izu60vz5Xjm4JAAs85d1YODlsL5DwI+ujLwF05OO4jwIfNrCQHx5Y8piCQwDOzJu/7ed4n4t+Y2Xozu9c7Q7tzzf3ve+vuv2BmE73tvzCzy7sfC/gucK63jvyXur1emZn92cxe8o53qbe91szWmdldlrq+w5/MrNh77F3e4mMrzewHXdet73LcUm9xshe8BQl7WqF0HvBH7znFZnaf97oPAcVdjnehmT3r1fmgmZV52y/yfj8rLLV+/8OQWreH1FLkF/fuX0COdwoCyTezgC+SWvP9RODsLo8ddM5NJ3XG9v9Lc5wFwNPOudOcc//e7bE24DLn3GzgfOBfOwOH1HIBi5xzpwIHSP2nDfCfwGedc6cBiR5e81ZgmXPudO+4P/CWfTjCW5Jgv0utVwXwOaDFOTcV+Dowx9uvCvgq8H6vznrgy2ZWBPyU1Nmsc4DqbjXUA+ci0oWCQPLNC865BudcktQSHrVdHlvc5fu7+/EaBvxfM1tFaomDMby1FPJm59xK7/YKoNbMhgDlzrlnve2/7uG4FwILzGwlqU/mRcC4bvuMAhq73H8P8F8AzrlVpJYzgNSFWU4B/sc73jXAeGAKqXWtNnv7LebtdgOje/7RZTDS6qOSb9q73E7w9vewO8rtON4HHjMLkbqCWDqfIPVJeo5zLmZmW0j9p3201y8mcwbMc8e+mFFrl9dKd6zHnXNXvW2j2WlpnlfkvYbIEWoRyPHkii7fOz+db8HrTgEuAaLe7cNAeQ/HqQR2eyFwPqlP2j1yzh0ADpvZGd6mK3vYdSnwj13GNWYdZZ+/8fZWzlN4g9pmNg2Y4W1/Dji7y1hIqZmdTGqRsxPNrPMYV/B2J/PWKpkigIJAji9Dve6cLwCdA8B3Ae81s1dIdRc1e9tXAQkze6X7YDFwL1BnZqtJLRW+nvQ+BdzlddOUAgePss+3SAXRKjNb491/G+dcM/B653/wpC5+UmZm64DbSHVH4ZxrBK4FFns/87PAFOdcK/APwB/NbAWpwOtay/mkZg+JHKHVR+W44HXf1Dnn9vj0+mXOuc7ZTQtILUn8hT4e6zJS3VJf7U8tXstjEfCac+7fzWwE8Gvn3AV9Oa4cv9QiEMmOv/Omjr5KalbOv/T1QM65h0h1afXVZ7yWyRpS3Vw/9baPA/65H8eV45RaBCIig5xaBCIig5yCQERkkFMQiIgMcgoCEZFBTkEgIjLI/X9XdwC1/vD/sAAAAABJRU5ErkJggg==\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "plt.figure()\n", - "ashraeiam.plot(label='ASHRAE')\n", - "physicaliam.plot(label='physical')\n", - "plt.ylabel('modifier')\n", - "plt.xlabel('input angle (deg)')\n", + "ashraeiam.plot(label=\"ASHRAE\")\n", + "physicaliam.plot(label=\"physical\")\n", + "plt.ylabel(\"modifier\")\n", + "plt.xlabel(\"input angle (deg)\")\n", "plt.legend();" ] }, @@ -210,18 +211,20 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": "43.509190983665746" }, + "execution_count": 7, "metadata": {}, - "execution_count": 7 + "output_type": "execute_result" } ], "source": [ "# scalar inputs\n", - "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']\n", - "pvlib.temperature.sapm_cell(900, 20, 5, **thermal_params) # irrad, temp, wind" + "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[\"sapm\"][\n", + " \"open_rack_glass_glass\"\n", + "]\n", + "pvlib.temperature.sapm_cell(900, 20, 5, **thermal_params) # irrad, temp, wind" ] }, { @@ -230,20 +233,20 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "# vector inputs\n", - "times = pd.date_range(start='2015-01-01', end='2015-01-02', freq='12H')\n", + "times = pd.date_range(start=\"2015-01-01\", end=\"2015-01-02\", freq=\"12H\")\n", "temps = pd.Series([0, 10, 5], index=times)\n", "irrads = pd.Series([0, 500, 0], index=times)\n", "winds = pd.Series([10, 5, 0], index=times)\n", @@ -265,24 +268,26 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "wind = np.linspace(0,20,21)\n", - "temps = pd.Series(pvlib.temperature.sapm_cell(900, 20, wind, **thermal_params), index=wind)\n", + "wind = np.linspace(0, 20, 21)\n", + "temps = pd.Series(\n", + " pvlib.temperature.sapm_cell(900, 20, wind, **thermal_params), index=wind\n", + ")\n", "\n", "temps.plot()\n", - "plt.xlabel('wind speed (m/s)')\n", - "plt.ylabel('temperature (deg C)');" + "plt.xlabel(\"wind speed (m/s)\")\n", + "plt.ylabel(\"temperature (deg C)\");" ] }, { @@ -298,24 +303,26 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX4AAAEHCAYAAACp9y31AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3dd5xU9fX/8deh997L0otUhQVBjCJqxIYiaGKMPaJJfsbEb6QoUYlGsURjigWNirEiRRAVRRR7A5Xdpfe69Lb0Lef3x73Ezbq7zC47O7O77+fjMY+duffOve8dhrN3PnPvuebuiIhI2VEu1gFERKR4qfCLiJQxKvwiImWMCr+ISBmjwi8iUsao8IuIlDEVorlyM/sD8CvAgWTgWqAp8CpQH5gPXOnuR/JbT4MGDbx169bRjCoiUurMnz9/u7s3zDndonUcv5k1Bz4Furj7QTObBLwNnAdMdfdXzexJYIG7P5HfuhITE33evHlRySkiUlqZ2Xx3T8w5PdpDPRWAqmZWAagGpAKDgMnh/InAxVHOICIi2USt8Lv7RuBhYB1Bwd9DMLSz290zwsU2AM1ze76ZjTCzeWY2b9u2bdGKKSJS5kSt8JtZXeAioA3QDKgODI70+e4+wd0T3T2xYcMfDVGJiEghRXOo5yxgtbtvc/d0YCowAKgTDv0AtAA2RjGDiIjkEM3Cvw7oZ2bVzMyAM4FFwIfA8HCZq4HpUcwgIiI5RHOM/yuCL3G/JTiUsxwwARgF3GpmKwgO6fx3tDKIiMiPRfU4fne/C7grx+RVQN9obldERPKmM3dFROLQxt0HGffmQjIys4p83VHd4xcRkYLJynJe+mot499ZggNDT2pOjxZ1inQbKvwiInFi5bZ9jJ6SxDdrdvGTDg24b2h3WtarVuTbUeEXEYmx9Mwsnv5kFX97fzlVKpTjoeE9GN67BcEBkUVPhV9EJIZSNu5h1JQkFm7ay+CuTfjzxV1pVLNKVLepwi8iEgOH0jP5xwfLefKjVdStVoknrujFud2bFsu2VfhFRIrZvDU7GTkliVXb9jO8dwvGnn8CdapVKrbtq/CLiBST/YczeOjdpUz8Yg3Naldl4nV9Ob1j8fciU+EXESkGHy3bxu1Tk9m05yBX9WvFyMGdqV45NiVYhV9EJIp2HzjCPTMXM+XbDbRtWJ3Xb+xPYut6Mc2kwi8iEiWzUlIZ+8ZCdh04wm/PaMfNgzpQpWL5WMdS4RcRKWpb0w5x1/SFvJOyma7NajHxuj50bVY71rH+S4VfRKSIuDuT52/g3rcWczA9k5GDOzHiJ22pUD6+2qKp8IuIFIH1Ow9w+7RkPlm+nT6t6zJ+WA/aNawR61i5UuEXETkOWVnOC1+s4cF3l2LAPRd15YqTW1GuXHTaLRQFFX4RkUJasXUfo6YkMX/tLk7v2JD7LulO8zpVYx3rmFT4RUQKKD0ziwkfr+Kx95dTrXJ5HrmsJ0NPah61pmpFTYVfRKQAUjbuYeTkJBal7uX87k25e0hXGtasHOtYBaLCLyISgUPpmTw2ZzkTPl5FveqVeOrK3pzTtUmsYxVK1Aq/mXUCXss2qS1wJ/BCOL01sAa4zN13RSuHiMjx+nr1TkZPSWLV9v38LLElt593ArWrVYx1rEKLWuF396XAiQBmVh7YCEwDRgNz3H28mY0OH4+KVg4RkcLadziDB95Zwn++XEuLulV58fqTObVDg1jHOm7FNdRzJrDS3dea2UXAwHD6RGAuKvwiEmc+XLqVO6Ymk7r3ENcOaM1t53SiWqXSMTpeXL/Fz4FXwvuN3T01vL8ZaFxMGUREjmnX/iPcM3MRU7/bSPtGNZh80yn0blU31rGKVNQLv5lVAoYAY3LOc3c3M8/jeSOAEQAJCQlRzSgi4u68lZzKXdMXsudgOr8b1J7fDmpP5Qqxb6pW1Ipjj/9c4Ft33xI+3mJmTd091cyaAltze5K7TwAmACQmJub6x0FEpChs2XuIP72RwnuLttC9eW1e/NXJnNC0VqxjRU1xFP7L+WGYB2AGcDUwPvw5vRgyiIj8iLszad567n1rMUcyshhzbmeuP7VN3DVVK2pRLfxmVh04G7gx2+TxwCQzux5YC1wWzQwiIrlZt+MAY6Yl8dmKHfRtU48HhvWgTYPqsY5VLKJa+N19P1A/x7QdBEf5iIgUu8ws5/nP1/Dwu0spX8649+Ju/KJvQlw3VStqpePYJBGRCCzfksbIKUl8t243Z3RqyF+GdqdZCWiqVtRU+EWk1DuSkcWTH63knx+soHrl8jz28xMZ0rNZiWmqVtRU+EWkVFuwfjejpiSxZHMaF/Zsxt0XdqF+jZLVVK2oqfCLSKl08Egmj76/jGc+WUXDmpV5+qpEzu6i80VBhV9ESqEvVu5gzNQk1uw4wOV9WzLmvBOoVaXkNlUrair8IlJq7D2Uzvh3lvDyV+toVb8aL99wMqe0K/lN1YqaCr+IlApzFm/hjmkpbE07xA0/acOtZ3eiaqXS126hKKjwi0iJtmPfYca9uYgZCzbRsXENnrxyACe2rBPrWHFNhV9ESiR3Z8aCTYx7cxFph9L5/Vkd+M3A9lSqULrbLRQFFX4RKXFS9xxk7LQU5izZSs+WdXhwWA86NakZ61glhgq/iJQYWVnOq9+s5/63F5OelcXY80/g2gFtKF+G2i0UBRV+ESkR1mzfz+ipSXy5aif929Zn/LDutKpfNpqqFTUVfhGJa5lZzrOfruavs5dSsVw5xl/SnZ/1aVlm2y0UBRV+EYlbSzenMXLyAhZs2MNZJzTi3ou706R2lVjHKvFU+EUk7hzJyOJfH67g8bkrqFmlIn+//CQu7NFUe/lFRIVfROLK9+t3M3LyApZt2cfFJzbjzgu7Uq96pVjHKlVU+EUkLhw4ksEj7y3j2c9W07hWFZ69JpFBndVULRpU+EUk5j5fsZ3RU5NZt/MAv+yXwKjBnamppmpRo8IvIjGz52A697+9mFe/WU/r+tV4dUQ/+rWtf+wnynFR4ReRmJi9aAtj30hmW9phbjy9LX84qyNVKqqpWnGIauE3szrAM0A3wIHrgKXAa0BrYA1wmbvvimYOEYkf2/cd5u4ZC5mZlErnJjV5+qpEerRQU7XiFO09/seAWe4+3MwqAdWA24E57j7ezEYDo4FRUc4hIjHm7kz/fhPj3lzIvsMZ3Hp2R246vZ2aqsVA1Aq/mdUGTgOuAXD3I8ARM7sIGBguNhGYiwq/SKm2afdBxr6RwgdLtnJSQtBUrUNjNVWLlWMWfjNrBAwAmgEHgRRgnrtnHeOpbYBtwHNm1hOYD9wCNHb31HCZzUCux2uZ2QhgBEBCQsKxfxMRiTtZWc7LX69j/DtLyMxy7rygC1ef0lpN1WIsz8JvZmcQDMPUA74DtgJVgIuBdmY2Gfiru+/NZ929gJvd/Sszeyxc33+5u5uZ5/Zkd58ATABITEzMdRkRiV+rt+9n9JQkvlq9kwHt63P/0B4k1K8W61hC/nv85wE3uPu6nDPMrAJwAXA2MCWP528ANrj7V+HjyQSFf4uZNXX3VDNrSvAHRURKiYzMLP796Woemb2MShXK8eCwHlya2ELtFuJInoXf3W/LZ14G8EZ+K3b3zWa23sw6uftS4ExgUXi7Ghgf/pxemOAiEn8Wp+5l1JQkkjbs4addGnPPxd1oXEtN1eJNfkM9twJ73P3fOaZfD9R0979FsP6bgZfCI3pWAdcC5YBJ4XrWApcVNryIxIfDGZn864MVPD53JXWqVeRfv+jFed2baC8/TuU31HMF0C+X6f8B5gHHLPzu/j2QmMusMyNKJyJxb/7aXYyaksSKrfu45KTm/OmCLtRVU7W4ll/hr+Du6TknuvsR059xkTLvwJEMHnp3Kc9/voamtarw3LV9OKNTo1jHkgjkV/jLmVljd9+SfaKZqV2eSBn36fLtjJ6axIZdB7mqfytGDu5MjcrqAFNS5Pcv9RDwlpn9H/BtOK13OP3haAcTkfiz50A69761iNfnb6BNg+pMurE/fdvUi3UsKaD8jup5wcy2AX/mh147C4E73f2dYsonInFiVspm/jQ9hZ37j3DT6e34/Vkd1FSthMr3s1lY4FXkRcqwbWlBU7W3klPp0rQWz13Th27Na8c6lhwHDcqJSK7cnWnfbeTPMxdx4HAmt53TiRGntaVieTVVK+lU+EXkRzbsOsAd01L4aNk2ereqywPDetC+UY1Yx5IiosIvIv+VleW8+NVaHnhnCQ6MG9KVK/u1opyaqpUqkXTnvDWXyXuA+eEJWiJSCqzcto/RU5L4Zs0uTuvYkPuGdqNFXTVVK40i2eNPDG9vho8vAJKAm8zsdXd/MFrhRCT60jOzmPDxKh6bs5yqFcvz8KU9GdarudotlGKRFP4WQC933wdgZncBbxFcZGU+oMIvUkKlbNzDqClJLNy0l3O7NWHcRV1pVFNN1Uq7SAp/I+BwtsfpBBdTOWhmh/N4jojEsUPpmfx9znKe+ngVdatV4okrenFu96axjiXFJJLC/xLwlZkdbZ98IfCymVUnaLEsIiXIvDU7GTkliVXb9nNp7xaMPb8LtatVjHUsKUbHLPzufo+ZvUNw+UWAm9x9Xnj/iqglE5Eite9wBg/NWsILX66lWe2qvHBdX07r2DDWsSQGIj2cswqw192fM7OGZtbG3VdHM5iIFJ2Plm3j9qnJbNpzkKv7t+a2czpRXU3VyqxIDue8i+Conk7Ac0BF4EV++AQgInFq94Ej3DNzMVO+3UC7htWZfFN/erdSU7WyLpI/+UOBkwg7dLr7JjOrGdVUInLc3k5O5c7pKew+kM7/O6M9/29QezVVEyCywn/E3d3MHCD8UldE4tTWvYe4c/pCZi3cTLfmtZh4XV+6NlNTNflBJIV/kpk9BdQxsxuA64CnoxtLRArK3Xl9/gbunbmIQxlZjBrcmRt+0oYKaqomOURyVM/DZnY2sJdgnP9Od58dycrNbA2QBmQCGe6eaGb1gNeA1sAa4DJ331Wo9CICwPqdB7h9WjKfLN9O39b1GD+sO20bqqma5C6ir/XDQh9Rsc/FGe6+Pdvj0cAcdx9vZqPDx6MKuW6RMi0zy3nhizU89O5SDLjnoq5ccbKaqkn+8iz8ZpZGcNWtXLl7rUJu8yJgYHh/IjAXFX6RAluxNY1RU5KZv3YXp3dsyH2XdKd5naqxjiUlQH6XXqwJYGb3AKnAfwAjOGkr0nO7HXgv/GL4KXefQNDuITWcvxnI9eLtZjYCGAGQkJAQ4eZESr/0zCye+mglf5+zgmqVy/PIZT0ZepKaqknkIhnqGeLuPbM9fsLMFgB3RvDcU919o5k1Amab2ZLsM7MfLZRT+EdiAkBiYmKenzxEypKUjXu4bXISi1P3cn73ptw9pCsNa1aOdSwpYSIp/PvN7ArgVYI9+MuB/ZGs3N03hj+3mtk0oC+wxcyaunuqmTUFthYuukjZcSg9k7+9v5ynP1lF/eqVeOrK3pzTtUmsY0kJFclxXr8ALgO2hLdLw2n5MrPqR0/0Co/9/ymQAswArg4XuxqYnvsaRATg69U7Oe+xT3jyo5UM79WC2beerqIvxyWSwznXEHwhW1CNgWnhuGMF4GV3n2Vm3xCcG3A9sJbgj4qI5JB2KJ0HZy3lP1+upWW9qrz0q5MZ0L5BrGNJKZDfUT1jgcfdfWce8wcB1dx9Zm7z3X0V0DOX6TuAMwsXV6Rs+HDpVu6Ymkzq3kNcN6ANfzynI9UqqamaFI383knJwJtmdoigT882gi6dHYATgfeB+6KeUKQM2bX/CPfMXMTU7zbSoVENpvz6FHol1I11LCll8jucczow3cw6EHTibEpw9u6LwAh3P1g8EUVKP3fnreRU7pq+kD0H0/ndoPb8dlB7KldQUzUpepGM8S8HlhdDFpEyacveQ4x9I4XZi7bQvXltXvzVyZzQtLDnR4ocmwYNRWLE3Zk0bz33vrWYIxlZjDm3M9efqqZqEn0q/CIxsG7HAUZPTeLzlTs4uU09xg/rQZsG6nguxUOFX6QYZWY5z3++hoffXUr5csZfhnbj8j4JaqomxSqSSy92BJ4g6LHTzcx6ELRxuDfq6URKkWVb0hg5OYnv1+9mUOdG/GVoN5rWVlM1KX6R7PE/DdwGPAXg7klm9jKgwi8SgSMZWTz50Ur+8cFyalSuwGM/P5EhPZupqZrETCSFv5q7f53jTZoRpTwipUrSht2MnJzEks1pDOnZjLsu7EL9GmqqJrEVSeHfbmbtCHvzm9lwgjbNIpKHg0cyefT9ZTzzySoa1azCM1clclaXXDuQixS7SAr/bwnaI3c2s43AaoKe/CKSiy9W7mDM1CTW7DjA5X0TGHNeZ2pVqRjrWCL/lW/hN7PywG/c/ayww2Y5d08rnmgiJUvaoXTuf2cJL3+1jlb1q/HyDSdzSjs1VZP4k2/hd/dMMzs1vB9RD36RsmjO4i3cMS2FrWmHuOEnbbj17E5UraR2CxKfIhnq+c7MZgCvk+0CLO4+NWqpREqIHfsOM+7NRcxYsIlOjWvy5JW9ObFlnVjHEslXJIW/CrADGJRtmgMq/FJmuTszFmxi3JuLSDuUzu/P6sBvBranUgW1W5D4F0mTtmuLI4hISZG65yBjp6UwZ8lWerasw4PDetCpSc1YxxKJWCRn7j5HeChndu5+XVQSicSprCznlW/Wcf/bS8jIymLs+Sdw7YA2lFe7BSlhIhnqyX6FrSrAUGBTdOKIxKc12/czemoSX67aySnt6nP/Jd1pVV9N1aRkimSoZ0r2x2b2CvBp1BKJxJGMzCye/Ww1f31vGZXKl2P8Jd35WZ+WarcgJVphunN2ABpFunB4LsA8YKO7X2BmbYBXgfrAfOBKdz9SiBwiUbVk815GTU5iwYY9nHVCY+69uBtNaleJdSyR4xbJGH8a/zvGvxkYVYBt3AIsBo5eUugB4FF3f9XMngSuJ+j+KRIXDmdk8viHK3l87gpqVanIPy4/iQt6NNVevpQakQz1FPpwBTNrAZwP/AW41YL/OYOAX4SLTATuRoVf4sR363YxakoSy7bs4+ITm3HnhV2pV71SrGOJFKlI9vjnuPuZx5qWh78BI4GjfzzqA7vd/Wh3zw1A8zy2OwIYAZCQkBDBpkQK78CRDP763jKe/Ww1TWpV4blr+nBG54hHNEVKlDwLv5lVAaoBDcysLnD0c24t8ijWOZ5/AbDV3eeb2cCCBnP3CQTN4UhMTPzR4aQiReXzFdsZPTWZdTsP8Mt+CYwa3JmaaqompVh+e/w3Ar8HmhF8CXu08O8F/hnBugcAQ8zsPILDQGsBjwF1zKxCuNffAthYyOwix2XPwXTuf3sxr36znjYNqvPqiH70a1s/1rFEoi7Pwu/ujwGPmdnN7v6Pgq7Y3ccAYwDCPf4/uvsVZvY6MJzgyJ6rgemFCS5yPGYv2sLYN5LZlnaYG09vyx/O6kiVimqqJmVDJF/u/sPMugFdCPbcj05/oZDbHAW8amb3At8B/y7kekQKbPu+w9w9YyEzk1Lp3KQmT1+VSI8WaqomZUskX+7eBQwkKPxvA+cSnMAVceF397nA3PD+KqBvgZOKHAd3543vNzLuzUUcOJzJ/53dkRtPb6emalImRXIC13CgJ/Cdu19rZo2BF6MbS6TobNp9kDumJfPh0m2clBA0VevQWE3VpOyKpPAfdPcsM8sws1rAVqBllHOJHLesLOelr9fxwDtLyMxy7rygC1ef0lpN1aTMi6TwzzOzOsDTBEf37AO+iGoqkeO0ats+Rk9N5uvVOzm1fQPuv6Q7LetVi3UskbhwrGvuGnC/u+8GnjSzWUAtd08qlnQiBZSRmcUzn67m0dnLqFShHA8O68GliS3UbkEkm2Ndc9fN7G2ge/h4TXGEEimMRZv2MnLKAlI27uWnXRpzz8XdaFxLTdVEcopkqOdbM+vj7t9EPY1IIRzOyOSfH6zgibkrqVOtIo9f0YtzuzXRXr5IHiIp/CcDV5jZWoKLrRvBh4EeUU0mEoH5a4Omaiu27uOSXs350/ldqKumaiL5iqTwnxP1FCIFtP9wBg+/t5TnP19Ds9pVef7aPgzspKZqIpGI5MzdtWZ2KtDB3Z8zs4ZAjehHE8ndJ8u3MWZqMht2HeSq/q0YObgzNSoX5ppCImVTpGfuJgKdgOeAigQncA2IbjSR/7XnQDp/eXsRk+ZtoE2D6ky6sT9929SLdSyREieS3aShwEnAtwDuvsnMdNqjFKtZKZv50/QUdu4/wq8HtuOWMzuoqZpIIUVS+I+Eh3U6gJlVj3Imkf/alnaYu2ak8HbyZk5oWovnrulDt+a1Yx1LpESLpPBPMrOnCPro3wBcR3AWr0jUuDtTvt3IPTMXcTA9k9vO6cSI09pSsbyaqokcr0i+3H3YzM4muABLR+BOd58d9WRSZm3YdYDbp6Xw8bJt9G5VlweG9aB9Ix1PIFJUIj0UIhmoCnh4X6TIZWU5//lyLQ/MWgLAuCFdubJfK8qpqZpIkYrkqJ5fAXcCHxCcvPUPM/uzuz8b7XBSdqzcto9Rk5OYt3YXp3VsyH1Du9GirpqqiURDJHv8twEnufsOADOrD3wOqPDLcUvPzGLCx6t4bM5yqlYsz8OX9mRYr+ZqtyASRZEU/h1AWrbHaeE0keOSsnEPIycnsSh1L+d1b8LdQ7rSqKaaqolEWySFfwXwlZlNJxjjvwhIMrNbAdz9kSjmk1LoUHomj81ZzoSPV1G3WiWe/GUvBndrGutYImVGJIV/ZXg7anr4M9+TuMysCvAxUDnczmR3v8vM2gCvAvUJLuxypbsfKWhwKZm+WbOTUZOTWLV9P5f2bsHY87tQu1rFWMcSKVMiOZxzXCHXfRgY5O77zKwi8KmZvQPcCjzq7q+a2ZPA9cAThdyGlBD7Dmfw4KwlvPDFWprXqcoL1/XltI4NYx1LpEyK5KieROAOoFX25Y/VltndneAyjRD096lIMFQ0CPhFOH0icDcq/KXaR8u2cfvUZDbtOcg1p7TmtnM6UV1N1URiJpL/fS8RHNmTDGQVZOVmVp5gOKc98C+CIaPd7p4RLrIBaF6QdUrJsfvAEf48cxFTv91Iu4bVmXxTf3q3UlM1kViLpPBvc/cZhVm5u2cCJ4YXa58GdI70uWY2AhgBkJCQUJjNSwy9nZzKndMXsvvAEW4e1J7fntFeTdVE4kQkhf8uM3sGmEMwbg+Au0+NdCPuvtvMPgT6E/T8qRDu9bcANubxnAnABIDExESPdFsSW1v3HuLO6QuZtXAz3ZrX4oXr+tKlWa1YxxKRbCIp/NcS7KlX5IehHgfyLfzhBVvSw6JfFTgbeAD4EBhOcGTP1fxwlJCUYO7O6/M3cO/MRRzOyGL0uZ351altqKCmaiJxJ5LC38fdOxVi3U2BieE4fzlgkrvPNLNFwKtmdi/wHfDvQqxb4sj6nQe4fVoynyzfTt/W9Rg/rDttG6qpmki8iqTwf25mXdx9UUFW7O5JBBdwyTl9FdC3IOuS+JSZ5bzwxRoenLWU8uWMey7uxhV9E9RUTSTORVL4+wHfm9lqgjF+IzhaM9/DOaV0W7E1jZGTk/h23W4GdmrIfUO706xO1VjHEpEIRFL4B0c9hZQY6ZlZPPXRSv4+ZwXVKpfn0Z/15OIT1VRNpCSJ5MzdtWZ2KtDB3Z8Lv7TVAG4ZlLxhD7dNXsCSzWmc36Mp44Z0pUGNyrGOJSIFFMmZu3cBiUAn4DmCo3teBAZEN5rEi0PpmTz6/jKe/ngVDWpU5qkre3NO1yaxjiUihRTJUM9Qgi9pvwVw901mlm+DNik9vlq1g9FTk1m9fT8/79OSMeedQO2qaqomUpJFUviPuLubmQOYWfUoZ5I4kHYonQdmLeHFL9fRsl5VXvrVyQxo3yDWsUSkCERS+CeZ2VMEZ9zeAFwHPBPdWBJLHy7Zyh3Tkknde4jrBrThj+d0pFolNVUTKS0i+XL3YTM7G9hLMM5/p7vPjnoyKXY79x/hnpmLmPbdRjo0qsGUX59Cr4S6sY4lIkUski93H3D3UcDsXKZJKeDuzExK5e4ZC9lzMJ1bzuzAb85oR+UKaqomUhpF0kjl7FymnVvUQSQ2tuw9xIj/zOfmV76jed2qzPzdqfzh7I4q+iKlWJ57/Gb2a+A3QFszS8o2qybwWbSDSXS5O699s56/vL2YIxlZ3HHeCVw7oLWaqomUAfkN9bwMvAPcD4zONj3N3XdGNZVE1dod+xkzNZnPV+6gX9t6jL+kB60b6GAtkbIiz8Lv7nuAPcDlxRdHoikzy3nus9U8/N5SKpYrx31Du/PzPi3VVE2kjNExemXE0s1pjJySxIL1uzmzcyPuHdqNprXVVE2kLFLhL+WOZGTx+NwV/OvDFdSsUpHHfn4iQ3o2U1M1kTJMhb8UW7B+NyMnJ7F0SxpDejbjrgu7UF9N1UTKPBX+UujgkUwemb2Uf3+6mkY1q/DMVYmc1aVxrGOJSJxQ4S9lvli5g9FTk1i74wC/ODmB0ed2plYVNVUTkR+o8JcSew+lc//bS3jl63W0ql+Nl284mVPaqamaiPyYCn8pMGfxFu6YlsLWtEOMOK0tfzirI1Ur6cxbEcld1Aq/mbUEXgAaAw5McPfHzKwe8BrQGlgDXObuu6KVozTbse8w495cxIwFm+jcpCZPXdmbni3rxDqWiMS5aO7xZwD/5+7fhhdumW9ms4FrgDnuPt7MRhOcFayGbwXg7sxYsIlxby4i7VA6fzirI78e2I5KFdRuQUSOLWqF391TgdTwfpqZLQaaAxcBA8PFJgJzUeGPWOqeg4ydlsKcJVs5sWUdHhzeg46NdUE0EYlcsYzxm1lrgss3fgU0Dv8oAGwmGArK7TkjgBEACQkJ0Q8Z57KynFe+Wcf9by8hIyuLseefwLUD2lBe7RZEpICiXvjNrAYwBfi9u+/NfsZo9ks65uTuE4AJAImJibkuU1as2b6f0VOT+HLVTk5pV5/xl/QgoX61WMcSkRIqqoXfzCoSFP2X3H1qOHmLmTV191QzawpsjWaGkiwjM4tnP1vNX99bRqUK5XhgWHcuS2ypdgsiclyieVSPAf8GFrv7I9lmzQCuBsaHP6dHK0NJtjh1L6OmJOLD7ioAAA68SURBVJG0YQ9nndCYey/uRpPaVWIdS0RKgWju8Q8ArgSSzez7cNrtBAV/kpldD6wFLotihhLncEYm//pwJY9/uILaVSvyz1+cxPndm2ovX0SKTDSP6vkUyKtanRmt7ZZk363bxcjJSSzfuo+hJzXnzgu6ULd6pVjHEpFSRmfuxoEDRzL463vLeO6z1TSuVYXnrunDGZ0bxTqWiJRSKvwx9tmK7YyemsT6nQf5Zb8ERg3uTE01VRORKFLhj5E9B9O5763FvDZvPW0aVOe1Ef04uW39WMcSkTJAhT8G3lu4mbFvpLB932FuPK0tfzi7I1UqqqmaiBQPFf5itC3tMHe/uZC3klLp3KQmz1ydSI8WaqomIsVLhb8YuDtvfL+RcW8u4sDhTP74047ceHo7KpZXUzURKX4q/FG2afdB7piWzIdLt9EroQ4PDOtBBzVVE5EYUuGPkqws56Wv1jL+nSU4cPeFXbiyf2s1VRORmFPhj4JV2/YxekoyX6/ZyU86NOC+od1pWU9N1UQkPqjwF6GMzCye+XQ1j85eRuUK5XhoeA+G926hdgsiEldU+IvIok17GTllASkb93JO18bcc1E3GtVSUzURiT8q/MfpcEYm//xgBU/MXUmdapV44openNu9aaxjiYjkSYX/OMxfu5ORk5NYuW0/w3q14E8XnECdamqqJiLxTYW/EPYfzuChd5cy8Ys1NKtdlYnX9eX0jg1jHUtEJCIq/AX08bJt3D4tmY27D3JVv1bcNrgzNSrrZRSRkkMVK0J7DqRzz1uLmDx/A20bVmfSjf3p07perGOJiBSYCn8EZqWk8qfpC9m5/wi/GdiO353ZQU3VRKTEUuHPx9a0Q9w1fSHvpGymS9NaPHdNH7o1rx3rWCIix0WFPxfuzpRvN3LPzEUcTM/ktnM6MeK0tmqqJiKlQtQKv5k9C1wAbHX3buG0esBrQGtgDXCZu++KVobCWL/zALdPS+aT5dtJbFWX8cN60L5RjVjHEhEpMtHchX0eGJxj2mhgjrt3AOaEj+NCVpYz8fM1nPO3j5m/dhfjhnRl0o39VfRFpNSJ2h6/u39sZq1zTL4IGBjenwjMBUZFK0OkVm7bx6jJScxbu4vTOjbkvqHdaFFXTdVEpHQq7jH+xu6eGt7fDDTOa0EzGwGMAEhISIhKmPTMLCZ8vIrH5iynasXyPHxpT4b1aq6maiJSqsXsy113dzPzfOZPACYAJCYm5rlcYaVs3MOoKUks3LSXc7s1YdxFXWlUU03VRKT0K+7Cv8XMmrp7qpk1BbYW8/Y5lJ7J3+cs56mPV1GveiWe/GUvBndTUzURKTuKu/DPAK4Gxoc/pxfnxuet2cnIKUms2rafS3u3YOz5XahdrWJxRhARibloHs75CsEXuQ3MbANwF0HBn2Rm1wNrgcuitf3s9h3O4MFZS3jhi7W0qFuV/1zfl590UFM1ESmbonlUz+V5zDozWtvMzdylW7ljWgqb9hzkmlNac9s5naiupmoiUoaV6go4Zmoyr3y9jnYNqzP5pv70bqWmaiIipbrwt65fjZsHtee3Z7RXUzURkVCpLvw3nt4u1hFEROKOuo6JiJQxKvwiImWMCr+ISBmjwi8iUsao8IuIlDEq/CIiZYwKv4hIGaPCLyJSxph7kbe6L3Jmto2gqVthNAC2F2GcaCtJeZU1ekpS3pKUFUpW3uPN2srdf9SRskQU/uNhZvPcPTHWOSJVkvIqa/SUpLwlKSuUrLzRyqqhHhGRMkaFX0SkjCkLhX9CrAMUUEnKq6zRU5LylqSsULLyRiVrqR/jFxGR/1UW9vhFRCQbFX4RkTKm1BZ+M3vIzJaYWZKZTTOzOtnmjTGzFWa21MzOiWXOMM+lZrbQzLLMLDHHvLjKepSZDQ4zrTCz0bHOk52ZPWtmW80sJdu0emY228yWhz/rxjLjUWbW0sw+NLNF4XvglnB6vOatYmZfm9mCMO+4cHobM/sqfD+8ZmaVYp31KDMrb2bfmdnM8HE8Z11jZslm9r2ZzQunFfl7odQWfmA20M3dewDLgDEAZtYF+DnQFRgMPG5msb4uYwpwCfBx9olxmpUww7+Ac4EuwOVh1njxPMHrld1oYI67dwDmhI/jQQbwf+7eBegH/DZ8LeM172FgkLv3BE4EBptZP+AB4FF3bw/sAq6PYcacbgEWZ3scz1kBznD3E7Mdv1/k74VSW/jd/T13zwgffgm0CO9fBLzq7ofdfTWwAugbi4xHuftid1+ay6y4yxrqC6xw91XufgR4lSBrXHD3j4GdOSZfBEwM708ELi7WUHlw91R3/za8n0ZQoJoTv3nd3feFDyuGNwcGAZPD6XGT18xaAOcDz4SPjTjNmo8ify+U2sKfw3XAO+H95sD6bPM2hNPiUbxmjddc+Wns7qnh/c1A41iGyY2ZtQZOAr4ijvOGQyffA1sJPlmvBHZn29GKp/fD34CRQFb4uD7xmxWCP6Lvmdl8MxsRTivy90KJvti6mb0PNMll1h3uPj1c5g6Cj9MvFWe2nCLJKsXD3d3M4uo4ZjOrAUwBfu/ue4Md00C85XX3TODE8HuzaUDnGEfKlZldAGx19/lmNjDWeSJ0qrtvNLNGwGwzW5J9ZlG9F0p04Xf3s/Kbb2bXABcAZ/oPJyxsBFpmW6xFOC2qjpU1DzHJGoF4zZWfLWbW1N1Tzawpwd5qXDCzigRF/yV3nxpOjtu8R7n7bjP7EOgP1DGzCuGedLy8HwYAQ8zsPKAKUAt4jPjMCoC7bwx/bjWzaQTDqkX+Xii1Qz1mNpjgI94Qdz+QbdYM4OdmVtnM2gAdgK9jkTEC8Zr1G6BDeHREJYIvoGfEONOxzACuDu9fDcTFp6xwzPnfwGJ3fyTbrHjN2/DoEXJmVhU4m+B7iQ+B4eFicZHX3ce4ewt3b03wHv3A3a8gDrMCmFl1M6t59D7wU4IDP4r+veDupfJG8EXoeuD78PZktnl3EIxLLgXOjYOsQwnGGg8DW4B34zVrtlznERwttZJguCrmmbJlewVIBdLD1/V6grHdOcBy4H2gXqxzhllPJRjXTcr2Xj0vjvP2AL4L86YAd4bT2xLslKwAXgcqxzprjtwDgZnxnDXMtSC8LTz6/yoa7wW1bBARKWNK7VCPiIjkToVfRKSMUeEXESljVPhFRMoYFX4RkTJGhV+ixsyuMbN/5jHv8+Ncb7OCzosnZjbQzE6J4vqrmtlHuTX1M7PnzWx4bs8rxHY6mtnbYefIb81skpk1NrPuZvZ8UWxDip4Kv8SEux9P0bsGyKu45zevWJlZfmfGDwQK9BocY305XQdM9aC9QlSYWRXgLeAJd+/g7r2Ax4GG7p4MtDCzhGhtXwpPhV/yZWZvhA2jFmZrGoWZ7bPgmgcLzex9M+trZnPNbJWZDcm2ipbh9OVmdlf252e7f5uZfWPBtROO9ndvbWaLzezpcBvvhXuxw4FE4KWwZ3nVbOv50Twz6x3u+c43s3fDU94JMz1qZvPC7fQxs6lhznuzZVhiZi+Fy0w2s2rhvPzW+zcLeqnfYmYXWtD7/bvwdWpsQTO2m4A/hDl/knMv/OjrE34y+MTMZgCLLGiQ9lC21+vGPP7priA8w9MC/7Tg+gnvA42ybSev36NPuP7vw+2l5LKNXwBfuPubRye4+1x3P7rsmwRnzEq8ifXZarrF943wLEGgKsGZmvXDx054JjFBo673CFr09gS+D6dfQ3AGbf1sz08M5+0Lf/6U4ILSRrAjMhM4DWhN0FzvxHC5ScAvw/tzj64nl7xzs22jIvA5wR4owM+AZ7Mt90B4/xZgE9AUqExwtm/9MIMDA8LlngX+GMF6H8+Wpy4/XNv6V8Bfw/t3A3/MttzzwPBsj4++PgOB/UCb8PEIYGx4vzIw7+i8bM+tBGzO9vgSgi6a5Qk+De0maFmQ3++RAvQP748HUnJ5rR8BbsnnvTMAeDPW72Hdfnwr0U3apFj8zsyGhvdbEvQL2gEcAWaF05OBw+6ebmbJBAXzqNnuvgPAzKYStCiYl23+T8Pbd+HjGuE21gGr3f37cPr8HOuNRCegG0GXQwgKX2q2+Uf7CyUDCz1sfWtmq8LfdTew3t0/C5d7Efhd+Hvnt97Xst1vAbwW7klXAlYX8HcA+NqD6zFA8Fr1yPbpoDbB65V9vQ3C7EedBrziwbDPJjP7IJye6+tjQS+emu7+RbjcywTNDgtqK3Ey7Cb/S4Vf8mRBK9uzCPb8DpjZXIIuhwDpHu7WEfQ6Pwzg7lk5xqJz9gTJ+diA+939qRzbbn10naFMgk8NBfoVCAp6/zzmH11/Fv+7rSx++L+RW/5jrXd/tvv/AB5x9xnh63l3Hs/JIBx6NbNyBH8kclufATe7+7t5rAfgID/8O+Un19/Dsl2m9BgWAqfnM79KmEXijMb4JT+1gV1h0e9McGnAgjrbgmuGViW4ctBnOea/C1xnQT96zKy5Bb3I85MG1Ixg3lKgoZn1D9dd0cy6FjB/wtHnE4xpf1rA9dbmh7a/V2ebnvN3WAP0Du8PIRiGyc27wK8taOV89Kia6tkXcPddQHkLvnyF4JKePwu/H2gKnBFOz/X3cPfdQJqZnRwul9c4/cvAKWZ2/tEJZnaamXULH3YkGDKSOKPCL/mZBVQws8UE47xfFmIdXxP0mk8Cprh79mEe3P09ggLyRThMNJm8i/pRzwNP5vxyN+c8gqGL4cADZraAoPNlQY8mWkpwHdzFBOP1T3hwuclI13s38LqZzQe2Z5v+JjD06Je7wNPA6eH6+vO/e/nZPQMsAr4Nv3B9itw/ub9HMKwGwXcwy8PnvQB8AXCM3+N64OnwdawO7Mm5AXc/SDAEdHP4pfgi4DfAtnCRMwiO+pE4o+6cInkIh5tmunu3Yywad8ysF/AHd7+ykM+v4eG1dc1sNNDU3W8pwPMrAx8RXFEq41jLS/HSGL9IKeTu35rZh2ZW3gt3LP/5ZjaGoEasJThCqyASgNEq+vFJe/wiImWMxvhFRMoYFX4RkTJGhV9EpIxR4RcRKWNU+EVEypj/D/f5fFnE8S4BAAAAAElFTkSuQmCC\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX4AAAEHCAYAAACp9y31AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3dd5xU9fX/8deh997L0otUhQVBjCJqxIYiaGKMPaJJfsbEb6QoUYlGsURjigWNirEiRRAVRRR7A5Xdpfe69Lb0Lef3x73Ezbq7zC47O7O77+fjMY+duffOve8dhrN3PnPvuebuiIhI2VEu1gFERKR4qfCLiJQxKvwiImWMCr+ISBmjwi8iUsao8IuIlDEVorlyM/sD8CvAgWTgWqAp8CpQH5gPXOnuR/JbT4MGDbx169bRjCoiUurMnz9/u7s3zDndonUcv5k1Bz4Furj7QTObBLwNnAdMdfdXzexJYIG7P5HfuhITE33evHlRySkiUlqZ2Xx3T8w5PdpDPRWAqmZWAagGpAKDgMnh/InAxVHOICIi2USt8Lv7RuBhYB1Bwd9DMLSz290zwsU2AM1ze76ZjTCzeWY2b9u2bdGKKSJS5kSt8JtZXeAioA3QDKgODI70+e4+wd0T3T2xYcMfDVGJiEghRXOo5yxgtbtvc/d0YCowAKgTDv0AtAA2RjGDiIjkEM3Cvw7oZ2bVzMyAM4FFwIfA8HCZq4HpUcwgIiI5RHOM/yuCL3G/JTiUsxwwARgF3GpmKwgO6fx3tDKIiMiPRfU4fne/C7grx+RVQN9obldERPKmM3dFROLQxt0HGffmQjIys4p83VHd4xcRkYLJynJe+mot499ZggNDT2pOjxZ1inQbKvwiInFi5bZ9jJ6SxDdrdvGTDg24b2h3WtarVuTbUeEXEYmx9Mwsnv5kFX97fzlVKpTjoeE9GN67BcEBkUVPhV9EJIZSNu5h1JQkFm7ay+CuTfjzxV1pVLNKVLepwi8iEgOH0jP5xwfLefKjVdStVoknrujFud2bFsu2VfhFRIrZvDU7GTkliVXb9jO8dwvGnn8CdapVKrbtq/CLiBST/YczeOjdpUz8Yg3Naldl4nV9Ob1j8fciU+EXESkGHy3bxu1Tk9m05yBX9WvFyMGdqV45NiVYhV9EJIp2HzjCPTMXM+XbDbRtWJ3Xb+xPYut6Mc2kwi8iEiWzUlIZ+8ZCdh04wm/PaMfNgzpQpWL5WMdS4RcRKWpb0w5x1/SFvJOyma7NajHxuj50bVY71rH+S4VfRKSIuDuT52/g3rcWczA9k5GDOzHiJ22pUD6+2qKp8IuIFIH1Ow9w+7RkPlm+nT6t6zJ+WA/aNawR61i5UuEXETkOWVnOC1+s4cF3l2LAPRd15YqTW1GuXHTaLRQFFX4RkUJasXUfo6YkMX/tLk7v2JD7LulO8zpVYx3rmFT4RUQKKD0ziwkfr+Kx95dTrXJ5HrmsJ0NPah61pmpFTYVfRKQAUjbuYeTkJBal7uX87k25e0hXGtasHOtYBaLCLyISgUPpmTw2ZzkTPl5FveqVeOrK3pzTtUmsYxVK1Aq/mXUCXss2qS1wJ/BCOL01sAa4zN13RSuHiMjx+nr1TkZPSWLV9v38LLElt593ArWrVYx1rEKLWuF396XAiQBmVh7YCEwDRgNz3H28mY0OH4+KVg4RkcLadziDB95Zwn++XEuLulV58fqTObVDg1jHOm7FNdRzJrDS3dea2UXAwHD6RGAuKvwiEmc+XLqVO6Ymk7r3ENcOaM1t53SiWqXSMTpeXL/Fz4FXwvuN3T01vL8ZaFxMGUREjmnX/iPcM3MRU7/bSPtGNZh80yn0blU31rGKVNQLv5lVAoYAY3LOc3c3M8/jeSOAEQAJCQlRzSgi4u68lZzKXdMXsudgOr8b1J7fDmpP5Qqxb6pW1Ipjj/9c4Ft33xI+3mJmTd091cyaAltze5K7TwAmACQmJub6x0FEpChs2XuIP72RwnuLttC9eW1e/NXJnNC0VqxjRU1xFP7L+WGYB2AGcDUwPvw5vRgyiIj8iLszad567n1rMUcyshhzbmeuP7VN3DVVK2pRLfxmVh04G7gx2+TxwCQzux5YC1wWzQwiIrlZt+MAY6Yl8dmKHfRtU48HhvWgTYPqsY5VLKJa+N19P1A/x7QdBEf5iIgUu8ws5/nP1/Dwu0spX8649+Ju/KJvQlw3VStqpePYJBGRCCzfksbIKUl8t243Z3RqyF+GdqdZCWiqVtRU+EWk1DuSkcWTH63knx+soHrl8jz28xMZ0rNZiWmqVtRU+EWkVFuwfjejpiSxZHMaF/Zsxt0XdqF+jZLVVK2oqfCLSKl08Egmj76/jGc+WUXDmpV5+qpEzu6i80VBhV9ESqEvVu5gzNQk1uw4wOV9WzLmvBOoVaXkNlUrair8IlJq7D2Uzvh3lvDyV+toVb8aL99wMqe0K/lN1YqaCr+IlApzFm/hjmkpbE07xA0/acOtZ3eiaqXS126hKKjwi0iJtmPfYca9uYgZCzbRsXENnrxyACe2rBPrWHFNhV9ESiR3Z8aCTYx7cxFph9L5/Vkd+M3A9lSqULrbLRQFFX4RKXFS9xxk7LQU5izZSs+WdXhwWA86NakZ61glhgq/iJQYWVnOq9+s5/63F5OelcXY80/g2gFtKF+G2i0UBRV+ESkR1mzfz+ipSXy5aif929Zn/LDutKpfNpqqFTUVfhGJa5lZzrOfruavs5dSsVw5xl/SnZ/1aVlm2y0UBRV+EYlbSzenMXLyAhZs2MNZJzTi3ou706R2lVjHKvFU+EUk7hzJyOJfH67g8bkrqFmlIn+//CQu7NFUe/lFRIVfROLK9+t3M3LyApZt2cfFJzbjzgu7Uq96pVjHKlVU+EUkLhw4ksEj7y3j2c9W07hWFZ69JpFBndVULRpU+EUk5j5fsZ3RU5NZt/MAv+yXwKjBnamppmpRo8IvIjGz52A697+9mFe/WU/r+tV4dUQ/+rWtf+wnynFR4ReRmJi9aAtj30hmW9phbjy9LX84qyNVKqqpWnGIauE3szrAM0A3wIHrgKXAa0BrYA1wmbvvimYOEYkf2/cd5u4ZC5mZlErnJjV5+qpEerRQU7XiFO09/seAWe4+3MwqAdWA24E57j7ezEYDo4FRUc4hIjHm7kz/fhPj3lzIvsMZ3Hp2R246vZ2aqsVA1Aq/mdUGTgOuAXD3I8ARM7sIGBguNhGYiwq/SKm2afdBxr6RwgdLtnJSQtBUrUNjNVWLlWMWfjNrBAwAmgEHgRRgnrtnHeOpbYBtwHNm1hOYD9wCNHb31HCZzUCux2uZ2QhgBEBCQsKxfxMRiTtZWc7LX69j/DtLyMxy7rygC1ef0lpN1WIsz8JvZmcQDMPUA74DtgJVgIuBdmY2Gfiru+/NZ929gJvd/Sszeyxc33+5u5uZ5/Zkd58ATABITEzMdRkRiV+rt+9n9JQkvlq9kwHt63P/0B4k1K8W61hC/nv85wE3uPu6nDPMrAJwAXA2MCWP528ANrj7V+HjyQSFf4uZNXX3VDNrSvAHRURKiYzMLP796Woemb2MShXK8eCwHlya2ELtFuJInoXf3W/LZ14G8EZ+K3b3zWa23sw6uftS4ExgUXi7Ghgf/pxemOAiEn8Wp+5l1JQkkjbs4addGnPPxd1oXEtN1eJNfkM9twJ73P3fOaZfD9R0979FsP6bgZfCI3pWAdcC5YBJ4XrWApcVNryIxIfDGZn864MVPD53JXWqVeRfv+jFed2baC8/TuU31HMF0C+X6f8B5gHHLPzu/j2QmMusMyNKJyJxb/7aXYyaksSKrfu45KTm/OmCLtRVU7W4ll/hr+Du6TknuvsR059xkTLvwJEMHnp3Kc9/voamtarw3LV9OKNTo1jHkgjkV/jLmVljd9+SfaKZqV2eSBn36fLtjJ6axIZdB7mqfytGDu5MjcrqAFNS5Pcv9RDwlpn9H/BtOK13OP3haAcTkfiz50A69761iNfnb6BNg+pMurE/fdvUi3UsKaD8jup5wcy2AX/mh147C4E73f2dYsonInFiVspm/jQ9hZ37j3DT6e34/Vkd1FSthMr3s1lY4FXkRcqwbWlBU7W3klPp0rQWz13Th27Na8c6lhwHDcqJSK7cnWnfbeTPMxdx4HAmt53TiRGntaVieTVVK+lU+EXkRzbsOsAd01L4aNk2ereqywPDetC+UY1Yx5IiosIvIv+VleW8+NVaHnhnCQ6MG9KVK/u1opyaqpUqkXTnvDWXyXuA+eEJWiJSCqzcto/RU5L4Zs0uTuvYkPuGdqNFXTVVK40i2eNPDG9vho8vAJKAm8zsdXd/MFrhRCT60jOzmPDxKh6bs5yqFcvz8KU9GdarudotlGKRFP4WQC933wdgZncBbxFcZGU+oMIvUkKlbNzDqClJLNy0l3O7NWHcRV1pVFNN1Uq7SAp/I+BwtsfpBBdTOWhmh/N4jojEsUPpmfx9znKe+ngVdatV4okrenFu96axjiXFJJLC/xLwlZkdbZ98IfCymVUnaLEsIiXIvDU7GTkliVXb9nNp7xaMPb8LtatVjHUsKUbHLPzufo+ZvUNw+UWAm9x9Xnj/iqglE5Eite9wBg/NWsILX66lWe2qvHBdX07r2DDWsSQGIj2cswqw192fM7OGZtbG3VdHM5iIFJ2Plm3j9qnJbNpzkKv7t+a2czpRXU3VyqxIDue8i+Conk7Ac0BF4EV++AQgInFq94Ej3DNzMVO+3UC7htWZfFN/erdSU7WyLpI/+UOBkwg7dLr7JjOrGdVUInLc3k5O5c7pKew+kM7/O6M9/29QezVVEyCywn/E3d3MHCD8UldE4tTWvYe4c/pCZi3cTLfmtZh4XV+6NlNTNflBJIV/kpk9BdQxsxuA64CnoxtLRArK3Xl9/gbunbmIQxlZjBrcmRt+0oYKaqomOURyVM/DZnY2sJdgnP9Od58dycrNbA2QBmQCGe6eaGb1gNeA1sAa4DJ331Wo9CICwPqdB7h9WjKfLN9O39b1GD+sO20bqqma5C6ir/XDQh9Rsc/FGe6+Pdvj0cAcdx9vZqPDx6MKuW6RMi0zy3nhizU89O5SDLjnoq5ccbKaqkn+8iz8ZpZGcNWtXLl7rUJu8yJgYHh/IjAXFX6RAluxNY1RU5KZv3YXp3dsyH2XdKd5naqxjiUlQH6XXqwJYGb3AKnAfwAjOGkr0nO7HXgv/GL4KXefQNDuITWcvxnI9eLtZjYCGAGQkJAQ4eZESr/0zCye+mglf5+zgmqVy/PIZT0ZepKaqknkIhnqGeLuPbM9fsLMFgB3RvDcU919o5k1Amab2ZLsM7MfLZRT+EdiAkBiYmKenzxEypKUjXu4bXISi1P3cn73ptw9pCsNa1aOdSwpYSIp/PvN7ArgVYI9+MuB/ZGs3N03hj+3mtk0oC+wxcyaunuqmTUFthYuukjZcSg9k7+9v5ynP1lF/eqVeOrK3pzTtUmsY0kJFclxXr8ALgO2hLdLw2n5MrPqR0/0Co/9/ymQAswArg4XuxqYnvsaRATg69U7Oe+xT3jyo5UM79WC2beerqIvxyWSwznXEHwhW1CNgWnhuGMF4GV3n2Vm3xCcG3A9sJbgj4qI5JB2KJ0HZy3lP1+upWW9qrz0q5MZ0L5BrGNJKZDfUT1jgcfdfWce8wcB1dx9Zm7z3X0V0DOX6TuAMwsXV6Rs+HDpVu6Ymkzq3kNcN6ANfzynI9UqqamaFI383knJwJtmdoigT882gi6dHYATgfeB+6KeUKQM2bX/CPfMXMTU7zbSoVENpvz6FHol1I11LCll8jucczow3cw6EHTibEpw9u6LwAh3P1g8EUVKP3fnreRU7pq+kD0H0/ndoPb8dlB7KldQUzUpepGM8S8HlhdDFpEyacveQ4x9I4XZi7bQvXltXvzVyZzQtLDnR4ocmwYNRWLE3Zk0bz33vrWYIxlZjDm3M9efqqZqEn0q/CIxsG7HAUZPTeLzlTs4uU09xg/rQZsG6nguxUOFX6QYZWY5z3++hoffXUr5csZfhnbj8j4JaqomxSqSSy92BJ4g6LHTzcx6ELRxuDfq6URKkWVb0hg5OYnv1+9mUOdG/GVoN5rWVlM1KX6R7PE/DdwGPAXg7klm9jKgwi8SgSMZWTz50Ur+8cFyalSuwGM/P5EhPZupqZrETCSFv5q7f53jTZoRpTwipUrSht2MnJzEks1pDOnZjLsu7EL9GmqqJrEVSeHfbmbtCHvzm9lwgjbNIpKHg0cyefT9ZTzzySoa1azCM1clclaXXDuQixS7SAr/bwnaI3c2s43AaoKe/CKSiy9W7mDM1CTW7DjA5X0TGHNeZ2pVqRjrWCL/lW/hN7PywG/c/ayww2Y5d08rnmgiJUvaoXTuf2cJL3+1jlb1q/HyDSdzSjs1VZP4k2/hd/dMMzs1vB9RD36RsmjO4i3cMS2FrWmHuOEnbbj17E5UraR2CxKfIhnq+c7MZgCvk+0CLO4+NWqpREqIHfsOM+7NRcxYsIlOjWvy5JW9ObFlnVjHEslXJIW/CrADGJRtmgMq/FJmuTszFmxi3JuLSDuUzu/P6sBvBranUgW1W5D4F0mTtmuLI4hISZG65yBjp6UwZ8lWerasw4PDetCpSc1YxxKJWCRn7j5HeChndu5+XVQSicSprCznlW/Wcf/bS8jIymLs+Sdw7YA2lFe7BSlhIhnqyX6FrSrAUGBTdOKIxKc12/czemoSX67aySnt6nP/Jd1pVV9N1aRkimSoZ0r2x2b2CvBp1BKJxJGMzCye/Ww1f31vGZXKl2P8Jd35WZ+WarcgJVphunN2ABpFunB4LsA8YKO7X2BmbYBXgfrAfOBKdz9SiBwiUbVk815GTU5iwYY9nHVCY+69uBtNaleJdSyR4xbJGH8a/zvGvxkYVYBt3AIsBo5eUugB4FF3f9XMngSuJ+j+KRIXDmdk8viHK3l87gpqVanIPy4/iQt6NNVevpQakQz1FPpwBTNrAZwP/AW41YL/OYOAX4SLTATuRoVf4sR363YxakoSy7bs4+ITm3HnhV2pV71SrGOJFKlI9vjnuPuZx5qWh78BI4GjfzzqA7vd/Wh3zw1A8zy2OwIYAZCQkBDBpkQK78CRDP763jKe/Ww1TWpV4blr+nBG54hHNEVKlDwLv5lVAaoBDcysLnD0c24t8ijWOZ5/AbDV3eeb2cCCBnP3CQTN4UhMTPzR4aQiReXzFdsZPTWZdTsP8Mt+CYwa3JmaaqompVh+e/w3Ar8HmhF8CXu08O8F/hnBugcAQ8zsPILDQGsBjwF1zKxCuNffAthYyOwix2XPwXTuf3sxr36znjYNqvPqiH70a1s/1rFEoi7Pwu/ujwGPmdnN7v6Pgq7Y3ccAYwDCPf4/uvsVZvY6MJzgyJ6rgemFCS5yPGYv2sLYN5LZlnaYG09vyx/O6kiVimqqJmVDJF/u/sPMugFdCPbcj05/oZDbHAW8amb3At8B/y7kekQKbPu+w9w9YyEzk1Lp3KQmT1+VSI8WaqomZUskX+7eBQwkKPxvA+cSnMAVceF397nA3PD+KqBvgZOKHAd3543vNzLuzUUcOJzJ/53dkRtPb6emalImRXIC13CgJ/Cdu19rZo2BF6MbS6TobNp9kDumJfPh0m2clBA0VevQWE3VpOyKpPAfdPcsM8sws1rAVqBllHOJHLesLOelr9fxwDtLyMxy7rygC1ef0lpN1aTMi6TwzzOzOsDTBEf37AO+iGoqkeO0ats+Rk9N5uvVOzm1fQPuv6Q7LetVi3UskbhwrGvuGnC/u+8GnjSzWUAtd08qlnQiBZSRmcUzn67m0dnLqFShHA8O68GliS3UbkEkm2Ndc9fN7G2ge/h4TXGEEimMRZv2MnLKAlI27uWnXRpzz8XdaFxLTdVEcopkqOdbM+vj7t9EPY1IIRzOyOSfH6zgibkrqVOtIo9f0YtzuzXRXr5IHiIp/CcDV5jZWoKLrRvBh4EeUU0mEoH5a4Omaiu27uOSXs350/ldqKumaiL5iqTwnxP1FCIFtP9wBg+/t5TnP19Ds9pVef7aPgzspKZqIpGI5MzdtWZ2KtDB3Z8zs4ZAjehHE8ndJ8u3MWZqMht2HeSq/q0YObgzNSoX5ppCImVTpGfuJgKdgOeAigQncA2IbjSR/7XnQDp/eXsRk+ZtoE2D6ky6sT9929SLdSyREieS3aShwEnAtwDuvsnMdNqjFKtZKZv50/QUdu4/wq8HtuOWMzuoqZpIIUVS+I+Eh3U6gJlVj3Imkf/alnaYu2ak8HbyZk5oWovnrulDt+a1Yx1LpESLpPBPMrOnCPro3wBcR3AWr0jUuDtTvt3IPTMXcTA9k9vO6cSI09pSsbyaqokcr0i+3H3YzM4muABLR+BOd58d9WRSZm3YdYDbp6Xw8bJt9G5VlweG9aB9Ix1PIFJUIj0UIhmoCnh4X6TIZWU5//lyLQ/MWgLAuCFdubJfK8qpqZpIkYrkqJ5fAXcCHxCcvPUPM/uzuz8b7XBSdqzcto9Rk5OYt3YXp3VsyH1Du9GirpqqiURDJHv8twEnufsOADOrD3wOqPDLcUvPzGLCx6t4bM5yqlYsz8OX9mRYr+ZqtyASRZEU/h1AWrbHaeE0keOSsnEPIycnsSh1L+d1b8LdQ7rSqKaaqolEWySFfwXwlZlNJxjjvwhIMrNbAdz9kSjmk1LoUHomj81ZzoSPV1G3WiWe/GUvBndrGutYImVGJIV/ZXg7anr4M9+TuMysCvAxUDnczmR3v8vM2gCvAvUJLuxypbsfKWhwKZm+WbOTUZOTWLV9P5f2bsHY87tQu1rFWMcSKVMiOZxzXCHXfRgY5O77zKwi8KmZvQPcCjzq7q+a2ZPA9cAThdyGlBD7Dmfw4KwlvPDFWprXqcoL1/XltI4NYx1LpEyK5KieROAOoFX25Y/VltndneAyjRD096lIMFQ0CPhFOH0icDcq/KXaR8u2cfvUZDbtOcg1p7TmtnM6UV1N1URiJpL/fS8RHNmTDGQVZOVmVp5gOKc98C+CIaPd7p4RLrIBaF6QdUrJsfvAEf48cxFTv91Iu4bVmXxTf3q3UlM1kViLpPBvc/cZhVm5u2cCJ4YXa58GdI70uWY2AhgBkJCQUJjNSwy9nZzKndMXsvvAEW4e1J7fntFeTdVE4kQkhf8uM3sGmEMwbg+Au0+NdCPuvtvMPgT6E/T8qRDu9bcANubxnAnABIDExESPdFsSW1v3HuLO6QuZtXAz3ZrX4oXr+tKlWa1YxxKRbCIp/NcS7KlX5IehHgfyLfzhBVvSw6JfFTgbeAD4EBhOcGTP1fxwlJCUYO7O6/M3cO/MRRzOyGL0uZ351altqKCmaiJxJ5LC38fdOxVi3U2BieE4fzlgkrvPNLNFwKtmdi/wHfDvQqxb4sj6nQe4fVoynyzfTt/W9Rg/rDttG6qpmki8iqTwf25mXdx9UUFW7O5JBBdwyTl9FdC3IOuS+JSZ5bzwxRoenLWU8uWMey7uxhV9E9RUTSTORVL4+wHfm9lqgjF+IzhaM9/DOaV0W7E1jZGTk/h23W4GdmrIfUO706xO1VjHEpEIRFL4B0c9hZQY6ZlZPPXRSv4+ZwXVKpfn0Z/15OIT1VRNpCSJ5MzdtWZ2KtDB3Z8Lv7TVAG4ZlLxhD7dNXsCSzWmc36Mp44Z0pUGNyrGOJSIFFMmZu3cBiUAn4DmCo3teBAZEN5rEi0PpmTz6/jKe/ngVDWpU5qkre3NO1yaxjiUihRTJUM9Qgi9pvwVw901mlm+DNik9vlq1g9FTk1m9fT8/79OSMeedQO2qaqomUpJFUviPuLubmQOYWfUoZ5I4kHYonQdmLeHFL9fRsl5VXvrVyQxo3yDWsUSkCERS+CeZ2VMEZ9zeAFwHPBPdWBJLHy7Zyh3Tkknde4jrBrThj+d0pFolNVUTKS0i+XL3YTM7G9hLMM5/p7vPjnoyKXY79x/hnpmLmPbdRjo0qsGUX59Cr4S6sY4lIkUski93H3D3UcDsXKZJKeDuzExK5e4ZC9lzMJ1bzuzAb85oR+UKaqomUhpF0kjl7FymnVvUQSQ2tuw9xIj/zOfmV76jed2qzPzdqfzh7I4q+iKlWJ57/Gb2a+A3QFszS8o2qybwWbSDSXS5O699s56/vL2YIxlZ3HHeCVw7oLWaqomUAfkN9bwMvAPcD4zONj3N3XdGNZVE1dod+xkzNZnPV+6gX9t6jL+kB60b6GAtkbIiz8Lv7nuAPcDlxRdHoikzy3nus9U8/N5SKpYrx31Du/PzPi3VVE2kjNExemXE0s1pjJySxIL1uzmzcyPuHdqNprXVVE2kLFLhL+WOZGTx+NwV/OvDFdSsUpHHfn4iQ3o2U1M1kTJMhb8UW7B+NyMnJ7F0SxpDejbjrgu7UF9N1UTKPBX+UujgkUwemb2Uf3+6mkY1q/DMVYmc1aVxrGOJSJxQ4S9lvli5g9FTk1i74wC/ODmB0ed2plYVNVUTkR+o8JcSew+lc//bS3jl63W0ql+Nl284mVPaqamaiPyYCn8pMGfxFu6YlsLWtEOMOK0tfzirI1Ur6cxbEcld1Aq/mbUEXgAaAw5McPfHzKwe8BrQGlgDXObuu6KVozTbse8w495cxIwFm+jcpCZPXdmbni3rxDqWiMS5aO7xZwD/5+7fhhdumW9ms4FrgDnuPt7MRhOcFayGbwXg7sxYsIlxby4i7VA6fzirI78e2I5KFdRuQUSOLWqF391TgdTwfpqZLQaaAxcBA8PFJgJzUeGPWOqeg4ydlsKcJVs5sWUdHhzeg46NdUE0EYlcsYzxm1lrgss3fgU0Dv8oAGwmGArK7TkjgBEACQkJ0Q8Z57KynFe+Wcf9by8hIyuLseefwLUD2lBe7RZEpICiXvjNrAYwBfi9u+/NfsZo9ks65uTuE4AJAImJibkuU1as2b6f0VOT+HLVTk5pV5/xl/QgoX61WMcSkRIqqoXfzCoSFP2X3H1qOHmLmTV191QzawpsjWaGkiwjM4tnP1vNX99bRqUK5XhgWHcuS2ypdgsiclyieVSPAf8GFrv7I9lmzQCuBsaHP6dHK0NJtjh1L6OmJOLD7ioAAA68SURBVJG0YQ9nndCYey/uRpPaVWIdS0RKgWju8Q8ArgSSzez7cNrtBAV/kpldD6wFLotihhLncEYm//pwJY9/uILaVSvyz1+cxPndm2ovX0SKTDSP6vkUyKtanRmt7ZZk363bxcjJSSzfuo+hJzXnzgu6ULd6pVjHEpFSRmfuxoEDRzL463vLeO6z1TSuVYXnrunDGZ0bxTqWiJRSKvwx9tmK7YyemsT6nQf5Zb8ERg3uTE01VRORKFLhj5E9B9O5763FvDZvPW0aVOe1Ef04uW39WMcSkTJAhT8G3lu4mbFvpLB932FuPK0tfzi7I1UqqqmaiBQPFf5itC3tMHe/uZC3klLp3KQmz1ydSI8WaqomIsVLhb8YuDtvfL+RcW8u4sDhTP74047ceHo7KpZXUzURKX4q/FG2afdB7piWzIdLt9EroQ4PDOtBBzVVE5EYUuGPkqws56Wv1jL+nSU4cPeFXbiyf2s1VRORmFPhj4JV2/YxekoyX6/ZyU86NOC+od1pWU9N1UQkPqjwF6GMzCye+XQ1j85eRuUK5XhoeA+G926hdgsiEldU+IvIok17GTllASkb93JO18bcc1E3GtVSUzURiT8q/MfpcEYm//xgBU/MXUmdapV44openNu9aaxjiYjkSYX/OMxfu5ORk5NYuW0/w3q14E8XnECdamqqJiLxTYW/EPYfzuChd5cy8Ys1NKtdlYnX9eX0jg1jHUtEJCIq/AX08bJt3D4tmY27D3JVv1bcNrgzNSrrZRSRkkMVK0J7DqRzz1uLmDx/A20bVmfSjf3p07perGOJiBSYCn8EZqWk8qfpC9m5/wi/GdiO353ZQU3VRKTEUuHPx9a0Q9w1fSHvpGymS9NaPHdNH7o1rx3rWCIix0WFPxfuzpRvN3LPzEUcTM/ktnM6MeK0tmqqJiKlQtQKv5k9C1wAbHX3buG0esBrQGtgDXCZu++KVobCWL/zALdPS+aT5dtJbFWX8cN60L5RjVjHEhEpMtHchX0eGJxj2mhgjrt3AOaEj+NCVpYz8fM1nPO3j5m/dhfjhnRl0o39VfRFpNSJ2h6/u39sZq1zTL4IGBjenwjMBUZFK0OkVm7bx6jJScxbu4vTOjbkvqHdaFFXTdVEpHQq7jH+xu6eGt7fDDTOa0EzGwGMAEhISIhKmPTMLCZ8vIrH5iynasXyPHxpT4b1aq6maiJSqsXsy113dzPzfOZPACYAJCYm5rlcYaVs3MOoKUks3LSXc7s1YdxFXWlUU03VRKT0K+7Cv8XMmrp7qpk1BbYW8/Y5lJ7J3+cs56mPV1GveiWe/GUvBndTUzURKTuKu/DPAK4Gxoc/pxfnxuet2cnIKUms2rafS3u3YOz5XahdrWJxRhARibloHs75CsEXuQ3MbANwF0HBn2Rm1wNrgcuitf3s9h3O4MFZS3jhi7W0qFuV/1zfl590UFM1ESmbonlUz+V5zDozWtvMzdylW7ljWgqb9hzkmlNac9s5naiupmoiUoaV6go4Zmoyr3y9jnYNqzP5pv70bqWmaiIipbrwt65fjZsHtee3Z7RXUzURkVCpLvw3nt4u1hFEROKOuo6JiJQxKvwiImWMCr+ISBmjwi8iUsao8IuIlDEq/CIiZYwKv4hIGaPCLyJSxph7kbe6L3Jmto2gqVthNAC2F2GcaCtJeZU1ekpS3pKUFUpW3uPN2srdf9SRskQU/uNhZvPcPTHWOSJVkvIqa/SUpLwlKSuUrLzRyqqhHhGRMkaFX0SkjCkLhX9CrAMUUEnKq6zRU5LylqSsULLyRiVrqR/jFxGR/1UW9vhFRCQbFX4RkTKm1BZ+M3vIzJaYWZKZTTOzOtnmjTGzFWa21MzOiWXOMM+lZrbQzLLMLDHHvLjKepSZDQ4zrTCz0bHOk52ZPWtmW80sJdu0emY228yWhz/rxjLjUWbW0sw+NLNF4XvglnB6vOatYmZfm9mCMO+4cHobM/sqfD+8ZmaVYp31KDMrb2bfmdnM8HE8Z11jZslm9r2ZzQunFfl7odQWfmA20M3dewDLgDEAZtYF+DnQFRgMPG5msb4uYwpwCfBx9olxmpUww7+Ac4EuwOVh1njxPMHrld1oYI67dwDmhI/jQQbwf+7eBegH/DZ8LeM172FgkLv3BE4EBptZP+AB4FF3bw/sAq6PYcacbgEWZ3scz1kBznD3E7Mdv1/k74VSW/jd/T13zwgffgm0CO9fBLzq7ofdfTWwAugbi4xHuftid1+ay6y4yxrqC6xw91XufgR4lSBrXHD3j4GdOSZfBEwM708ELi7WUHlw91R3/za8n0ZQoJoTv3nd3feFDyuGNwcGAZPD6XGT18xaAOcDz4SPjTjNmo8ify+U2sKfw3XAO+H95sD6bPM2hNPiUbxmjddc+Wns7qnh/c1A41iGyY2ZtQZOAr4ijvOGQyffA1sJPlmvBHZn29GKp/fD34CRQFb4uD7xmxWCP6Lvmdl8MxsRTivy90KJvti6mb0PNMll1h3uPj1c5g6Cj9MvFWe2nCLJKsXD3d3M4uo4ZjOrAUwBfu/ue4Md00C85XX3TODE8HuzaUDnGEfKlZldAGx19/lmNjDWeSJ0qrtvNLNGwGwzW5J9ZlG9F0p04Xf3s/Kbb2bXABcAZ/oPJyxsBFpmW6xFOC2qjpU1DzHJGoF4zZWfLWbW1N1Tzawpwd5qXDCzigRF/yV3nxpOjtu8R7n7bjP7EOgP1DGzCuGedLy8HwYAQ8zsPKAKUAt4jPjMCoC7bwx/bjWzaQTDqkX+Xii1Qz1mNpjgI94Qdz+QbdYM4OdmVtnM2gAdgK9jkTEC8Zr1G6BDeHREJYIvoGfEONOxzACuDu9fDcTFp6xwzPnfwGJ3fyTbrHjN2/DoEXJmVhU4m+B7iQ+B4eFicZHX3ce4ewt3b03wHv3A3a8gDrMCmFl1M6t59D7wU4IDP4r+veDupfJG8EXoeuD78PZktnl3EIxLLgXOjYOsQwnGGg8DW4B34zVrtlznERwttZJguCrmmbJlewVIBdLD1/V6grHdOcBy4H2gXqxzhllPJRjXTcr2Xj0vjvP2AL4L86YAd4bT2xLslKwAXgcqxzprjtwDgZnxnDXMtSC8LTz6/yoa7wW1bBARKWNK7VCPiIjkToVfRKSMUeEXESljVPhFRMoYFX4RkTJGhV+ixsyuMbN/5jHv8+Ncb7OCzosnZjbQzE6J4vqrmtlHuTX1M7PnzWx4bs8rxHY6mtnbYefIb81skpk1NrPuZvZ8UWxDip4Kv8SEux9P0bsGyKu45zevWJlZfmfGDwQK9BocY305XQdM9aC9QlSYWRXgLeAJd+/g7r2Ax4GG7p4MtDCzhGhtXwpPhV/yZWZvhA2jFmZrGoWZ7bPgmgcLzex9M+trZnPNbJWZDcm2ipbh9OVmdlf252e7f5uZfWPBtROO9ndvbWaLzezpcBvvhXuxw4FE4KWwZ3nVbOv50Twz6x3u+c43s3fDU94JMz1qZvPC7fQxs6lhznuzZVhiZi+Fy0w2s2rhvPzW+zcLeqnfYmYXWtD7/bvwdWpsQTO2m4A/hDl/knMv/OjrE34y+MTMZgCLLGiQ9lC21+vGPP7priA8w9MC/7Tg+gnvA42ybSev36NPuP7vw+2l5LKNXwBfuPubRye4+1x3P7rsmwRnzEq8ifXZarrF943wLEGgKsGZmvXDx054JjFBo673CFr09gS+D6dfQ3AGbf1sz08M5+0Lf/6U4ILSRrAjMhM4DWhN0FzvxHC5ScAvw/tzj64nl7xzs22jIvA5wR4owM+AZ7Mt90B4/xZgE9AUqExwtm/9MIMDA8LlngX+GMF6H8+Wpy4/XNv6V8Bfw/t3A3/MttzzwPBsj4++PgOB/UCb8PEIYGx4vzIw7+i8bM+tBGzO9vgSgi6a5Qk+De0maFmQ3++RAvQP748HUnJ5rR8BbsnnvTMAeDPW72Hdfnwr0U3apFj8zsyGhvdbEvQL2gEcAWaF05OBw+6ebmbJBAXzqNnuvgPAzKYStCiYl23+T8Pbd+HjGuE21gGr3f37cPr8HOuNRCegG0GXQwgKX2q2+Uf7CyUDCz1sfWtmq8LfdTew3t0/C5d7Efhd+Hvnt97Xst1vAbwW7klXAlYX8HcA+NqD6zFA8Fr1yPbpoDbB65V9vQ3C7EedBrziwbDPJjP7IJye6+tjQS+emu7+RbjcywTNDgtqK3Ey7Cb/S4Vf8mRBK9uzCPb8DpjZXIIuhwDpHu7WEfQ6Pwzg7lk5xqJz9gTJ+diA+939qRzbbn10naFMgk8NBfoVCAp6/zzmH11/Fv+7rSx++L+RW/5jrXd/tvv/AB5x9xnh63l3Hs/JIBx6NbNyBH8kclufATe7+7t5rAfgID/8O+Un19/Dsl2m9BgWAqfnM79KmEXijMb4JT+1gV1h0e9McGnAgjrbgmuGViW4ctBnOea/C1xnQT96zKy5Bb3I85MG1Ixg3lKgoZn1D9dd0cy6FjB/wtHnE4xpf1rA9dbmh7a/V2ebnvN3WAP0Du8PIRiGyc27wK8taOV89Kia6tkXcPddQHkLvnyF4JKePwu/H2gKnBFOz/X3cPfdQJqZnRwul9c4/cvAKWZ2/tEJZnaamXULH3YkGDKSOKPCL/mZBVQws8UE47xfFmIdXxP0mk8Cprh79mEe3P09ggLyRThMNJm8i/pRzwNP5vxyN+c8gqGL4cADZraAoPNlQY8mWkpwHdzFBOP1T3hwuclI13s38LqZzQe2Z5v+JjD06Je7wNPA6eH6+vO/e/nZPQMsAr4Nv3B9itw/ub9HMKwGwXcwy8PnvQB8AXCM3+N64OnwdawO7Mm5AXc/SDAEdHP4pfgi4DfAtnCRMwiO+pE4o+6cInkIh5tmunu3Yywad8ysF/AHd7+ykM+v4eG1dc1sNNDU3W8pwPMrAx8RXFEq41jLS/HSGL9IKeTu35rZh2ZW3gt3LP/5ZjaGoEasJThCqyASgNEq+vFJe/wiImWMxvhFRMoYFX4RkTJGhV9EpIxR4RcRKWNU+EVEypj/D/f5fFnE8S4BAAAAAElFTkSuQmCC\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "atemp = np.linspace(-20,50,71)\n", - "temps = pd.Series(pvlib.temperature.sapm_cell(900, atemp, 2, **thermal_params), index=atemp)\n", + "atemp = np.linspace(-20, 50, 71)\n", + "temps = pd.Series(\n", + " pvlib.temperature.sapm_cell(900, atemp, 2, **thermal_params), index=atemp\n", + ")\n", "\n", "temps.plot()\n", - "plt.xlabel('ambient temperature (deg C)')\n", - "plt.ylabel('temperature (deg C)');" + "plt.xlabel(\"ambient temperature (deg C)\")\n", + "plt.ylabel(\"temperature (deg C)\");" ] }, { @@ -331,24 +338,26 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "irrad = np.linspace(0,1000,101)\n", - "temps = pd.Series(pvlib.temperature.sapm_cell(irrad, 20, 2, **thermal_params), index=irrad)\n", + "irrad = np.linspace(0, 1000, 101)\n", + "temps = pd.Series(\n", + " pvlib.temperature.sapm_cell(irrad, 20, 2, **thermal_params), index=irrad\n", + ")\n", "\n", "temps.plot()\n", - "plt.xlabel('incident irradiance (W/m**2)')\n", - "plt.ylabel('temperature (deg C)');" + "plt.xlabel(\"incident irradiance (W/m**2)\")\n", + "plt.ylabel(\"temperature (deg C)\");" ] }, { @@ -364,31 +373,33 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "models = ['open_rack_glass_glass',\n", - " 'close_mount_glass_glass',\n", - " 'open_rack_glass_polymer',\n", - " 'insulated_back_glass_polymer']\n", + "models = [\n", + " \"open_rack_glass_glass\",\n", + " \"close_mount_glass_glass\",\n", + " \"open_rack_glass_polymer\",\n", + " \"insulated_back_glass_polymer\",\n", + "]\n", "\n", "temps = pd.Series(dtype=float)\n", "\n", "for model in models:\n", - " params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][model]\n", + " params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[\"sapm\"][model]\n", " temps[model] = pvlib.temperature.sapm_cell(1000, 20, 5, **params)\n", "\n", - "temps.plot(kind='bar')\n", - "plt.ylabel('temperature (deg C)');" + "temps.plot(kind=\"bar\")\n", + "plt.ylabel(\"temperature (deg C)\");" ] }, { @@ -404,17 +415,17 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": " ABB__MICRO_0_25_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 2.08961 \nPaco 250 \nPdco 259.589 \nVdco 40 \nC0 -4.1e-05 \nC1 -9.1e-05 \nC2 0.000494 \nC3 -0.013171 \nPnt 0.075 \nVdcmax 50 \nIdcmax 6.48972 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_25_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 2.24041 \nPaco 250 \nPdco 259.492 \nVdco 40 \nC0 -3.9e-05 \nC1 -0.000132 \nC2 0.002418 \nC3 -0.014926 \nPnt 0.075 \nVdcmax 50 \nIdcmax 6.4873 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 1.84651 \nPaco 300 \nPdco 311.669 \nVdco 40 \nC0 -3.3e-05 \nC1 -0.000192 \nC2 0.000907 \nC3 -0.031742 \nPnt 0.09 \nVdcmax 50 \nIdcmax 7.79173 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 1.95054 \nPaco 300 \nPdco 311.581 \nVdco 40 \nC0 -3.4e-05 \nC1 -0.000256 \nC2 0.002453 \nC3 -0.028223 \nPnt 0.09 \nVdcmax 50 \nIdcmax 7.78952 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3HV_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 1.76944 \nPaco 300 \nPdco 312.421 \nVdco 45 \nC0 -4.5e-05 \nC1 -0.000196 \nC2 0.001959 \nC3 -0.023725 \nPnt 0.09 \nVdcmax 60 \nIdcmax 6.94269 \nMppt_low 30 \nMppt_high 60 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3HV_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 1.84378 \nPaco 300 \nPdco 312.005 \nVdco 45 \nC0 -3.5e-05 \nC1 -0.000227 \nC2 -0.000526 \nC3 -0.041214 \nPnt 0.09 \nVdcmax 60 \nIdcmax 6.93344 \nMppt_low 30 \nMppt_high 60 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_10_0_I_OUTD_x_US_208_y__208V_ \\\nVac 208 \nPso 46.8638 \nPaco 10000 \nPdco 10488.3 \nVdco 320 \nC0 -2.7759e-06 \nC1 -3.6e-05 \nC2 0.000305 \nC3 -0.002351 \nPnt 0.1 \nVdcmax 416 \nIdcmax 32.776 \nMppt_low 220 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_10_0_I_OUTD_x_US_480_y_z__480V_ \\\nVac 480 \nPso 67.7909 \nPaco 10000 \nPdco 10296 \nVdco 362 \nC0 -1.38839e-06 \nC1 -4.9e-05 \nC2 -0.00052 \nC3 -0.003855 \nPnt 0.4 \nVdcmax 416 \nIdcmax 28.4419 \nMppt_low 220 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_12_0_I_OUTD_x_US_480_y__480V_ \\\nVac 480 \nPso 62.5547 \nPaco 12000 \nPdco 12358.8 \nVdco 370 \nC0 -1.009e-06 \nC1 -5.6e-05 \nC2 -0.001437 \nC3 -0.007112 \nPnt 0.4 \nVdcmax 416 \nIdcmax 33.4022 \nMppt_low 250 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_3_0_OUTD_S_US__208V_ ... Zigor__Sunzet_3_TL_US__240V_ \\\nVac 208 ... 240 \nPso 18.1663 ... 36.0334 \nPaco 3000 ... 3180 \nPdco 3142.3 ... 3315.65 \nVdco 310 ... 375 \nC0 -8.03949e-06 ... -7.98467e-06 \nC1 -1.1e-05 ... -7.5e-05 \nC2 0.000999 ... 0.000544 \nC3 -0.000287 ... -0.000338 \nPnt 0.1 ... 0.954 \nVdcmax 480 ... 400 \nIdcmax 10.1365 ... 8.84174 \nMppt_low 100 ... 100 \nMppt_high 480 ... 400 \nCEC_Date NaN ... NaN \nCEC_Type Utility Interactive ... Utility Interactive \n\n i_Energy__GT260__240V_ iPower__SHO_1_1__120V_ \\\nVac 240 120 \nPso 2.5301 22.0954 \nPaco 230 1100 \nPdco 245.63 1194.09 \nVdco 40 182 \nC0 6.2e-05 -2.1e-05 \nC1 -9.8e-05 5.7e-05 \nC2 0.000231 0.002001 \nC3 0.121032 0.000623 \nPnt 0.069 0.33 \nVdcmax 49 380 \nIdcmax 6.14076 6.56096 \nMppt_low 30 100 \nMppt_high 49 380 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_2_0__240V_ iPower__SHO_2_5__240V_ \\\nVac 240 240 \nPso 24.4658 42.7765 \nPaco 2000 2500 \nPdco 2161.88 2632.84 \nVdco 199 218 \nC0 -1.3e-05 -1.4e-05 \nC1 5.5e-05 6.1e-05 \nC2 0.001703 0.002053 \nC3 0.000315 0.00153 \nPnt 0.6 0.75 \nVdcmax 380 400 \nIdcmax 10.8637 12.0772 \nMppt_low 100 100 \nMppt_high 380 400 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_3_0__240V_ iPower__SHO_3_5__240V_ \\\nVac 240 240 \nPso 31.682 64.7742 \nPaco 3000 3500 \nPdco 3205.93 3641.84 \nVdco 222.5 263 \nC0 -8.21046e-06 -9.08073e-06 \nC1 3.6e-05 3.5e-05 \nC2 0.001708 0.001417 \nC3 0.00086 0.001218 \nPnt 0.9 1.05 \nVdcmax 380 400 \nIdcmax 14.4087 13.8473 \nMppt_low 100 100 \nMppt_high 380 400 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_4_6__208V_ iPower__SHO_4_8__240V_ iPower__SHO_5_2__240V_ \nVac 208 240 240 \nPso 54.5701 85.1457 62.4867 \nPaco 4600 4800 5200 \nPdco 4797.81 4968.03 5382.86 \nVdco 254 263 280 \nC0 -5.99928e-06 -6.16035e-06 -4.63524e-06 \nC1 2.8e-05 3.4e-05 4.4e-05 \nC2 0.001381 0.000586 0.00126 \nC3 0.000889 0.000195 0.000367 \nPnt 1.38 1.44 1.56 \nVdcmax 400 400 400 \nIdcmax 18.889 18.8898 19.2245 \nMppt_low 100 100 240 \nMppt_high 400 400 400 \nCEC_Date NaN NaN NaN \nCEC_Type Utility Interactive Utility Interactive Utility Interactive \n\n[16 rows x 3264 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
ABB__MICRO_0_25_I_OUTD_US_208__208V_ABB__MICRO_0_25_I_OUTD_US_240__240V_ABB__MICRO_0_3_I_OUTD_US_208__208V_ABB__MICRO_0_3_I_OUTD_US_240__240V_ABB__MICRO_0_3HV_I_OUTD_US_208__208V_ABB__MICRO_0_3HV_I_OUTD_US_240__240V_ABB__PVI_10_0_I_OUTD_x_US_208_y__208V_ABB__PVI_10_0_I_OUTD_x_US_480_y_z__480V_ABB__PVI_12_0_I_OUTD_x_US_480_y__480V_ABB__PVI_3_0_OUTD_S_US__208V_...Zigor__Sunzet_3_TL_US__240V_i_Energy__GT260__240V_iPower__SHO_1_1__120V_iPower__SHO_2_0__240V_iPower__SHO_2_5__240V_iPower__SHO_3_0__240V_iPower__SHO_3_5__240V_iPower__SHO_4_6__208V_iPower__SHO_4_8__240V_iPower__SHO_5_2__240V_
Vac208240208240208240208480480208...240240120240240240240208240240
Pso2.089612.240411.846511.950541.769441.8437846.863867.790962.554718.1663...36.03342.530122.095424.465842.776531.68264.774254.570185.145762.4867
Paco2502503003003003001000010000120003000...318023011002000250030003500460048005200
Pdco259.589259.492311.669311.581312.421312.00510488.31029612358.83142.3...3315.65245.631194.092161.882632.843205.933641.844797.814968.035382.86
Vdco404040404545320362370310...37540182199218222.5263254263280
C0-4.1e-05-3.9e-05-3.3e-05-3.4e-05-4.5e-05-3.5e-05-2.7759e-06-1.38839e-06-1.009e-06-8.03949e-06...-7.98467e-066.2e-05-2.1e-05-1.3e-05-1.4e-05-8.21046e-06-9.08073e-06-5.99928e-06-6.16035e-06-4.63524e-06
C1-9.1e-05-0.000132-0.000192-0.000256-0.000196-0.000227-3.6e-05-4.9e-05-5.6e-05-1.1e-05...-7.5e-05-9.8e-055.7e-055.5e-056.1e-053.6e-053.5e-052.8e-053.4e-054.4e-05
C20.0004940.0024180.0009070.0024530.001959-0.0005260.000305-0.00052-0.0014370.000999...0.0005440.0002310.0020010.0017030.0020530.0017080.0014170.0013810.0005860.00126
C3-0.013171-0.014926-0.031742-0.028223-0.023725-0.041214-0.002351-0.003855-0.007112-0.000287...-0.0003380.1210320.0006230.0003150.001530.000860.0012180.0008890.0001950.000367
Pnt0.0750.0750.090.090.090.090.10.40.40.1...0.9540.0690.330.60.750.91.051.381.441.56
Vdcmax505050506060416416416480...40049380380400380400400400400
Idcmax6.489726.48737.791737.789526.942696.9334432.77628.441933.402210.1365...8.841746.140766.5609610.863712.077214.408713.847318.88918.889819.2245
Mppt_low303030303030220220250100...10030100100100100100100100240
Mppt_high505050506060416416416480...40049380380400380400400400400
CEC_DateNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
CEC_TypeUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility Interactive...Utility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility Interactive
\n

16 rows × 3264 columns

\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
ABB__MICRO_0_25_I_OUTD_US_208__208V_ABB__MICRO_0_25_I_OUTD_US_240__240V_ABB__MICRO_0_3_I_OUTD_US_208__208V_ABB__MICRO_0_3_I_OUTD_US_240__240V_ABB__MICRO_0_3HV_I_OUTD_US_208__208V_ABB__MICRO_0_3HV_I_OUTD_US_240__240V_ABB__PVI_10_0_I_OUTD_x_US_208_y__208V_ABB__PVI_10_0_I_OUTD_x_US_480_y_z__480V_ABB__PVI_12_0_I_OUTD_x_US_480_y__480V_ABB__PVI_3_0_OUTD_S_US__208V_...Zigor__Sunzet_3_TL_US__240V_i_Energy__GT260__240V_iPower__SHO_1_1__120V_iPower__SHO_2_0__240V_iPower__SHO_2_5__240V_iPower__SHO_3_0__240V_iPower__SHO_3_5__240V_iPower__SHO_4_6__208V_iPower__SHO_4_8__240V_iPower__SHO_5_2__240V_
Vac208240208240208240208480480208...240240120240240240240208240240
Pso2.089612.240411.846511.950541.769441.8437846.863867.790962.554718.1663...36.03342.530122.095424.465842.776531.68264.774254.570185.145762.4867
Paco2502503003003003001000010000120003000...318023011002000250030003500460048005200
Pdco259.589259.492311.669311.581312.421312.00510488.31029612358.83142.3...3315.65245.631194.092161.882632.843205.933641.844797.814968.035382.86
Vdco404040404545320362370310...37540182199218222.5263254263280
C0-4.1e-05-3.9e-05-3.3e-05-3.4e-05-4.5e-05-3.5e-05-2.7759e-06-1.38839e-06-1.009e-06-8.03949e-06...-7.98467e-066.2e-05-2.1e-05-1.3e-05-1.4e-05-8.21046e-06-9.08073e-06-5.99928e-06-6.16035e-06-4.63524e-06
C1-9.1e-05-0.000132-0.000192-0.000256-0.000196-0.000227-3.6e-05-4.9e-05-5.6e-05-1.1e-05...-7.5e-05-9.8e-055.7e-055.5e-056.1e-053.6e-053.5e-052.8e-053.4e-054.4e-05
C20.0004940.0024180.0009070.0024530.001959-0.0005260.000305-0.00052-0.0014370.000999...0.0005440.0002310.0020010.0017030.0020530.0017080.0014170.0013810.0005860.00126
C3-0.013171-0.014926-0.031742-0.028223-0.023725-0.041214-0.002351-0.003855-0.007112-0.000287...-0.0003380.1210320.0006230.0003150.001530.000860.0012180.0008890.0001950.000367
Pnt0.0750.0750.090.090.090.090.10.40.40.1...0.9540.0690.330.60.750.91.051.381.441.56
Vdcmax505050506060416416416480...40049380380400380400400400400
Idcmax6.489726.48737.791737.789526.942696.9334432.77628.441933.402210.1365...8.841746.140766.5609610.863712.077214.408713.847318.88918.889819.2245
Mppt_low303030303030220220250100...10030100100100100100100100240
Mppt_high505050506060416416416480...40049380380400380400400400400
CEC_DateNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
CEC_TypeUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility Interactive...Utility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility InteractiveUtility Interactive
\n

16 rows × 3264 columns

\n
", + "text/plain": " ABB__MICRO_0_25_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 2.08961 \nPaco 250 \nPdco 259.589 \nVdco 40 \nC0 -4.1e-05 \nC1 -9.1e-05 \nC2 0.000494 \nC3 -0.013171 \nPnt 0.075 \nVdcmax 50 \nIdcmax 6.48972 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_25_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 2.24041 \nPaco 250 \nPdco 259.492 \nVdco 40 \nC0 -3.9e-05 \nC1 -0.000132 \nC2 0.002418 \nC3 -0.014926 \nPnt 0.075 \nVdcmax 50 \nIdcmax 6.4873 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 1.84651 \nPaco 300 \nPdco 311.669 \nVdco 40 \nC0 -3.3e-05 \nC1 -0.000192 \nC2 0.000907 \nC3 -0.031742 \nPnt 0.09 \nVdcmax 50 \nIdcmax 7.79173 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 1.95054 \nPaco 300 \nPdco 311.581 \nVdco 40 \nC0 -3.4e-05 \nC1 -0.000256 \nC2 0.002453 \nC3 -0.028223 \nPnt 0.09 \nVdcmax 50 \nIdcmax 7.78952 \nMppt_low 30 \nMppt_high 50 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3HV_I_OUTD_US_208__208V_ \\\nVac 208 \nPso 1.76944 \nPaco 300 \nPdco 312.421 \nVdco 45 \nC0 -4.5e-05 \nC1 -0.000196 \nC2 0.001959 \nC3 -0.023725 \nPnt 0.09 \nVdcmax 60 \nIdcmax 6.94269 \nMppt_low 30 \nMppt_high 60 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__MICRO_0_3HV_I_OUTD_US_240__240V_ \\\nVac 240 \nPso 1.84378 \nPaco 300 \nPdco 312.005 \nVdco 45 \nC0 -3.5e-05 \nC1 -0.000227 \nC2 -0.000526 \nC3 -0.041214 \nPnt 0.09 \nVdcmax 60 \nIdcmax 6.93344 \nMppt_low 30 \nMppt_high 60 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_10_0_I_OUTD_x_US_208_y__208V_ \\\nVac 208 \nPso 46.8638 \nPaco 10000 \nPdco 10488.3 \nVdco 320 \nC0 -2.7759e-06 \nC1 -3.6e-05 \nC2 0.000305 \nC3 -0.002351 \nPnt 0.1 \nVdcmax 416 \nIdcmax 32.776 \nMppt_low 220 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_10_0_I_OUTD_x_US_480_y_z__480V_ \\\nVac 480 \nPso 67.7909 \nPaco 10000 \nPdco 10296 \nVdco 362 \nC0 -1.38839e-06 \nC1 -4.9e-05 \nC2 -0.00052 \nC3 -0.003855 \nPnt 0.4 \nVdcmax 416 \nIdcmax 28.4419 \nMppt_low 220 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_12_0_I_OUTD_x_US_480_y__480V_ \\\nVac 480 \nPso 62.5547 \nPaco 12000 \nPdco 12358.8 \nVdco 370 \nC0 -1.009e-06 \nC1 -5.6e-05 \nC2 -0.001437 \nC3 -0.007112 \nPnt 0.4 \nVdcmax 416 \nIdcmax 33.4022 \nMppt_low 250 \nMppt_high 416 \nCEC_Date NaN \nCEC_Type Utility Interactive \n\n ABB__PVI_3_0_OUTD_S_US__208V_ ... Zigor__Sunzet_3_TL_US__240V_ \\\nVac 208 ... 240 \nPso 18.1663 ... 36.0334 \nPaco 3000 ... 3180 \nPdco 3142.3 ... 3315.65 \nVdco 310 ... 375 \nC0 -8.03949e-06 ... -7.98467e-06 \nC1 -1.1e-05 ... -7.5e-05 \nC2 0.000999 ... 0.000544 \nC3 -0.000287 ... -0.000338 \nPnt 0.1 ... 0.954 \nVdcmax 480 ... 400 \nIdcmax 10.1365 ... 8.84174 \nMppt_low 100 ... 100 \nMppt_high 480 ... 400 \nCEC_Date NaN ... NaN \nCEC_Type Utility Interactive ... Utility Interactive \n\n i_Energy__GT260__240V_ iPower__SHO_1_1__120V_ \\\nVac 240 120 \nPso 2.5301 22.0954 \nPaco 230 1100 \nPdco 245.63 1194.09 \nVdco 40 182 \nC0 6.2e-05 -2.1e-05 \nC1 -9.8e-05 5.7e-05 \nC2 0.000231 0.002001 \nC3 0.121032 0.000623 \nPnt 0.069 0.33 \nVdcmax 49 380 \nIdcmax 6.14076 6.56096 \nMppt_low 30 100 \nMppt_high 49 380 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_2_0__240V_ iPower__SHO_2_5__240V_ \\\nVac 240 240 \nPso 24.4658 42.7765 \nPaco 2000 2500 \nPdco 2161.88 2632.84 \nVdco 199 218 \nC0 -1.3e-05 -1.4e-05 \nC1 5.5e-05 6.1e-05 \nC2 0.001703 0.002053 \nC3 0.000315 0.00153 \nPnt 0.6 0.75 \nVdcmax 380 400 \nIdcmax 10.8637 12.0772 \nMppt_low 100 100 \nMppt_high 380 400 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_3_0__240V_ iPower__SHO_3_5__240V_ \\\nVac 240 240 \nPso 31.682 64.7742 \nPaco 3000 3500 \nPdco 3205.93 3641.84 \nVdco 222.5 263 \nC0 -8.21046e-06 -9.08073e-06 \nC1 3.6e-05 3.5e-05 \nC2 0.001708 0.001417 \nC3 0.00086 0.001218 \nPnt 0.9 1.05 \nVdcmax 380 400 \nIdcmax 14.4087 13.8473 \nMppt_low 100 100 \nMppt_high 380 400 \nCEC_Date NaN NaN \nCEC_Type Utility Interactive Utility Interactive \n\n iPower__SHO_4_6__208V_ iPower__SHO_4_8__240V_ iPower__SHO_5_2__240V_ \nVac 208 240 240 \nPso 54.5701 85.1457 62.4867 \nPaco 4600 4800 5200 \nPdco 4797.81 4968.03 5382.86 \nVdco 254 263 280 \nC0 -5.99928e-06 -6.16035e-06 -4.63524e-06 \nC1 2.8e-05 3.4e-05 4.4e-05 \nC2 0.001381 0.000586 0.00126 \nC3 0.000889 0.000195 0.000367 \nPnt 1.38 1.44 1.56 \nVdcmax 400 400 400 \nIdcmax 18.889 18.8898 19.2245 \nMppt_low 100 100 240 \nMppt_high 400 400 400 \nCEC_Date NaN NaN NaN \nCEC_Type Utility Interactive Utility Interactive Utility Interactive \n\n[16 rows x 3264 columns]" }, + "execution_count": 13, "metadata": {}, - "execution_count": 13 + "output_type": "execute_result" } ], "source": [ - "inverters = pvsystem.retrieve_sam('sandiainverter')\n", + "inverters = pvsystem.retrieve_sam(\"sandiainverter\")\n", "inverters" ] }, @@ -424,27 +435,29 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "vdcs = pd.Series(np.linspace(0,50,51))\n", - "idcs = pd.Series(np.linspace(0,11,110))\n", + "vdcs = pd.Series(np.linspace(0, 50, 51))\n", + "idcs = pd.Series(np.linspace(0, 11, 110))\n", "pdcs = idcs * vdcs\n", "\n", - "pacs = inverter.sandia(vdcs, pdcs, inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'])\n", - "#pacs.plot()\n", + "pacs = inverter.sandia(\n", + " vdcs, pdcs, inverters[\"ABB__MICRO_0_25_I_OUTD_US_208__208V_\"]\n", + ")\n", + "# pacs.plot()\n", "plt.plot(pdcs, pacs)\n", - "plt.ylabel('ac power')\n", - "plt.xlabel('dc power');" + "plt.ylabel(\"ac power\")\n", + "plt.xlabel(\"dc power\");" ] }, { @@ -476,17 +489,17 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": " A10Green_Technology_A10J_S72_175 A10Green_Technology_A10J_S72_180 \\\nTechnology Mono-c-Si Mono-c-Si \nBifacial 0 0 \nSTC 175.091 179.928 \nPTC 151.2 155.7 \nA_c 1.3 1.3 \nLength 1.576 1.576 \nWidth 0.825 0.825 \nN_s 72 72 \nI_sc_ref 5.17 5.31 \nV_oc_ref 43.99 44.06 \nI_mp_ref 4.78 4.9 \nV_mp_ref 36.63 36.72 \nalpha_sc 0.002146 0.002204 \nbeta_oc -0.159068 -0.159321 \nT_NOCT 49.9 49.9 \na_ref 1.9817 1.98841 \nI_L_ref 5.1757 5.31615 \nI_o_ref 1.14916e-09 1.22524e-09 \nR_s 0.316688 0.299919 \nR_sh_ref 287.102 259.048 \nAdjust 16.0571 16.419 \ngamma_r -0.5072 -0.5072 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_S72_185 A10Green_Technology_A10J_M60_220 \\\nTechnology Mono-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 184.702 219.876 \nPTC 160.2 189.1 \nA_c 1.3 1.624 \nLength 1.576 1.632 \nWidth 0.825 0.995 \nN_s 72 60 \nI_sc_ref 5.43 7.95 \nV_oc_ref 44.14 36.06 \nI_mp_ref 5.03 7.3 \nV_mp_ref 36.72 30.12 \nalpha_sc 0.002253 0.004357 \nbeta_oc -0.15961 -0.130681 \nT_NOCT 49.9 50.2 \na_ref 1.98482 1.67309 \nI_L_ref 5.43568 7.95906 \nI_o_ref 1.16164e-09 3.34415e-09 \nR_s 0.311962 0.140393 \nR_sh_ref 298.424 123.168 \nAdjust 15.6882 21.8752 \ngamma_r -0.5072 -0.5196 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_M60_225 A10Green_Technology_A10J_M60_230 \\\nTechnology Multi-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 224.986 230.129 \nPTC 193.5 204.1 \nA_c 1.624 1.624 \nLength 1.632 1.632 \nWidth 0.995 0.995 \nN_s 60 60 \nI_sc_ref 8.04 8.1 \nV_oc_ref 36.24 36.42 \nI_mp_ref 7.44 7.58 \nV_mp_ref 30.24 30.36 \nalpha_sc 0.004406 0.007857 \nbeta_oc -0.131334 -0.130748 \nT_NOCT 50.2 46.4 \na_ref 1.67178 1.68048 \nI_L_ref 8.04721 8.10361 \nI_o_ref 3.01424e-09 3.09549e-09 \nR_s 0.14737 0.152058 \nR_sh_ref 164.419 340.983 \nAdjust 20.6984 21.5544 \ngamma_r -0.5196 -0.493 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_M60_235 A10Green_Technology_A10J_M60_240 \\\nTechnology Multi-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 235.008 240.538 \nPTC 208.7 213.3 \nA_c 1.624 1.624 \nLength 1.632 1.632 \nWidth 0.995 0.995 \nN_s 60 60 \nI_sc_ref 8.23 8.32 \nV_oc_ref 36.72 36.84 \nI_mp_ref 7.68 7.83 \nV_mp_ref 30.6 30.72 \nalpha_sc 0.007983 0.00807 \nbeta_oc -0.131825 -0.132256 \nT_NOCT 46.4 46.4 \na_ref 1.69698 1.69423 \nI_L_ref 8.23464 8.32177 \nI_o_ref 3.24284e-09 2.97878e-09 \nR_s 0.151504 0.150077 \nR_sh_ref 268.701 706.27 \nAdjust 21.8719 20.881 \ngamma_r -0.493 -0.493 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A2Peak_Power_POWER_ON_P220_6x10 Aavid_Solar_ASMS_165P ... \\\nTechnology Multi-c-Si Multi-c-Si ... \nBifacial 0 0 ... \nSTC 219.978 164.85 ... \nPTC 195 146.3 ... \nA_c 1.633 1.301 ... \nLength 1.633 1.575 ... \nWidth 1 0.826 ... \nN_s 60 72 ... \nI_sc_ref 7.98 5.25 ... \nV_oc_ref 36.72 43.5 ... \nI_mp_ref 7.26 4.71 ... \nV_mp_ref 30.3 35 ... \nalpha_sc 0.00399 0.001575 ... \nbeta_oc -0.12852 -0.170955 ... \nT_NOCT 47.9 45 ... \na_ref 1.59703 1.96463 ... \nI_L_ref 8.00023 5.27415 ... \nI_o_ref 7.85133e-10 1.19571e-09 ... \nR_s 0.229644 0.595855 ... \nR_sh_ref 90.5774 129.523 ... \nAdjust 12.2172 7.16388 ... \ngamma_r -0.46 -0.519 ... \nBIPV N N ... \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 ... \nDate 1/3/2019 1/3/2019 ... \n\n Zytech_Solar_ZT275P Zytech_Solar_ZT280P Zytech_Solar_ZT285P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 275.014 280.329 285.326 \nPTC 248 252.6 257.3 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.31 8.4 8.48 \nV_oc_ref 45.1 45.25 45.43 \nI_mp_ref 7.76 7.87 7.97 \nV_mp_ref 35.44 35.62 35.8 \nalpha_sc 0.004014 0.004057 0.004096 \nbeta_oc -0.144275 -0.144755 -0.145331 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.81027 1.81485 1.8201 \nI_L_ref 8.32377 8.41015 8.4867 \nI_o_ref 1.24062e-10 1.2341e-10 1.21696e-10 \nR_s 0.566493 0.552584 0.543536 \nR_sh_ref 341.758 457.468 687.561 \nAdjust 5.42178 5.27464 5.06509 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT290P Zytech_Solar_ZT295P Zytech_Solar_ZT300P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 290.036 295.066 300.003 \nPTC 261.9 266.5 271.2 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.55 8.64 8.71 \nV_oc_ref 45.59 45.75 45.96 \nI_mp_ref 8.07 8.16 8.26 \nV_mp_ref 35.94 36.16 36.32 \nalpha_sc 0.00413 0.004173 0.004207 \nbeta_oc -0.145842 -0.146354 -0.147026 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.82278 1.83125 1.84441 \nI_L_ref 8.55196 8.64154 8.80531 \nI_o_ref 1.17172e-10 1.21851e-10 1.31413e-10 \nR_s 0.538499 0.521134 0.515735 \nR_sh_ref 2348.68 2917.76 552.455 \nAdjust 4.66051 4.9013 5.41555 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT305P Zytech_Solar_ZT310P Zytech_Solar_ZT315P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 305.056 310.144 315.094 \nPTC 275.8 280.5 285.1 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.87 8.9 9.01 \nV_oc_ref 46.12 46.28 46.44 \nI_mp_ref 8.36 8.46 8.56 \nV_mp_ref 36.49 36.66 36.81 \nalpha_sc 0.004284 0.004299 0.004352 \nbeta_oc -0.147538 -0.14805 -0.148562 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.84915 1.8574 1.86502 \nI_L_ref 8.87402 8.9948 9.10661 \nI_o_ref 1.30106e-10 1.34888e-10 1.38664e-10 \nR_s 0.506611 0.495904 0.488376 \nR_sh_ref 1119.07 767.958 682.292 \nAdjust 5.24155 5.44634 5.57874 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT320P \nTechnology Multi-c-Si \nBifacial 0 \nSTC 320.42 \nPTC 289.8 \nA_c 1.931 \nLength 1.95 \nWidth 0.99 \nN_s 72 \nI_sc_ref 9.12 \nV_oc_ref 46.6 \nI_mp_ref 8.66 \nV_mp_ref 37 \nalpha_sc 0.004405 \nbeta_oc -0.149073 \nT_NOCT 46.4 \na_ref 1.87378 \nI_L_ref 9.21845 \nI_o_ref 1.44659e-10 \nR_s 0.475581 \nR_sh_ref 604.221 \nAdjust 5.83833 \ngamma_r -0.4308 \nBIPV N \nVersion SAM 2018.11.11 r2 \nDate 1/3/2019 \n\n[25 rows x 21535 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
A10Green_Technology_A10J_S72_175A10Green_Technology_A10J_S72_180A10Green_Technology_A10J_S72_185A10Green_Technology_A10J_M60_220A10Green_Technology_A10J_M60_225A10Green_Technology_A10J_M60_230A10Green_Technology_A10J_M60_235A10Green_Technology_A10J_M60_240A2Peak_Power_POWER_ON_P220_6x10Aavid_Solar_ASMS_165P...Zytech_Solar_ZT275PZytech_Solar_ZT280PZytech_Solar_ZT285PZytech_Solar_ZT290PZytech_Solar_ZT295PZytech_Solar_ZT300PZytech_Solar_ZT305PZytech_Solar_ZT310PZytech_Solar_ZT315PZytech_Solar_ZT320P
TechnologyMono-c-SiMono-c-SiMono-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-Si...Multi-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-Si
Bifacial0000000000...0000000000
STC175.091179.928184.702219.876224.986230.129235.008240.538219.978164.85...275.014280.329285.326290.036295.066300.003305.056310.144315.094320.42
PTC151.2155.7160.2189.1193.5204.1208.7213.3195146.3...248252.6257.3261.9266.5271.2275.8280.5285.1289.8
A_c1.31.31.31.6241.6241.6241.6241.6241.6331.301...1.9311.9311.9311.9311.9311.9311.9311.9311.9311.931
Length1.5761.5761.5761.6321.6321.6321.6321.6321.6331.575...1.951.951.951.951.951.951.951.951.951.95
Width0.8250.8250.8250.9950.9950.9950.9950.99510.826...0.990.990.990.990.990.990.990.990.990.99
N_s72727260606060606072...72727272727272727272
I_sc_ref5.175.315.437.958.048.18.238.327.985.25...8.318.48.488.558.648.718.878.99.019.12
V_oc_ref43.9944.0644.1436.0636.2436.4236.7236.8436.7243.5...45.145.2545.4345.5945.7545.9646.1246.2846.4446.6
I_mp_ref4.784.95.037.37.447.587.687.837.264.71...7.767.877.978.078.168.268.368.468.568.66
V_mp_ref36.6336.7236.7230.1230.2430.3630.630.7230.335...35.4435.6235.835.9436.1636.3236.4936.6636.8137
alpha_sc0.0021460.0022040.0022530.0043570.0044060.0078570.0079830.008070.003990.001575...0.0040140.0040570.0040960.004130.0041730.0042070.0042840.0042990.0043520.004405
beta_oc-0.159068-0.159321-0.15961-0.130681-0.131334-0.130748-0.131825-0.132256-0.12852-0.170955...-0.144275-0.144755-0.145331-0.145842-0.146354-0.147026-0.147538-0.14805-0.148562-0.149073
T_NOCT49.949.949.950.250.246.446.446.447.945...46.446.446.446.446.446.446.446.446.446.4
a_ref1.98171.988411.984821.673091.671781.680481.696981.694231.597031.96463...1.810271.814851.82011.822781.831251.844411.849151.85741.865021.87378
I_L_ref5.17575.316155.435687.959068.047218.103618.234648.321778.000235.27415...8.323778.410158.48678.551968.641548.805318.874028.99489.106619.21845
I_o_ref1.14916e-091.22524e-091.16164e-093.34415e-093.01424e-093.09549e-093.24284e-092.97878e-097.85133e-101.19571e-09...1.24062e-101.2341e-101.21696e-101.17172e-101.21851e-101.31413e-101.30106e-101.34888e-101.38664e-101.44659e-10
R_s0.3166880.2999190.3119620.1403930.147370.1520580.1515040.1500770.2296440.595855...0.5664930.5525840.5435360.5384990.5211340.5157350.5066110.4959040.4883760.475581
R_sh_ref287.102259.048298.424123.168164.419340.983268.701706.2790.5774129.523...341.758457.468687.5612348.682917.76552.4551119.07767.958682.292604.221
Adjust16.057116.41915.688221.875220.698421.554421.871920.88112.21727.16388...5.421785.274645.065094.660514.90135.415555.241555.446345.578745.83833
gamma_r-0.5072-0.5072-0.5072-0.5196-0.5196-0.493-0.493-0.493-0.46-0.519...-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308
BIPVNNNNNNNNNN...NNNNNNNNNN
VersionSAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2...SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2
Date1/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/2019...1/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/2019
\n

25 rows × 21535 columns

\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
A10Green_Technology_A10J_S72_175A10Green_Technology_A10J_S72_180A10Green_Technology_A10J_S72_185A10Green_Technology_A10J_M60_220A10Green_Technology_A10J_M60_225A10Green_Technology_A10J_M60_230A10Green_Technology_A10J_M60_235A10Green_Technology_A10J_M60_240A2Peak_Power_POWER_ON_P220_6x10Aavid_Solar_ASMS_165P...Zytech_Solar_ZT275PZytech_Solar_ZT280PZytech_Solar_ZT285PZytech_Solar_ZT290PZytech_Solar_ZT295PZytech_Solar_ZT300PZytech_Solar_ZT305PZytech_Solar_ZT310PZytech_Solar_ZT315PZytech_Solar_ZT320P
TechnologyMono-c-SiMono-c-SiMono-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-Si...Multi-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-SiMulti-c-Si
Bifacial0000000000...0000000000
STC175.091179.928184.702219.876224.986230.129235.008240.538219.978164.85...275.014280.329285.326290.036295.066300.003305.056310.144315.094320.42
PTC151.2155.7160.2189.1193.5204.1208.7213.3195146.3...248252.6257.3261.9266.5271.2275.8280.5285.1289.8
A_c1.31.31.31.6241.6241.6241.6241.6241.6331.301...1.9311.9311.9311.9311.9311.9311.9311.9311.9311.931
Length1.5761.5761.5761.6321.6321.6321.6321.6321.6331.575...1.951.951.951.951.951.951.951.951.951.95
Width0.8250.8250.8250.9950.9950.9950.9950.99510.826...0.990.990.990.990.990.990.990.990.990.99
N_s72727260606060606072...72727272727272727272
I_sc_ref5.175.315.437.958.048.18.238.327.985.25...8.318.48.488.558.648.718.878.99.019.12
V_oc_ref43.9944.0644.1436.0636.2436.4236.7236.8436.7243.5...45.145.2545.4345.5945.7545.9646.1246.2846.4446.6
I_mp_ref4.784.95.037.37.447.587.687.837.264.71...7.767.877.978.078.168.268.368.468.568.66
V_mp_ref36.6336.7236.7230.1230.2430.3630.630.7230.335...35.4435.6235.835.9436.1636.3236.4936.6636.8137
alpha_sc0.0021460.0022040.0022530.0043570.0044060.0078570.0079830.008070.003990.001575...0.0040140.0040570.0040960.004130.0041730.0042070.0042840.0042990.0043520.004405
beta_oc-0.159068-0.159321-0.15961-0.130681-0.131334-0.130748-0.131825-0.132256-0.12852-0.170955...-0.144275-0.144755-0.145331-0.145842-0.146354-0.147026-0.147538-0.14805-0.148562-0.149073
T_NOCT49.949.949.950.250.246.446.446.447.945...46.446.446.446.446.446.446.446.446.446.4
a_ref1.98171.988411.984821.673091.671781.680481.696981.694231.597031.96463...1.810271.814851.82011.822781.831251.844411.849151.85741.865021.87378
I_L_ref5.17575.316155.435687.959068.047218.103618.234648.321778.000235.27415...8.323778.410158.48678.551968.641548.805318.874028.99489.106619.21845
I_o_ref1.14916e-091.22524e-091.16164e-093.34415e-093.01424e-093.09549e-093.24284e-092.97878e-097.85133e-101.19571e-09...1.24062e-101.2341e-101.21696e-101.17172e-101.21851e-101.31413e-101.30106e-101.34888e-101.38664e-101.44659e-10
R_s0.3166880.2999190.3119620.1403930.147370.1520580.1515040.1500770.2296440.595855...0.5664930.5525840.5435360.5384990.5211340.5157350.5066110.4959040.4883760.475581
R_sh_ref287.102259.048298.424123.168164.419340.983268.701706.2790.5774129.523...341.758457.468687.5612348.682917.76552.4551119.07767.958682.292604.221
Adjust16.057116.41915.688221.875220.698421.554421.871920.88112.21727.16388...5.421785.274645.065094.660514.90135.415555.241555.446345.578745.83833
gamma_r-0.5072-0.5072-0.5072-0.5196-0.5196-0.493-0.493-0.493-0.46-0.519...-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308-0.4308
BIPVNNNNNNNNNN...NNNNNNNNNN
VersionSAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2...SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2SAM 2018.11.11 r2
Date1/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/2019...1/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/20191/3/2019
\n

25 rows × 21535 columns

\n
", + "text/plain": " A10Green_Technology_A10J_S72_175 A10Green_Technology_A10J_S72_180 \\\nTechnology Mono-c-Si Mono-c-Si \nBifacial 0 0 \nSTC 175.091 179.928 \nPTC 151.2 155.7 \nA_c 1.3 1.3 \nLength 1.576 1.576 \nWidth 0.825 0.825 \nN_s 72 72 \nI_sc_ref 5.17 5.31 \nV_oc_ref 43.99 44.06 \nI_mp_ref 4.78 4.9 \nV_mp_ref 36.63 36.72 \nalpha_sc 0.002146 0.002204 \nbeta_oc -0.159068 -0.159321 \nT_NOCT 49.9 49.9 \na_ref 1.9817 1.98841 \nI_L_ref 5.1757 5.31615 \nI_o_ref 1.14916e-09 1.22524e-09 \nR_s 0.316688 0.299919 \nR_sh_ref 287.102 259.048 \nAdjust 16.0571 16.419 \ngamma_r -0.5072 -0.5072 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_S72_185 A10Green_Technology_A10J_M60_220 \\\nTechnology Mono-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 184.702 219.876 \nPTC 160.2 189.1 \nA_c 1.3 1.624 \nLength 1.576 1.632 \nWidth 0.825 0.995 \nN_s 72 60 \nI_sc_ref 5.43 7.95 \nV_oc_ref 44.14 36.06 \nI_mp_ref 5.03 7.3 \nV_mp_ref 36.72 30.12 \nalpha_sc 0.002253 0.004357 \nbeta_oc -0.15961 -0.130681 \nT_NOCT 49.9 50.2 \na_ref 1.98482 1.67309 \nI_L_ref 5.43568 7.95906 \nI_o_ref 1.16164e-09 3.34415e-09 \nR_s 0.311962 0.140393 \nR_sh_ref 298.424 123.168 \nAdjust 15.6882 21.8752 \ngamma_r -0.5072 -0.5196 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_M60_225 A10Green_Technology_A10J_M60_230 \\\nTechnology Multi-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 224.986 230.129 \nPTC 193.5 204.1 \nA_c 1.624 1.624 \nLength 1.632 1.632 \nWidth 0.995 0.995 \nN_s 60 60 \nI_sc_ref 8.04 8.1 \nV_oc_ref 36.24 36.42 \nI_mp_ref 7.44 7.58 \nV_mp_ref 30.24 30.36 \nalpha_sc 0.004406 0.007857 \nbeta_oc -0.131334 -0.130748 \nT_NOCT 50.2 46.4 \na_ref 1.67178 1.68048 \nI_L_ref 8.04721 8.10361 \nI_o_ref 3.01424e-09 3.09549e-09 \nR_s 0.14737 0.152058 \nR_sh_ref 164.419 340.983 \nAdjust 20.6984 21.5544 \ngamma_r -0.5196 -0.493 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A10Green_Technology_A10J_M60_235 A10Green_Technology_A10J_M60_240 \\\nTechnology Multi-c-Si Multi-c-Si \nBifacial 0 0 \nSTC 235.008 240.538 \nPTC 208.7 213.3 \nA_c 1.624 1.624 \nLength 1.632 1.632 \nWidth 0.995 0.995 \nN_s 60 60 \nI_sc_ref 8.23 8.32 \nV_oc_ref 36.72 36.84 \nI_mp_ref 7.68 7.83 \nV_mp_ref 30.6 30.72 \nalpha_sc 0.007983 0.00807 \nbeta_oc -0.131825 -0.132256 \nT_NOCT 46.4 46.4 \na_ref 1.69698 1.69423 \nI_L_ref 8.23464 8.32177 \nI_o_ref 3.24284e-09 2.97878e-09 \nR_s 0.151504 0.150077 \nR_sh_ref 268.701 706.27 \nAdjust 21.8719 20.881 \ngamma_r -0.493 -0.493 \nBIPV N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 \n\n A2Peak_Power_POWER_ON_P220_6x10 Aavid_Solar_ASMS_165P ... \\\nTechnology Multi-c-Si Multi-c-Si ... \nBifacial 0 0 ... \nSTC 219.978 164.85 ... \nPTC 195 146.3 ... \nA_c 1.633 1.301 ... \nLength 1.633 1.575 ... \nWidth 1 0.826 ... \nN_s 60 72 ... \nI_sc_ref 7.98 5.25 ... \nV_oc_ref 36.72 43.5 ... \nI_mp_ref 7.26 4.71 ... \nV_mp_ref 30.3 35 ... \nalpha_sc 0.00399 0.001575 ... \nbeta_oc -0.12852 -0.170955 ... \nT_NOCT 47.9 45 ... \na_ref 1.59703 1.96463 ... \nI_L_ref 8.00023 5.27415 ... \nI_o_ref 7.85133e-10 1.19571e-09 ... \nR_s 0.229644 0.595855 ... \nR_sh_ref 90.5774 129.523 ... \nAdjust 12.2172 7.16388 ... \ngamma_r -0.46 -0.519 ... \nBIPV N N ... \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 ... \nDate 1/3/2019 1/3/2019 ... \n\n Zytech_Solar_ZT275P Zytech_Solar_ZT280P Zytech_Solar_ZT285P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 275.014 280.329 285.326 \nPTC 248 252.6 257.3 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.31 8.4 8.48 \nV_oc_ref 45.1 45.25 45.43 \nI_mp_ref 7.76 7.87 7.97 \nV_mp_ref 35.44 35.62 35.8 \nalpha_sc 0.004014 0.004057 0.004096 \nbeta_oc -0.144275 -0.144755 -0.145331 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.81027 1.81485 1.8201 \nI_L_ref 8.32377 8.41015 8.4867 \nI_o_ref 1.24062e-10 1.2341e-10 1.21696e-10 \nR_s 0.566493 0.552584 0.543536 \nR_sh_ref 341.758 457.468 687.561 \nAdjust 5.42178 5.27464 5.06509 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT290P Zytech_Solar_ZT295P Zytech_Solar_ZT300P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 290.036 295.066 300.003 \nPTC 261.9 266.5 271.2 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.55 8.64 8.71 \nV_oc_ref 45.59 45.75 45.96 \nI_mp_ref 8.07 8.16 8.26 \nV_mp_ref 35.94 36.16 36.32 \nalpha_sc 0.00413 0.004173 0.004207 \nbeta_oc -0.145842 -0.146354 -0.147026 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.82278 1.83125 1.84441 \nI_L_ref 8.55196 8.64154 8.80531 \nI_o_ref 1.17172e-10 1.21851e-10 1.31413e-10 \nR_s 0.538499 0.521134 0.515735 \nR_sh_ref 2348.68 2917.76 552.455 \nAdjust 4.66051 4.9013 5.41555 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT305P Zytech_Solar_ZT310P Zytech_Solar_ZT315P \\\nTechnology Multi-c-Si Multi-c-Si Multi-c-Si \nBifacial 0 0 0 \nSTC 305.056 310.144 315.094 \nPTC 275.8 280.5 285.1 \nA_c 1.931 1.931 1.931 \nLength 1.95 1.95 1.95 \nWidth 0.99 0.99 0.99 \nN_s 72 72 72 \nI_sc_ref 8.87 8.9 9.01 \nV_oc_ref 46.12 46.28 46.44 \nI_mp_ref 8.36 8.46 8.56 \nV_mp_ref 36.49 36.66 36.81 \nalpha_sc 0.004284 0.004299 0.004352 \nbeta_oc -0.147538 -0.14805 -0.148562 \nT_NOCT 46.4 46.4 46.4 \na_ref 1.84915 1.8574 1.86502 \nI_L_ref 8.87402 8.9948 9.10661 \nI_o_ref 1.30106e-10 1.34888e-10 1.38664e-10 \nR_s 0.506611 0.495904 0.488376 \nR_sh_ref 1119.07 767.958 682.292 \nAdjust 5.24155 5.44634 5.57874 \ngamma_r -0.4308 -0.4308 -0.4308 \nBIPV N N N \nVersion SAM 2018.11.11 r2 SAM 2018.11.11 r2 SAM 2018.11.11 r2 \nDate 1/3/2019 1/3/2019 1/3/2019 \n\n Zytech_Solar_ZT320P \nTechnology Multi-c-Si \nBifacial 0 \nSTC 320.42 \nPTC 289.8 \nA_c 1.931 \nLength 1.95 \nWidth 0.99 \nN_s 72 \nI_sc_ref 9.12 \nV_oc_ref 46.6 \nI_mp_ref 8.66 \nV_mp_ref 37 \nalpha_sc 0.004405 \nbeta_oc -0.149073 \nT_NOCT 46.4 \na_ref 1.87378 \nI_L_ref 9.21845 \nI_o_ref 1.44659e-10 \nR_s 0.475581 \nR_sh_ref 604.221 \nAdjust 5.83833 \ngamma_r -0.4308 \nBIPV N \nVersion SAM 2018.11.11 r2 \nDate 1/3/2019 \n\n[25 rows x 21535 columns]" }, + "execution_count": 15, "metadata": {}, - "execution_count": 15 + "output_type": "execute_result" } ], "source": [ - "cec_modules = pvsystem.retrieve_sam('cecmod')\n", + "cec_modules = pvsystem.retrieve_sam(\"cecmod\")\n", "cec_modules" ] }, @@ -496,16 +509,16 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": "Technology Mono-c-Si\nBifacial 0\nSTC 219.961\nPTC 200.1\nA_c 1.7\nLength 1.602\nWidth 1.061\nN_s 96\nI_sc_ref 5.1\nV_oc_ref 59.4\nI_mp_ref 4.69\nV_mp_ref 46.9\nalpha_sc 0.004539\nbeta_oc -0.222156\nT_NOCT 42.4\na_ref 2.63593\nI_L_ref 5.11426\nI_o_ref 8.10251e-10\nR_s 1.06602\nR_sh_ref 381.254\nAdjust 8.61952\ngamma_r -0.476\nBIPV N\nVersion SAM 2018.11.11 r2\nDate 1/3/2019\nName: Canadian_Solar_Inc__CS5P_220M, dtype: object" }, + "execution_count": 16, "metadata": {}, - "execution_count": 16 + "output_type": "execute_result" } ], "source": [ - "cecmodule = cec_modules.Canadian_Solar_Inc__CS5P_220M \n", + "cecmodule = cec_modules.Canadian_Solar_Inc__CS5P_220M\n", "cecmodule" ] }, @@ -522,17 +535,17 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { - "text/plain": " -0.355 \nC3 -13.0643 \nA0 0.9327 \nA1 0.07283 \nA2 -0.02402 \nA3 0.003819 \nA4 -0.000235 \nB0 1 \nB1 -0.006801 \nB2 0.0007968 \nB3 -3.095e-05 \nB4 4.896e-07 \nB5 -2.78e-09 \nDTC 2.58 \nFD 1 \nA -3.7566 \nB -0.156 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Canadian_Solar_CS6X_300M__2013_ \\\nVintage 2013 \nArea 1.91 \nMaterial c-Si \nCells_in_Series 72 \nParallel_Strings 1 \nIsco 8.6388 \nVoco 43.5918 \nImpo 8.1359 \nVmpo 34.9531 \nAisc 0.0005 \nAimp -0.0001 \nC0 1.0121 \nC1 -0.0121 \nBvoco -0.1532 \nMbvoc 0 \nBvmpo -0.1634 \nMbvmp 0 \nN 1.0025 \nC2 -0.171 \nC3 -9.39745 \nA0 0.9371 \nA1 0.06262 \nA2 -0.01667 \nA3 0.002168 \nA4 -0.0001087 \nB0 1 \nB1 -0.00789 \nB2 0.0008656 \nB3 -3.298e-05 \nB4 5.178e-07 \nB5 -2.918e-09 \nDTC 3.2 \nFD 1 \nA -3.6024 \nB -0.2106 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n LG_LG290N1C_G3__2013_ \\\nVintage 2013 \nArea 1.64 \nMaterial c-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 9.8525 \nVoco 39.6117 \nImpo 9.2473 \nVmpo 31.2921 \nAisc 0.0002 \nAimp -0.0004 \nC0 1.0145 \nC1 -0.0145 \nBvoco -0.1205 \nMbvoc 0 \nBvmpo -0.1337 \nMbvmp 0 \nN 1.0925 \nC2 -0.4647 \nC3 -11.9008 \nA0 0.9731 \nA1 0.02966 \nA2 -0.01024 \nA3 0.001793 \nA4 -0.0001286 \nB0 1 \nB1 -0.0154 \nB2 0.001572 \nB3 -5.525e-05 \nB4 8.04e-07 \nB5 -4.202e-09 \nDTC 3.05 \nFD 1 \nA -3.4247 \nB -0.0951 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Sharp_NDQ235F4__2013_ \\\nVintage 2013 \nArea 1.56 \nMaterial mc-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 8.6739 \nVoco 36.8276 \nImpo 8.1243 \nVmpo 29.1988 \nAisc 0.0006 \nAimp -0.0002 \nC0 1.0049 \nC1 -0.0049 \nBvoco -0.1279 \nMbvoc 0 \nBvmpo -0.1348 \nMbvmp 0 \nN 1.0695 \nC2 -0.2718 \nC3 -11.4033 \nA0 0.9436 \nA1 0.04765 \nA2 -0.007405 \nA3 0.0003818 \nA4 -1.101e-05 \nB0 1 \nB1 -0.00464 \nB2 0.000559 \nB3 -2.249e-05 \nB4 3.673e-07 \nB5 -2.144e-09 \nDTC 3.27 \nFD 1 \nA -3.7445 \nB -0.149 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Solar_Frontier_SF_160S__2013_ \\\nVintage 2013 \nArea 1.22 \nMaterial CIS \nCells_in_Series 172 \nParallel_Strings 1 \nIsco 2.0259 \nVoco 112.505 \nImpo 1.8356 \nVmpo 86.6752 \nAisc 0.0001 \nAimp -0.0003 \nC0 1.0096 \nC1 -0.0096 \nBvoco -0.3044 \nMbvoc 0 \nBvmpo -0.2339 \nMbvmp 0 \nN 1.2066 \nC2 -0.5426 \nC3 -15.2598 \nA0 0.9354 \nA1 0.06809 \nA2 -0.02094 \nA3 0.00293 \nA4 -0.0001564 \nB0 1 \nB1 -0.0152 \nB2 0.001598 \nB3 -5.682e-05 \nB4 8.326e-07 \nB5 -4.363e-09 \nDTC 3.29 \nFD 1 \nA -3.6836 \nB -0.1483 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n SolarWorld_Sunmodule_250_Poly__2013_ \\\nVintage 2013 \nArea 1.68 \nMaterial mc-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 8.3768 \nVoco 36.3806 \nImpo 7.6921 \nVmpo 28.348 \nAisc 0.0006 \nAimp -0.0001 \nC0 1.0158 \nC1 -0.0158 \nBvoco -0.1393 \nMbvoc 0 \nBvmpo -0.1449 \nMbvmp 0 \nN 1.226 \nC2 -0.09677 \nC3 -8.51148 \nA0 0.9288 \nA1 0.07201 \nA2 -0.02065 \nA3 0.002862 \nA4 -0.0001544 \nB0 1 \nB1 -0.00308 \nB2 0.0004053 \nB3 -1.729e-05 \nB4 2.997e-07 \nB5 -1.878e-09 \nDTC 3.19 \nFD 1 \nA -3.73 \nB -0.1483 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Silevo_Triex_U300_Black__2014_ \nVintage 2014 \nArea 1.68 \nMaterial c-Si \nCells_in_Series 96 \nParallel_Strings 1 \nIsco 5.771 \nVoco 68.5983 \nImpo 5.383 \nVmpo 55.4547 \nAisc 0.0003 \nAimp -0.0003 \nC0 0.995 \nC1 0.005 \nBvoco -0.1913 \nMbvoc 0 \nBvmpo -0.184 \nMbvmp 0 \nN 1.345 \nC2 0.3221 \nC3 -6.7178 \nA0 0.9191 \nA1 0.09988 \nA2 -0.04273 \nA3 0.00937 \nA4 -0.0007643 \nB0 1 \nB1 -0.006498 \nB2 0.0006908 \nB3 -2.678e-05 \nB4 4.322e-07 \nB5 -2.508e-09 \nDTC 3.13 \nFD 1 \nA -3.6866 \nB -0.104 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2014. Mo... \n\n[42 rows x 523 columns]", - "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Advent_Solar_AS160___2006_Advent_Solar_Ventura_210___2008_Advent_Solar_Ventura_215___2009_Aleo_S03_160__2007__E__Aleo_S03_165__2007__E__Aleo_S16_165__2007__E__Aleo_S16_170__2007__E__Aleo_S16_175__2007__E__Aleo_S16_180__2007__E__Aleo_S16_185__2007__E__...Panasonic_VBHN235SA06B__2013_Trina_TSM_240PA05__2013_Hanwha_HSL60P6_PA_4_250T__2013_Suniva_OPT300_72_4_100__2013_Canadian_Solar_CS6X_300M__2013_LG_LG290N1C_G3__2013_Sharp_NDQ235F4__2013_Solar_Frontier_SF_160S__2013_SolarWorld_Sunmodule_250_Poly__2013_Silevo_Triex_U300_Black__2014_
Vintage2006200820092007 (E)2007 (E)2007 (E)2007 (E)2007 (E)2007 (E)2007 (E)...2013201320132013201320132013201320132014
Area1.3121.6461.6461.281.281.3781.3781.3781.3781.378...1.261.631.651.931.911.641.561.221.681.68
Materialmc-Simc-Simc-Sic-Sic-Simc-Simc-Simc-Simc-Simc-Si...a-Si / mono-Simc-Simc-Sic-Sic-Sic-Simc-SiCISmc-Sic-Si
Cells_in_Series72606072725050505050...726060727260601726096
Parallel_Strings1111111111...1111111111
Isco5.5648.348.495.15.27.97.958.18.158.2...5.87388.84498.59358.57538.63889.85258.67392.02598.37685.771
Voco42.83235.3135.9243.543.63030.130.230.330.5...52.004236.892636.807544.292143.591839.611736.8276112.50536.380668.5983
Impo5.0287.497.744.554.657.087.237.387.537.67...5.53838.29558.08227.9638.13599.24738.12431.83567.69215.383
Vmpo32.4127.6127.9235.635.823.323.523.723.924.1...43.120429.06629.201135.083734.953131.292129.198886.675228.34855.4547
Aisc0.0005370.000770.000820.00030.00030.00080.00080.00080.00080.0008...0.00050.00040.00040.00060.00050.00020.00060.00010.00060.0003
Aimp-0.000491-0.00015-0.00013-0.00025-0.00025-0.0003-0.0003-0.0003-0.0003-0.0003...-0.0001-0.0003-0.0003-0.0002-0.0001-0.0004-0.0002-0.0003-0.0001-0.0003
C01.02330.9371.0150.990.990.990.990.990.990.99...1.00151.01161.00610.9991.01211.01451.00491.00961.01580.995
C1-0.02330.063-0.0150.010.010.010.010.010.010.01...-0.0015-0.0116-0.00610.001-0.0121-0.0145-0.0049-0.0096-0.01580.005
Bvoco-0.1703-0.133-0.135-0.152-0.152-0.11-0.11-0.11-0.11-0.11...-0.1411-0.137-0.1263-0.155-0.1532-0.1205-0.1279-0.3044-0.1393-0.1913
Mbvoc0000000000...0000000000
Bvmpo-0.1731-0.135-0.136-0.158-0.158-0.115-0.115-0.115-0.115-0.115...-0.1366-0.1441-0.1314-0.1669-0.1634-0.1337-0.1348-0.2339-0.1449-0.184
Mbvmp0000000000...0000000000
N1.1741.4951.3731.251.251.351.351.351.351.35...1.0291.20731.06861.07711.00251.09251.06951.20661.2261.345
C2-0.764440.01820.0036-0.15-0.15-0.12-0.12-0.12-0.12-0.12...0.2859-0.07993-0.2585-0.355-0.171-0.4647-0.2718-0.5426-0.096770.3221
C3-15.5087-10.758-7.2509-8.96-8.96-11.08-11.08-11.08-11.08-11.08...-5.48455-7.27624-9.85905-13.0643-9.39745-11.9008-11.4033-15.2598-8.51148-6.7178
A00.92810.90670.93230.9380.9380.9240.9240.9240.9240.924...0.91610.96450.94280.93270.93710.97310.94360.93540.92880.9191
A10.066150.095730.065260.054220.054220.067490.067490.067490.067490.06749...0.079680.027530.05360.072830.062620.029660.047650.068090.072010.09988
A2-0.01384-0.0266-0.01567-0.009903-0.009903-0.012549-0.012549-0.012549-0.012549-0.012549...-0.01866-0.002848-0.01281-0.02402-0.01667-0.01024-0.007405-0.02094-0.02065-0.04273
A30.0012980.003430.001930.00072970.00072970.00100490.00100490.00100490.00100490.0010049...0.002278-0.00014390.0018260.0038190.0021680.0017930.00038180.002930.0028620.00937
A4-4.6e-05-0.0001794-9.81e-05-1.907e-05-1.907e-05-2.8797e-05-2.8797e-05-2.8797e-05-2.8797e-05-2.8797e-05...-0.00011182.219e-05-0.0001048-0.000235-0.0001087-0.0001286-1.101e-05-0.0001564-0.0001544-0.0007643
B01111111111...1111111111
B1-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438...-0.01053-0.00261-0.007861-0.006801-0.00789-0.0154-0.00464-0.0152-0.00308-0.006498
B20.00031030.000310.000310.00031030.00031030.00031030.00031030.00031030.00031030.0003103...0.0011490.00032790.00090580.00079680.00086560.0015720.0005590.0015980.00040530.0006908
B3-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05...-4.268e-05-1.458e-05-3.496e-05-3.095e-05-3.298e-05-5.525e-05-2.249e-05-5.682e-05-1.729e-05-2.678e-05
B42.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-07...6.517e-072.654e-075.473e-074.896e-075.178e-078.04e-073.673e-078.326e-072.997e-074.322e-07
B5-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09...-3.556e-09-1.732e-09-3.058e-09-2.78e-09-2.918e-09-4.202e-09-2.144e-09-4.363e-09-1.878e-09-2.508e-09
DTC3333333333...2.033.032.552.583.23.053.273.293.193.13
FD1111111111...1111111111
A-3.35-3.45-3.47-3.56-3.56-3.56-3.56-3.56-3.56-3.56...-3.7489-3.5924-3.5578-3.7566-3.6024-3.4247-3.7445-3.6836-3.73-3.6866
B-0.1161-0.077-0.087-0.075-0.075-0.075-0.075-0.075-0.075-0.075...-0.1287-0.1319-0.1766-0.156-0.2106-0.0951-0.149-0.1483-0.1483-0.104
C40.99740.9720.9890.9950.9950.9950.9950.9950.9950.995...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C50.00260.0280.0120.0050.0050.0050.0050.0050.0050.005...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
IXO5.548.258.495.045.147.87.8588.058.1...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
IXXO3.565.25.453.163.254.925.085.185.395.54...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C61.1731.0671.1371.151.151.151.151.151.151.15...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C7-0.173-0.067-0.137-0.15-0.15-0.15-0.15-0.15-0.15-0.15...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
NotesSource: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9......Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2014. Mo...
\n

42 rows × 523 columns

\n
" + "text/html": "
\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
Advent_Solar_AS160___2006_Advent_Solar_Ventura_210___2008_Advent_Solar_Ventura_215___2009_Aleo_S03_160__2007__E__Aleo_S03_165__2007__E__Aleo_S16_165__2007__E__Aleo_S16_170__2007__E__Aleo_S16_175__2007__E__Aleo_S16_180__2007__E__Aleo_S16_185__2007__E__...Panasonic_VBHN235SA06B__2013_Trina_TSM_240PA05__2013_Hanwha_HSL60P6_PA_4_250T__2013_Suniva_OPT300_72_4_100__2013_Canadian_Solar_CS6X_300M__2013_LG_LG290N1C_G3__2013_Sharp_NDQ235F4__2013_Solar_Frontier_SF_160S__2013_SolarWorld_Sunmodule_250_Poly__2013_Silevo_Triex_U300_Black__2014_
Vintage2006200820092007 (E)2007 (E)2007 (E)2007 (E)2007 (E)2007 (E)2007 (E)...2013201320132013201320132013201320132014
Area1.3121.6461.6461.281.281.3781.3781.3781.3781.378...1.261.631.651.931.911.641.561.221.681.68
Materialmc-Simc-Simc-Sic-Sic-Simc-Simc-Simc-Simc-Simc-Si...a-Si / mono-Simc-Simc-Sic-Sic-Sic-Simc-SiCISmc-Sic-Si
Cells_in_Series72606072725050505050...726060727260601726096
Parallel_Strings1111111111...1111111111
Isco5.5648.348.495.15.27.97.958.18.158.2...5.87388.84498.59358.57538.63889.85258.67392.02598.37685.771
Voco42.83235.3135.9243.543.63030.130.230.330.5...52.004236.892636.807544.292143.591839.611736.8276112.50536.380668.5983
Impo5.0287.497.744.554.657.087.237.387.537.67...5.53838.29558.08227.9638.13599.24738.12431.83567.69215.383
Vmpo32.4127.6127.9235.635.823.323.523.723.924.1...43.120429.06629.201135.083734.953131.292129.198886.675228.34855.4547
Aisc0.0005370.000770.000820.00030.00030.00080.00080.00080.00080.0008...0.00050.00040.00040.00060.00050.00020.00060.00010.00060.0003
Aimp-0.000491-0.00015-0.00013-0.00025-0.00025-0.0003-0.0003-0.0003-0.0003-0.0003...-0.0001-0.0003-0.0003-0.0002-0.0001-0.0004-0.0002-0.0003-0.0001-0.0003
C01.02330.9371.0150.990.990.990.990.990.990.99...1.00151.01161.00610.9991.01211.01451.00491.00961.01580.995
C1-0.02330.063-0.0150.010.010.010.010.010.010.01...-0.0015-0.0116-0.00610.001-0.0121-0.0145-0.0049-0.0096-0.01580.005
Bvoco-0.1703-0.133-0.135-0.152-0.152-0.11-0.11-0.11-0.11-0.11...-0.1411-0.137-0.1263-0.155-0.1532-0.1205-0.1279-0.3044-0.1393-0.1913
Mbvoc0000000000...0000000000
Bvmpo-0.1731-0.135-0.136-0.158-0.158-0.115-0.115-0.115-0.115-0.115...-0.1366-0.1441-0.1314-0.1669-0.1634-0.1337-0.1348-0.2339-0.1449-0.184
Mbvmp0000000000...0000000000
N1.1741.4951.3731.251.251.351.351.351.351.35...1.0291.20731.06861.07711.00251.09251.06951.20661.2261.345
C2-0.764440.01820.0036-0.15-0.15-0.12-0.12-0.12-0.12-0.12...0.2859-0.07993-0.2585-0.355-0.171-0.4647-0.2718-0.5426-0.096770.3221
C3-15.5087-10.758-7.2509-8.96-8.96-11.08-11.08-11.08-11.08-11.08...-5.48455-7.27624-9.85905-13.0643-9.39745-11.9008-11.4033-15.2598-8.51148-6.7178
A00.92810.90670.93230.9380.9380.9240.9240.9240.9240.924...0.91610.96450.94280.93270.93710.97310.94360.93540.92880.9191
A10.066150.095730.065260.054220.054220.067490.067490.067490.067490.06749...0.079680.027530.05360.072830.062620.029660.047650.068090.072010.09988
A2-0.01384-0.0266-0.01567-0.009903-0.009903-0.012549-0.012549-0.012549-0.012549-0.012549...-0.01866-0.002848-0.01281-0.02402-0.01667-0.01024-0.007405-0.02094-0.02065-0.04273
A30.0012980.003430.001930.00072970.00072970.00100490.00100490.00100490.00100490.0010049...0.002278-0.00014390.0018260.0038190.0021680.0017930.00038180.002930.0028620.00937
A4-4.6e-05-0.0001794-9.81e-05-1.907e-05-1.907e-05-2.8797e-05-2.8797e-05-2.8797e-05-2.8797e-05-2.8797e-05...-0.00011182.219e-05-0.0001048-0.000235-0.0001087-0.0001286-1.101e-05-0.0001564-0.0001544-0.0007643
B01111111111...1111111111
B1-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438-0.002438...-0.01053-0.00261-0.007861-0.006801-0.00789-0.0154-0.00464-0.0152-0.00308-0.006498
B20.00031030.000310.000310.00031030.00031030.00031030.00031030.00031030.00031030.0003103...0.0011490.00032790.00090580.00079680.00086560.0015720.0005590.0015980.00040530.0006908
B3-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05-1.246e-05...-4.268e-05-1.458e-05-3.496e-05-3.095e-05-3.298e-05-5.525e-05-2.249e-05-5.682e-05-1.729e-05-2.678e-05
B42.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-072.11e-07...6.517e-072.654e-075.473e-074.896e-075.178e-078.04e-073.673e-078.326e-072.997e-074.322e-07
B5-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09-1.36e-09...-3.556e-09-1.732e-09-3.058e-09-2.78e-09-2.918e-09-4.202e-09-2.144e-09-4.363e-09-1.878e-09-2.508e-09
DTC3333333333...2.033.032.552.583.23.053.273.293.193.13
FD1111111111...1111111111
A-3.35-3.45-3.47-3.56-3.56-3.56-3.56-3.56-3.56-3.56...-3.7489-3.5924-3.5578-3.7566-3.6024-3.4247-3.7445-3.6836-3.73-3.6866
B-0.1161-0.077-0.087-0.075-0.075-0.075-0.075-0.075-0.075-0.075...-0.1287-0.1319-0.1766-0.156-0.2106-0.0951-0.149-0.1483-0.1483-0.104
C40.99740.9720.9890.9950.9950.9950.9950.9950.9950.995...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C50.00260.0280.0120.0050.0050.0050.0050.0050.0050.005...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
IXO5.548.258.495.045.147.87.8588.058.1...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
IXXO3.565.25.453.163.254.925.085.185.395.54...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C61.1731.0671.1371.151.151.151.151.151.151.15...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
C7-0.173-0.067-0.137-0.15-0.15-0.15-0.15-0.15-0.15-0.15...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
NotesSource: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9...Source: Sandia National Laboratories Updated 9......Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2013. Mo...Source: CFV Solar Test Lab. Tested 2014. Mo...
\n

42 rows × 523 columns

\n
", + "text/plain": " -0.355 \nC3 -13.0643 \nA0 0.9327 \nA1 0.07283 \nA2 -0.02402 \nA3 0.003819 \nA4 -0.000235 \nB0 1 \nB1 -0.006801 \nB2 0.0007968 \nB3 -3.095e-05 \nB4 4.896e-07 \nB5 -2.78e-09 \nDTC 2.58 \nFD 1 \nA -3.7566 \nB -0.156 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Canadian_Solar_CS6X_300M__2013_ \\\nVintage 2013 \nArea 1.91 \nMaterial c-Si \nCells_in_Series 72 \nParallel_Strings 1 \nIsco 8.6388 \nVoco 43.5918 \nImpo 8.1359 \nVmpo 34.9531 \nAisc 0.0005 \nAimp -0.0001 \nC0 1.0121 \nC1 -0.0121 \nBvoco -0.1532 \nMbvoc 0 \nBvmpo -0.1634 \nMbvmp 0 \nN 1.0025 \nC2 -0.171 \nC3 -9.39745 \nA0 0.9371 \nA1 0.06262 \nA2 -0.01667 \nA3 0.002168 \nA4 -0.0001087 \nB0 1 \nB1 -0.00789 \nB2 0.0008656 \nB3 -3.298e-05 \nB4 5.178e-07 \nB5 -2.918e-09 \nDTC 3.2 \nFD 1 \nA -3.6024 \nB -0.2106 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n LG_LG290N1C_G3__2013_ \\\nVintage 2013 \nArea 1.64 \nMaterial c-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 9.8525 \nVoco 39.6117 \nImpo 9.2473 \nVmpo 31.2921 \nAisc 0.0002 \nAimp -0.0004 \nC0 1.0145 \nC1 -0.0145 \nBvoco -0.1205 \nMbvoc 0 \nBvmpo -0.1337 \nMbvmp 0 \nN 1.0925 \nC2 -0.4647 \nC3 -11.9008 \nA0 0.9731 \nA1 0.02966 \nA2 -0.01024 \nA3 0.001793 \nA4 -0.0001286 \nB0 1 \nB1 -0.0154 \nB2 0.001572 \nB3 -5.525e-05 \nB4 8.04e-07 \nB5 -4.202e-09 \nDTC 3.05 \nFD 1 \nA -3.4247 \nB -0.0951 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Sharp_NDQ235F4__2013_ \\\nVintage 2013 \nArea 1.56 \nMaterial mc-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 8.6739 \nVoco 36.8276 \nImpo 8.1243 \nVmpo 29.1988 \nAisc 0.0006 \nAimp -0.0002 \nC0 1.0049 \nC1 -0.0049 \nBvoco -0.1279 \nMbvoc 0 \nBvmpo -0.1348 \nMbvmp 0 \nN 1.0695 \nC2 -0.2718 \nC3 -11.4033 \nA0 0.9436 \nA1 0.04765 \nA2 -0.007405 \nA3 0.0003818 \nA4 -1.101e-05 \nB0 1 \nB1 -0.00464 \nB2 0.000559 \nB3 -2.249e-05 \nB4 3.673e-07 \nB5 -2.144e-09 \nDTC 3.27 \nFD 1 \nA -3.7445 \nB -0.149 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Solar_Frontier_SF_160S__2013_ \\\nVintage 2013 \nArea 1.22 \nMaterial CIS \nCells_in_Series 172 \nParallel_Strings 1 \nIsco 2.0259 \nVoco 112.505 \nImpo 1.8356 \nVmpo 86.6752 \nAisc 0.0001 \nAimp -0.0003 \nC0 1.0096 \nC1 -0.0096 \nBvoco -0.3044 \nMbvoc 0 \nBvmpo -0.2339 \nMbvmp 0 \nN 1.2066 \nC2 -0.5426 \nC3 -15.2598 \nA0 0.9354 \nA1 0.06809 \nA2 -0.02094 \nA3 0.00293 \nA4 -0.0001564 \nB0 1 \nB1 -0.0152 \nB2 0.001598 \nB3 -5.682e-05 \nB4 8.326e-07 \nB5 -4.363e-09 \nDTC 3.29 \nFD 1 \nA -3.6836 \nB -0.1483 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n SolarWorld_Sunmodule_250_Poly__2013_ \\\nVintage 2013 \nArea 1.68 \nMaterial mc-Si \nCells_in_Series 60 \nParallel_Strings 1 \nIsco 8.3768 \nVoco 36.3806 \nImpo 7.6921 \nVmpo 28.348 \nAisc 0.0006 \nAimp -0.0001 \nC0 1.0158 \nC1 -0.0158 \nBvoco -0.1393 \nMbvoc 0 \nBvmpo -0.1449 \nMbvmp 0 \nN 1.226 \nC2 -0.09677 \nC3 -8.51148 \nA0 0.9288 \nA1 0.07201 \nA2 -0.02065 \nA3 0.002862 \nA4 -0.0001544 \nB0 1 \nB1 -0.00308 \nB2 0.0004053 \nB3 -1.729e-05 \nB4 2.997e-07 \nB5 -1.878e-09 \nDTC 3.19 \nFD 1 \nA -3.73 \nB -0.1483 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2013. Mo... \n\n Silevo_Triex_U300_Black__2014_ \nVintage 2014 \nArea 1.68 \nMaterial c-Si \nCells_in_Series 96 \nParallel_Strings 1 \nIsco 5.771 \nVoco 68.5983 \nImpo 5.383 \nVmpo 55.4547 \nAisc 0.0003 \nAimp -0.0003 \nC0 0.995 \nC1 0.005 \nBvoco -0.1913 \nMbvoc 0 \nBvmpo -0.184 \nMbvmp 0 \nN 1.345 \nC2 0.3221 \nC3 -6.7178 \nA0 0.9191 \nA1 0.09988 \nA2 -0.04273 \nA3 0.00937 \nA4 -0.0007643 \nB0 1 \nB1 -0.006498 \nB2 0.0006908 \nB3 -2.678e-05 \nB4 4.322e-07 \nB5 -2.508e-09 \nDTC 3.13 \nFD 1 \nA -3.6866 \nB -0.104 \nC4 NaN \nC5 NaN \nIXO NaN \nIXXO NaN \nC6 NaN \nC7 NaN \nNotes Source: CFV Solar Test Lab. Tested 2014. Mo... \n\n[42 rows x 523 columns]" }, + "execution_count": 17, "metadata": {}, - "execution_count": 17 + "output_type": "execute_result" } ], "source": [ - "sandia_modules = pvsystem.retrieve_sam(name='SandiaMod')\n", + "sandia_modules = pvsystem.retrieve_sam(name=\"SandiaMod\")\n", "sandia_modules" ] }, @@ -542,12 +555,12 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": "Vintage 2009\nArea 1.701\nMaterial c-Si\nCells_in_Series 96\nParallel_Strings 1\nIsco 5.09115\nVoco 59.2608\nImpo 4.54629\nVmpo 48.3156\nAisc 0.000397\nAimp 0.000181\nC0 1.01284\nC1 -0.0128398\nBvoco -0.21696\nMbvoc 0\nBvmpo -0.235488\nMbvmp 0\nN 1.4032\nC2 0.279317\nC3 -7.24463\nA0 0.928385\nA1 0.068093\nA2 -0.0157738\nA3 0.0016606\nA4 -6.93e-05\nB0 1\nB1 -0.002438\nB2 0.0003103\nB3 -1.246e-05\nB4 2.11e-07\nB5 -1.36e-09\nDTC 3\nFD 1\nA -3.40641\nB -0.0842075\nC4 0.996446\nC5 0.003554\nIXO 4.97599\nIXXO 3.18803\nC6 1.15535\nC7 -0.155353\nNotes Source: Sandia National Laboratories Updated 9...\nName: Canadian_Solar_CS5P_220M___2009_, dtype: object" }, + "execution_count": 18, "metadata": {}, - "execution_count": 18 + "output_type": "execute_result" } ], "source": [ @@ -568,17 +581,21 @@ "metadata": {}, "outputs": [], "source": [ - "from pvlib import clearsky\n", - "from pvlib import irradiance\n", - "from pvlib import atmosphere\n", "from pvlib.location import Location\n", "\n", - "tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')\n", + "tus = Location(32.2, -111, \"US/Arizona\", 700, \"Tucson\")\n", "\n", - "times_loc = pd.date_range(start=datetime.datetime(2014,4,1), end=datetime.datetime(2014,4,2), freq='30s', tz=tus.tz)\n", - "solpos = pvlib.solarposition.get_solarposition(times_loc, tus.latitude, tus.longitude)\n", + "times_loc = pd.date_range(\n", + " start=datetime.datetime(2014, 4, 1),\n", + " end=datetime.datetime(2014, 4, 2),\n", + " freq=\"30s\",\n", + " tz=tus.tz,\n", + ")\n", + "solpos = pvlib.solarposition.get_solarposition(\n", + " times_loc, tus.latitude, tus.longitude\n", + ")\n", "dni_extra = pvlib.irradiance.get_extra_radiation(times_loc)\n", - "airmass = pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith'])\n", + "airmass = pvlib.atmosphere.get_relative_airmass(solpos[\"apparent_zenith\"])\n", "pressure = pvlib.atmosphere.alt2pres(tus.altitude)\n", "am_abs = pvlib.atmosphere.get_absolute_airmass(airmass, pressure)\n", "cs = tus.get_clearsky(times_loc)\n", @@ -586,15 +603,20 @@ "surface_tilt = tus.latitude\n", "surface_azimuth = 180 # pointing south\n", "\n", - "aoi = pvlib.irradiance.aoi(surface_tilt, surface_azimuth,\n", - " solpos['apparent_zenith'], solpos['azimuth'])\n", - "total_irrad = pvlib.irradiance.get_total_irradiance(surface_tilt,\n", - " surface_azimuth,\n", - " solpos['apparent_zenith'],\n", - " solpos['azimuth'],\n", - " cs['dni'], cs['ghi'], cs['dhi'],\n", - " dni_extra=dni_extra,\n", - " model='haydavies')" + "aoi = pvlib.irradiance.aoi(\n", + " surface_tilt, surface_azimuth, solpos[\"apparent_zenith\"], solpos[\"azimuth\"]\n", + ")\n", + "total_irrad = pvlib.irradiance.get_total_irradiance(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " solpos[\"apparent_zenith\"],\n", + " solpos[\"azimuth\"],\n", + " cs[\"dni\"],\n", + " cs[\"ghi\"],\n", + " cs[\"dhi\"],\n", + " dni_extra=dni_extra,\n", + " model=\"haydavies\",\n", + ")" ] }, { @@ -610,27 +632,31 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "module = sandia_module\n", "\n", "# a sunny, calm, and hot day in the desert\n", - "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']\n", - "temps = pvlib.temperature.sapm_cell(total_irrad['poa_global'], 30, 0, **thermal_params)\n", + "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[\"sapm\"][\n", + " \"open_rack_glass_glass\"\n", + "]\n", + "temps = pvlib.temperature.sapm_cell(\n", + " total_irrad[\"poa_global\"], 30, 0, **thermal_params\n", + ")\n", "\n", "effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(\n", - " total_irrad['poa_direct'], total_irrad['poa_diffuse'],\n", - " am_abs, aoi, module)\n", + " total_irrad[\"poa_direct\"], total_irrad[\"poa_diffuse\"], am_abs, aoi, module\n", + ")\n", "\n", "sapm_1 = pvlib.pvsystem.sapm(effective_irradiance, temps, module)\n", "\n", @@ -646,45 +672,51 @@ "def plot_sapm(sapm_data, effective_irradiance):\n", " \"\"\"\n", " Makes a nice figure with the SAPM data.\n", - " \n", + "\n", " Parameters\n", " ----------\n", " sapm_data : DataFrame\n", " The output of ``pvsystem.sapm``\n", " \"\"\"\n", - " fig, axes = plt.subplots(2, 3, figsize=(16,10), sharex=False, sharey=False, squeeze=False)\n", - " plt.subplots_adjust(wspace=.2, hspace=.3)\n", + " fig, axes = plt.subplots(\n", + " 2, 3, figsize=(16, 10), sharex=False, sharey=False, squeeze=False\n", + " )\n", + " plt.subplots_adjust(wspace=0.2, hspace=0.3)\n", "\n", - " ax = axes[0,0]\n", - " sapm_data.filter(like='i_').plot(ax=ax)\n", - " ax.set_ylabel('Current (A)')\n", + " ax = axes[0, 0]\n", + " sapm_data.filter(like=\"i_\").plot(ax=ax)\n", + " ax.set_ylabel(\"Current (A)\")\n", "\n", - " ax = axes[0,1]\n", - " sapm_data.filter(like='v_').plot(ax=ax)\n", - " ax.set_ylabel('Voltage (V)')\n", + " ax = axes[0, 1]\n", + " sapm_data.filter(like=\"v_\").plot(ax=ax)\n", + " ax.set_ylabel(\"Voltage (V)\")\n", "\n", - " ax = axes[0,2]\n", - " sapm_data.filter(like='p_').plot(ax=ax)\n", - " ax.set_ylabel('Power (W)')\n", + " ax = axes[0, 2]\n", + " sapm_data.filter(like=\"p_\").plot(ax=ax)\n", + " ax.set_ylabel(\"Power (W)\")\n", "\n", - " ax = axes[1,0]\n", - " [ax.plot(effective_irradiance, current, label=name) for name, current in\n", - " sapm_data.filter(like='i_').iteritems()]\n", - " ax.set_ylabel('Current (A)')\n", - " ax.set_xlabel('Effective Irradiance')\n", + " ax = axes[1, 0]\n", + " [\n", + " ax.plot(effective_irradiance, current, label=name)\n", + " for name, current in sapm_data.filter(like=\"i_\").iteritems()\n", + " ]\n", + " ax.set_ylabel(\"Current (A)\")\n", + " ax.set_xlabel(\"Effective Irradiance\")\n", " ax.legend(loc=2)\n", "\n", - " ax = axes[1,1]\n", - " [ax.plot(effective_irradiance, voltage, label=name) for name, voltage in\n", - " sapm_data.filter(like='v_').iteritems()]\n", - " ax.set_ylabel('Voltage (V)')\n", - " ax.set_xlabel('Effective Irradiance')\n", + " ax = axes[1, 1]\n", + " [\n", + " ax.plot(effective_irradiance, voltage, label=name)\n", + " for name, voltage in sapm_data.filter(like=\"v_\").iteritems()\n", + " ]\n", + " ax.set_ylabel(\"Voltage (V)\")\n", + " ax.set_xlabel(\"Effective Irradiance\")\n", " ax.legend(loc=4)\n", "\n", - " ax = axes[1,2]\n", - " ax.plot(effective_irradiance, sapm_data['p_mp'], label='p_mp')\n", - " ax.set_ylabel('Power (W)')\n", - " ax.set_xlabel('Effective Irradiance')\n", + " ax = axes[1, 2]\n", + " ax.plot(effective_irradiance, sapm_data[\"p_mp\"], label=\"p_mp\")\n", + " ax.set_ylabel(\"Power (W)\")\n", + " ax.set_xlabel(\"Effective Irradiance\")\n", " ax.legend(loc=2)\n", "\n", " # needed to show the time ticks\n", @@ -699,15 +731,15 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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+6S4qmMVN0BleUXVYw7OcMeDAay1i4F2eBq5xpiwAmqpK27Bxw2mc1XcwosbqILtfQqmuMF0roqrui47wfY+Ka7qWMUNUNk2VK21O4EpQG9jzNXGmJpAApDkeT65IBJT3IKPG0znqriFFzdQBdv7FExxg+lcFVd1XXaC7X1S3IKPG0znGoxxXWVzPtpUb+B9nM65d+f0dfldFLdoxlTcoh83UJdge5+CKW4wnaviqq7z15+X4hbduMF0rsEY143FeE44R4wx4YC1hdxiV9yiGVNxi37cQBVs71MwxQ2mc1VcyU6wvU+KWzRjKm7RlauGqoiIiIiIiEhBy8lgSiIiIiIiIiKFRg1VERERERER8StqqIqIiIiIiIhfUUNVRERERERE/IoaqiIiIiIiIuJX1FAVERERERERv6KGqoiIiIiIiPgVNVRFRERERETEr6ihKiIiIiIiIn5FDVURERERERHxK2qoioiIiIiIiF9RQ1VERERERET8ihqqIiIiIiIi4lfUUBURERERERG/ooaqiIiIiIiI+BU1VEVERERERMSvqKEqIiIiIiIifkUNVREREREREfEraqiKiIiIiIiIX1FDVURERERERPyKGqoiIiIiIiLiV9RQFREREREREb+ihqqIiIiIiIj4FTVURURERERExK+ooSoiIiIiIiJ+RQ1VERERERER8StqqIqIiIiIiIhfCXM7gazExMTYmjVrup2GiPiRxYsX77PWVnA7D19SXScimamuE5FgkFVd59cN1Zo1a/Lnn3+6nYaI+BFjzGa3c/A11XUikpnqOhEJBlnVdbr1V0RERERERPyKGqoiIiIiIiLiV9RQFREREREREb/i131UzyQpKYlt27aRkJDgdioFKjIyktjYWMLDw91ORURERKRABMvnusz0OU8kewHXUN22bRvR0dHUrFkTY4zb6RQIay379+9n27Zt1KpVy+10RERERApEMHyuy0yf80RyJuBu/U1ISKB8+fJFujIzxlC+fPmg+3ZRREREgkswfK7LTJ/zRHIm4K6oAkFRmQXDOYoAJCSlEBke6nYaInmWkmpJTE7hZHIqJ5NTSfQuKSSlWFJSU0lJdfYDMAYMafV8WhnAYAyEGENYiCE0xBAeaggPDSEyPJSIsBAiwpzHkBD9n5CiIRg/8wTjOUtwOHEyheLFfPO5LiAbqiIS+PYfTaTl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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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+6S4qmMVN0BleUXVYw7OcMeDAay1i4F2eBq5xpiwAmqpK27Bxw2mc1XcwosbqILtfQqmuMF0roqrui47wfY+Ka7qWMUNUNk2VK21O4EpQG9jzNXGmJpAApDkeT65IBJT3IKPG0znqriFFzdQBdv7FExxg+lcFVd1XXaC7X1S3IKPG0znGoxxXWVzPtpUb+B9nM65d+f0dfldFLdoxlTcoh83UJdge5+CKW4wnaviqq7z15+X4hbduMF0rsEY143FeE44R4wx4YC1hdxiV9yiGVNxi37cQBVs71MwxQ2mc1VcyU6wvU+KWzRjKm7RlauGqoiIiIiIiEhBy8lgSiIiIiIiIiKFRg1VERERERER8StqqIqIiIiIiIhfUUNVRERERERE/IoaqiIiIiIiIuJX1FAVERERERERv6KGqoiIiIiIiPgVNVRFRERERETEr6ihKiIiIiIiIn5FDVURERERERHxK2qoioiIiIiIiF9RQ1VERERERET8ihqqIiIiIiIi4lfUUBURERERERG/ooaqiIiIiIiI+BU1VEVERERERMSvqKEqIiIiIiIifkUNVREREREREfEraqiKiIiIiIiIX1FDVURERERERPyKGqoiIiIiIiLiV9RQFREREREREb+ihqqIiIiIiIj4FTVURURERERExK+ooSoiIiIiIiJ+RQ1VERERERER8StqqIqIiIiIiIhfCXM7gazExMTYmjVrup2GiPiRxYsX77PWVnA7D19SXScimamuE5FgkFVd59cN1Zo1a/Lnn3+6nYaI+BFjzGa3c/A11XUikpnqOhEJBlnVdbr1V0RERERERPyKGqoiIiIiIiLiV9RQFREREREREb/i131UzyQpKYlt27aRkJDgdioFKjIyktjYWMLDw91ORURERKRABMvnusz0OU8kewHXUN22bRvR0dHUrFkTY4zb6RQIay379+9n27Zt1KpVy+10RERERApEMHyuy0yf80RyJuBu/U1ISKB8+fJFujIzxlC+fPmg+3ZRREREgkswfK7LTJ/zRHIm4K6oAkFRmQXDOYoAJCSlEBke6nYaInmWkmpJTE7hZHIqJ5NTSfQuKSSlWFJSU0lJdfYDMAYMafV8WhnAYAyEGENYiCE0xBAeaggPDSEyPJSIsBAiwpzHkBD9n5CiIRg/8wTjOUtwOHEyheLFfPO5LiAbqiIS+PYfTaTl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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ @@ -727,19 +759,21 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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c4U8rRaCfS395gOWj6eV4dGBDI8OZP5Yo40tr2vmnZ0lvPBFAbe/uIPUuGAeXphKarz79YI88MADJCQkcNdddwHwm9/8hoCAAO67774TjtNa89Of/pS1a9eilOJXv/oVixcvBuD3v/89L774IhaLhQULFvDII4/0+fsQwpVorXnykzz+9N/9xAb58PSNE7lsTHSXba9SimFRgfzi8hTumjOMJz7O5Z+fF/D5gUqeuGEC6QkhJryD/qegoID58+czceJEduzYwZgxY3jhhRfw8/M75djExESWLFnC2rVr8fDwYMWKFfz85z8nLy+P+++/n9tvv53169fz4IMPEhgYSF5eHnPmzOGpp57CYrmwUY6SqAqX81luBbc8v43IQG/e/sH0jt5Rh3aws2Inuyt3U9lUSaBXIClhKUyMnoiPhw9XjhvElORwvv/v7dz18g4q68dw09REc99ML/vt23v4+sixHj3n6EFB/PqqMef9+pdffpnLLruMX/7yl9jtdhobG6moqODWW29lw4YNJCUlcfTo0R6MWIgzyyk5xm3/3sbho018e/Jg7r54OFEy96nHRAX6sGx6EjdNTeTtnUd46L0crnlyIz+bP4rbZiaf940AM9q3xYsXc88993Qkqq+99hrvv//+KcetXr2arKwsdu7cSWVlJZMmTWLmzJlkZWXx5ptvsnnzZvz8/KStEwOeze7g/lW7WPNVMdemD+J/rh1LoM+53bgL9vXkl1eMZkFqLD98+SsW//0Lnr5xInNGRfVy1H3HzM9x+/bt49lnn2XatGksW7aMp5566pSbcu0GDx5MVlYW9957LzfffDOff/45zc3NjB07lttvvx2ALVu28PXXXzNkyBDmz5/P6tWrue666y7ovUiiKlzK1oKj3PrCNpIi/HnxlslEBHhT21LLK3tf4bV9r1HRZFRH9VAe2LQNgEDPQC5Pvpzvjf0esQGxvHTLZH70ylf8+q09+HpZWZSRYOZbGnAmTZrEsmXLaGtr49prryU9PZ3169czc+bMjrWzwsLCTI5SDBTr95Vz50s7CPTx4LXvX0Rmkvzf6y1Wi+La8XHMGRXFL1Zn839r97KvrI4/XpeG1U2GAo8fP57y8nKOHDlCRUUFoaGhJCSc+jdk48aNLFmyBKvVSnR0NLNmzWLr1q18+umnLF26tKNXQto6MZA5HJr7Xt/Jf7KO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RAKKElUREREREREJKEpURUREREREJKAoURUREREREZGAokRVREREREREAooSVREREREREQkoSlRFREREREQkoChRFRERERERkYCiRFVEREREREQCihJVERERERERCShKVEVERERERCSgKFEVERERERGRgKJEVURERERERAKKElUREREREREJKEpURUREREREJKAoURUREREREZGAokRVREREREREAooSVREREREREQkoSlRFREREREQkoChRFRERERERkYCiRFVEREREREQCihJVERERERERCShKVEVERERERCSgKFEVERERERGRgKJEVURERERERAJKhNsBnEm5cuVsjRo13A5DRALI4sWL91hry7sdhz/pWicip9K1TkRCwZmudQGdqNaoUYNFixa5HYaIBBBjzCa3Y/A3XetE5FS61olIKDjTtU6P/oqIiIiIiEhAUaIqIpJLxphSxpjPjDErjTErjDEXGWPKGGN+NMas8b6XdjtOERERkWCjRFVEJPdeAaZba+sDTYEVwFDgJ2ttXeAnb1tEREREciCgx6hmJSUlhcTERJKSktwOJV9FR0cTFxdHZGSk26GISBaMMSWBi4H+ANbaZCDZGHMV0MG72XvATOChgo9QRCTwhcr3ulPpe57I2QVdopqYmEhsbCw1atTAGON2OPnCWsvevXtJTEykZs2abocjIlmrCewG3jHGNAUWA/cAFa21273b7AAquhSfiEjAC4XvdafS9zyR7Am6R3+TkpIoW7Zsob6YGWMoW7ZsyN1dFAkyEcD5wOvW2mbAUU55zNdaawGb1c7GmIHGmEXGmEW7d+/O92BFRAJRKHyvO5W+54lkT9D1qAIhcTELhZ9RBCApJY3oyHC3w8iNRCDRWrvA2/4MJ1HdaYypbK3dboypDOzKamdr7XhgPEDz5s2zTGZF8pvHY0lO85CS5iElzZKS5iE51WmneSypHkuax+KxFmvT77pYa7GAtXByrfV+eHL9yW0yf5Z+EGMMxkCYMYQZpx0RZgj3vjIuO+0wwsIgIizsn9t4j6W/ncEpFH9vofgzS2jw5/e6oExURST47T58ghajZgCwdlRXIsKD6wEPa+0OY8wWY0w9a+0q4FLgb++rH/Cs9/1LF8MUl6SmeTh4PIX9x1I4cCyZ/cdS2H8smQPHkjlwLON6p33gWApHTqRyNDnVl9RJcChW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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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c4U8rRaCfS395gOWj6eV4dGBDI8OZP5Yo40tr2vmnZ0lvPBFAbe/uIPUuGAeXphKarz79YI88MADJCQkcNdddwHwm9/8hoCAAO67774TjtNa89Of/pS1a9eilOJXv/oVixcvBuD3v/89L774IhaLhQULFvDII4/0+fsQwpVorXnykzz+9N/9xAb58PSNE7lsTHSXba9SimFRgfzi8hTumjOMJz7O5Z+fF/D5gUqeuGEC6QkhJryD/qegoID58+czceJEduzYwZgxY3jhhRfw8/M75djExESWLFnC2rVr8fDwYMWKFfz85z8nLy+P+++/n9tvv53169fz4IMPEhgYSF5eHnPmzOGpp57CYrmwUY6SqAqX81luBbc8v43IQG/e/sH0jt5Rh3aws2Inuyt3U9lUSaBXIClhKUyMnoiPhw9XjhvElORwvv/v7dz18g4q68dw09REc99ML/vt23v4+sixHj3n6EFB/PqqMef9+pdffpnLLruMX/7yl9jtdhobG6moqODWW29lw4YNJCUlcfTo0R6MWIgzyyk5xm3/3sbho018e/Jg7r54OFEy96nHRAX6sGx6EjdNTeTtnUd46L0crnlyIz+bP4rbZiaf940AM9q3xYsXc88993Qkqq+99hrvv//+KcetXr2arKwsdu7cSWVlJZMmTWLmzJlkZWXx5ptvsnnzZvz8/KStEwOeze7g/lW7WPNVMdemD+J/rh1LoM+53bgL9vXkl1eMZkFqLD98+SsW//0Lnr5xInNGRfVy1H3HzM9x+/bt49lnn2XatGksW7aMp5566pSbcu0GDx5MVlYW9957LzfffDOff/45zc3NjB07lttvvx2ALVu28PXXXzNkyBDmz5/P6tWrue666y7ovUiiKlzK1oKj3PrCNpIi/HnxlslEBHhT21LLK3tf4bV9r1HRZFRH9VAe2LQNgEDPQC5Pvpzvjf0esQGxvHTLZH70ylf8+q09+HpZWZSRYOZbGnAmTZrEsmXLaGtr49prryU9PZ3169czc+bMjrWzwsLCTI5SDBTr95Vz50s7CPTx4LXvX0Rmkvzf6y1Wi+La8XHMGRXFL1Zn839r97KvrI4/XpeG1U2GAo8fP57y8nKOHDlCRUUFoaGhJCSc+jdk48aNLFmyBKvVSnR0NLNmzWLr1q18+umnLF26tKNXQto6MZA5HJr7Xt/Jf7KO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RAKKElUREREREREJKEpURUREREREJKAoURUREREREZGAokRVREREREREAooSVREREREREQkoSlRFREREREQkoChRFRERERERkYCiRFVEREREREQCihJVERERERERCShKVEVERERERCSgKFEVERERERGRgKJEVURERERERAKKElUREREREREJKEpURUREREREJKAoURUREREREZGAokRVREREREREAooSVREREREREQkoSlRFREREREQkoChRFRERERERkYCiRFVEREREREQCihJVERERERERCShKVEVERERERCSgKFEVERERERGRgKJEVURERERERAJKhNsBnEm5cuVsjRo13A5DRALI4sWL91hry7sdhz/pWicip9K1TkRCwZmudQGdqNaoUYNFixa5HYaIBBBjzCa3Y/A3XetE5FS61olIKDjTtU6P/oqIiIiIiEhAUaIqIpJLxphSxpjPjDErjTErjDEXGWPKGGN+NMas8b6XdjtOERERkWCjRFVEJPdeAaZba+sDTYEVwFDgJ2ttXeAnb1tEREREciCgx6hmJSUlhcTERJKSktwOJV9FR0cTFxdHZGSk26GISBaMMSWBi4H+ANbaZCDZGHMV0MG72XvATOChgo9QRCTwhcr3ulPpe57I2QVdopqYmEhsbCw1atTAGON2OPnCWsvevXtJTEykZs2abocjIlmrCewG3jHGNAUWA/cAFa21273b7AAquhSfiEjAC4XvdafS9zyR7Am6R3+TkpIoW7Zsob6YGWMoW7ZsyN1dFAkyEcD5wOvW2mbAUU55zNdaawGb1c7GmIHGmEXGmEW7d+/O92BFRAJRKHyvO5W+54lkT9D1qAIhcTELhZ9RBCApJY3oyHC3w8iNRCDRWrvA2/4MJ1HdaYypbK3dboypDOzKamdr7XhgPEDz5s2zTGZF8pvHY0lO85CS5iElzZKS5iE51WmneSypHkuax+KxFmvT77pYa7GAtXByrfV+eHL9yW0yf5Z+EGMMxkCYMYQZpx0RZgj3vjIuO+0wwsIgIizsn9t4j6W/ncEpFH9vofgzS2jw5/e6oExURST47T58ghajZgCwdlRXIsKD6wEPa+0OY8wWY0w9a+0q4FLgb++rH/Cs9/1LF8MUl6SmeTh4PIX9x1I4cCyZ/cdS2H8smQPHkjlwLON6p33gWApHTqRyNDnVl9RJcChW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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "temps = pvlib.temperature.sapm_cell(total_irrad['poa_global'], 5, 10, **thermal_params)\n", + "temps = pvlib.temperature.sapm_cell(\n", + " total_irrad[\"poa_global\"], 5, 10, **thermal_params\n", + ")\n", "\n", "sapm_2 = pvlib.pvsystem.sapm(effective_irradiance, temps, module)\n", "\n", @@ -752,23 +786,23 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "sapm_1['p_mp'].plot(label='30 C, 0 m/s')\n", - "sapm_2['p_mp'].plot(label=' 5 C, 10 m/s')\n", + "sapm_1[\"p_mp\"].plot(label=\"30 C, 0 m/s\")\n", + "sapm_2[\"p_mp\"].plot(label=\" 5 C, 10 m/s\")\n", "plt.legend()\n", - "plt.ylabel('Pmp')\n", - "plt.title('Comparison of a hot, calm day and a cold, windy day');" + "plt.ylabel(\"Pmp\")\n", + "plt.title(\"Comparison of a hot, calm day and a cold, windy day\");" ] }, { @@ -792,7 +826,8 @@ "outputs": [], "source": [ "import warnings\n", - "warnings.simplefilter('ignore', np.RankWarning)" + "\n", + "warnings.simplefilter(\"ignore\", np.RankWarning)" ] }, { @@ -804,21 +839,24 @@ "def sapm_to_ivframe(sapm_row):\n", " pnt = sapm_row\n", "\n", - " ivframe = {'Isc': (pnt['i_sc'], 0),\n", - " 'Pmp': (pnt['i_mp'], pnt['v_mp']),\n", - " 'Ix': (pnt['i_x'], 0.5*pnt['v_oc']),\n", - " 'Ixx': (pnt['i_xx'], 0.5*(pnt['v_oc']+pnt['v_mp'])),\n", - " 'Voc': (0, pnt['v_oc'])}\n", - " ivframe = pd.DataFrame(ivframe, index=['current', 'voltage']).T\n", - " ivframe = ivframe.sort_values(by='voltage')\n", - " \n", + " ivframe = {\n", + " \"Isc\": (pnt[\"i_sc\"], 0),\n", + " \"Pmp\": (pnt[\"i_mp\"], pnt[\"v_mp\"]),\n", + " \"Ix\": (pnt[\"i_x\"], 0.5 * pnt[\"v_oc\"]),\n", + " \"Ixx\": (pnt[\"i_xx\"], 0.5 * (pnt[\"v_oc\"] + pnt[\"v_mp\"])),\n", + " \"Voc\": (0, pnt[\"v_oc\"]),\n", + " }\n", + " ivframe = pd.DataFrame(ivframe, index=[\"current\", \"voltage\"]).T\n", + " ivframe = ivframe.sort_values(by=\"voltage\")\n", + "\n", " return ivframe\n", "\n", + "\n", "def ivframe_to_ivcurve(ivframe, points=100):\n", - " ivfit_coefs = np.polyfit(ivframe['voltage'], ivframe['current'], 30)\n", - " fit_voltages = np.linspace(0, ivframe.loc['Voc', 'voltage'], points)\n", + " ivfit_coefs = np.polyfit(ivframe[\"voltage\"], ivframe[\"current\"], 30)\n", + " fit_voltages = np.linspace(0, ivframe.loc[\"Voc\", \"voltage\"], points)\n", " fit_currents = np.polyval(ivfit_coefs, fit_voltages)\n", - " \n", + "\n", " return fit_voltages, fit_currents" ] }, @@ -828,23 +866,29 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "times = ['2014-04-01 07:00:00', '2014-04-01 08:00:00', '2014-04-01 09:00:00', \n", - " '2014-04-01 10:00:00', '2014-04-01 11:00:00', '2014-04-01 12:00:00']\n", + "times = [\n", + " \"2014-04-01 07:00:00\",\n", + " \"2014-04-01 08:00:00\",\n", + " \"2014-04-01 09:00:00\",\n", + " \"2014-04-01 10:00:00\",\n", + " \"2014-04-01 11:00:00\",\n", + " \"2014-04-01 12:00:00\",\n", + "]\n", "times.reverse()\n", "\n", - "fig, ax = plt.subplots(1, 1, figsize=(12,8))\n", + "fig, ax = plt.subplots(1, 1, figsize=(12, 8))\n", "\n", "for time in times:\n", " ivframe = sapm_to_ivframe(sapm_1.loc[time])\n", @@ -852,12 +896,12 @@ " fit_voltages, fit_currents = ivframe_to_ivcurve(ivframe)\n", "\n", " ax.plot(fit_voltages, fit_currents, label=time)\n", - " ax.plot(ivframe['voltage'], ivframe['current'], 'ko')\n", - " \n", - "ax.set_xlabel('Voltage (V)')\n", - "ax.set_ylabel('Current (A)')\n", + " ax.plot(ivframe[\"voltage\"], ivframe[\"current\"], \"ko\")\n", + "\n", + "ax.set_xlabel(\"Voltage (V)\")\n", + "ax.set_ylabel(\"Current (A)\")\n", "ax.set_ylim(0, None)\n", - "ax.set_title('IV curves at multiple times')\n", + "ax.set_title(\"IV curves at multiple times\")\n", "ax.legend();" ] }, @@ -881,15 +925,22 @@ "metadata": {}, "outputs": [], "source": [ - "photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth = (\n", - " pvsystem.calcparams_desoto(total_irrad['poa_global'],\n", - " temp_cell=temps,\n", - " alpha_sc=cecmodule['alpha_sc'],\n", - " a_ref=cecmodule['a_ref'],\n", - " I_L_ref=cecmodule['I_L_ref'],\n", - " I_o_ref=cecmodule['I_o_ref'],\n", - " R_sh_ref=cecmodule['R_sh_ref'],\n", - " R_s=cecmodule['R_s']) )" + "(\n", + " photocurrent,\n", + " saturation_current,\n", + " resistance_series,\n", + " resistance_shunt,\n", + " nNsVth,\n", + ") = pvsystem.calcparams_desoto(\n", + " total_irrad[\"poa_global\"],\n", + " temp_cell=temps,\n", + " alpha_sc=cecmodule[\"alpha_sc\"],\n", + " a_ref=cecmodule[\"a_ref\"],\n", + " I_L_ref=cecmodule[\"I_L_ref\"],\n", + " I_o_ref=cecmodule[\"I_o_ref\"],\n", + " R_sh_ref=cecmodule[\"R_sh_ref\"],\n", + " R_s=cecmodule[\"R_s\"],\n", + ")" ] }, { @@ -898,20 +949,20 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "photocurrent.plot()\n", - "plt.ylabel('Light current I_L (A)');" + "plt.ylabel(\"Light current I_L (A)\");" ] }, { @@ -920,20 +971,20 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "saturation_current.plot()\n", - "plt.ylabel('Saturation current I_0 (A)');" + "plt.ylabel(\"Saturation current I_0 (A)\");" ] }, { @@ -942,12 +993,12 @@ "metadata": {}, "outputs": [ { - "output_type": "execute_result", "data": { "text/plain": "1.066023" }, + "execution_count": 31, "metadata": {}, - "execution_count": 31 + "output_type": "execute_result" } ], "source": [ @@ -960,20 +1011,20 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "resistance_shunt.plot()\n", - "plt.ylabel('Shunt resistance (ohms)')\n", + "plt.ylabel(\"Shunt resistance (ohms)\")\n", "plt.ylim(0, 5000);" ] }, @@ -983,20 +1034,20 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAY4AAAD4CAYAAAD7CAEUAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3deXxdZbn3/883Q+d0TNJ5btPSFjqQlqmlpWUogwyCgB4RPCjiBHh4VI7PwZ+Pj+c5Cgp6RPGAKIIIiowiUqa2UJBCWjqn6USndEhaOqRD2gzX74+1AtuYNEmbvdfe2df79cqrO2vY+5t0Z19r3ete9y0zwznnnGuujKgDOOecSy1eOJxzzrWIFw7nnHMt4oXDOedci3jhcM451yJeOJxzzrVIVtQBEiE3N9eGDBkSdQznnEspixYt2mVmefWXp0XhGDJkCEVFRVHHcM65lCJpU0PLvanKOedci3jhcM451yJeOJxzzrWIFw7nnHMt4oXDOedci3jhcM451yJp0R3XuWRWWVXDOxt2s2DtLtaUHWDnvkqqamvp3jGbQT07ceqQnpwzKo8BPTpFHdU5wAuHc5Epq6jkt29t5A8LN7PvcBXtszIY2bsLg3t1Ijsrgz0Hj/L3Dbt5dsk2JJg6IpcvnT2cqSNzo47u0lzcCoekgcAjQG/AgAfM7GcNbDcD+CmQDewys+nh8o1ABVADVJtZYbi8J/BHYAiwEbjazPbE6+dwrrVV1dTy8Fsb+dlrazl0tJoLxvbh6sKBnDG8Fx2yM/9hWzNj4+5DPLeklCfe3cJnH1rItJG5/ODycQzu1Tmin8ClO8VrBkBJfYG+ZrZYUg6wCLjczFbFbNMdeBuYbWabJeWbWVm4biNQaGa76j3vXcCHZvZDSXcAPczs28fKUlhYaH7nuEsGW/cc4uuPv8/7m/cyc3Q+/3HxSQzL69KsfSuravj9O5v42atrqa41/uOSk/jMlEFIinNql64kLao7aI8VtzMOM9sObA8fV0gqBvoDq2I2+wzwtJltDrcra8ZTXwbMCB//DpgHHLNwOJcM3lq3i688tpjaWuPnn57IJ8b3a9H+HbIz+cK0YVx0cl++/dQy/vczK1hRuo//c+k42mV5PxeXOAl5t0kaAkwEFtZbVQD0kDRP0iJJn4tZZ8DL4fKbYpb3DosSwA6CpjDnktpflm7jht++S++u7XnhlqktLhqx+nXvyO8+P4WvzBjO4+9u4V8ffo/DR2taMa1zxxb3wiGpC/AUcJuZ7a+3Ogs4FbgYuAC4U1JBuG6qmU0CLgS+Kuns+s9tQTtbg21tkm6SVCSpqLy8vJV+Guda7unFW7nlifeZOLAHT37pzFa5NpGRIb41ezR3X3UKb6/fxecffpeDR6pbIa1zTYtr4ZCUTVA0HjOzpxvYZCswx8wOhtcy3gDGA5hZafhvGfAMMCXcZ2d4/aTuOkqDzVtm9oCZFZpZYV7eP40K7FxCvLRiB9/88zLOGNaLR26cQrdO2a36/J8qHMi910zg3Q8+5KZHizhaXduqz+9cQ+JWOBRcsXsIKDazexrZ7DlgqqQsSZ2A04BiSZ3DC+pI6gycD6wI93keuD58fH34HM4lnfc2fsgtj7/PKQO68eDnCv+px1RruWxCf+66ajxvrdvNHU8vI14dXpyrE8/7OM4CrgOWS1oSLvsOMAjAzH5lZsWSXgKWAbXAr81shaRhwDNhb5Es4A9m9lL4HD8E/iTpRmATcHUcfwbnjsu2vYf58u8X0b9HR357w2Q6t4/vLVNXnTqA0j2HuffVNQzp1ZlbZo2M6+u59BbPXlULgCb7CZrZ3cDd9ZZtIGyyamD73cCs1sjoXDxUVtVw06NFHKmq5YmbCuneqV1CXveWWSPYtPsg9766hgkDu3N2gTfRuvjwPnzOtbL/+8IqVpTu56fXTmBEfvPu0WgNkvjBFeMoyM/h1ifeZ9vewwl7bZdevHA414peWbWTxxZu5ktnD2PWSYnvKd6pXRa//OwkjlbXcusT71NT69c7XOvzwuFcKynbX8m3n1rG2H5d+bfzC5reIU6G53Xh+5eN472Ne/jtWx9ElsO1XV44nGsFZsZ3nlnOoaPV/OzaCbTPik8Pqub65KT+nHtSb+6aU8K6sopIs7i2xwuHc63gpRU7eLW4jNvPG8WI/Jyo4yCJ//fJcXRul8ntTy7zJivXqrxwOHeC9h2u4v97fiVj+3Xl82cNiTrOR/JzOvC9S8eydMteHn93c9RxXBvihcO5E3TXS6vZdeAIP/zkKWRlJtef1KXj+3HGsF7cPaeE3QeORB3HtRHJ9S53LsWs3LaPP7y7mevPHMLJA7pFHeefSOL7l43l4JFqfvTS6qjjuDbCC4dzx8nM+MELxXTvmM1t50bXi6opI3vncOO0ofypaCvvb/Y5z9yJ88Lh3HF6tbiMv2/YzTfOK6Bbx9YdvLC13TJzJLld2vNfL672sazcCfPC4dxxOFpdy/97sZjheZ359JRBUcdpUuf2Wdx27kje3fghrxY3Z7405xrnhcO54/D4u5v5YNdB/uPiMWQn2QXxxlwzeSDDcjvzo5dWU13jw6+745ca73jnkkhlVQ33zV3HlKE9mTEqdQYSzM7M4FuzR7Ou7ABPLtoadRyXwrxwONdCv39nE+UVR7j9vALCof9TxgVjezNpUHf++7W1HKn26Wbd8YnnRE4DJc2VtErSSkm3NrLdDElLwm3mN7WvpO9JKg33WSLponj9DM7Vd/BINffPW8/UEbmcNqxX1HFaTBK3nVvA9n2VPFnkZx3u+MRzdplq4HYzWxzO5rdI0itmtqpuA0ndgV8Cs81ss6T8Zu57r5n9OI7ZnWvQI3/fxO6DR/nGecnb/bYp00bmMnFQd+6ft56rCwfSLssbHlzLxO0dY2bbzWxx+LgCKAb619vsM8DTZrY53K6sBfs6l1AHj1TzP2+sZ8aoPE4d3CPqOMdNErfOGknp3sP82a91uOOQkEMNSUOAicDCeqsKgB6S5klaJOlzzdz3a5KWSfqNpNT9C3Yp5fF3N7P3UFWbmJZ1ekEe4wd25xdz13G02ntYuZaJe+GQ1AV4CrjNzPbXW50FnApcDFwA3CmpoIl97weGAxOA7cBPGnndmyQVSSoqLy9vzR/JpaGqmloeWvABU4b2ZNKg1D9WCc46RlC69zDPL90WdRyXYuJaOCRlE3zwP2ZmTzewyVZgjpkdNLNdwBuEc403tq+Z7TSzGjOrBR4EpjT02mb2gJkVmllhXl7qdJl0yen5JdvYvq+SL08fHnWUVnPOqHxG9c7h129u8LvJXYvEs1eVgIeAYjO7p5HNngOmSsqS1Ak4DSg+1r6S+sZ8ewWwovXTO/ex2lrjf95Yz6jeOSl130ZTJPGFaUNZvaOCN9fuijqOSyHxPOM4C7gOmBnbdVbSzZJuBjCzYuAlYBnwLvBrM1vR2L7h894labmkZcA5wDfi+DM4x7w1ZazZeYAvTR+WcvdtNOXSCf3Iz2nPg29uiDqKSyFx645rZguAJv/KzOxu4O7m7mtm17VKQOea6VfzN9C/e0c+Mb5f1FFaXfusTG44awh3vVTCqm37GdOva9SRXArwDtzOHcOqbft594MPuf7MwSkzJlVL/cuUwXRql8mv/azDNVPb/EtwrpU88veNdMjO4OrCgVFHiZtunbK5ZvJAnl+6jbL9lVHHcSnAC4dzjdh76CjPLinlion96d6pXdRx4upzZwyhutZ44r0tUUdxKcALh3ON+ON7W6isquX6M4dEHSXuhuZ25uyCPP6wcLMPue6a5IXDuQbU1BqPvrOJ04b2ZHSf9LhgfN3pg9mxv5JXVu2MOopLcl44nGvA66vL2LrnMDekwdlGnZmj8+nfvSOPvrMp6iguyXnhcK4Bj/x9I327deC8Mb2jjpIwmRniX04fxNvrd7OurCLqOC6JeeFwrp4tHx7izbW7uHbyILLaaBfcxlxTOJB2mRk8+nc/63CNS6+/Cuea4U9FW8gQfKpwQNRREq5Xl/ZcdHIfnn6/lMoqnyHQNcwLh3MxqmtqebJoK9ML8ujXvWPUcSJxzeRBVFRW87cV26OO4pKUFw7nYryxtpwd+yu5ZvKgqKNE5vRhPRncqxN/9Hs6XCO8cDgX44l3t5DbpR2zTspveuM2ShJXFw7knQ0fsmn3wajjuCTkhcO5UNn+Sl5bXcaVpw5os+NSNdeVkwaQoeB6j3P1pfdfh3Mx/rx4KzW1xjVteFyq5urTrQPTC/L486Ktfie5+yfxnMhpoKS5klZJWinp1ka2mxHOt7FS0vyY5bMllUhaJ+mOmOVDJS0Ml/9RUtseRMglhJnxp/e2MGVoT4bldYk6TlK4ZvJAdu4/whtrfepl94/iecZRDdxuZmOA04GvShoTu4Gk7sAvgUvNbCzwqXB5JvAL4EJgDPDpmH1/BNxrZiOAPcCNcfwZXJpYvHkPG3cf4lOnpl8X3MbMHN2bXp3b+UVy90/iVjjMbLuZLQ4fVwDFQP96m30GeNrMNofblYXLpwDrzGyDmR0FngAuC6eUnQn8Odzud8Dl8foZXPp4enEpHbIzuPDkvk1vnCbaZWVwxcT+vL66jD0Hj0YdxyWRhFzjkDQEmAgsrLeqAOghaZ6kRZI+Fy7vD8Qe5mwNl/UC9ppZdb3lDb3mTZKKJBWVl/uptmvc0epaXli2nfPH9KFL+7hNipmSrpjUn6oa46/L/Z4O97G4Fw5JXYCngNvMbH+91VnAqcDFwAXAnZIKWuN1zewBMys0s8K8vLzWeErXRs0tKWPf4SqumNTgMUhaG9O3KwW9u/Ds+6VRR3FJJK6FQ1I2QdF4zMyebmCTrcAcMztoZruAN4DxQCkQ27VlQLhsN9BdUla95c4dt2cWl5LbpR3TRuRGHSXpSOLyif0p2rSHzbsPRR3HJYl49qoS8BBQbGb3NLLZc8BUSVmSOgGnEVwLeQ8YGfagagdcCzxvZgbMBa4K978+fA7njsu+Q1W8vrqMT4zvl3YDGjbXZROCM7HnlvgxmgvE8y/lLOA6YGbY3XaJpIsk3SzpZgAzKwZeApYB7wK/NrMV4TWMrwFzCArJn8xsZfi83wb+TdI6gmseD8XxZ3Bt3AvLt3G0ppZPTvTeVI3p370jpw3tyTNLSgmO3Vy6i9uVQDNbAKgZ290N3N3A8heBFxtYvoGg15VzJ+zZ90sZkd+Fcf3TY5a/43XFxP7c8fRylpfu45QB3aOO4yLm5+YubW358BDvbdzDFRP7E7SsusZceHJf2mVm8IxfJHd44XBp7Pml2wC4bEK/iJMkv24ds5k5Op+/LN3mQ5A4Lxwufb2wbDuTBnVnQI9OUUdJCZdP7M+uA0d5e/3uqKO4iHnhcGlpffkBirfv55JT/GyjuWaMyqNzu0xe9JsB054XDpeW/rpsOxJc5EOMNFuH7EzOG9Obl1buoMqbq9KaFw6Xlv66bDuTB/ekT7cOUUdJKRef0o+9h6p4a92uqKO4CHnhcGln7c4KSnZWcPEpfrbRUtNG5pLTPou/LvPmqnTmhcOlnRfCZqoLx/WJOkrKqWuumrNyB0ervbkqXXnhcGnFzHhh2TZOG9qT/K7eTHU8Lhnfl/2V1d5clca8cLi0UrKzgvXlB7nYe1Mdt6kj8sjpkMUL3lyVtrxwuLTywtLtZHgz1Qlpl5XBBWP78PKqHRyprok6jouAFw6XNsyCCYnOGN6L3C7to46T0i4+pS8VldUsWOvNVenIC4dLG6t3VPDBroN+70YrOGt4Lt06ZnvvqjQVz/k4BkqaK2mVpJWSbm1gmxmS9sUMu/7dcPmomGVLJO2XdFu47nuSSmOHao/Xz+DaljkrdyDB+WO8mepEtcvK4NyTevNq8U6/GTANxXOC5WrgdjNbLCkHWCTpFTNbVW+7N83sktgFZlYCTACQlEkwy98zMZvca2Y/jmN21wbNWbmTUwf1IC/Hm6lawwVje/PU4q28s2E300b69MzpJG5nHGa23cwWh48rCCZkOp5JnWcB681sU2vmc+lly4eHKN6+nwvG+tlGazm7II+O2ZnMWbkj6iguwRJyjUPSEGAisLCB1WdIWirpb5LGNrD+WuDxesu+JmmZpN9I6tG6aV1bVPfh5oWj9XTIzmR6QR4vr9xJba3PDJhOml04JJ0p6TOSPlf31cz9ugBPAbeZ2f56qxcDg81sPPBz4Nl6+7YDLgWejFl8PzCcoClrO/CTRl73JklFkorKy8ubE9W1YXNW7mB0nxwG9fIh1FvTBeN6U1ZxhCVb90YdxSVQswqHpEeBHwNTgcnhV2Ez9ssmKBqPmdnT9deb2X4zOxA+fhHIlpQbs8mFwGIz2xmzz04zqzGzWuBBGplG1sweMLNCMyvMy/P213RWXnGEok17mO33brS6maN6k5Uhb65KM829OF4IjLEWzFSvYC7Oh4BiM7unkW36ADvNzCRNIShksbPEfJp6zVSS+ppZXR/AK4AVzc3k0tMrq3Zi5s1U8dCtUzZnDO/Fyyt3csfs0T4Fb5poblPVCqClf3VnAdcBM2O7zkq6WdLN4TZXASskLQX+G7i2rjhJ6gycB9Q/U7lL0nJJy4BzgG+0MJdLM3NW7mBQz06M7pMTdZQ26fyxffhg10HWlh2IOopLkGOecUj6C2BADrBK0rvAkbr1ZnZpY/ua2QLgmIcfZnYfcF8j6w4CvRpYft2xntO5WPsrq3h7/S5uOHOIHw3HyfljenPnsyuYs2IHBb29OKeDppqq/F4Jl9Lmri6jqsa8mSqOenftwMRB3ZmzagdfnzUy6jguAY7ZVGVm881sPnBR3ePYZYmJ6Nzxe3nlTnK7tGfSIO+1HU8XjO3DitL9bN1zKOooLgGae43jvAaWXdiaQZxrbZVVNcwrKeO8Mb3JyPBmqniqO6N7eeXOJrZ0bcExC4ekL0taDowKb7ir+/oAWJaYiM4dn3c27Obg0RrOH9M76iht3tDczozM78KrxV440kFT1zgeA14EfgjcEbO8wsw+jFsq51rBa8VldMzO5Izh/9THwsXBrJN68+s3N7C/soquHbKjjuPiqKmmqscJbvr7opltivnyouGSmpnxWvFOpo7MpUN2ZtRx0sK5J+VTXWu8scZHamjrmiocDwCXABsk/UnSFeEwIM4ltdU7Kti2r5JzT8qPOkramDioBz06ZfNacVnUUVycNdWr6jkz+zQwhGDokM8BmyX9VlJDF8ydSwqvhW3t54zywpEomRninFH5zC0po9rn6GjTmtWryswOmdkfzewK4HyCAQZfimsy507Aq8VljB/QjfyuHaKOklZmndSbvYeqeH+LD3rYljV3kMPekr4u6S2CEWznAJPimsy541RecYSlW/cy6yTvTZVo0wpyycqQ965q45rqjvtFSa8TDH8+EvimmQ0zszvMbGlCEjrXQnNLyjCDmaO9mSrRunbI5rRhPf06RxvX1BnHGcB/AQPN7BYzezsBmZw7Ia8V76Rvtw6M7dc16ihpadbo3qwrO8Cm3QejjuLipKmL4/9qZq8QzNLXGUDSZyXdI2lwQhI61wJHqmt4c+0uZo7O90ENIzIr7MnmZx1tV3OHHLkfOCRpPHA7sB54JG6pnDtO72z4kENHaz768HKJN7hXZ0bkd+G11X6do61qbuGoDufJuAy4z8x+QTDUeqMkDZQ0V9IqSSsl3drANjMk7YuZr+O7Mes2hvNuLJFUFLO8p6RXJK0N//XR69xHXiveSYfsDM4cntv0xi5uZp2Uz8INH7K/sirqKC4Omls4KiT9O/BZ4K+SMoCmxhSoBm43szHA6cBXJY1pYLs3zWxC+PX9euvOCZfHTlN7B/CamY0EXuMfh0JxaSy4W7yMqSPy/G7xiJ17Um+/i7wNa27huIZgAqcbzWwHMAC4+1g7mNl2M1scPq4AioH+J5C1zmXA78LHvwMub4XndG1Ayc4KSvce9maqJDBxYHe6d8rmdb/O0SY19wbAHWZ2j5m9GX6/2cyafY1D0hBgIrCwgdVnSFoq6W+Sxsa+LPCypEWSbopZ3jtmzvEdgHfWd8DHF2NneTfcyGVlZnx0F3lNrUUdx7WypqaO/YDgA7whZmbDm3oBSV0Ihiu5zcz211u9GBhsZgckXURwc2HdFGJTzaxUUj7wiqTVZvZG/QCSGswXFpubAAYNGtRUTNcGvL66jJP7+93iyWLGqDyeeb+UpVv3+kRabUxTZxyFwOSYr9OAnxDMJb6kqSeXlE1QNB4zs6frrzez/WZ2IHz8IpAtKTf8vjT8twx4BpgS7rZTUt/w+fsCDZ4Lm9kDZlZoZoV5eXlNRXUpbu+ho7y/eQ/njPL/62Rx9sg8MgTzSvw6R1vT1H0cu81sN7CHYJTcuQQ3BV5sZlcea18FnegfAorN7J5GtukTboekKWGe3ZI6S8oJl3cmGB9rRbjb88D14ePrgeea/Cldm/fm2l3UGkz3wpE0enRux4SB3Zlf4tc52pqmmqqygX8FvgEsAC43s3XNfO6zgOuA5ZLqzk6+AwwCMLNfAVcBX5ZUDRwGrg2bn3oDz4Q1JQv4g5nVDar4Q+BPkm4ENgFXNzOPa8PmlZTTrWM2EwZ6k0gymTEqn3teWcOuA0fI7dI+6jiulTQ1A+AHBN1qfwpsBk6RdErdyoaan2LWLSBo0mqUmd0H3NfA8g3A+Eb22Q3MaiK3SyO1tcb8NeVMG5lLps8tnlRmjMrjnlfW8Maacj45aUDUcVwraapwvEpwcXw8cErMcoXLGy0cziXKqu372XXgCDN87o2kM65fN3K7tGNeiReOtuSYhcPMbgCQ1AG4kmBCp7p9vI+dSwrzw5vMphf49Y1kk5Ehzi7I4/XVQbdcPyNsG5p7A+CzwCeAKuBAzJdzkZtXUsa4/l3Jy/E29GQ0Y1Q+ew9VscQnd2ozmmqqqjPAzGbHNYlzx2Hf4SoWb97Ll6c3eUuRi8jZI3PJEMwvKePUwd55oS1o7hnH25JOjmsS547DgrW7qKk1Zng33KTVvVM7Jg7qwVy/n6PNaG7hmAosklQiaVk4au2yeAZzrjnmlZTRtUMWEwZ2jzqKO4YZBXksL91HecWRqKO4VtDcwnEhwVAg5xNc67gk/Ne5yJjVdcPNIyuzuW9lF4VzwvHDfLTctqFZ1zjMbFO8gzjXUsXbKyirOOJ3i6eAMX27ktulPXNLyrjyVO+Wm+r8MM2lrHlrgqEsZng33KSXkSGmF+Tx5tpdVNfURh3HnSAvHC5lzSspZ0zfrj4aboo4Z3Qe+w5XsXSrd8tNdV44XEraX1nFok17vDdVCpk2Ihgtd+5qv86R6rxwuJT01kfdcH2YkVTRrVM2kwb1+KiJ0aUuLxwuJc0rKSenQxaTBnk33FRyzuh8VpTup6yiMuoo7gTErXBIGihprqRVklZKurWBbWZI2idpSfj13ab2lfQ9SaUx+1wUr5/BJaePu+HmejfcFFM3ntgba3ZFnMSdiOYOOXI8qoHbzWxxOCnTIkmvmNmqetu9aWaXtHDfe83sx3HM7pJYyc4KduyvZEaBN1OlmrpuufNKyrjKu+WmrLgdrpnZdjNbHD6uAIqB/vHe17V9dVORnu3dcFOOd8ttGxJyni9pCDARWNjA6jMkLZX0N0ljm7nv18KhT34jyUdNSzPzSsoY3SeHPt28G24qmjHKu+WmurgXDkldgKeA28xsf73Vi4HBZjYe+DnB8O1N7Xs/MByYAGwHftLI694kqUhSUXm5d/9rKyoqqyjauMd7U6WwaR+Nlut/l6kqroUjnLP8KeCxhqaZNbP9ZnYgfPwikC0p91j7mtlOM6sxs1rgQWBKQ69tZg+YWaGZFebleZNGW/H2+t1U+2i4Ka1utNx5Pm5VyopnryoBDwHFZnZPI9v0CbdD0pQwz+5j7Supb8y3VwAr4pHfJaf5a8rp0j6LSYO8hTKVTS/IY9nWfew64KPlpqJ4nnGcBVwHzIztOivpZkk3h9tcBayQtBT4b+BaM7PG9g33uStmWPdzgG/E8WdwScTMmF9SzpnDe9Euy7vhprK6M0YfLTc1xa07rpktAI45wbCZ3Qfc15J9zey6VgnoUs6GXQcp3XuYL8/w2f5S3bh+3cjt0o75a8r55CTvlptq/LDNpYy6i6nTvRtuysvIEGePzOONNeXU1FrUcVwLeeFwKWP+mnKG5XVmYM9OUUdxrWD6qDz2HKpimXfLTTleOFxKqKyqYeEHu/1sow2ZNjIP6eMbOl3q8MLhUsK7H3xIZVWt3y3ehvTs3I7xA7oz3y+QpxwvHC4lzF9TTrusDE4f2ivqKK4VzRiVx9Kte/nw4NGoo7gW8MLhUsIba8o5bWhPOrbLjDqKa0UzRuVjBm+u9bOOVOKFwyW90r2HWVt2wK9vtEEn9+9Gj07Zfp0jxXjhcEmv7iYxLxxtT2aGOLsg6JZb691yU4YXDpf05peU069bB0bkd4k6iouDGaPy2H3wKCu27Ys6imsmLxwuqVXV1PLWul2cXZBHOKyZa2PO9m65KccLh0tqS7bspeJItTdTtWG9urTn5P7dmFdSFnUU10xeOFxSm19STmaGOHNEbtRRXBzNKMhjyZa97D3k3XJTgRcOl9TeWFvOpEHd6dYxO+ooLo6mj8qn1uDNtbuijuKawQuHS1q7Dhxh2dZ9nD3Sm6naugkDu9Pdu+WmjHhO5DRQ0lxJqyStlHRrA9vMkLQvZs6N78asmy2pRNI6SXfELB8qaWG4/I+S2sXrZ3DRWhAefU732f7avMwMMW1kHvO9W25KiOcZRzVwu5mNAU4HvippTAPbvWlmE8Kv7wNIygR+AVwIjAE+HbPvj4B7zWwEsAe4MY4/g4vQ/DXl9OzcjnH9ukUdxSXA9II8dh04wqrt+6OO4poQt8JhZtvNbHH4uAIoBvo3c/cpwDoz22BmR4EngMvCKWVnAn8Ot/sdcHnrJnfJoLbWeHNtOdNG5pKR4d1w00Fdzzkf9DD5JeQah6QhwERgYQOrz5C0VNLfJI0Nl/UHtsRsszVc1gvYa2bV9Za7NmbV9v3sOnDUu+Gmkbyc9ozr39W75aaAuBcOSV2Ap4DbzKz+OehiYLCZjQd+Djzbiq97k6QiSUXl5X4Ek2rqjo+vv/IAABPjSURBVDqn+YXxtDKjIJ/Fm/ey73BV1FHcMcS1cEjKJigaj5nZ0/XXm9l+MzsQPn4RyJaUC5QCA2M2HRAu2w10l5RVb/k/MbMHzKzQzArz8vzDJ9XMX1PO2H5dyctpH3UUl0AzRuVRU2u8tc675SazePaqEvAQUGxm9zSyTZ9wOyRNCfPsBt4DRoY9qNoB1wLPm5kBc4Grwqe4HnguXj+Di8b+yioWb9rjzVRpaMLA7nTtkOXNVUkuq+lNjttZwHXAcklLwmXfAQYBmNmvCArAlyVVA4eBa8PiUC3pa8AcIBP4jZmtDJ/j28ATkn4AvE9QnFwb8tbaXVTXmheONJSVmfFRt1wz8/HJklTcCoeZLQCO+b9uZvcB9zWy7kXgxQaWbyDodeXaqLklZeR0yOLUwT2ijuIiMH1UHn9dvp3VOyo4qW/XqOO4Bvid4y6p1NYac0vKObsgj6xMf3umoxnhmabfRZ68/C/TJZWV2/ZTXnGEmaPyo47iIpLftQMn9fVuucnMC4dLKq+vLkMKete49DVjVB6LNu2hotK75SYjLxwuqcwtKWP8gO706uLdcNPZjII8qmuNt9btjjqKa4AXDpc0dh84wtKteznHm6nS3qTBPchpn8X8Nd5clYy8cLikMa+kHDOYOdoLR7rLzsxg6sjc8D3ho+UmGy8cLmm8XlJGXk57xvbzLpguGPRw+75K1uw8EHUUV48XDpcUqmpqeWNNOeeMyvPRcB3w8Tws3lyVfLxwuKSweNMeKiqr/fqG+0jfbh0Z3SfH7+dIQl44XFJ4vaSM7EwxdWRu1FFcEpk+Ko/3Nn7IgSPVTW/sEsYLh4ucmfHKyp2cNrQXOR2yo47jksj0gjyqany03GTjhcNFbn35ATbsOsgFY3tHHcUlmclDetKtYzYvr9wZdRQXwwuHi9yc8EPhvDF9Ik7ikk12ZgbnntSbV4t3UlVTG3UcF/LC4SL38sodjB/YnT7dOkQdxSWh2eP6sO9wFQs3fBh1FBeK50ROAyXNlbRK0kpJtx5j28mSqiVdFX5/jqQlMV+Vki4P1z0s6YOYdRPi9TO4+Nu+7zBLt+7zZirXqGkjc+nULpOXVm6POooLxfOMoxq43czGAKcDX5U0pv5GkjKBHwEv1y0zs7lmNsHMJgAzgUOx64Fv1q03syW4lPXKqqCZ6nxvpnKN6JCdyYxRecxZuZPaWr+LPBnErXCY2XYzWxw+rgCKgf4NbPp1gnnJG7vL5yrgb2Z2KC5BXaTmrNzB8LzOjMjvEnUUl8QuGNuH8oojvL9lT9RRHAm6xiFpCDARWFhveX/gCuD+Y+x+LfB4vWX/KWmZpHsl+TCqKWr3gSO8s+FDZo/zsw13bDNH59MuM4OXVuyIOoojAYVDUheCM4rbzGx/vdU/Bb5tZg12l5DUFziZYO7xOv8OjAYmAz0J5iBvaN+bJBVJKiov9ztPk9GLy7dTU2t8Yny/qKO4JJfTIZuzRvTixeU7fNDDJBDXwiEpm6BoPGZmTzewSSHwhKSNBE1Sv6y7CB66GnjGzD6azSVsAjMzOwL8lkbmHzezB8ys0MwK8/J8UqBk9NySbYzqncPoPj6ooWvaJ8b3o3TvYYo2eXNV1OLZq0rAQ0Cxmd3T0DZmNtTMhpjZEODPwFfM7NmYTT5NvWaq8Cyk7vkvB1bEIb6Ls617DlG0aQ+XTvCzDdc8F4ztQ8fsTJ5eXBp1lLQXzzOOs4DrgJkxXWcvknSzpJub2jm8LjIQmF9v1WOSlgPLgVzgB60b2yXCX5YGXSs/cYoXDtc8ndtnMXtcH/66bBtHqmuijpPWsuL1xGa2AGj2+NhmdkO97zfSQC8sM5t5otlctMyMpxZvZeKg7gzq1SnqOC6FXD6xP8+8X8rc1WXMHtc36jhpy+8cdwlXtGkP68oO8OnJg6KO4lLMWcN7kZfTnieLtkYdJa154XAJ94eFm8lpn8Ul4/2I0bVMVmYG104eyOslZWze7bd2RcULh0uo3QeO8Nfl27l8Yn86tYtbS6lrw/7ltMFkSjzy941RR0lbXjhcQj389kaqamq5/szBUUdxKapPtw7MHteHPxZt8QmeIuKFwyVMRWUVD7+9kQvG9GFEfk7UcVwK+8K0YVRUVvObBR9EHSUteeFwCXPf6+uoqKzmq+eMiDqKS3ETBnbn/DG9eeCNDew+cCTqOGnHC4dLiBWl+3howQdcXTiAkwd0izqOawO+NXsUlVU13PncCh+GJMG8cLi4qq01VpTu46ZHisjPac8dF54UdSTXRozIz+Hfzi/gxeU7+PHLJew9dDTqSGnDu7Ucw89fW8tflm0DIPaAJvbYJvZI5x+OeeodADVnn398DWt4+TEOrE7oeRvZnka2/+d9Gn7tI1W1HK6qoWfndjzyr1Po2bld4z+Acy1089nD2bjrIL+Yu55fzF1PXk57ctpnkZUp1Pz7j9u0H1wxjslDerbqc3rhOIbcnPYMz/t4ngjFvA//4U3Z8MNwHzW4To3s09j2NPLaqveCjb9Gw/uokRdpLF+znzf8Nyszg1G9c5gxKo/8rj41rGtdGRniR1eewuUT+7N86z7Wlx/gcFUtVdU+P3mdjtmZrf6cSoe2wcLCQisqKoo6hnPOpRRJi8yssP5yv8bhnHOuRbxwOOeca5F4zscxUNJcSaskrZR06zG2nSypWtJVMctqYoZjfz5m+VBJCyWtk/RHSX611TnnEiieZxzVwO1mNgY4HfiqpDH1N5KUCfwIeLneqsNmNiH8ujRm+Y+Ae81sBLAHuDE+8Z1zzjUkboUjnOJ1cfi4Aiimgfk1gK8TTC9b1tRzhrP+zSSYLRDgdwSzADrnnEuQhFzjCGfzmwgsrLe8P3AFcH8Du3WQVCTpnZh5yHsBe82sbmSzrTRcjJxzzsVJ3O/jkNSF4IziNjPbX2/1T4Fvm1mt6t+QAIPNrFTSMOD1cLrYfS143ZuAmwAGDfIJg5xzrrXE9T4OSdnAC8AcM7ungfUf8PG9YrnAIeAmM3u23nYPh8/zFFAO9DGzaklnAN8zswuayFEObDrOHyMX2HWc+8aT52oZz9UynqtlkjUXnFi2wWaWV39h3M44wusRDwHFDRUNADMbGrP9w8ALZvaspB7AITM7IikXOAu4y8xM0lzgKuAJ4HrguaayNPSDt+DnKGroBpioea6W8Vwt47laJllzQXyyxbOp6izgOmC5pCXhsu8AgwDM7FfH2Pck4H8k1RJch/mhma0K130beELSD4D3CYqTc865BIlb4TCzBfzzMEfH2v6GmMdvAyc3st0GYMqJ5nPOOXd8/M7xpj0QdYBGeK6W8Vwt47laJllzQRyypcUgh84551qPn3E455xrES8cgKSOUWdoiKTOUWdoiKRhkkZFnaM+/39sGUmDJXWPOkd9ko67F2S8qYEbzqIWxfs+rQuHpC6S7gN+LWm2pKSYDDvM9VPgN5KulJQfdSYASR0k/RKYAwxNlgEmw9/XvcB/S5qRZP+P9wK/l/RZSYOjzgQf5boH+CvQL+o8dcJcPwFekvSfks6KOhOApBxJP5c0ypKobT/Kz6+0LhwEd663A54GPg3cEW0ckHQJ8BZQBTwOfAk4NdJQH7sa6GVmI83sJTOLfJLncGSC3xD8vv4CXAx8M9JQgKSpwJvAYYJ80wjeY5GSVEjw/uoJTIzp5h4pSVnALwh6en6OYAbiWZGGAiSNILhn7IvA9yOOU19kn19pN3WsJIU3EuYSHG1dbWYHJK0DviHpi2b2YAS5MsysFvgAuNHMisLlVwP1h2pJOEkZQB/g9+H35xDk2mBmeyLIo/Dorx8wwsyuDpcbcKekFWb2RKJzxdgN/LLuvSRpADAsfKwIj1wrgfUEI0xXSZoA7AW2xowBF4U8YIiZTQeQ1AlYGmGeOgeBu4HLgCWSZpvZS1H9HybL51fanHFIGi3pV8Atkrqa2S6gluBIAmA18AxwiaTWndm9ZblWmlmRpDxJfyMYkv4WSVeHR9cJzSXp1jBXLVAATJP0VYLh7b8CPCqpb6Jz8fHvaw2wSdKXwk0OERTfq8IRCBKVa7ikz9d9b2bFwB9i2sRLgcHhuoR94DSQawXBGcctkuYBPwfuBe6S1CvCXNsBk/RbSQuBS4BLJT2b4PfXSEk/k3SzpB5hrvfCovoz4Lth3oQWjWT7/EqLwiFpKMGR8npgPHB/eKR1N3BB+AY5Aiwj+NCZFEGuU4D7JJ0Wrv4Q+IOZDSO4O/5MEjSEfAO/r19JKgD+C/gMMNrMphAUjrXAnRHlui9sB/8p8B1J9wP3EIxrtpngDCkRub4CLCI44rsyXJZhZgdjPmAmACsTkedYuUKPAJnAM2Y2Dfg/4fcJmdvmGLk+QTBVQrGZFQBfIBhj7rsJynUHwYdvKTCDYPSKTIKDEcIj+VodY1K6OOVKus+vtCgcwGhgl5ndTXDNoITgQ7iS4HT43wHM7ANgCMHpaRS51gEXSxpuZjVm9miY62WgO1ARUa7VBOOCHQCeJ2ivJ3yzvgnsiCDXzQS/rwsJ/mDOBP4GTA9/b9MIri8kwnqCD7k7gc9I6hCeodVNVAbQF3g7XDZLUu8ocgGYWTnwv8zsZ+H3SwjeW7sTkOlYuSqAgQR/l3XvrwU0Y66eE6Wg59sB4Bozuwu4ARgHjAubhrLDTf8DuFFStqRPJKjDQ9J9fqVL4VgBVEoabWZVBB8wnQiaXh4ALpf0SUmnE7S1JqrLXf1cL4a5zozdSNIpwFASN/pmQ7k6AtOB24Eekq6QNAv4XwRHaInOdZSPf1+XmFmpmT1vZnslnUlwpJqQQmtmcwguUC4hOFP8Mnx01lETXh/qC4yS9CLBxd/aCHMpbOog/P4U4Bxge7wzNZLr5pjVLxM0UV0QXsj/NxLz/joEPGVmKyW1N7NKYDHBmRjh3wFmNo/ggGQ/8FWCmU7jLek+v9KlcLQnmIFwKoCZvUfwRzLMzNYD3yIY/+pB4P5wrKwochURTE41RFKGgvnVnyV4c9xvZm9FmGsLwdHXYYIPvr4ER4w/M7NEDTTZUK7NBEdZSMqV9CDBxGBPmlmijqAJzzBKCT4Qz5U0su6sAxgOXEowqvMjZnZ9eNQfVS4DkNRT0p+BXwM/N7MXE5GpgVznSRoZLt8J/G+CM9wHgZ+aWdyH87DA9vDxkfBMcRLB9NQASGoXNrP1AT5vZrPNrNWKmurd7xNzfSz5Pr/MrE18EVys/RyQ0cj6LwA/Bs4Ivz8dWJGkuZaFjzsCNyRRruXJ/PsKv78oilwx2/UhuBb0H+H3I8N/b02yXAXhv59Kslx1v68OEeeaRjDNw0c5w3/HxSnXd4H5BF1qp4fLMmPWR/L51dhXyp9xSOoh6WfA5wku3A6ut76uar8M7CToqtmF4Ch1oYJuf8mW6z1Jnc3ssJk9nES53k3i31cXAGvlo+amctVnZjuAh4HrJR0kmBoZC68nJFGuy8LlTyZZrkvDJr7KKHLFvM+6EXw+XClpFTA7zLuilXMNCZsvhxBMGSHgZkk59nEzJyT486tJUVWsVqjQHev+JTiFywB+S3DzV7tG9hFBT4RnCdoNp3guz9WKuTKAfGAh8A4wzXOlXq5w+wcJrkM9GadcncJ/ewFfjFl+KkEh7dPAPnF/3zc7f1QvfAK/8F7A/wCPEtxZ2ilm3WTgdYI7YhvbX0Ce5/JcccrVgTg0/3iuxOUK31s3Eodm4nq5ziW481uETWfAAIJC2uUY2Vr9fd/Sr5QbVl3S4wSnbO8C5wNbzOzOmPU/Ibgj/k4zS9gd157Lc4W9leLyB+W5EpMrnpmamWsm8CUzuyZeGVpF1JWrhdW6N/ASH88jMgl4DPhMzDb9CLqrnUHQ13mq5/JcnstzpUiuzwP/N3x8HjAh3rmO5ytpL47HXKT6iAVd9ToS/HIh6KL2PMHwEp3DbbaFy18juCO2VftZey7P5bk8Vxxy1Q0nNAnoKuk3BN1sa1ozV6uJunI1Upk7xDyuq851bYBXEAwpUXfRawRwH+ERA8GNTJuA2zyX5/JcnitVchE0nS0lKChfbu1crfmVdGccCiYI2izpB+Gi+hkXEAxx8S0AM1tH0DXtQLi+hGAspZ96Ls/luTxXiuQ6aMFAivcChWZ2f2vmanVRV64GqvVIoIhgeI2+4bKsmPWDCcZuWUfQt3o6wWifp3ouz+W5PFeK5pocz1yt/nNGHuAff6kiuCPyauCHwJyY5YOBp4BfhcuuBu4ClgNXei7P5bk8l+dKzFd0Lxy05/2YYIz7c2OWzwIeDB/vJOjr3I9gfP7/9Fyey3N5Ls8V7Vc0LxpU4F8SjDH/L8ArBCNNZoe/+M+H2/2R4O7NH9bb/5jjzHguz+W5PFe65UrkV1RTx+YQTGxzgZlVSNpFUJEvJhjt9JeSrg9/6WsJ5oOom9ug1j4eddRzeS7P5bk8V4JF0qvKgjs1NxJMlgLBxaFFBHdSdiEYt+ZRM5tJMJLlNyVlWjC5kXkuz+W5PJfnik5UZxwQTNE4W1JfM9suaTkwFjhiZtfDR7f/LwyXey7P5bk8l+dKAlHex7GAoMvaDQBmtojg9v8sAElZEVVnz+W5PJfnStVcCRFZ4bBgtq3ngAslfUrSEII5dOumaEzElIyey3N5Ls/VZnIlSuSj40q6EPgUwTzb95nZfZEGCnmulvFcLeO5WsZzJZfICweApGyCaX+Tqkp7rpbxXC3juVrGcyWPpCgczjnnUkfSDXLonHMuuXnhcM451yJeOJxzzrWIFw7nnHMt4oXDOedci3jhcM451yJeOJxzzrWIFw7nnHMt8v8DzbiHJvF3fvgAAAAASUVORK5CYII=\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ "nNsVth.plot()\n", - "plt.ylabel('nNsVth');" + "plt.ylabel(\"nNsVth\");" ] }, { @@ -1012,8 +1063,13 @@ "metadata": {}, "outputs": [], "source": [ - "single_diode_out = pvsystem.singlediode(photocurrent, saturation_current,\n", - " resistance_series, resistance_shunt, nNsVth)" + "single_diode_out = pvsystem.singlediode(\n", + " photocurrent,\n", + " saturation_current,\n", + " resistance_series,\n", + " resistance_shunt,\n", + " nNsVth,\n", + ")" ] }, { @@ -1022,19 +1078,19 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXkAAAD4CAYAAAAJmJb0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3dd3xc1Zn/8c+jLlvNRdVNbnLHJcIUYzC2wQ4moWWBsEmAEEg22V2SzSa/7Gaz2Zpkk01CElIWQkhII5ulhmYDbpQAlsFNtuWGu6ziJrmon98fdwTCWLZl6869M/N9v156qcx47tej0TNnzjznXHPOISIi8Skp6AAiIuIfFXkRkTimIi8iEsdU5EVE4piKvIhIHFORFxGJYylBB+hq4MCBrrS0NOgYIiIxZeXKlfXOufyTXRaqIl9aWkpFRUXQMUREYoqZ7ejuMk3XiIjEMRV5EZE4piIvIhLHVORFROKYiryISBxTkRcRiWOhaqEUCbud+4/x4sYa3tp5iB37j3K0pZ2UJKM4N4PxJTnMGDmQC0YMIDnJgo4qAqjIi5yWc44lVbXct3wbr207AEBJbgYjC7IY1C+TljbHnkPHWb55Gz9espVBeZnccsFQbru4lL7p+hOTYOkRKHIKW2ob+fqTlbyyZT8luRn8v/ljuWpSEcMG9H3fdY80t7Gsqo6HV+zkOwurePCV7Xx1wViunTIIM43sJRgWpjNDlZeXO614lTBwzvG7N3byr39aT0ZKEl+8cgy3XDCU1OQzextr5Y6D/MfT63lr5yHmTyjiWzdMIq9Pms+pJVGZ2UrnXPlJL1ORF3mv1vYOvvLIWh55czczRw/kezdOIT87vce3097h+PlL2/juok0M6pfJA7eWMyI/y4fEkuhOVeTVXSPSxdHmNu74VQWPvLmbu+eM5le3Tz+rAg+QnGR8+rKR/O7OCzh8vJVrf/wKq3Yd6uXEIqemIi8Scbylndt/uYJXttTzXzdM4gtXlJHUC10y5aX9eeJzM8jrk8bHf/46b+082AtpRc6MirwI0NLWwWd+s5IV2w9wz01TuOn8ob16+0P69+Hhuy6kf1Yan3jgDTbua+jV2xfpjoq8JDznHF95dA3LNtXxzesm8aHJJb4cpyQvk9/feSF90pP55IMrqGlo8uU4Il2pyEvCe+Dlt3n0zT18YW4ZN0/v3RH8iUryMnng1vM5dLyVO361gqbWdl+PJ6IiLwnt5c31fOOZDcyfUMTfzB4VlWNOHJTLjz46lXV7GviXJyujckxJXCrykrDqGpv5/B/eYlRBFt+9cXKvvMl6puaMK+Szs0by8IpdPPrm7qgdVxKPirwkJOccX/6/1TQ2tXHvLdMC2X7g764oY/rw/nz1sXW8XX806seXxKAiLwnp16/tYElVHf941TjKCrMDyZCSnMQPb55KarLxpT+upr0jPAsTJX6oyEvC2XXgGN94ZgOzxuTziYuGBZqlKDeDr39oAhU7DvLgK28HmkXik4q8JBTnHF99fB3JZnzz+kmh2Djs+mmDmDO2gO8srGJb3ZGg40icUZGXhPLk6r0s31THl+aNoTg3M+g4AJgZ37h+EmkpSXz9yUrCtJ+UxD7fi7yZbTeztWa2ysy0+5gE5vCxVv7tT+uZPCSPj19UGnSc9yjMyeDvrijjpc31LKzcF3QciSPRGslf7pyb0t0uaSLR8MPFmzl4rIVvXDcxlGdu+viFwxhblM2/P7WB4y1aJCW9Q9M1khDerj/KQ3/ezk3nD2FCSW7QcU4qJTmJf7tmInsOHecnS7cEHUfiRDSKvAMWmdlKM7srCscTeZ9vPbuBtOQkvnBFWdBRTmn68P58eHIJ97+0jX2HtbeNnLtoFPlLnHPTgA8CnzOzS7teaGZ3mVmFmVXU1dVFIY4kmte27WdhZQ2fvXwUBdkZQcc5rS/NG0N7h+MHL24KOorEAd+LvHNuT+RzLfAYMP2Ey+9zzpU758rz8/P9jiMJxjnHN5/ZQEluBndcMjzoOGdkSP8+/OUFw/jfit1sqVVLpZwbX4u8mfU1s+zOr4ErgXV+HlOkqxc31LJ692E+P7eMjNTkoOOcsb+ZPYrM1GT+e2FV0FEkxvk9ki8EXjaz1cAbwNPOued8PqYI4I3iv/f8JoYN6MN10wYFHadHBmSlc+fMETxXuY81u3XKQDl7vhZ559w259zkyMcE59x/+nk8ka4WVtawvrqBv509mtTk2Gsk++QlpeRmpvLDF9VpI2cv9h75Imego8NxzwubGDGwL9dM8edMT37LzkjlkzOG88KGGir3Hg46jsQoFXmJSwsr97FxXyN3zx1NSgyO4jvdNqOU7PQU7l2s0bycndh99It0wznHz5ZtpXRAH64+LzZH8Z1yM1O5bUYpz67bx6aaxqDjSAxSkZe48+dt+1m9+zB3XjoilNsX9NQnZwynb1oyP16i0bz0nIq8xJ3/WbaNgVlp3DBtcNBRekW/vml8dPpQnlpTzd5Dx4OOIzFGRV7iyobqBpZtquP2GcNjqi/+dG6PLOT65avbgw0iMUdFXuLKfcu30TctmY9dEOwZn3rboLxMrppUzO9f30ljU2vQcSSGqMhL3Nh76DhPrt7LR6cPJbdPatBxet2nLhlOY3Mbf1ixK+goEkNU5CVu/Pb1HTjnuG1GadBRfDF5SB7TS/vz4CvbaWvvCDqOxAgVeYkLTa3t/P6NXcwdV8jgfn2CjuObT80czp5Dx3lOZ4+SM6QiL3Hh6TXVHDjawq0XlwYdxVdzxhUytH8fHvrzjqCjSIxQkZe48NCftzMyvy8XjxwQdBRfJScZt1wwlDfePqDFUXJGVOQl5r218yCrdx/m1otLMYv9xU+nc2P5ENJSkvjNaxrNy+mpyEvMe+jPO8hKT+H6OFn8dDr9+6Zx9aRiHn1zD0ea24KOIyGnIi8xbf+RZp5eU81HPjCYrPSUoONEzccuGsaR5jYef2tP0FEk5FTkJaY9+uYeWto7uOWCoUFHiaqpQ/KYUJLDb17z2kZFuqMiLzHLOcfDK3YybWgeZYXZQceJKjPjYxcOY+O+RlbuOBh0HAkxFXmJWSt3HGRr3VFuPj+xRvGdPjy5hL5pyfxvhVbASvdU5CVmPbxiF33TkllwXnHQUQLRNz2Fq88r4ak11RzVG7DSDRV5iUmNTa08vaaaD08poW8CveF6ohvPH8yxlnaeXlsddBQJKRV5iUl/Wl3N8dZ2bkrQqZpO04b2Y0R+X/6oKRvphoq8xKQ/rNjJ2KJsJg/ODTpKoMyMG8uHsGL7QbbVHQk6joSQirzEnA3VDazefZgby4ckxArX07l+6iCSk4w/rtwddBQJoagUeTNLNrO3zOypaBxP4tujb+4mJcm4duqgoKOEQkFOBpePyeeRlbu1BbG8T7RG8ncDG6J0LIlj7R2OJ1btZdaYAvr3TQs6Tmj8RfkQahubWb65LugoEjK+F3kzGwwsAH7u97Ek/r26tZ7axmau0yj+PWaPLaBfn1Qee2tv0FEkZKIxkr8H+DJw0teRZnaXmVWYWUVdnUYhcmqPvbWH7IwU5owrCDpKqKQmJ7HgvGKeX79Pm5bJe/ha5M3saqDWObeyu+s45+5zzpU758rz8/P9jCMx7lhLG8+t28eCScVkpCYHHSd0rps6iKbWDhau01mj5F1+j+RnAB82s+3Aw8BsM/uNz8eUOLWosoZjLe16w7Ub04b2Y0j/TB5fpZ0p5V2+Fnnn3D845wY750qBm4HFzrmP+XlMiV+PvbWHQXmZTC/tH3SUUDIzrpk8iFe21FPb2BR0HAkJ9clLTKhtbOKlzXVcM6WEpCT1xnfn2qkldDhvRbAIRLHIO+eWOueujtbxJL78aXU1HQ6un6apmlMZVZDNxEE5PKEpG4nQSF5iwpOr9jChJIdRBYm1b/zZuHbKINbsPsxWbXMgqMhLDNh14Birdx/mQ5NLgo4SEz40uYQkgyd0akBBRV5iQOc2ugsmJea+8T1VmJPBhSMG8NSaap0aUFTkJfyeWrOXyUPyGNK/T9BRYsaC84rZVn+Ujfsag44iAVORl1DbXn+UdXsauFqj+B6ZP6GIJIOn16jLJtGpyEuodU7VXJWgp/g7WwOy0rlo5ACeXqspm0SnIi+h9tSaaqYNzWNQXmbQUWLOgkklvF1/lA3VmrJJZCryElpb646wobqBq89TV83ZmDehkOQk4+m12pkykanIS2h1zidfpfn4szIgK52LRgzgmbX7NGWTwFTkJbSeXlPN+aX9KMrNCDpKzFpwXjFv1x9lfXVD0FEkICryEkqbaxqpqmnUVM05mjehyJuyUZdNwlKRl1B6dt0+zOCDE4uCjhLT+vdN4+KRA3hGXTYJS0VeQmlh5T6mDe1HQY6mas7VgknFbN9/jMq9mrJJRCryEjq7DngFad6EwqCjxIUrxheSZLBofU3QUSQAKvISOgsrvdPXzZugqZreMCArnfLS/iyq1GkBE5GKvITOosoaxhZlM2xA36CjxI15E4rYuK+RHfuPBh1FokxFXkKl/kgzK3Yc4EqN4nvVleO9qa+FGs0nHBV5CZUX1tfgHJqP72VD+vdhfHEOiyo1L59oVOQlVBZW7mNwv0zGF+cEHSXuzJtQxMqdB6lrbA46ikSRiryERmNTK69s2c+8CUWY6WTdvW3exEKcg+fVZZNQVOQlNJZW1dHS3sF8LYDyxZjCbIb278Oi9ZqXTyQq8hIaCyv3MTArjWlD+wUdJS6ZGfMmFPLqlv00NrUGHUeixNcib2YZZvaGma02s0oz+1c/jyexq6m1nSUba7livLc9rvjjyglFtLR3sKSqLugoEiV+j+SbgdnOucnAFGC+mV3o8zElBr26tZ6jLe1qnfTZtKH9GJiVpoVRCcTXIu88RyLfpkY+tEuSvM/z62vpm5bMxSMHBB0lriUnGVeML2RpVR3Nbe1Bx5Eo8H1O3sySzWwVUAs875x73e9jSmxxzrF4Yw2XluWTnpIcdJy4d8X4Qo40t/HatgNBR5Eo8L3IO+fanXNTgMHAdDOb2PVyM7vLzCrMrKKuTvOEiahybwM1Dc3MGacFUNFw8ciBZKQm8eIGtVImgqh11zjnDgFLgPkn/Pw+51y5c648Pz8/WnEkRF7YUIMZXD5Gv/9oyEhN5pJR+by4oVZ7zCcAv7tr8s0sL/J1JnAFsNHPY0rseXFDLVOH5DEgKz3oKAljzrgC9hw6TlVNY9BRxGd+j+SLgSVmtgZYgTcn/5TPx5QYUtPQxNo9hzVVE2VzxhYA3hOsxLcUP2/cObcGmOrnMSS2Ld7oFZm5KvJRVZCTwXmDc3lxQw2fu3xU0HHER1rxKoF6cUMNg/IyKSvMCjpKwpk9toC3dh2i/og2LItnKvISmKbWdl7eUs/ccQXakCwAc8d5G5Yt1erXuKYiL4F5dWs9Ta0dmo8PyISSHApz0tVKGedU5CUwL2zwVrleMKJ/0FESkpkxe2whyzfV0dLWEXQc8YmKvATCOcfiDbXMHK1VrkGaM7aAoy3tvP72/qCjiE9U5CUQlXsb2NfQxJxxBUFHSWgzRg0kPSVJrZRxTEVeAvHihlpvletYFfkgZaYlM2PUQF7cWKPVr3FKRV4CsXhjDVOG5DFQq1wDN2dcAbsOHGdz7ZHTX1lijoq8RF1tYxOrdx/WAqiQmK3Vr3FNRV6irrMv+/IxmqoJg+LcTCaU5LB4o1op45GKvETdsqo6inIyGFecHXQUiZg1Jp83dx7i8HGd+zXeqMhLVLW1d/DS5jouK8vXKtcQuXxMAe0djpc31wcdRXqZirxE1Vu7DtHQ1MYs7R0fKlOG5JGTkcLSKs3LxxsVeYmqpVW1JCcZM0YPDDqKdJGSnMTMsnyWbqqjo0OtlPFERV6iamlVHR8Y2o+cjNSgo8gJLh9TQF1jM+urG4KOIr1IRV6ipraxicq9DVymqZpQuqzM+70s26RdKeOJirxEzbJI66Tm48MpPzudiYNyWLJR8/LxREVeombppjoKstMZX5wTdBTpxuVjCnhz50EOH1MrZbxQkZeoaGvv4KVNap0Mu1lj8ulw8NIWTdnECxV5iYpV77ROapVrmE0Z0o/czFSWbFSRjxcq8hIVS6vqSE4yLlHrZKglJxmXluWzTK2UcUNFXqJi6aZapg3NIzdTrZNhN6ssn/ojaqWMFyry4rvaxibW7WnQVE2MuDTSSqnVr/HB1yJvZkPMbImZrTezSjO728/jSTgt3+Tth9LZhy3hlp+dznmDc1lSpXn5eOD3SL4N+KJzbjxwIfA5Mxvv8zElZJZW1ZKfnc6EErVOxopZZfm8tfMgh461BB1FzpGvRd45V+2cezPydSOwARjk5zElXLxdJ+vVOhljLhtT4LVSalfKmBe1OXkzKwWmAq9H65gSvNW7vT3Ktco1tkwZkkden1SWaF4+5kWlyJtZFvAI8HnnXMMJl91lZhVmVlFXpznAeLO0qo4kg5mjVORjSXKScenofJarlTLm+V7kzSwVr8D/1jn36ImXO+fuc86VO+fK8/NVCOLN0qo6pg3tR24ftU7Gmllj8qk/0kLlXrVSxjK/u2sMeADY4Jz7np/HkvCpa2xm7Z7DmqqJUZeW5WOGpmxinN8j+RnAx4HZZrYq8nGVz8eUkFi+qXPXSfXHx6KBWelMGpSrfvkYl+LnjTvnXgbUUpGglm6qY2CWdp2MZbPGFHDv4s0cOtZCXp+0oOPIWdCKV/FFe4d754TdSUl6no9VnbtSLlcrZcxSkRdfrNp1iEPH1DoZ6yYPzqNfn1RN2cQwFXnxxbKqWq91UrtOxrTkJGOmWiljmoq8+GLpprrIghrN48Y6tVLGNhV56XX1R5pZs/uwumrihHaljG0q8tLrXtqsE3bHk4FZ3q6USzdpRXosUpGXXresqo4BfdOYWJIbdBTpJdqVMnapyEuv6uhwLN9cz6VqnYwr2pUydqnIS69au+cwB4626AQhcaZzV8qlOpFIzFGRl161bFMdptbJuNPZSqkTfMceFXnpVUurajlvUC4DstKDjiK9TCf4jk0q8tJrDh1rYdWuQ5qqiVNqpYxNKvLSa17eUk+H896kk/iTn925K6Xm5WOJirz0mqVVdeRmpjJ5sFon49WsMfm8ufMgh4+1Bh1FzpCKvPQK5xzLNtVxyeiBpCTrYRWvOnelfGmLRvOxQn+N0ivWVzdQ19jMLM3Hx7UpQ/qRm6lWyliiIi+9YllkybvedI1vXivlQLVSxhAVeekVy6rqGFecQ0FORtBRxGezxhRQ16hWylihIi/nrLGplZU7DmpDsgTR+WptmTYsiwkq8nLOXtmyn7YOp6maBJGfnc7EQTnql48RKvJyzpZtqiMrPYUPDOsXdBSJklllBby585BaKWOAirycE+ccy6pqmTFqAKlqnUwYs8bkeydrVytl6OmvUs7Jltoj7D3cxGVlWuWaSKYMySMnI0WtlDHA1yJvZr8ws1ozW+fncSQ477RO6k3XhJKSnMTMMu1KGQv8Hsn/Epjv8zEkQEur6hhdkMWgvMygo0iUzSrLVytlDPC1yDvnlgMH/DyGBOdIcxtvvH1ArZMJqvPVm1opwy3wOXkzu8vMKsysoq5OD5ZY8vLmelraO5g9tjDoKBKAguwMJpSolTLsAi/yzrn7nHPlzrny/HyNCGPJ4o01ZGekUF6q1slE5e1KeYjDx9VKGVaBF3mJTR0djiVVdVxalq/WyQQ2a0wB7R2Ol3WC79DSX6eclXV7D1PX2MycsWqdTGRT32ml1JRNWPndQvl74M/AGDPbbWZ3+Hk8iZ7FG2sx80Zykri6tlI6p1bKMPK7u+ajzrli51yqc26wc+4BP48n0bN4Yy1Th+TRv29a0FEkYLPK8qlVK2VoabpGeqy2oYk1uw8zZ5y6auTdVkqtfg0nFXnpsc4/5tmajxfebaVcslHz8mGkIi899uLGGopzMxhblB10FAmJueMKWbnzIPVHmoOOIidQkZceaW5r56XN9cweW4CZBR1HQmLehCKcgxfW1wQdRU6gIi898urW/RxraWfOOE3VyLvGFWczuF8mi1TkQ0dFXnpk4bp9ZKWncPHIgUFHkRAxM64cX8TLW+o50twWdBzpQkVezlh7h+P59TVcPraAjNTkoONIyFw5oZCWtg6WqcsmVFTk5YxVbD/A/qMtzJ9QFHQUCaHyYf3o3zeNRev3BR1FulCRlzP2XOU+0lKStLWwnFRKchJzxhaweGMtLW0dQceRCBV5OSPOORZV1nDp6IH0TU8JOo6E1JUTimhsauO1bfuDjiIRKvJyRtbtaWDPoePM01SNnMLM0QPJTE3muUpN2YSFiryckWfWVZOcZMzVVgZyChmpycwdX8iza6tpbdeUTRioyMtpdXQ4nly1l5mjB9JPG5LJaVwzuYSDx1q1x3xIqMjLaVXsOMieQ8e5dsqgoKNIDLi0LJ/czFSeWLUn6CiCirycgSdW7SEzNZkrxmuqRk4vLSWJqyYVs2h9DcdatDAqaCryckotbR08vbaaK8YXqqtGztg1U0o41tLOCxu0M2XQVOTllBZvrOXQsVaunVoSdBSJIdNL+1OSm8EfK3YFHSXhqcjLKf3ujZ0U5WRw6WgtgJIzl5Rk3HT+UF7aXM+O/UeDjpPQVOSlWzv3H2P5pjpunj6ElGQ9VKRnbp4+hOQk43ev7ww6SkLTX65067ev7yA5ybj5/KFBR5EYVJiTwZXjC/nfil00tbYHHSdhqcjLSR0+1spvX9/J/IlFFOVmBB1HYtQnLirl4LFWzc0HSEVeTuqXr27nSHMbf335qKCjSAy7cER/zi/tx4+XbKW5TaP5IPhe5M1svplVmdkWM/uK38eTc1fT0MR9y7dy5fhCxhXnBB1HYpiZ8YW5ZexraOKhV3cEHSch+VrkzSwZ+DHwQWA88FEzG+/nMeXcOOf42uPraO1wfHXBuKDjSBy4aOQA5o4r4LvPV/F2vTptos3vkfx0YItzbptzrgV4GLjG52PKWWpoauWbz25k0foavjxvDMMG9A06ksQBM+M/rp1Eekoyd/xyBev3NtDe4YKOlTD8XsI4COj6jstu4ILePkhNQxMff+B1ANwJj52u37ouF7purnQm13//MdxJLzvxej253e5u89RZuslxhsc+fLwVgI9dOJQ7Lhl+8vAiZ6EoN4MHbi3ntgdXcNUPXyIjNYmC7AzSU5JIMgs6XihMHpLLtz8yuddvN/B16mZ2F3AXwNChZ9eql5JkjMzP6nKbJxwD6/rNyb7Euvyj9/789Nc/8bL3HqPL7XZ7W6e//vu/753b7Xr9/Ox0pg3rx0UjBrzv/ydyrspL+7P4i5exdFMdVfsaOXC0habW9m4HQ4mmKDfTl9s15+M9bGYXAf/inJsX+f4fAJxz3zzZ9cvLy11FRYVveURE4pGZrXTOlZ/sMr/n5FcAo81suJmlATcDT/p8TBERifB1usY512Zmfw0sBJKBXzjnKv08poiIvMv3OXnn3DPAM34fR0RE3k8rXkVE4piKvIhIHFORFxGJYyryIiJxzNc++Z4yszrgXHYxGgjU91Kc3qRcPaNcPaNcPROPuYY55056+rZQFflzZWYV3S0ICJJy9Yxy9Yxy9Uyi5dJ0jYhIHFORFxGJY/FW5O8LOkA3lKtnlKtnlKtnEipXXM3Ji4jIe8XbSF5ERLqIqSJvZv5suHyOzCyUp1AysxFmNiboHCfS77FnzGyYmeUFneNEZnbSlr0wsJCeECGIx35MFHkzyzKze4GfR04Mnht0Jngn1z3AL8zsBjMrCDoTgJllmNlP8Hb/7NzmOXCR++v7wA/NbFbIfo/fB35jZh8zs2FBZ4J3cn0PeBooCTpPp0iu7wLPmdl/mtmMoDMBmFm2mf3IzMa4kM1DB1nDYqLIA/cAacCjwEeBrwQbB8zsauAVoBX4PfBp4AOBhnrXjcAA59xo59xzkfPrBsrMsoBf4N1ffwIWAF8KNBRgZpcALwHH8fLNxHuMBcrMyvEeX/2Bqc659QFHAsDMUoAf4+1g+wm8s0jOCTQUYGaj8M4hfSfwbwHHOZnAaljgp//rjpmZc86Z2UC8UcyNzrkjZrYF+IKZ3emcuz+AXEnOuQ7gbeAO51xF5Oc3Ag3RznMiM0sCioDfRL6/HC/XNufcwQDyWGRUVQKMcs7dGPm5A75mZuuccw9HO1cX+4GfdD6WzGwwMCLytQU4ImwCtgLfd861mtkU4BCw2znXFlAmgHyg1Dl3GYCZ9QFWB5in01HgO8A1wCozm++cey7I32FYaljoRvJmNtbMfgb8rZnlOOfqgQ68Z2iAjcBjwNVm1j/AXJXOuQozyzezZ4ELI5fdGBm1RjWXmd0dydUBlAEzzexzwH8BnwV+bWbF0c7Fu/fXJmCHmX06cpVjeE+UHzGzflHMNdLMbu/83jm3AfhdlzncPcCwyGVRKw4nybUObyT/t2a2FPgR8H3g22Y2IMBc1YAzswfN7HXgauDDZvZ4lB9fo83sB2b2GTPrF8m1IvIE+APgnyN5o17gw1bDQlXkzWw43gh0KzAZ+GlkBPMdYF7kl9kMrMErENMCyHUecK+ZXRC5+ADwO+fcCOAB4GLg2gByTQZ+ZmZlwDeBW4CxzrnpeEV+M/C1gHLdG5m3vQf4RzP7KfA94ClgJ94rj2jk+iywEm8UdUPkZ0nOuaNdisEUIKpnLztZroiH8M6o9phzbibwr5Hv7wg414eAXwEbnHNlwKfw9pz65yjl+gpekdwDzAL+x8yS8QYOREbHHWZ2dzTynJAtdDUsVEUeGAvUO+e+gzfHXYVXMJvwXhJ2ngj8baAU7yVaELm2AAvMbKRzrt059+tIrkVAHtAYUK6NwK3AEbxz6c6M5GrGm3feF0Cuz+DdXx/Ee2BfDDwLXBa532bizYdHw1a8gvQ14BYzy4i88iFSJACKgVcjP5tjZoVB5AJwztUBf++c+0Hk+1V4j639Uch0qlyNwBC8v8vOx9fLQK3fgczrgDoC3OSc+zZwGzARmBiZGkmNXPWfgDvMLNXMPhTFN9NDV8PCVuTXAU1mNtY514pXDPrgTT/cB1xrZteb2YV4c4PRapM6MdczkVwXd72SmZ0HDCd6O9ydLFcmcKDm4R4AAAX3SURBVBnwRaCfmV1nZnOAv8cb+UQ7Vwvv3l9XO+f2OOeedM4dMrOL8UaAUXlSdM4txHvjaxXeK7C/gndG8+2R9zOKgTFm9gzeG4sdAeayyEt9It+fB1wOVPudqZtcn+ly8SK8aZp5kTeJ/47oPL6OAY845yrNLN051wS8ifcKh8jfAc65pXiDhwbgc0C03scIXQ0LW5FPBzYAlwA451bgPaBHOOe2Al8GpgP3Az91zr0aUK4KYDdQamZJZjbczB7H+yX+1Dn3SoC5duGNao7jFalivJHYD5xzDwSYayfeyAUzG2hm9wM/Bf7onIvWyJTIyH0PXvGaa2ajO0fzwEjgw8BHgIecc7dGRtNB5XIAZtbfzP4P+Dnwo8h5k6PihFxXmNnoyM9rgK/ivXK8H7jHOef7dgHOUx35ujnyCmwa8E5TgZmlRaaaioDbnXPznXO9+gRkJ6yp6PKeTvhqmHMuqh94bwR+Akjq5vJPAf8NXBT5/kJgXUhzrYl8nQncFqJca8N8f0W+vyqIXF2uV4T33sU/Rb4fHfl8d8hylUU+/0XIcnXeXxkB55oJPNU1Z+TzRD9yRW77n4FleG2Ql0V+ltzl8kBqWLd5o3Yg6If3rnct8Bww/ITLO/fRGYrXP/0MkAXcjPeGZp+Q5uob0lxhvb+ygsjVzb8Zg/eG9FHgyyHN9aWQ5vri6Qqwn7m6PM6uxnulegOwHp8GW5FjlUYez7+IFO5/wFsjkx25PCnyOap/k6fN7fsBILPzM95LmCTgwcidkNbdLxDv3ejH8ea4piuXcvViriSgAHgdeA2YqVyxlyty/fvx3jf5ox+5IsfoE/k8ALizy88/APySyKuHE/6N74/9M87v2w17d8j/AL/GWxHXp8tl5wOL8VbydffvDchXLuXyKVcGPkyBKFf0ckUeW3fg31Rp12xz8VasGu+O2AfjPfGd9NWpX4/9nn74ttWwmf0eqAHeAK4Edjnnvtbl8u/irbj9mnMuaitFlUu5Il0rvjzwlSs6ufzM1INss4FPO+du8jPHOfPpGbAQbz6t80lkGvBb4JYu1ynBay+6CK+P9BK/n9GUS7mUS7l6MdvtwL9Hvr4CmBKNbD39OOcWyi6tQ+9wXntVZuROAK+l6Em8Jex9I9fZG/n5i3gr+Xq1j1W5lEu5lMunbJ3blkwDcszsF3itke29na1XnOOzXUaXrzuf8Trnq67DW7be+YbKKOBeIs/EeIs6dgCf9+FZWLmUS7mUy7dseFNIq/GK/1/5ka23Ps56JG/eySh2mtl/RH504m29jLeM/ssAzrkteC1IRyKXV+HtrXLP2WZQLuVSLuUKINtR522E9n2g3Dn3097O1qvO4RlwNFCBt4S/OPKzlC6XD8Pbx2ELMB9vqf0rwAf8fNZSLuVSLuXyOdv5fmfr1f9nD+6Qrv95w1sMcCPwLWBhl58PAx4Bfhb52Y3At4G1wA0+/KKUS7mUS7liOpufH2d0x+At0f0BMLfLz+cA90e+rsHrIy3BW4H2n74HVy7lUi7livFsUfn/n+bOMeAnePsj/yXwPN6ObqmRO+j2yPX+gLfq7Fsn/PteX/asXMqlXMoVD9mi9XG60/9l451EYZ5zrtHM6vGe5Rbg7Sr4EzO7NXLnbMbbz7xzb+4O9+7ufr1NuZRLuZQr1rNFxSm7a5y3wmw73sb84L3psBJv9VcW3j4Wv3bOzcbbMe5LZpbsvBNpOL9CK5dyKZdyxXq2aDmTE3k/Bsw3s2LnXLWZrQUmAM3OuVvhnSXGr0d+Hi3KpVzKpVyxns13Z9In/zJei9FtAM65lXhLjFMAzCwloGc85VIu5VKuWM/mu9MWeeedheUJ4INm9hdmVop3vsLO02xF67RayqVcyqVccZUtKtyZv0v9QbzN8jcCf32m/87vD+VSLuVSrljP5udHj7YaNu9M6M6F7JlPuXpGuXpGuXomrLkg3Nn84tt+8iIiErxz3mpYRETCS0VeRCSOqciLiMQxFXkRkTimIi8iEsdU5EVE4piKvIhIHFORFxGJY/8f3kJFtscI+egAAAAASUVORK5CYII=\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "single_diode_out['i_sc'].plot();" + "single_diode_out[\"i_sc\"].plot();" ] }, { @@ -1043,19 +1099,19 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "single_diode_out['v_oc'].plot();" + "single_diode_out[\"v_oc\"].plot();" ] }, { @@ -1064,19 +1120,19 @@ "metadata": {}, "outputs": [ { - "output_type": "display_data", "data": { - "text/plain": "
", + "image/png": "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\n", "image/svg+xml": "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", - "image/png": "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\n" + "text/plain": "
" }, "metadata": { "needs_background": "light" - } + }, + "output_type": "display_data" } ], "source": [ - "single_diode_out['p_mp'].plot();" + "single_diode_out[\"p_mp\"].plot();" ] }, { diff --git a/docs/tutorials/solarposition.ipynb b/docs/tutorials/solarposition.ipynb index bd3245445d..16f9abf959 100644 --- a/docs/tutorials/solarposition.ipynb +++ b/docs/tutorials/solarposition.ipynb @@ -38,7 +38,6 @@ "import datetime\n", "\n", "# scientific python add-ons\n", - "import numpy as np\n", "import pandas as pd\n", "\n", "# plotting stuff\n", @@ -56,7 +55,6 @@ "metadata": {}, "outputs": [], "source": [ - "import pvlib\n", "from pvlib.location import Location" ] }, @@ -104,13 +102,13 @@ } ], "source": [ - "tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')\n", + "tus = Location(32.2, -111, \"US/Arizona\", 700, \"Tucson\")\n", "print(tus)\n", - "golden = Location(39.742476, -105.1786, 'America/Denver', 1830, 'Golden')\n", + "golden = Location(39.742476, -105.1786, \"America/Denver\", 1830, \"Golden\")\n", "print(golden)\n", - "golden_mst = Location(39.742476, -105.1786, 'MST', 1830, 'Golden MST')\n", + "golden_mst = Location(39.742476, -105.1786, \"MST\", 1830, \"Golden MST\")\n", "print(golden_mst)\n", - "berlin = Location(52.5167, 13.3833, 'Europe/Berlin', 34, 'Berlin')\n", + "berlin = Location(52.5167, 13.3833, \"Europe/Berlin\", 34, \"Berlin\")\n", "print(berlin)" ] }, @@ -120,7 +118,11 @@ "metadata": {}, "outputs": [], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,23), end=datetime.datetime(2014,6,24), freq='1Min')\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 23),\n", + " end=datetime.datetime(2014, 6, 24),\n", + " freq=\"1Min\",\n", + ")\n", "times_loc = times.tz_localize(tus.pytz)" ] }, @@ -208,18 +210,20 @@ } ], "source": [ - "pyephemout = pvlib.solarposition.pyephem(times_loc, tus.latitude, tus.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times_loc, tus.latitude, tus.longitude\n", + ")\n", "spaout = pvlib.solarposition.spa_python(times_loc, tus.latitude, tus.longitude)\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "spaout['elevation'].plot(label='spa')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "spaout[\"elevation\"].plot(label=\"spa\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('spa')\n", + "print(\"spa\")\n", "print(spaout.head())" ] }, @@ -279,32 +283,32 @@ ], "source": [ "plt.figure()\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "spaout['elevation'].plot(label='spa')\n", - "(pyephemout['elevation'] - spaout['elevation']).plot(label='diff')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "spaout[\"elevation\"].plot(label=\"spa\")\n", + "(pyephemout[\"elevation\"] - spaout[\"elevation\"]).plot(label=\"diff\")\n", "plt.legend(ncol=3)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", "plt.figure()\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "spaout['elevation'].plot(label='spa')\n", - "(pyephemout['apparent_elevation'] - spaout['elevation']).plot(label='diff')\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "spaout[\"elevation\"].plot(label=\"spa\")\n", + "(pyephemout[\"apparent_elevation\"] - spaout[\"elevation\"]).plot(label=\"diff\")\n", "plt.legend(ncol=3)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", "plt.figure()\n", - "pyephemout['apparent_zenith'].plot(label='pyephem apparent')\n", - "spaout['zenith'].plot(label='spa')\n", - "(pyephemout['apparent_zenith'] - spaout['zenith']).plot(label='diff')\n", + "pyephemout[\"apparent_zenith\"].plot(label=\"pyephem apparent\")\n", + "spaout[\"zenith\"].plot(label=\"spa\")\n", + "(pyephemout[\"apparent_zenith\"] - spaout[\"zenith\"]).plot(label=\"diff\")\n", "plt.legend(ncol=3)\n", - "plt.title('zenith')\n", + "plt.title(\"zenith\")\n", "\n", "plt.figure()\n", - "pyephemout['apparent_azimuth'].plot(label='pyephem apparent')\n", - "spaout['azimuth'].plot(label='spa')\n", - "(pyephemout['apparent_azimuth'] - spaout['azimuth']).plot(label='diff')\n", + "pyephemout[\"apparent_azimuth\"].plot(label=\"pyephem apparent\")\n", + "spaout[\"azimuth\"].plot(label=\"spa\")\n", + "(pyephemout[\"apparent_azimuth\"] - spaout[\"azimuth\"]).plot(label=\"diff\")\n", "plt.legend(ncol=3)\n", - "plt.title('azimuth');" + "plt.title(\"azimuth\");" ] }, { @@ -360,18 +364,22 @@ } ], "source": [ - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(golden.tz), golden.latitude, golden.longitude)\n", - "spaout = pvlib.solarposition.spa_python(times.tz_localize(golden.tz), golden.latitude, golden.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(golden.tz), golden.latitude, golden.longitude\n", + ")\n", + "spaout = pvlib.solarposition.spa_python(\n", + " times.tz_localize(golden.tz), golden.latitude, golden.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "spaout['elevation'].plot(label='spa')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "spaout[\"elevation\"].plot(label=\"spa\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('spa')\n", + "print(\"spa\")\n", "print(spaout.head())" ] }, @@ -428,18 +436,22 @@ } ], "source": [ - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(golden.tz), golden.latitude, golden.longitude)\n", - "ephemout = pvlib.solarposition.ephemeris(times.tz_localize(golden.tz), golden.latitude, golden.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(golden.tz), golden.latitude, golden.longitude\n", + ")\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times.tz_localize(golden.tz), golden.latitude, golden.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('ephem')\n", + "print(\"ephem\")\n", "print(ephemout.head())" ] }, @@ -498,19 +510,23 @@ "source": [ "loc = berlin\n", "\n", - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", - "ephemout = pvlib.solarposition.ephemeris(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", - "ephemout['apparent_elevation'].plot(label='ephem apparent')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", + "ephemout[\"apparent_elevation\"].plot(label=\"ephem apparent\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('ephem')\n", + "print(\"ephem\")\n", "print(ephemout.head())" ] }, @@ -580,26 +596,34 @@ ], "source": [ "loc = berlin\n", - "times = pd.date_range(start=datetime.date(2015,3,28), end=datetime.date(2015,3,29), freq='5min')\n", + "times = pd.date_range(\n", + " start=datetime.date(2015, 3, 28),\n", + " end=datetime.date(2015, 3, 29),\n", + " freq=\"5min\",\n", + ")\n", "\n", - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", - "ephemout = pvlib.solarposition.ephemeris(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", "plt.figure()\n", - "pyephemout['azimuth'].plot(label='pyephem')\n", - "ephemout['azimuth'].plot(label='ephem')\n", + "pyephemout[\"azimuth\"].plot(label=\"pyephem\")\n", + "ephemout[\"azimuth\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('azimuth')\n", + "plt.title(\"azimuth\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('ephem')\n", + "print(\"ephem\")\n", "print(ephemout.head())" ] }, @@ -669,26 +693,34 @@ ], "source": [ "loc = berlin\n", - "times = pd.date_range(start=datetime.date(2015,3,30), end=datetime.date(2015,3,31), freq='5min')\n", + "times = pd.date_range(\n", + " start=datetime.date(2015, 3, 30),\n", + " end=datetime.date(2015, 3, 31),\n", + " freq=\"5min\",\n", + ")\n", "\n", - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", - "ephemout = pvlib.solarposition.ephemeris(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", "plt.figure()\n", - "pyephemout['azimuth'].plot(label='pyephem')\n", - "ephemout['azimuth'].plot(label='ephem')\n", + "pyephemout[\"azimuth\"].plot(label=\"pyephem\")\n", + "ephemout[\"azimuth\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('azimuth')\n", + "plt.title(\"azimuth\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('ephem')\n", + "print(\"ephem\")\n", "print(ephemout.head())" ] }, @@ -758,26 +790,34 @@ ], "source": [ "loc = berlin\n", - "times = pd.date_range(start=datetime.date(2015,6,28), end=datetime.date(2015,6,29), freq='5min')\n", + "times = pd.date_range(\n", + " start=datetime.date(2015, 6, 28),\n", + " end=datetime.date(2015, 6, 29),\n", + " freq=\"5min\",\n", + ")\n", "\n", - "pyephemout = pvlib.solarposition.pyephem(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", - "ephemout = pvlib.solarposition.ephemeris(times.tz_localize(loc.tz), loc.latitude, loc.longitude)\n", + "pyephemout = pvlib.solarposition.pyephem(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times.tz_localize(loc.tz), loc.latitude, loc.longitude\n", + ")\n", "\n", - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", + "plt.title(\"elevation\")\n", "\n", "plt.figure()\n", - "pyephemout['azimuth'].plot(label='pyephem')\n", - "ephemout['azimuth'].plot(label='ephem')\n", + "pyephemout[\"azimuth\"].plot(label=\"pyephem\")\n", + "ephemout[\"azimuth\"].plot(label=\"ephem\")\n", "plt.legend(ncol=2)\n", - "plt.title('azimuth')\n", + "plt.title(\"azimuth\")\n", "\n", - "print('pyephem')\n", + "print(\"pyephem\")\n", "print(pyephemout.head())\n", - "print('ephem')\n", + "print(\"ephem\")\n", "print(ephemout.head())" ] }, @@ -800,14 +840,17 @@ } ], "source": [ - "pyephemout['elevation'].plot(label='pyephem')\n", - "pyephemout['apparent_elevation'].plot(label='pyephem apparent')\n", - "ephemout['elevation'].plot(label='ephem')\n", - "ephemout['apparent_elevation'].plot(label='ephem apparent')\n", + "pyephemout[\"elevation\"].plot(label=\"pyephem\")\n", + "pyephemout[\"apparent_elevation\"].plot(label=\"pyephem apparent\")\n", + "ephemout[\"elevation\"].plot(label=\"ephem\")\n", + "ephemout[\"apparent_elevation\"].plot(label=\"ephem apparent\")\n", "plt.legend(ncol=2)\n", - "plt.title('elevation')\n", - "plt.xlim(pd.Timestamp('2015-06-28 02:00:00+02:00'), pd.Timestamp('2015-06-28 06:00:00+02:00'))\n", - "plt.ylim(-10,10);" + "plt.title(\"elevation\")\n", + "plt.xlim(\n", + " pd.Timestamp(\"2015-06-28 02:00:00+02:00\"),\n", + " pd.Timestamp(\"2015-06-28 06:00:00+02:00\"),\n", + ")\n", + "plt.ylim(-10, 10);" ] }, { @@ -833,7 +876,7 @@ " datetime.datetime(2020, 9, 14, 15),\n", " 32.2,\n", " -110.9,\n", - " 'alt',\n", + " \"alt\",\n", " 0.05235987755982988, # 3 degrees in radians\n", ")" ] @@ -860,7 +903,7 @@ " datetime.datetime(2020, 9, 15, 4),\n", " 32.2,\n", " -110.9,\n", - " 'alt',\n", + " \"alt\",\n", " 0.05235987755982988, # 3 degrees in radians\n", ")" ] @@ -878,7 +921,7 @@ "metadata": {}, "outputs": [], "source": [ - "times = pd.date_range(start='20180601', freq='1min', periods=14400)\n", + "times = pd.date_range(start=\"20180601\", freq=\"1min\", periods=14400)\n", "times_loc = times.tz_localize(loc.tz)" ] }, @@ -899,8 +942,10 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "pyephemout = pvlib.solarposition.pyephem(times_loc, loc.latitude, loc.longitude)\n", - "#ephemout = pvlib.solarposition.ephemeris(times, loc)" + "pyephemout = pvlib.solarposition.pyephem(\n", + " times_loc, loc.latitude, loc.longitude\n", + ")\n", + "# ephemout = pvlib.solarposition.ephemeris(times, loc)" ] }, { @@ -920,8 +965,10 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "#pyephemout = pvlib.solarposition.pyephem(times, loc)\n", - "ephemout = pvlib.solarposition.ephemeris(times_loc, loc.latitude, loc.longitude)" + "# pyephemout = pvlib.solarposition.pyephem(times, loc)\n", + "ephemout = pvlib.solarposition.ephemeris(\n", + " times_loc, loc.latitude, loc.longitude\n", + ")" ] }, { @@ -941,9 +988,10 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "#pyephemout = pvlib.solarposition.pyephem(times, loc)\n", - "ephemout = pvlib.solarposition.get_solarposition(times_loc, loc.latitude, loc.longitude,\n", - " method='nrel_numpy')" + "# pyephemout = pvlib.solarposition.pyephem(times, loc)\n", + "ephemout = pvlib.solarposition.get_solarposition(\n", + " times_loc, loc.latitude, loc.longitude, method=\"nrel_numpy\"\n", + ")" ] }, { @@ -978,9 +1026,10 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "#pyephemout = pvlib.solarposition.pyephem(times, loc)\n", - "ephemout = pvlib.solarposition.get_solarposition(times_loc, loc.latitude, loc.longitude,\n", - " method='nrel_numba')" + "# pyephemout = pvlib.solarposition.pyephem(times, loc)\n", + "ephemout = pvlib.solarposition.get_solarposition(\n", + " times_loc, loc.latitude, loc.longitude, method=\"nrel_numba\"\n", + ")" ] }, { @@ -1007,9 +1056,10 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "#pyephemout = pvlib.solarposition.pyephem(times, loc)\n", - "ephemout = pvlib.solarposition.get_solarposition(times_loc, loc.latitude, loc.longitude,\n", - " method='nrel_numba', numthreads=16)" + "# pyephemout = pvlib.solarposition.pyephem(times, loc)\n", + "ephemout = pvlib.solarposition.get_solarposition(\n", + " times_loc, loc.latitude, loc.longitude, method=\"nrel_numba\", numthreads=16\n", + ")" ] }, { @@ -1029,8 +1079,9 @@ "%%timeit\n", "# NBVAL_SKIP\n", "\n", - "ephemout = pvlib.solarposition.spa_python(times_loc, loc.latitude, loc.longitude,\n", - " how='numba', numthreads=16)" + "ephemout = pvlib.solarposition.spa_python(\n", + " times_loc, loc.latitude, loc.longitude, how=\"numba\", numthreads=16\n", + ")" ] }, { diff --git a/docs/tutorials/tmy.ipynb b/docs/tutorials/tmy.ipynb index 7b371919c0..36d7a971e6 100644 --- a/docs/tutorials/tmy.ipynb +++ b/docs/tutorials/tmy.ipynb @@ -32,17 +32,14 @@ "outputs": [], "source": [ "# built in python modules\n", - "import datetime\n", "import os\n", "import inspect\n", "\n", "# python add-ons\n", - "import numpy as np\n", - "import pandas as pd\n", "\n", "# plotting libraries\n", "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", + "\n", "try:\n", " import seaborn as sns\n", "except ImportError:\n", @@ -81,8 +78,12 @@ "metadata": {}, "outputs": [], "source": [ - "tmy3_data, tmy3_metadata = pvlib.iotools.read_tmy3(os.path.join(pvlib_abspath, 'data', '703165TY.csv'))\n", - "tmy2_data, tmy2_metadata = pvlib.iotools.read_tmy2(os.path.join(pvlib_abspath, 'data', '12839.tm2'))" + "tmy3_data, tmy3_metadata = pvlib.iotools.read_tmy3(\n", + " os.path.join(pvlib_abspath, \"data\", \"703165TY.csv\")\n", + ")\n", + "tmy2_data, tmy2_metadata = pvlib.iotools.read_tmy2(\n", + " os.path.join(pvlib_abspath, \"data\", \"12839.tm2\")\n", + ")" ] }, { @@ -349,7 +350,7 @@ } ], "source": [ - "tmy3_data['GHI'].plot();" + "tmy3_data[\"GHI\"].plot();" ] }, { @@ -365,7 +366,9 @@ "metadata": {}, "outputs": [], "source": [ - "tmy3_data, tmy3_metadata = pvlib.iotools.read_tmy3(os.path.join(pvlib_abspath, 'data', '703165TY.csv'), coerce_year=1987)" + "tmy3_data, tmy3_metadata = pvlib.iotools.read_tmy3(\n", + " os.path.join(pvlib_abspath, \"data\", \"703165TY.csv\"), coerce_year=1987\n", + ")" ] }, { @@ -387,7 +390,7 @@ } ], "source": [ - "tmy3_data['GHI'].plot();" + "tmy3_data[\"GHI\"].plot();" ] }, { diff --git a/docs/tutorials/tmy_and_diffuse_irrad_models.ipynb b/docs/tutorials/tmy_and_diffuse_irrad_models.ipynb index 28b4630b01..32217a2707 100644 --- a/docs/tutorials/tmy_and_diffuse_irrad_models.ipynb +++ b/docs/tutorials/tmy_and_diffuse_irrad_models.ipynb @@ -40,7 +40,6 @@ "import inspect\n", "\n", "# scientific python add-ons\n", - "import numpy as np\n", "import pandas as pd\n", "\n", "# plotting stuff\n", @@ -62,15 +61,15 @@ "pvlib_abspath = os.path.dirname(os.path.abspath(inspect.getfile(pvlib)))\n", "\n", "# absolute path to a data file\n", - "datapath = os.path.join(pvlib_abspath, 'data', '703165TY.csv')\n", + "datapath = os.path.join(pvlib_abspath, \"data\", \"703165TY.csv\")\n", "\n", "# read tmy data with year values coerced to a single year\n", "tmy_data, meta = pvlib.iotools.read_tmy3(datapath, coerce_year=2015)\n", - "tmy_data.index.name = 'Time'\n", + "tmy_data.index.name = \"Time\"\n", "\n", "# TMY data seems to be given as hourly data with time stamp at the end\n", "# shift the index 30 Minutes back for calculation of sun positions\n", - "tmy_data = tmy_data.shift(freq='-30Min')['2015']" + "tmy_data = tmy_data.shift(freq=\"-30Min\")[\"2015\"]" ] }, { @@ -93,7 +92,7 @@ ], "source": [ "tmy_data.GHI.plot()\n", - "plt.ylabel('Irradiance (W/m**2)');" + "plt.ylabel(\"Irradiance (W/m**2)\");" ] }, { @@ -116,7 +115,7 @@ ], "source": [ "tmy_data.DHI.plot()\n", - "plt.ylabel('Irradiance (W/m**2)');" + "plt.ylabel(\"Irradiance (W/m**2)\");" ] }, { @@ -139,12 +138,19 @@ ], "source": [ "surface_tilt = 30\n", - "surface_azimuth = 180 # pvlib uses 0=North, 90=East, 180=South, 270=West convention\n", + "surface_azimuth = (\n", + " 180 # pvlib uses 0=North, 90=East, 180=South, 270=West convention\n", + ")\n", "albedo = 0.2\n", "\n", "# create pvlib Location object based on meta data\n", - "sand_point = pvlib.location.Location(meta['latitude'], meta['longitude'], tz='US/Alaska', \n", - " altitude=meta['altitude'], name=meta['Name'].replace('\"',''))\n", + "sand_point = pvlib.location.Location(\n", + " meta[\"latitude\"],\n", + " meta[\"longitude\"],\n", + " tz=\"US/Alaska\",\n", + " altitude=meta[\"altitude\"],\n", + " name=meta[\"Name\"].replace('\"', \"\"),\n", + ")\n", "print(sand_point)" ] }, @@ -167,7 +173,9 @@ } ], "source": [ - "solpos = pvlib.solarposition.get_solarposition(tmy_data.index, sand_point.latitude, sand_point.longitude)\n", + "solpos = pvlib.solarposition.get_solarposition(\n", + " tmy_data.index, sand_point.latitude, sand_point.longitude\n", + ")\n", "\n", "solpos.plot();" ] @@ -198,7 +206,7 @@ "dni_extra = pd.Series(dni_extra, index=tmy_data.index)\n", "\n", "dni_extra.plot()\n", - "plt.ylabel('Extra terrestrial radiation (W/m**2)');" + "plt.ylabel(\"Extra terrestrial radiation (W/m**2)\");" ] }, { @@ -220,10 +228,10 @@ } ], "source": [ - "airmass = pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith'])\n", + "airmass = pvlib.atmosphere.get_relative_airmass(solpos[\"apparent_zenith\"])\n", "\n", "airmass.plot()\n", - "plt.ylabel('Airmass');" + "plt.ylabel(\"Airmass\");" ] }, { @@ -255,7 +263,7 @@ "metadata": {}, "outputs": [], "source": [ - "models = ['Perez', 'Hay-Davies', 'Isotropic', 'King', 'Klucher', 'Reindl']" + "models = [\"Perez\", \"Hay-Davies\", \"Isotropic\", \"King\", \"Klucher\", \"Reindl\"]" ] }, { @@ -271,14 +279,16 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['Perez'] = pvlib.irradiance.perez(surface_tilt,\n", - " surface_azimuth,\n", - " dhi=tmy_data.DHI,\n", - " dni=tmy_data.DNI,\n", - " dni_extra=dni_extra,\n", - " solar_zenith=solpos.apparent_zenith,\n", - " solar_azimuth=solpos.azimuth,\n", - " airmass=airmass)" + "diffuse_irrad[\"Perez\"] = pvlib.irradiance.perez(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " dhi=tmy_data.DHI,\n", + " dni=tmy_data.DNI,\n", + " dni_extra=dni_extra,\n", + " solar_zenith=solpos.apparent_zenith,\n", + " solar_azimuth=solpos.azimuth,\n", + " airmass=airmass,\n", + ")" ] }, { @@ -294,13 +304,15 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['Hay-Davies'] = pvlib.irradiance.haydavies(surface_tilt,\n", - " surface_azimuth,\n", - " dhi=tmy_data.DHI,\n", - " dni=tmy_data.DNI,\n", - " dni_extra=dni_extra,\n", - " solar_zenith=solpos.apparent_zenith,\n", - " solar_azimuth=solpos.azimuth)" + "diffuse_irrad[\"Hay-Davies\"] = pvlib.irradiance.haydavies(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " dhi=tmy_data.DHI,\n", + " dni=tmy_data.DNI,\n", + " dni_extra=dni_extra,\n", + " solar_zenith=solpos.apparent_zenith,\n", + " solar_azimuth=solpos.azimuth,\n", + ")" ] }, { @@ -316,8 +328,9 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['Isotropic'] = pvlib.irradiance.isotropic(surface_tilt,\n", - " dhi=tmy_data.DHI)" + "diffuse_irrad[\"Isotropic\"] = pvlib.irradiance.isotropic(\n", + " surface_tilt, dhi=tmy_data.DHI\n", + ")" ] }, { @@ -333,10 +346,12 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['King'] = pvlib.irradiance.king(surface_tilt,\n", - " dhi=tmy_data.DHI,\n", - " ghi=tmy_data.GHI,\n", - " solar_zenith=solpos.apparent_zenith)" + "diffuse_irrad[\"King\"] = pvlib.irradiance.king(\n", + " surface_tilt,\n", + " dhi=tmy_data.DHI,\n", + " ghi=tmy_data.GHI,\n", + " solar_zenith=solpos.apparent_zenith,\n", + ")" ] }, { @@ -352,11 +367,14 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['Klucher'] = pvlib.irradiance.klucher(surface_tilt, surface_azimuth,\n", - " dhi=tmy_data.DHI,\n", - " ghi=tmy_data.GHI,\n", - " solar_zenith=solpos.apparent_zenith,\n", - " solar_azimuth=solpos.azimuth)" + "diffuse_irrad[\"Klucher\"] = pvlib.irradiance.klucher(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " dhi=tmy_data.DHI,\n", + " ghi=tmy_data.GHI,\n", + " solar_zenith=solpos.apparent_zenith,\n", + " solar_azimuth=solpos.azimuth,\n", + ")" ] }, { @@ -372,14 +390,16 @@ "metadata": {}, "outputs": [], "source": [ - "diffuse_irrad['Reindl'] = pvlib.irradiance.reindl(surface_tilt,\n", - " surface_azimuth,\n", - " dhi=tmy_data.DHI,\n", - " dni=tmy_data.DNI,\n", - " ghi=tmy_data.GHI,\n", - " dni_extra=dni_extra,\n", - " solar_zenith=solpos.apparent_zenith,\n", - " solar_azimuth=solpos.azimuth)" + "diffuse_irrad[\"Reindl\"] = pvlib.irradiance.reindl(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " dhi=tmy_data.DHI,\n", + " dni=tmy_data.DNI,\n", + " ghi=tmy_data.GHI,\n", + " dni_extra=dni_extra,\n", + " solar_zenith=solpos.apparent_zenith,\n", + " solar_azimuth=solpos.azimuth,\n", + ")" ] }, { @@ -395,9 +415,9 @@ "metadata": {}, "outputs": [], "source": [ - "yearly = diffuse_irrad.resample('A').sum().dropna().squeeze() / 1000.0 # kWh\n", - "monthly = diffuse_irrad.resample('M', kind='period').sum() / 1000.0\n", - "daily = diffuse_irrad.resample('D').sum() / 1000.0" + "yearly = diffuse_irrad.resample(\"A\").sum().dropna().squeeze() / 1000.0 # kWh\n", + "monthly = diffuse_irrad.resample(\"M\", kind=\"period\").sum() / 1000.0\n", + "daily = diffuse_irrad.resample(\"D\").sum() / 1000.0" ] }, { @@ -426,9 +446,9 @@ } ], "source": [ - "ax = diffuse_irrad.plot(title='In-plane diffuse irradiance', alpha=.75, lw=1)\n", + "ax = diffuse_irrad.plot(title=\"In-plane diffuse irradiance\", alpha=0.75, lw=1)\n", "ax.set_ylim(0, 800)\n", - "ylabel = ax.set_ylabel('Diffuse Irradiance [W]')\n", + "ylabel = ax.set_ylabel(\"Diffuse Irradiance [W]\")\n", "plt.legend();" ] }, @@ -593,7 +613,7 @@ } ], "source": [ - "diffuse_irrad.dropna().plot(kind='density');" + "diffuse_irrad.dropna().plot(kind=\"density\");" ] }, { @@ -622,8 +642,8 @@ } ], "source": [ - "ax_daily = daily.tz_convert('UTC').plot(title='Daily diffuse irradiation')\n", - "ylabel = ax_daily.set_ylabel('Irradiation [kWh]')" + "ax_daily = daily.tz_convert(\"UTC\").plot(title=\"Daily diffuse irradiation\")\n", + "ylabel = ax_daily.set_ylabel(\"Irradiation [kWh]\")" ] }, { @@ -652,8 +672,10 @@ } ], "source": [ - "ax_monthly = monthly.plot(title='Monthly average diffuse irradiation', kind='bar')\n", - "ylabel = ax_monthly.set_ylabel('Irradiation [kWh]')" + "ax_monthly = monthly.plot(\n", + " title=\"Monthly average diffuse irradiation\", kind=\"bar\"\n", + ")\n", + "ylabel = ax_monthly.set_ylabel(\"Irradiation [kWh]\")" ] }, { @@ -682,7 +704,7 @@ } ], "source": [ - "yearly.plot(kind='barh');" + "yearly.plot(kind=\"barh\");" ] }, { @@ -713,7 +735,7 @@ "source": [ "mean_yearly = yearly.mean()\n", "yearly_mean_deviation = (yearly - mean_yearly) / yearly * 100.0\n", - "yearly_mean_deviation.plot(kind='bar');" + "yearly_mean_deviation.plot(kind=\"bar\");" ] }, { diff --git a/docs/tutorials/tmy_to_power.ipynb b/docs/tutorials/tmy_to_power.ipynb index ae8fa43eb0..2a36ed2b0c 100644 --- a/docs/tutorials/tmy_to_power.ipynb +++ b/docs/tutorials/tmy_to_power.ipynb @@ -86,15 +86,15 @@ "pvlib_abspath = os.path.dirname(os.path.abspath(inspect.getfile(pvlib)))\n", "\n", "# absolute path to a data file\n", - "datapath = os.path.join(pvlib_abspath, 'data', '703165TY.csv')\n", + "datapath = os.path.join(pvlib_abspath, \"data\", \"703165TY.csv\")\n", "\n", "# read tmy data with year values coerced to a single year\n", "tmy_data, meta = pvlib.iotools.read_tmy3(datapath, coerce_year=2015)\n", - "tmy_data.index.name = 'Time'\n", + "tmy_data.index.name = \"Time\"\n", "\n", "# TMY data seems to be given as hourly data with time stamp at the end\n", "# shift the index 30 Minutes back for calculation of sun positions\n", - "tmy_data = tmy_data.shift(freq='-30Min')['2015']" + "tmy_data = tmy_data.shift(freq=\"-30Min\")[\"2015\"]" ] }, { @@ -404,8 +404,8 @@ } ], "source": [ - "tmy_data['GHI'].plot()\n", - "plt.ylabel('Irradiance (W/m**2)');" + "tmy_data[\"GHI\"].plot()\n", + "plt.ylabel(\"Irradiance (W/m**2)\");" ] }, { @@ -455,12 +455,19 @@ ], "source": [ "surface_tilt = 30\n", - "surface_azimuth = 180 # pvlib uses 0=North, 90=East, 180=South, 270=West convention\n", + "surface_azimuth = (\n", + " 180 # pvlib uses 0=North, 90=East, 180=South, 270=West convention\n", + ")\n", "albedo = 0.2\n", "\n", "# create pvlib Location object based on meta data\n", - "sand_point = pvlib.location.Location(meta['latitude'], meta['longitude'], tz='US/Alaska', \n", - " altitude=meta['altitude'], name=meta['Name'].replace('\"',''))\n", + "sand_point = pvlib.location.Location(\n", + " meta[\"latitude\"],\n", + " meta[\"longitude\"],\n", + " tz=\"US/Alaska\",\n", + " altitude=meta[\"altitude\"],\n", + " name=meta[\"Name\"].replace('\"', \"\"),\n", + ")\n", "print(sand_point)" ] }, @@ -499,7 +506,9 @@ } ], "source": [ - "solpos = pvlib.solarposition.get_solarposition(tmy_data.index, sand_point.latitude, sand_point.longitude)\n", + "solpos = pvlib.solarposition.get_solarposition(\n", + " tmy_data.index, sand_point.latitude, sand_point.longitude\n", + ")\n", "\n", "solpos.plot();" ] @@ -546,7 +555,7 @@ "dni_extra = pd.Series(dni_extra, index=tmy_data.index)\n", "\n", "dni_extra.plot()\n", - "plt.ylabel('Extra terrestrial radiation (W/m**2)');" + "plt.ylabel(\"Extra terrestrial radiation (W/m**2)\");" ] }, { @@ -577,10 +586,10 @@ } ], "source": [ - "airmass = pvlib.atmosphere.get_relative_airmass(solpos['apparent_zenith'])\n", + "airmass = pvlib.atmosphere.get_relative_airmass(solpos[\"apparent_zenith\"])\n", "\n", "airmass.plot()\n", - "plt.ylabel('Airmass');" + "plt.ylabel(\"Airmass\");" ] }, { @@ -623,12 +632,18 @@ } ], "source": [ - "poa_sky_diffuse = pvlib.irradiance.haydavies(surface_tilt, surface_azimuth,\n", - " tmy_data['DHI'], tmy_data['DNI'], dni_extra,\n", - " solpos['apparent_zenith'], solpos['azimuth'])\n", + "poa_sky_diffuse = pvlib.irradiance.haydavies(\n", + " surface_tilt,\n", + " surface_azimuth,\n", + " tmy_data[\"DHI\"],\n", + " tmy_data[\"DNI\"],\n", + " dni_extra,\n", + " solpos[\"apparent_zenith\"],\n", + " solpos[\"azimuth\"],\n", + ")\n", "\n", "poa_sky_diffuse.plot()\n", - "plt.ylabel('Irradiance (W/m**2)');" + "plt.ylabel(\"Irradiance (W/m**2)\");" ] }, { @@ -659,10 +674,12 @@ } ], "source": [ - "poa_ground_diffuse = pvlib.irradiance.get_ground_diffuse(surface_tilt, tmy_data['GHI'], albedo=albedo)\n", + "poa_ground_diffuse = pvlib.irradiance.get_ground_diffuse(\n", + " surface_tilt, tmy_data[\"GHI\"], albedo=albedo\n", + ")\n", "\n", "poa_ground_diffuse.plot()\n", - "plt.ylabel('Irradiance (W/m**2)');" + "plt.ylabel(\"Irradiance (W/m**2)\");" ] }, { @@ -693,10 +710,12 @@ } ], "source": [ - "aoi = pvlib.irradiance.aoi(surface_tilt, surface_azimuth, solpos['apparent_zenith'], solpos['azimuth'])\n", + "aoi = pvlib.irradiance.aoi(\n", + " surface_tilt, surface_azimuth, solpos[\"apparent_zenith\"], solpos[\"azimuth\"]\n", + ")\n", "\n", "aoi.plot()\n", - "plt.ylabel('Angle of incidence (deg)');" + "plt.ylabel(\"Angle of incidence (deg)\");" ] }, { @@ -734,11 +753,13 @@ } ], "source": [ - "poa_irrad = pvlib.irradiance.poa_components(aoi, tmy_data['DNI'], poa_sky_diffuse, poa_ground_diffuse)\n", + "poa_irrad = pvlib.irradiance.poa_components(\n", + " aoi, tmy_data[\"DNI\"], poa_sky_diffuse, poa_ground_diffuse\n", + ")\n", "\n", "poa_irrad.plot()\n", - "plt.ylabel('Irradiance (W/m**2)')\n", - "plt.title('POA Irradiance');" + "plt.ylabel(\"Irradiance (W/m**2)\")\n", + "plt.title(\"POA Irradiance\");" ] }, { @@ -769,11 +790,18 @@ } ], "source": [ - "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer']\n", - "pvtemps = pvlib.temperature.sapm_cell(poa_irrad['poa_global'], tmy_data['DryBulb'], tmy_data['Wspd'], **thermal_params)\n", + "thermal_params = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[\"sapm\"][\n", + " \"open_rack_glass_polymer\"\n", + "]\n", + "pvtemps = pvlib.temperature.sapm_cell(\n", + " poa_irrad[\"poa_global\"],\n", + " tmy_data[\"DryBulb\"],\n", + " tmy_data[\"Wspd\"],\n", + " **thermal_params,\n", + ")\n", "\n", "pvtemps.plot()\n", - "plt.ylabel('Temperature (C)');" + "plt.ylabel(\"Temperature (C)\");" ] }, { @@ -796,7 +824,7 @@ "metadata": {}, "outputs": [], "source": [ - "sandia_modules = pvlib.pvsystem.retrieve_sam(name='SandiaMod')" + "sandia_modules = pvlib.pvsystem.retrieve_sam(name=\"SandiaMod\")" ] }, { @@ -882,7 +910,9 @@ "metadata": {}, "outputs": [], "source": [ - "effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(poa_irrad.poa_direct, poa_irrad.poa_diffuse, airmass, aoi, sandia_module)" + "effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(\n", + " poa_irrad.poa_direct, poa_irrad.poa_diffuse, airmass, aoi, sandia_module\n", + ")" ] }, { @@ -927,8 +957,8 @@ "sapm_out = pvlib.pvsystem.sapm(effective_irradiance, pvtemps, sandia_module)\n", "print(sapm_out.head())\n", "\n", - "sapm_out[['p_mp']].plot()\n", - "plt.ylabel('DC Power (W)');" + "sapm_out[[\"p_mp\"]].plot()\n", + "plt.ylabel(\"DC Power (W)\");" ] }, { @@ -944,7 +974,7 @@ "metadata": {}, "outputs": [], "source": [ - "cec_modules = pvlib.pvsystem.retrieve_sam(name='CECMod')\n", + "cec_modules = pvlib.pvsystem.retrieve_sam(name=\"CECMod\")\n", "cec_module = cec_modules.Canadian_Solar_Inc__CS5P_220M" ] }, @@ -954,7 +984,10 @@ "metadata": {}, "outputs": [], "source": [ - "d = {k: cec_module[k] for k in ['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s']}" + "d = {\n", + " k: cec_module[k]\n", + " for k in [\"a_ref\", \"I_L_ref\", \"I_o_ref\", \"R_sh_ref\", \"R_s\"]\n", + "}" ] }, { @@ -963,12 +996,20 @@ "metadata": {}, "outputs": [], "source": [ - "photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth = (\n", - " pvlib.pvsystem.calcparams_desoto(poa_irrad.poa_global,\n", - " pvtemps,\n", - " cec_module['alpha_sc'],\n", - " EgRef=1.121,\n", - " dEgdT=-0.0002677, **d))" + "(\n", + " photocurrent,\n", + " saturation_current,\n", + " resistance_series,\n", + " resistance_shunt,\n", + " nNsVth,\n", + ") = pvlib.pvsystem.calcparams_desoto(\n", + " poa_irrad.poa_global,\n", + " pvtemps,\n", + " cec_module[\"alpha_sc\"],\n", + " EgRef=1.121,\n", + " dEgdT=-0.0002677,\n", + " **d,\n", + ")" ] }, { @@ -977,8 +1018,13 @@ "metadata": {}, "outputs": [], "source": [ - "single_diode_out = pvlib.pvsystem.singlediode(photocurrent, saturation_current,\n", - " resistance_series, resistance_shunt, nNsVth)" + "single_diode_out = pvlib.pvsystem.singlediode(\n", + " photocurrent,\n", + " saturation_current,\n", + " resistance_series,\n", + " resistance_shunt,\n", + " nNsVth,\n", + ")" ] }, { @@ -1000,8 +1046,8 @@ } ], "source": [ - "single_diode_out[['p_mp']].plot()\n", - "plt.ylabel('DC Power (W)');" + "single_diode_out[[\"p_mp\"]].plot()\n", + "plt.ylabel(\"DC Power (W)\");" ] }, { @@ -1024,7 +1070,7 @@ "metadata": {}, "outputs": [], "source": [ - "sapm_inverters = pvlib.pvsystem.retrieve_sam('sandiainverter')" + "sapm_inverters = pvlib.pvsystem.retrieve_sam(\"sandiainverter\")" ] }, { @@ -1067,7 +1113,7 @@ } ], "source": [ - "sapm_inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']\n", + "sapm_inverter = sapm_inverters[\"ABB__MICRO_0_25_I_OUTD_US_208__208V_\"]\n", "sapm_inverter" ] }, @@ -1091,11 +1137,15 @@ ], "source": [ "p_acs = pd.DataFrame()\n", - "p_acs['sapm'] = pvlib.inverter.sandia(sapm_out.v_mp, sapm_out.p_mp, sapm_inverter)\n", - "p_acs['sd'] = pvlib.inverter.sandia(single_diode_out.v_mp, single_diode_out.p_mp, sapm_inverter)\n", + "p_acs[\"sapm\"] = pvlib.inverter.sandia(\n", + " sapm_out.v_mp, sapm_out.p_mp, sapm_inverter\n", + ")\n", + "p_acs[\"sd\"] = pvlib.inverter.sandia(\n", + " single_diode_out.v_mp, single_diode_out.p_mp, sapm_inverter\n", + ")\n", "\n", "p_acs.plot()\n", - "plt.ylabel('AC Power (W)');" + "plt.ylabel(\"AC Power (W)\");" ] }, { @@ -1117,9 +1167,9 @@ } ], "source": [ - "diff = p_acs['sapm'] - p_acs['sd']\n", + "diff = p_acs[\"sapm\"] - p_acs[\"sd\"]\n", "diff.plot()\n", - "plt.ylabel('SAPM - SD Power (W)');" + "plt.ylabel(\"SAPM - SD Power (W)\");" ] }, { @@ -1148,7 +1198,7 @@ } ], "source": [ - "p_acs.loc['2015-07-05':'2015-07-06'].plot();" + "p_acs.loc[\"2015-07-05\":\"2015-07-06\"].plot();" ] }, { @@ -1299,15 +1349,15 @@ "p_ac_max = p_acs.max().max()\n", "yxline = np.arange(0, p_ac_max)\n", "\n", - "fig = plt.figure(figsize=(12,12))\n", - "ax = fig.add_subplot(111, aspect='equal')\n", - "sc = ax.scatter(p_acs['sd'], p_acs['sapm'], c=poa_irrad.poa_global, alpha=1) \n", - "ax.plot(yxline, yxline, 'r', linewidth=3)\n", + "fig = plt.figure(figsize=(12, 12))\n", + "ax = fig.add_subplot(111, aspect=\"equal\")\n", + "sc = ax.scatter(p_acs[\"sd\"], p_acs[\"sapm\"], c=poa_irrad.poa_global, alpha=1)\n", + "ax.plot(yxline, yxline, \"r\", linewidth=3)\n", "ax.set_xlim(0, None)\n", "ax.set_ylim(0, None)\n", - "ax.set_xlabel('Single Diode model')\n", - "ax.set_ylabel('Sandia model')\n", - "fig.colorbar(sc, label='POA Global (W/m**2)');" + "ax.set_xlabel(\"Single Diode model\")\n", + "ax.set_ylabel(\"Sandia model\")\n", + "fig.colorbar(sc, label=\"POA Global (W/m**2)\");" ] }, { @@ -1325,32 +1375,32 @@ "source": [ "def sapm_sd_scatter(c_data, label=None, **kwargs):\n", " \"\"\"Display a scatter plot of SAPM p_ac vs. single diode p_ac.\n", - " \n", + "\n", " You need to re-execute this cell if you re-run the p_ac calculation.\n", - " \n", + "\n", " Parameters\n", " ----------\n", " c_data : array-like\n", " Determines the color of each point on the scatter plot.\n", " Must be same length as p_acs.\n", - " \n", + "\n", " kwargs passed to ``scatter``.\n", - " \n", + "\n", " Returns\n", " -------\n", " tuple of fig, ax objects\n", " \"\"\"\n", - " \n", - " fig = plt.figure(figsize=(12,12))\n", - " ax = fig.add_subplot(111, aspect='equal')\n", - " sc = ax.scatter(p_acs['sd'], p_acs['sapm'], c=c_data, alpha=1, **kwargs) \n", - " ax.plot(yxline, yxline, 'r', linewidth=3)\n", + "\n", + " fig = plt.figure(figsize=(12, 12))\n", + " ax = fig.add_subplot(111, aspect=\"equal\")\n", + " sc = ax.scatter(p_acs[\"sd\"], p_acs[\"sapm\"], c=c_data, alpha=1, **kwargs)\n", + " ax.plot(yxline, yxline, \"r\", linewidth=3)\n", " ax.set_xlim(0, None)\n", " ax.set_ylim(0, None)\n", - " ax.set_xlabel('Single diode model power (W)')\n", - " ax.set_ylabel('Sandia model power (W)')\n", - " fig.colorbar(sc, label='{}'.format(label), shrink=0.75)\n", - " \n", + " ax.set_xlabel(\"Single diode model power (W)\")\n", + " ax.set_ylabel(\"Sandia model power (W)\")\n", + " fig.colorbar(sc, label=\"{}\".format(label), shrink=0.75)\n", + "\n", " return fig, ax" ] }, @@ -1373,7 +1423,7 @@ } ], "source": [ - "sapm_sd_scatter(tmy_data.DryBulb, label='Temperature (deg C)');" + "sapm_sd_scatter(tmy_data.DryBulb, label=\"Temperature (deg C)\");" ] }, { @@ -1395,7 +1445,7 @@ } ], "source": [ - "sapm_sd_scatter(tmy_data.DNI, label='DNI (W/m**2)');" + "sapm_sd_scatter(tmy_data.DNI, label=\"DNI (W/m**2)\");" ] }, { @@ -1417,7 +1467,7 @@ } ], "source": [ - "sapm_sd_scatter(tmy_data.AOD, label='AOD');" + "sapm_sd_scatter(tmy_data.AOD, label=\"AOD\");" ] }, { @@ -1439,7 +1489,7 @@ } ], "source": [ - "sapm_sd_scatter(tmy_data.Wspd, label='Wind speed', vmax=10);" + "sapm_sd_scatter(tmy_data.Wspd, label=\"Wind speed\", vmax=10);" ] }, { @@ -1455,38 +1505,47 @@ "metadata": {}, "outputs": [], "source": [ - "def sapm_other_scatter(c_data, x_data, clabel=None, xlabel=None, aspect_equal=False, **kwargs):\n", + "def sapm_other_scatter(\n", + " c_data, x_data, clabel=None, xlabel=None, aspect_equal=False, **kwargs\n", + "):\n", " \"\"\"Display a scatter plot of SAPM p_ac vs. something else.\n", - " \n", + "\n", " You need to re-execute this cell if you re-run the p_ac calculation.\n", - " \n", + "\n", " Parameters\n", " ----------\n", " c_data : array-like\n", " Determines the color of each point on the scatter plot.\n", " Must be same length as p_acs.\n", " x_data : array-like\n", - " \n", + "\n", " kwargs passed to ``scatter``.\n", - " \n", + "\n", " Returns\n", " -------\n", " tuple of fig, ax objects\n", " \"\"\"\n", - " \n", - " fig = plt.figure(figsize=(12,12))\n", - " \n", + "\n", + " fig = plt.figure(figsize=(12, 12))\n", + "\n", " if aspect_equal:\n", - " ax = fig.add_subplot(111, aspect='equal')\n", + " ax = fig.add_subplot(111, aspect=\"equal\")\n", " else:\n", " ax = fig.add_subplot(111)\n", - " sc = ax.scatter(x_data, p_acs['sapm'], c=c_data, alpha=1, cmap=mpl.cm.YlGnBu_r, **kwargs) \n", + " sc = ax.scatter(\n", + " x_data,\n", + " p_acs[\"sapm\"],\n", + " c=c_data,\n", + " alpha=1,\n", + " cmap=mpl.cm.YlGnBu_r,\n", + " **kwargs,\n", + " )\n", " ax.set_xlim(0, None)\n", " ax.set_ylim(0, None)\n", - " ax.set_xlabel('{}'.format(xlabel))\n", - " ax.set_ylabel('Sandia model power (W)')\n", - " fig.colorbar(sc, label='{}'.format(clabel), shrink=0.75)\n", - " \n", + " ax.set_xlabel(\"{}\".format(xlabel))\n", + " ax.set_ylabel(\"Sandia model power (W)\")\n", + " fig.colorbar(sc, label=\"{}\".format(clabel), shrink=0.75)\n", + "\n", " return fig, ax" ] }, @@ -1509,7 +1568,12 @@ } ], "source": [ - "sapm_other_scatter(tmy_data.DryBulb, tmy_data.GHI, clabel='Temperature (deg C)', xlabel='GHI (W/m**2)');" + "sapm_other_scatter(\n", + " tmy_data.DryBulb,\n", + " tmy_data.GHI,\n", + " clabel=\"Temperature (deg C)\",\n", + " xlabel=\"GHI (W/m**2)\",\n", + ");" ] }, { @@ -1532,15 +1596,15 @@ "def pvusa(pvusa_data, a, b, c, d):\n", " \"\"\"\n", " Calculates system power according to the PVUSA equation\n", - " \n", + "\n", " P = I * (a + b*I + c*W + d*T)\n", - " \n", + "\n", " where\n", " P is the output power,\n", " I is the plane of array irradiance,\n", " W is the wind speed, and\n", " T is the temperature\n", - " \n", + "\n", " Parameters\n", " ----------\n", " pvusa_data : pd.DataFrame\n", @@ -1553,13 +1617,15 @@ " I*W coefficient\n", " d : float\n", " I*T coefficient\n", - " \n", + "\n", " Returns\n", " -------\n", " power : pd.Series\n", " Power calculated using the PVUSA model.\n", " \"\"\"\n", - " return pvusa_data['I'] * (a + b*pvusa_data['I'] + c*pvusa_data['W'] + d*pvusa_data['T'])" + " return pvusa_data[\"I\"] * (\n", + " a + b * pvusa_data[\"I\"] + c * pvusa_data[\"W\"] + d * pvusa_data[\"T\"]\n", + " )" ] }, { @@ -1578,9 +1644,9 @@ "outputs": [], "source": [ "pvusa_data = pd.DataFrame()\n", - "pvusa_data['I'] = poa_irrad.poa_global\n", - "pvusa_data['W'] = tmy_data.Wspd\n", - "pvusa_data['T'] = tmy_data.DryBulb" + "pvusa_data[\"I\"] = poa_irrad.poa_global\n", + "pvusa_data[\"W\"] = tmy_data.Wspd\n", + "pvusa_data[\"T\"] = tmy_data.DryBulb" ] }, { @@ -1603,9 +1669,14 @@ } ], "source": [ - "popt, pcov = optimize.curve_fit(pvusa, pvusa_data.dropna(), p_acs.sapm.values, p0=(.0001,0.0001,.001,.001))\n", - "print('optimized coefs:\\n{}'.format(popt))\n", - "print('covariances:\\n{}'.format(pcov))" + "popt, pcov = optimize.curve_fit(\n", + " pvusa,\n", + " pvusa_data.dropna(),\n", + " p_acs.sapm.values,\n", + " p0=(0.0001, 0.0001, 0.001, 0.001),\n", + ")\n", + "print(\"optimized coefs:\\n{}\".format(popt))\n", + "print(\"covariances:\\n{}\".format(pcov))" ] }, { @@ -1629,13 +1700,18 @@ "source": [ "power_pvusa = pvusa(pvusa_data, *popt)\n", "\n", - "fig, ax = sapm_other_scatter(tmy_data.DryBulb, power_pvusa, clabel='Temperature (deg C)',\n", - " aspect_equal=True, xlabel='PVUSA (W)')\n", + "fig, ax = sapm_other_scatter(\n", + " tmy_data.DryBulb,\n", + " power_pvusa,\n", + " clabel=\"Temperature (deg C)\",\n", + " aspect_equal=True,\n", + " xlabel=\"PVUSA (W)\",\n", + ")\n", "\n", "maxmax = max(ax.get_xlim()[1], ax.get_ylim()[1])\n", "ax.set_ylim(None, maxmax)\n", "ax.set_xlim(None, maxmax)\n", - "ax.plot(np.arange(maxmax), np.arange(maxmax), 'r');" + "ax.plot(np.arange(maxmax), np.arange(maxmax), \"r\");" ] }, { diff --git a/docs/tutorials/tracking.ipynb b/docs/tutorials/tracking.ipynb index 05b06587a8..ef7968887d 100644 --- a/docs/tutorials/tracking.ipynb +++ b/docs/tutorials/tracking.ipynb @@ -50,7 +50,7 @@ "# plotting modules\n", "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", - " \n", + "\n", "# built in python modules\n", "import datetime\n", "\n", @@ -102,9 +102,11 @@ } ], "source": [ - "tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson')\n", + "tus = Location(32.2, -111, \"US/Arizona\", 700, \"Tucson\")\n", "print(tus)\n", - "johannesburg = Location(-26.2044, 28.0456, 'Africa/Johannesburg', 1753, 'Johannesburg')\n", + "johannesburg = Location(\n", + " -26.2044, 28.0456, \"Africa/Johannesburg\", 1753, \"Johannesburg\"\n", + ")\n", "print(johannesburg)" ] }, @@ -121,12 +123,21 @@ "metadata": {}, "outputs": [], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,3,23), end=datetime.datetime(2014,3,24), freq='5Min')\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 3, 23),\n", + " end=datetime.datetime(2014, 3, 24),\n", + " freq=\"5Min\",\n", + ")\n", "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "ephemout = ephem_tus # default for notebook" + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "ephemout = ephem_tus # default for notebook" ] }, { @@ -263,15 +274,15 @@ "metadata": {}, "outputs": [], "source": [ - "azimuth = ephemout['azimuth']\n", - "apparent_azimuth = ephemout['azimuth']\n", - "apparent_zenith = ephemout['apparent_zenith']\n", + "azimuth = ephemout[\"azimuth\"]\n", + "apparent_azimuth = ephemout[\"azimuth\"]\n", + "apparent_zenith = ephemout[\"apparent_zenith\"]\n", "axis_tilt = 10\n", "axis_azimuth = 170\n", "latitude = 32\n", - "max_angle = 65 \n", + "max_angle = 65\n", "backtrack = True\n", - "gcr = 2.0/7.0\n", + "gcr = 2.0 / 7.0\n", "\n", "times = azimuth.index" ] @@ -315,10 +326,10 @@ "y = cosd(apparent_elevation) * cosd(az)\n", "z = sind(apparent_elevation)\n", "\n", - "earth_coords = pd.DataFrame({'x':x,'y':y,'z':z})\n", + "earth_coords = pd.DataFrame({\"x\": x, \"y\": y, \"z\": z})\n", "\n", "earth_coords.plot()\n", - "plt.title('sun position in Earth coordinate system');" + "plt.title(\"sun position in Earth coordinate system\");" ] }, { @@ -364,23 +375,33 @@ "source": [ "axis_azimuth_south = axis_azimuth - 180\n", "\n", - "print('cos(axis_azimuth_south)={}, sin(axis_azimuth_south)={}'\n", - " .format(cosd(axis_azimuth_south), sind(axis_azimuth_south)))\n", - "print('cos(axis_tilt)={}, sin(axis_tilt)={}'\n", - " .format(cosd(axis_tilt), sind(axis_tilt)))\n", - "\n", - "xp = x*cosd(axis_azimuth_south) - y*sind(axis_azimuth_south);\n", - "yp = (x*cosd(axis_tilt)*sind(axis_azimuth_south) +\n", - " y*cosd(axis_tilt)*cosd(axis_azimuth_south) -\n", - " z*sind(axis_tilt))\n", - "zp = (x*sind(axis_tilt)*sind(axis_azimuth_south) +\n", - " y*sind(axis_tilt)*cosd(axis_azimuth_south) +\n", - " z*cosd(axis_tilt))\n", - "\n", - "panel_coords = pd.DataFrame({'x':xp,'y':yp,'z':zp})\n", + "print(\n", + " \"cos(axis_azimuth_south)={}, sin(axis_azimuth_south)={}\".format(\n", + " cosd(axis_azimuth_south), sind(axis_azimuth_south)\n", + " )\n", + ")\n", + "print(\n", + " \"cos(axis_tilt)={}, sin(axis_tilt)={}\".format(\n", + " cosd(axis_tilt), sind(axis_tilt)\n", + " )\n", + ")\n", + "\n", + "xp = x * cosd(axis_azimuth_south) - y * sind(axis_azimuth_south)\n", + "yp = (\n", + " x * cosd(axis_tilt) * sind(axis_azimuth_south)\n", + " + y * cosd(axis_tilt) * cosd(axis_azimuth_south)\n", + " - z * sind(axis_tilt)\n", + ")\n", + "zp = (\n", + " x * sind(axis_tilt) * sind(axis_azimuth_south)\n", + " + y * sind(axis_tilt) * cosd(axis_azimuth_south)\n", + " + z * cosd(axis_tilt)\n", + ")\n", + "\n", + "panel_coords = pd.DataFrame({\"x\": xp, \"y\": yp, \"z\": zp})\n", "\n", "panel_coords.plot()\n", - "plt.title('sun position in panel coordinate system');" + "plt.title(\"sun position in panel coordinate system\");" ] }, { @@ -437,10 +458,10 @@ "# filter for sun above panel horizon\n", "wid[zp <= 0] = np.nan\n", "\n", - "wid.plot(label='tracking angle')\n", - "ephemout['apparent_elevation'].plot(label='apparent elevation')\n", + "wid.plot(label=\"tracking angle\")\n", + "ephemout[\"apparent_elevation\"].plot(label=\"apparent elevation\")\n", "plt.legend()\n", - "plt.title('Ideal tracking angle without backtracking');" + "plt.title(\"Ideal tracking angle without backtracking\");" ] }, { @@ -469,25 +490,27 @@ } ], "source": [ - "tmp = np.degrees(np.arctan(zp/xp)) # angle from x-y plane to projection of sun vector onto x-z plane\n", + "tmp = np.degrees(\n", + " np.arctan(zp / xp)\n", + ") # angle from x-y plane to projection of sun vector onto x-z plane\n", "\n", "# Obtain wid by translating tmp to convention for rotation angles.\n", - "# Have to account for which quadrant of the x-z plane in which the sun \n", - "# vector lies. Complete solution here but probably not necessary to \n", + "# Have to account for which quadrant of the x-z plane in which the sun\n", + "# vector lies. Complete solution here but probably not necessary to\n", "# consider QIII and QIV.\n", "wid = pd.Series(index=times, dtype=float)\n", - "wid[(xp>=0) & (zp>=0)] = 90 - tmp[(xp>=0) & (zp>=0)]; # QI\n", - "wid[(xp<0) & (zp>=0)] = -90 - tmp[(xp<0) & (zp>=0)]; # QII\n", - "wid[(xp<0) & (zp<0)] = -90 - tmp[(xp<0) & (zp<0)]; # QIII\n", - "wid[(xp>=0) & (zp<0)] = 90 - tmp[(xp>=0) & (zp<0)]; # QIV\n", + "wid[(xp >= 0) & (zp >= 0)] = 90 - tmp[(xp >= 0) & (zp >= 0)] # QI\n", + "wid[(xp < 0) & (zp >= 0)] = -90 - tmp[(xp < 0) & (zp >= 0)] # QII\n", + "wid[(xp < 0) & (zp < 0)] = -90 - tmp[(xp < 0) & (zp < 0)] # QIII\n", + "wid[(xp >= 0) & (zp < 0)] = 90 - tmp[(xp >= 0) & (zp < 0)] # QIV\n", "\n", "# filter for sun above panel horizon\n", "wid[zp <= 0] = np.nan\n", "\n", - "wid.plot(label='tracking angle')\n", - "ephemout['apparent_elevation'].plot(label='apparent elevation')\n", + "wid.plot(label=\"tracking angle\")\n", + "ephemout[\"apparent_elevation\"].plot(label=\"apparent elevation\")\n", "plt.legend()\n", - "plt.title('Ideal tracking angle without backtracking');" + "plt.title(\"Ideal tracking angle without backtracking\");" ] }, { @@ -524,8 +547,8 @@ ], "source": [ "if backtrack:\n", - " axes_distance = 1/gcr\n", - " temp = np.minimum(axes_distance*cosd(wid), 1)\n", + " axes_distance = 1 / gcr\n", + " temp = np.minimum(axes_distance * cosd(wid), 1)\n", "\n", " # backtrack angle\n", " # (always positive b/c acosd returns values between 0 and 180)\n", @@ -533,15 +556,15 @@ "\n", " v = wid < 0\n", " widc = pd.Series(index=times, dtype=float)\n", - " widc[~v] = wid[~v] - wc[~v]; # Eq 4 applied when wid in QI\n", - " widc[v] = wid[v] + wc[v]; # Eq 4 applied when wid in QIV\n", + " widc[~v] = wid[~v] - wc[~v] # Eq 4 applied when wid in QI\n", + " widc[v] = wid[v] + wc[v] # Eq 4 applied when wid in QIV\n", "else:\n", " widc = wid\n", "\n", - "widc.plot(label='tracking angle')\n", - "#pyephemout['apparent_elevation'].plot(label='apparent elevation')\n", + "widc.plot(label=\"tracking angle\")\n", + "# pyephemout['apparent_elevation'].plot(label='apparent elevation')\n", "plt.legend(loc=2)\n", - "plt.title('Ideal tracking angle with backtracking');" + "plt.title(\"Ideal tracking angle with backtracking\");" ] }, { @@ -570,9 +593,11 @@ } ], "source": [ - "tracking_angles = pd.DataFrame({'with backtracking':widc,'without backtracking':wid})\n", + "tracking_angles = pd.DataFrame(\n", + " {\"with backtracking\": widc, \"without backtracking\": wid}\n", + ")\n", "tracking_angles.plot()\n", - "#pyephemout['apparent_elevation'].plot(label='apparent elevation')\n", + "# pyephemout['apparent_elevation'].plot(label='apparent elevation')\n", "plt.legend();" ] }, @@ -613,7 +638,7 @@ "tracker_theta[tracker_theta > max_angle] = max_angle\n", "tracker_theta[tracker_theta < -max_angle] = -max_angle\n", "\n", - "tracking_angles['with restriction'] = tracker_theta\n", + "tracking_angles[\"with restriction\"] = tracker_theta\n", "tracking_angles.plot();" ] }, @@ -652,13 +677,15 @@ } ], "source": [ - "panel_norm = np.array([sind(tracker_theta), \n", - " tracker_theta*0,\n", - " cosd(tracker_theta)])\n", + "panel_norm = np.array(\n", + " [sind(tracker_theta), tracker_theta * 0, cosd(tracker_theta)]\n", + ")\n", "\n", - "panel_norm_df = pd.DataFrame(panel_norm.T, columns=('x','y','z'), index=times)\n", + "panel_norm_df = pd.DataFrame(\n", + " panel_norm.T, columns=(\"x\", \"y\", \"z\"), index=times\n", + ")\n", "panel_norm_df.plot()\n", - "plt.title('panel normal vector components in panel coordinate system')\n", + "plt.title(\"panel normal vector components in panel coordinate system\")\n", "plt.legend();" ] }, @@ -692,10 +719,10 @@ "source": [ "sun_vec = np.array([xp, yp, zp])\n", "\n", - "panel_coords = pd.DataFrame(sun_vec.T, columns=('x','y','z'), index=times)\n", + "panel_coords = pd.DataFrame(sun_vec.T, columns=(\"x\", \"y\", \"z\"), index=times)\n", "\n", "panel_coords.plot()\n", - "plt.title('sun position in panel coordinate system');" + "plt.title(\"sun position in panel coordinate system\");" ] }, { @@ -733,11 +760,11 @@ } ], "source": [ - "aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec*panel_norm, axis=0))))\n", + "aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec * panel_norm, axis=0))))\n", "aoi = pd.Series(aoi, index=times)\n", "\n", "aoi.plot()\n", - "plt.title('angle of incidence');" + "plt.title(\"angle of incidence\");" ] }, { @@ -836,14 +863,24 @@ ], "source": [ "# Calculate standard rotation matrix\n", - "print('cos(axis_azimuth_south)={}, sin(axis_azimuth_south)={}'\n", - " .format(cosd(axis_azimuth_south), sind(axis_azimuth_south)))\n", - "print('cos(axis_tilt)={}, sin(axis_tilt)={}'\n", - " .format(cosd(axis_tilt), sind(axis_tilt)))\n", - "\n", - "rot_x = np.array([[1, 0, 0], \n", - " [0, cosd(-axis_tilt), -sind(-axis_tilt)], \n", - " [0, sind(-axis_tilt), cosd(-axis_tilt)]])\n", + "print(\n", + " \"cos(axis_azimuth_south)={}, sin(axis_azimuth_south)={}\".format(\n", + " cosd(axis_azimuth_south), sind(axis_azimuth_south)\n", + " )\n", + ")\n", + "print(\n", + " \"cos(axis_tilt)={}, sin(axis_tilt)={}\".format(\n", + " cosd(axis_tilt), sind(axis_tilt)\n", + " )\n", + ")\n", + "\n", + "rot_x = np.array(\n", + " [\n", + " [1, 0, 0],\n", + " [0, cosd(-axis_tilt), -sind(-axis_tilt)],\n", + " [0, sind(-axis_tilt), cosd(-axis_tilt)],\n", + " ]\n", + ")\n", "\n", "# panel_norm_earth contains the normal vector expressed in earth-surface coordinates\n", "# (z normal to surface, y aligned with tracker axis parallel to earth)\n", @@ -851,22 +888,34 @@ "\n", "# projection to plane tangent to earth surface,\n", "# in earth surface coordinates\n", - "projected_normal = np.array([panel_norm_earth[:,0], panel_norm_earth[:,1], panel_norm_earth[:,2]*0]).T\n", + "projected_normal = np.array(\n", + " [\n", + " panel_norm_earth[:, 0],\n", + " panel_norm_earth[:, 1],\n", + " panel_norm_earth[:, 2] * 0,\n", + " ]\n", + ").T\n", "\n", "# calculate magnitudes\n", "panel_norm_earth_mag = np.sqrt(np.nansum(panel_norm_earth**2, axis=1))\n", "projected_normal_mag = np.sqrt(np.nansum(projected_normal**2, axis=1))\n", - "#print('panel_norm_earth_mag={}, projected_normal_mag={}'.format(panel_norm_earth_mag, projected_normal_mag))\n", + "# print('panel_norm_earth_mag={}, projected_normal_mag={}'.format(panel_norm_earth_mag, projected_normal_mag))\n", "\n", "projected_normal = (projected_normal.T / projected_normal_mag).T\n", "\n", - "panel_norm_earth_df = pd.DataFrame(panel_norm_earth, columns=('x','y','z'), index=times)\n", + "panel_norm_earth_df = pd.DataFrame(\n", + " panel_norm_earth, columns=(\"x\", \"y\", \"z\"), index=times\n", + ")\n", "panel_norm_earth_df.plot()\n", - "plt.title('panel normal vector components in Earth coordinate system')\n", + "plt.title(\"panel normal vector components in Earth coordinate system\")\n", "\n", - "projected_normal_df = pd.DataFrame(projected_normal, columns=('x','y','z'), index=times)\n", + "projected_normal_df = pd.DataFrame(\n", + " projected_normal, columns=(\"x\", \"y\", \"z\"), index=times\n", + ")\n", "projected_normal_df.plot()\n", - "plt.title('panel normal vector projected to surface in Earth coordinate system');" + "plt.title(\n", + " \"panel normal vector projected to surface in Earth coordinate system\"\n", + ");" ] }, { @@ -904,17 +953,20 @@ "source": [ "# calculation of surface_azimuth\n", "# 1. Find the angle.\n", - "surface_azimuth = pd.Series(np.degrees(np.arctan2(projected_normal[:,1], projected_normal[:,0])), index=times)\n", - "surface_azimuth.plot(label='orig')\n", + "surface_azimuth = pd.Series(\n", + " np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])),\n", + " index=times,\n", + ")\n", + "surface_azimuth.plot(label=\"orig\")\n", "\n", "# 2. Rotate 0 reference from panel's x axis to it's y axis and\n", "# then back to North.\n", "surface_azimuth = 90 - surface_azimuth + axis_azimuth\n", "\n", "# 3. Map azimuth into [0,360) domain.\n", - "surface_azimuth[surface_azimuth<0] += 360\n", - "surface_azimuth[surface_azimuth>=360] -= 360\n", - "surface_azimuth.plot(label='compass angle north')\n", + "surface_azimuth[surface_azimuth < 0] += 360\n", + "surface_azimuth[surface_azimuth >= 360] -= 360\n", + "surface_azimuth.plot(label=\"compass angle north\")\n", "\n", "plt.legend();" ] @@ -967,20 +1019,35 @@ "source": [ "# calculation of surface_azimuth\n", "# 1. Find the angle.\n", - "surface_azimuth = pd.Series(np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])), index=times)\n", - "surface_azimuth.plot(label='orig')\n", + "surface_azimuth = pd.Series(\n", + " np.degrees(np.arctan(projected_normal[:, 1] / projected_normal[:, 0])),\n", + " index=times,\n", + ")\n", + "surface_azimuth.plot(label=\"orig\")\n", "\n", "# 2. Clean up atan when x-coord or y-coord is zero\n", - "surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]>0)] = 90\n", - "surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]<0)] = -90\n", - "surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]>0)] = 0\n", - "surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]<0)] = 180\n", - "surface_azimuth.plot(label='x or y 0 corrected')\n", + "surface_azimuth[\n", + " (projected_normal[:, 0] == 0) & (projected_normal[:, 1] > 0)\n", + "] = 90\n", + "surface_azimuth[\n", + " (projected_normal[:, 0] == 0) & (projected_normal[:, 1] < 0)\n", + "] = -90\n", + "surface_azimuth[\n", + " (projected_normal[:, 1] == 0) & (projected_normal[:, 0] > 0)\n", + "] = 0\n", + "surface_azimuth[\n", + " (projected_normal[:, 1] == 0) & (projected_normal[:, 0] < 0)\n", + "] = 180\n", + "surface_azimuth.plot(label=\"x or y 0 corrected\")\n", "\n", "# 3. Correct atan for QII and QIII\n", - "surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]>0)] += 180 # QII\n", - "surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]<0)] += 180 # QIII\n", - "surface_azimuth.plot(label='q2, q3 corrected')\n", + "surface_azimuth[\n", + " (projected_normal[:, 0] < 0) & (projected_normal[:, 1] > 0)\n", + "] += 180 # QII\n", + "surface_azimuth[\n", + " (projected_normal[:, 0] < 0) & (projected_normal[:, 1] < 0)\n", + "] += 180 # QIII\n", + "surface_azimuth.plot(label=\"q2, q3 corrected\")\n", "\n", "# 4. Skip to below\n", "\n", @@ -1020,9 +1087,9 @@ "surface_azimuth = 90 - surface_azimuth + axis_azimuth\n", "\n", "# 5. Map azimuth into [0,360) domain.\n", - "surface_azimuth[surface_azimuth<0] += 360\n", - "surface_azimuth[surface_azimuth>=360] -= 360\n", - "surface_azimuth.plot(label='compass angle north')\n", + "surface_azimuth[surface_azimuth < 0] += 360\n", + "surface_azimuth[surface_azimuth >= 360] -= 360\n", + "surface_azimuth.plot(label=\"compass angle north\")\n", "\n", "plt.legend();" ] @@ -1067,8 +1134,13 @@ } ], "source": [ - "surface_tilt = (90 - np.degrees(np.arccos(\n", - " pd.DataFrame(panel_norm_earth * projected_normal, index=times).sum(axis=1))))\n", + "surface_tilt = 90 - np.degrees(\n", + " np.arccos(\n", + " pd.DataFrame(panel_norm_earth * projected_normal, index=times).sum(\n", + " axis=1\n", + " )\n", + " )\n", + ")\n", "\n", "surface_tilt.plot();" ] @@ -1109,9 +1181,15 @@ } ], "source": [ - "tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)" + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")" ] }, { @@ -1170,9 +1248,15 @@ } ], "source": [ - "tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot();" ] }, @@ -1203,10 +1287,16 @@ "aois = pd.DataFrame(index=ephemout.index)\n", "\n", "for gcr in np.linspace(0, 1, 6):\n", - " tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=gcr)\n", - " aois[gcr] = tracker_data['aoi']" + " tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=gcr,\n", + " )\n", + " aois[gcr] = tracker_data[\"aoi\"]" ] }, { @@ -1265,9 +1355,15 @@ } ], "source": [ - "tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=45,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=45,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot();" ] }, @@ -1313,35 +1409,41 @@ "thetas = pd.DataFrame(index=ephemout.index)\n", "\n", "for tilt in np.linspace(0, 90, 7):\n", - " tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=tilt, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=2/7.)\n", - " aois[tilt] = tracker_data['aoi']\n", - " tilts[tilt] = tracker_data['surface_tilt']\n", - " azis[tilt] = tracker_data['surface_azimuth']\n", - " thetas[tilt] = tracker_data['tracker_theta']\n", - " \n", - "fig, axes = plt.subplots(2, 2, figsize=(16,12), sharex=True)\n", - "ax = axes[0,0]\n", + " tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=tilt,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2 / 7.0,\n", + " )\n", + " aois[tilt] = tracker_data[\"aoi\"]\n", + " tilts[tilt] = tracker_data[\"surface_tilt\"]\n", + " azis[tilt] = tracker_data[\"surface_azimuth\"]\n", + " thetas[tilt] = tracker_data[\"tracker_theta\"]\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(16, 12), sharex=True)\n", + "ax = axes[0, 0]\n", "aois.plot(ax=ax)\n", - "ax.set_ylim(0,90)\n", - "ax.set_title('aoi')\n", + "ax.set_ylim(0, 90)\n", + "ax.set_title(\"aoi\")\n", "\n", - "ax = axes[0,1]\n", + "ax = axes[0, 1]\n", "thetas.plot(ax=ax)\n", - "ax.set_ylim(-90,90)\n", - "ax.set_title('tracker theta')\n", + "ax.set_ylim(-90, 90)\n", + "ax.set_title(\"tracker theta\")\n", "\n", - "ax = axes[1,1]\n", + "ax = axes[1, 1]\n", "tilts.plot(ax=ax)\n", - "ax.set_title('surface tilt')\n", - "ax.set_ylim(0,90)\n", + "ax.set_title(\"surface tilt\")\n", + "ax.set_ylim(0, 90)\n", "\n", - "ax = axes[1,0]\n", + "ax = axes[1, 0]\n", "azis.plot(ax=ax)\n", - "ax.set_title('surface azimuth')\n", - "ax.set_ylim(0,360);\n", - "#ax.hlines([0, 90, 180, 270, 360], *ax.get_xlim(), colors='0.25', lw=1, alpha=0.25)" + "ax.set_title(\"surface azimuth\")\n", + "ax.set_ylim(0, 360);\n", + "# ax.hlines([0, 90, 180, 270, 360], *ax.get_xlim(), colors='0.25', lw=1, alpha=0.25)" ] }, { @@ -1388,35 +1490,41 @@ "thetas = pd.DataFrame(index=ephemout.index)\n", "\n", "for tilt in np.linspace(0, -90, 7):\n", - " tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=tilt, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=2/7.)\n", - " aois[tilt] = tracker_data['aoi']\n", - " tilts[tilt] = tracker_data['surface_tilt']\n", - " azis[tilt] = tracker_data['surface_azimuth']\n", - " thetas[tilt] = tracker_data['tracker_theta']\n", - " \n", - "fig, axes = plt.subplots(2, 2, figsize=(16,12), sharex=True)\n", - "ax = axes[0,0]\n", + " tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=tilt,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2 / 7.0,\n", + " )\n", + " aois[tilt] = tracker_data[\"aoi\"]\n", + " tilts[tilt] = tracker_data[\"surface_tilt\"]\n", + " azis[tilt] = tracker_data[\"surface_azimuth\"]\n", + " thetas[tilt] = tracker_data[\"tracker_theta\"]\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(16, 12), sharex=True)\n", + "ax = axes[0, 0]\n", "aois.plot(ax=ax)\n", - "ax.set_ylim(0,90)\n", - "ax.set_title('aoi')\n", + "ax.set_ylim(0, 90)\n", + "ax.set_title(\"aoi\")\n", "\n", - "ax = axes[0,1]\n", + "ax = axes[0, 1]\n", "thetas.plot(ax=ax)\n", - "ax.set_ylim(-90,90)\n", - "ax.set_title('tracker theta')\n", + "ax.set_ylim(-90, 90)\n", + "ax.set_title(\"tracker theta\")\n", "\n", - "ax = axes[1,1]\n", + "ax = axes[1, 1]\n", "tilts.plot(ax=ax)\n", - "ax.set_title('surface tilt')\n", - "ax.set_ylim(0,90)\n", + "ax.set_title(\"surface tilt\")\n", + "ax.set_ylim(0, 90)\n", "\n", - "ax = axes[1,0]\n", + "ax = axes[1, 0]\n", "azis.plot(ax=ax)\n", - "ax.set_title('surface azimuth')\n", - "ax.set_ylim(0,360);\n", - "#ax.hlines([0, 90, 180, 270, 360], *ax.get_xlim(), colors='0.25', lw=1, alpha=0.25)" + "ax.set_title(\"surface azimuth\")\n", + "ax.set_ylim(0, 360);\n", + "# ax.hlines([0, 90, 180, 270, 360], *ax.get_xlim(), colors='0.25', lw=1, alpha=0.25)" ] }, { @@ -1468,34 +1576,40 @@ "thetas = pd.DataFrame(index=ephemout.index)\n", "\n", "for azi in np.linspace(90, 270, 5):\n", - " tracker_data = pvlib.tracking.singleaxis(ephemout['apparent_zenith'], ephemout['azimuth'],\n", - " axis_tilt=0, axis_azimuth=azi, max_angle=90,\n", - " backtrack=True, gcr=2/7.)\n", - " aois[azi] = tracker_data['aoi']\n", - " tilts[azi] = tracker_data['surface_tilt']\n", - " azis[azi] = tracker_data['surface_azimuth']\n", - " thetas[azi] = tracker_data['tracker_theta']\n", - " \n", - "fig, axes = plt.subplots(2, 2, figsize=(16,12), sharex=True)\n", - "ax=axes[0,0]\n", + " tracker_data = pvlib.tracking.singleaxis(\n", + " ephemout[\"apparent_zenith\"],\n", + " ephemout[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=azi,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2 / 7.0,\n", + " )\n", + " aois[azi] = tracker_data[\"aoi\"]\n", + " tilts[azi] = tracker_data[\"surface_tilt\"]\n", + " azis[azi] = tracker_data[\"surface_azimuth\"]\n", + " thetas[azi] = tracker_data[\"tracker_theta\"]\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(16, 12), sharex=True)\n", + "ax = axes[0, 0]\n", "aois.plot(ax=ax)\n", - "ax.set_ylim(0,90)\n", - "ax.set_title('aoi')\n", + "ax.set_ylim(0, 90)\n", + "ax.set_title(\"aoi\")\n", "\n", - "ax=axes[0,1]\n", + "ax = axes[0, 1]\n", "thetas.plot(ax=ax)\n", - "ax.set_ylim(-90,90)\n", - "ax.set_title('tracker theta')\n", + "ax.set_ylim(-90, 90)\n", + "ax.set_title(\"tracker theta\")\n", "\n", - "ax=axes[1,1]\n", + "ax = axes[1, 1]\n", "tilts.plot(ax=ax)\n", - "ax.set_title('surface tilt')\n", - "ax.set_ylim(0,90)\n", + "ax.set_title(\"surface tilt\")\n", + "ax.set_ylim(0, 90)\n", "\n", - "ax=axes[1,0]\n", + "ax = axes[1, 0]\n", "azis.plot(ax=ax)\n", - "ax.set_title('surface azimuth')\n", - "ax.set_ylim(0,360);" + "ax.set_title(\"surface azimuth\")\n", + "ax.set_ylim(0, 360);" ] }, { @@ -1543,9 +1657,15 @@ } ], "source": [ - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot();" ] }, @@ -1597,25 +1717,46 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,3,23), end=datetime.datetime(2014,3,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 3, 23),\n", + " end=datetime.datetime(2014, 3, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Tucson, March')\n", - "plt.ylim(-100,100)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "plt.title(\"Tucson, March\")\n", + "plt.ylim(-100, 100)\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Johannesburg, March')\n", - "plt.ylim(-100,100);" + "plt.title(\"Johannesburg, March\")\n", + "plt.ylim(-100, 100);" ] }, { @@ -1659,25 +1800,46 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,23), end=datetime.datetime(2014,6,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 23),\n", + " end=datetime.datetime(2014, 6, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Tucson, June')\n", - "plt.ylim(-100,100)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "plt.title(\"Tucson, June\")\n", + "plt.ylim(-100, 100)\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Johannesburg, June')\n", - "plt.ylim(-100,100);" + "plt.title(\"Johannesburg, June\")\n", + "plt.ylim(-100, 100);" ] }, { @@ -1721,25 +1883,46 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,12,23), end=datetime.datetime(2014,12,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 12, 23),\n", + " end=datetime.datetime(2014, 12, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Tucson, December')\n", - "plt.ylim(-100,100)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "plt.title(\"Tucson, December\")\n", + "plt.ylim(-100, 100)\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.title('Johannesburg, December')\n", - "plt.ylim(-100,100);" + "plt.title(\"Johannesburg, December\")\n", + "plt.ylim(-100, 100);" ] }, { @@ -1783,24 +1966,45 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,12,23), end=datetime.datetime(2014,12,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", - "tracker_data['aoi'].plot()\n", - "plt.title('Tucson, December, no backtrack')\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 12, 23),\n", + " end=datetime.datetime(2014, 12, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", + "tracker_data[\"aoi\"].plot()\n", + "plt.title(\"Tucson, December, no backtrack\")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "plt.figure()\n", - "tracker_data['aoi'].plot()\n", - "plt.title('Johannesburg, December, no backtrack');" + "tracker_data[\"aoi\"].plot()\n", + "plt.title(\"Johannesburg, December, no backtrack\");" ] }, { @@ -1844,24 +2048,45 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,5,5), end=datetime.datetime(2014,5,6), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", - "tracker_data['aoi'].plot()\n", - "plt.title('Tucson, May, no backtrack')\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_joh['apparent_zenith'], ephem_joh['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 5, 5),\n", + " end=datetime.datetime(2014, 5, 6),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", + "tracker_data[\"aoi\"].plot()\n", + "plt.title(\"Tucson, May, no backtrack\")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_joh[\"apparent_zenith\"],\n", + " ephem_joh[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "plt.figure()\n", - "tracker_data['aoi'].plot()\n", - "plt.title('Johannesburg, May, no backtrack');" + "tracker_data[\"aoi\"].plot()\n", + "plt.title(\"Johannesburg, May, no backtrack\");" ] }, { @@ -1905,17 +2130,32 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,3,23), end=datetime.datetime(2014,3,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 3, 23),\n", + " end=datetime.datetime(2014, 3, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-100,100);" + "plt.ylim(-100, 100);" ] }, { @@ -1938,9 +2178,9 @@ ], "source": [ "irrad_data = tus.get_clearsky(times.tz_localize(tus.tz))\n", - "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method='asce')\n", + "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method=\"asce\")\n", "irrad_data.plot()\n", - "dni_et.plot(label='DNI ET');" + "dni_et.plot(label=\"DNI ET\");" ] }, { @@ -1962,7 +2202,9 @@ } ], "source": [ - "ground_irrad = pvlib.irradiance.get_ground_diffuse(tracker_data['surface_tilt'], irrad_data['ghi'], albedo=.25)\n", + "ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " tracker_data[\"surface_tilt\"], irrad_data[\"ghi\"], albedo=0.25\n", + ")\n", "ground_irrad.plot();" ] }, @@ -1987,10 +2229,16 @@ "source": [ "ephem_data = ephem_tus\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(tracker_data['surface_tilt'], tracker_data['surface_azimuth'], \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse');" + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " tracker_data[\"surface_tilt\"],\n", + " tracker_data[\"surface_azimuth\"],\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\");" ] }, { @@ -2012,7 +2260,11 @@ } ], "source": [ - "global_in_plane = cosd(tracker_data['aoi'])*irrad_data['dni'] + haydavies_diffuse + ground_irrad\n", + "global_in_plane = (\n", + " cosd(tracker_data[\"aoi\"]) * irrad_data[\"dni\"]\n", + " + haydavies_diffuse\n", + " + ground_irrad\n", + ")\n", "global_in_plane.plot();" ] }, @@ -2071,36 +2323,63 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,23), end=datetime.datetime(2014,6,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 23),\n", + " end=datetime.datetime(2014, 6, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-100,100)\n", + "plt.ylim(-100, 100)\n", "\n", "irrad_data = tus.get_clearsky(times.tz_localize(tus.tz))\n", - "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method='asce')\n", + "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method=\"asce\")\n", "plt.figure()\n", "irrad_data.plot()\n", - "dni_et.plot(label='DNI ET')\n", + "dni_et.plot(label=\"DNI ET\")\n", "\n", - "ground_irrad = pvlib.irradiance.get_ground_diffuse(tracker_data['surface_tilt'], irrad_data['ghi'], albedo=.25)\n", + "ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " tracker_data[\"surface_tilt\"], irrad_data[\"ghi\"], albedo=0.25\n", + ")\n", "ground_irrad.plot()\n", "\n", "ephem_data = ephem_tus\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(tracker_data['surface_tilt'], tracker_data['surface_azimuth'], \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "global_in_plane = cosd(tracker_data['aoi'])*irrad_data['dni'] + haydavies_diffuse + ground_irrad\n", - "global_in_plane.plot(label='global in plane')\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " tracker_data[\"surface_tilt\"],\n", + " tracker_data[\"surface_azimuth\"],\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "global_in_plane = (\n", + " cosd(tracker_data[\"aoi\"]) * irrad_data[\"dni\"]\n", + " + haydavies_diffuse\n", + " + ground_irrad\n", + ")\n", + "global_in_plane.plot(label=\"global in plane\")\n", "\n", "plt.legend();" ] @@ -2153,36 +2432,63 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,12,23), end=datetime.datetime(2014,12,24), freq='5Min')\n", - "\n", - "ephem_tus = pvlib.solarposition.get_solarposition(times.tz_localize(tus.tz), tus.latitude, tus.longitude)\n", - "ephem_joh = pvlib.solarposition.get_solarposition(times.tz_localize(johannesburg.tz),\n", - " johannesburg.latitude, johannesburg.longitude)\n", - "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_tus['apparent_zenith'], ephem_tus['azimuth'],\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 12, 23),\n", + " end=datetime.datetime(2014, 12, 24),\n", + " freq=\"5Min\",\n", + ")\n", + "\n", + "ephem_tus = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(tus.tz), tus.latitude, tus.longitude\n", + ")\n", + "ephem_joh = pvlib.solarposition.get_solarposition(\n", + " times.tz_localize(johannesburg.tz),\n", + " johannesburg.latitude,\n", + " johannesburg.longitude,\n", + ")\n", + "\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_tus[\"apparent_zenith\"],\n", + " ephem_tus[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-100,100)\n", + "plt.ylim(-100, 100)\n", "\n", "irrad_data = tus.get_clearsky(times.tz_localize(tus.tz))\n", - "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method='asce')\n", + "dni_et = pvlib.irradiance.get_extra_radiation(irrad_data.index, method=\"asce\")\n", "plt.figure()\n", "irrad_data.plot()\n", - "dni_et.plot(label='DNI ET')\n", + "dni_et.plot(label=\"DNI ET\")\n", "\n", - "ground_irrad = pvlib.irradiance.get_ground_diffuse(tracker_data['surface_tilt'], irrad_data['ghi'], albedo=.25)\n", + "ground_irrad = pvlib.irradiance.get_ground_diffuse(\n", + " tracker_data[\"surface_tilt\"], irrad_data[\"ghi\"], albedo=0.25\n", + ")\n", "ground_irrad.plot()\n", "\n", "ephem_data = ephem_tus\n", "\n", - "haydavies_diffuse = pvlib.irradiance.haydavies(tracker_data['surface_tilt'], tracker_data['surface_azimuth'], \n", - " irrad_data['dhi'], irrad_data['dni'], dni_et,\n", - " ephem_data['apparent_zenith'], ephem_data['azimuth'])\n", - "haydavies_diffuse.plot(label='haydavies diffuse')\n", - "\n", - "global_in_plane = cosd(tracker_data['aoi'])*irrad_data['dni'] + haydavies_diffuse + ground_irrad\n", - "global_in_plane.plot(label='global in plane')\n", + "haydavies_diffuse = pvlib.irradiance.haydavies(\n", + " tracker_data[\"surface_tilt\"],\n", + " tracker_data[\"surface_azimuth\"],\n", + " irrad_data[\"dhi\"],\n", + " irrad_data[\"dni\"],\n", + " dni_et,\n", + " ephem_data[\"apparent_zenith\"],\n", + " ephem_data[\"azimuth\"],\n", + ")\n", + "haydavies_diffuse.plot(label=\"haydavies diffuse\")\n", + "\n", + "global_in_plane = (\n", + " cosd(tracker_data[\"aoi\"]) * irrad_data[\"dni\"]\n", + " + haydavies_diffuse\n", + " + ground_irrad\n", + ")\n", + "global_in_plane.plot(label=\"global in plane\")\n", "\n", "plt.legend();" ] @@ -2213,7 +2519,7 @@ } ], "source": [ - "abq = Location(35, -106, 'US/Mountain', 0, 'Albuquerque')\n", + "abq = Location(35, -106, \"US/Mountain\", 0, \"Albuquerque\")\n", "print(abq)" ] }, @@ -2253,16 +2559,26 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,1), end=datetime.datetime(2014,6,2), freq='5Min')\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 1),\n", + " end=datetime.datetime(2014, 6, 2),\n", + " freq=\"5Min\",\n", + ")\n", "\n", "ephem_abq = abq.get_solarposition(times.tz_localize(abq.tz))\n", "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_abq['apparent_zenith'], ephem_abq['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=45,\n", - " backtrack=False, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_abq[\"apparent_zenith\"],\n", + " ephem_abq[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=45,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-100,100)\n", - "plt.title('June 1, Albuquerque, NS Horizontal Single-Axis, no backtrack');" + "plt.ylim(-100, 100)\n", + "plt.title(\"June 1, Albuquerque, NS Horizontal Single-Axis, no backtrack\");" ] }, { @@ -2301,16 +2617,26 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,1), end=datetime.datetime(2014,6,2), freq='5Min')\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 1),\n", + " end=datetime.datetime(2014, 6, 2),\n", + " freq=\"5Min\",\n", + ")\n", "\n", "ephem_abq = abq.get_solarposition(times.tz_localize(abq.tz))\n", "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_abq['apparent_zenith'], ephem_abq['azimuth'],\n", - " axis_tilt=0, axis_azimuth=180, max_angle=45,\n", - " backtrack=True, gcr=.3)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_abq[\"apparent_zenith\"],\n", + " ephem_abq[\"azimuth\"],\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=45,\n", + " backtrack=True,\n", + " gcr=0.3,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-100,100)\n", - "plt.title('June 1, Albuquerque, NS Horizontal Single-Axis, with backtracking');" + "plt.ylim(-100, 100)\n", + "plt.title(\"June 1, Albuquerque, NS Horizontal Single-Axis, with backtracking\");" ] }, { @@ -2349,16 +2675,28 @@ } ], "source": [ - "times = pd.date_range(start=datetime.datetime(2014,6,1), end=datetime.datetime(2014,6,2), freq='5Min')\n", + "times = pd.date_range(\n", + " start=datetime.datetime(2014, 6, 1),\n", + " end=datetime.datetime(2014, 6, 2),\n", + " freq=\"5Min\",\n", + ")\n", "\n", "ephem_abq = abq.get_solarposition(times.tz_localize(abq.tz))\n", "\n", - "tracker_data = pvlib.tracking.singleaxis(ephem_abq['apparent_zenith'], ephem_abq['azimuth'],\n", - " axis_tilt=20, axis_azimuth=180, max_angle=45,\n", - " backtrack=True, gcr=.3)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " ephem_abq[\"apparent_zenith\"],\n", + " ephem_abq[\"azimuth\"],\n", + " axis_tilt=20,\n", + " axis_azimuth=180,\n", + " max_angle=45,\n", + " backtrack=True,\n", + " gcr=0.3,\n", + ")\n", "tracker_data.plot()\n", - "plt.ylim(-50,300)\n", - "plt.title('June 1, Albuquerque, 20 deg S-Tilted Single-Axis, with backtracking');" + "plt.ylim(-50, 300)\n", + "plt.title(\n", + " \"June 1, Albuquerque, 20 deg S-Tilted Single-Axis, with backtracking\"\n", + ");" ] }, { @@ -2432,9 +2770,15 @@ "source": [ "apparent_zenith = pd.Series([10])\n", "apparent_azimuth = pd.Series([180])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2495,9 +2839,15 @@ "source": [ "apparent_zenith = pd.Series([60])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2558,9 +2908,15 @@ "source": [ "apparent_zenith = pd.Series([60])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2628,9 +2984,15 @@ "source": [ "apparent_zenith = pd.Series([60])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=45,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=45,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2698,9 +3060,15 @@ "source": [ "apparent_zenith = pd.Series([80])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=False, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=False,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2761,9 +3129,15 @@ "source": [ "apparent_zenith = pd.Series([80])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2831,9 +3205,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([135])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=30, axis_azimuth=180, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=30,\n", + " axis_azimuth=180,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2894,9 +3274,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([135])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=30, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=30,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -2964,9 +3350,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=90, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=90,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3027,9 +3419,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([180])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=90, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=90,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3090,9 +3488,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([180])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=90, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=90,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3153,9 +3557,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([150])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=170, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=170,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3216,9 +3626,15 @@ "source": [ "apparent_zenith = pd.Series([30])\n", "apparent_azimuth = pd.Series([180])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=170, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=170,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3279,9 +3695,15 @@ "source": [ "apparent_zenith = pd.Series([10])\n", "apparent_azimuth = pd.Series([180])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, @@ -3314,10 +3736,16 @@ "# NBVAL_RAISES_EXCEPTION\n", "\n", "apparent_zenith = pd.Series([10])\n", - "apparent_azimuth = pd.Series([180,90])\n", - "tracker_data = pvlib.tracking.singleaxis(apparent_zenith, apparent_azimuth,\n", - " axis_tilt=0, axis_azimuth=0, max_angle=90,\n", - " backtrack=True, gcr=2.0/7.0)\n", + "apparent_azimuth = pd.Series([180, 90])\n", + "tracker_data = pvlib.tracking.singleaxis(\n", + " apparent_zenith,\n", + " apparent_azimuth,\n", + " axis_tilt=0,\n", + " axis_azimuth=0,\n", + " max_angle=90,\n", + " backtrack=True,\n", + " gcr=2.0 / 7.0,\n", + ")\n", "tracker_data" ] }, diff --git a/pvlib/__init__.py b/pvlib/__init__.py index b5b07866a4..be9bdb8c5e 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -3,7 +3,6 @@ from pvlib import ( # noqa: F401 # list spectrum first so it's available for atmosphere & pvsystem (GH 1628) spectrum, - albedo, atmosphere, bifacial, diff --git a/pvlib/_deprecation.py b/pvlib/_deprecation.py index aedb4d5096..429bebce6b 100644 --- a/pvlib/_deprecation.py +++ b/pvlib/_deprecation.py @@ -130,43 +130,67 @@ class pvlibDeprecationWarning(UserWarning): # make it easier for others to copy paste this code into their projects -_projectName = 'pvlib' +_projectName = "pvlib" _projectWarning = pvlibDeprecationWarning def _generate_deprecation_message( - since, message='', name='', alternative='', pending=False, - obj_type='attribute', addendum='', removal=''): - + since, + message="", + name="", + alternative="", + pending=False, + obj_type="attribute", + addendum="", + removal="", +): if removal == "": removal = "soon" elif removal: if pending: raise ValueError( - "A pending deprecation cannot have a scheduled removal") + "A pending deprecation cannot have a scheduled removal" + ) removal = "in {}".format(removal) if not message: message = ( "The %(name)s %(obj_type)s" - + (" will be deprecated in a future version" - if pending else - (" was deprecated in %(projectName)s %(since)s" - + (" and will be removed %(removal)s" - if removal else - ""))) + + ( + " will be deprecated in a future version" + if pending + else ( + " was deprecated in %(projectName)s %(since)s" + + (" and will be removed %(removal)s" if removal else "") + ) + ) + "." + (" Use %(alternative)s instead." if alternative else "") - + (" %(addendum)s" if addendum else "")) + + (" %(addendum)s" if addendum else "") + ) return message % dict( - func=name, name=name, obj_type=obj_type, since=since, removal=removal, - alternative=alternative, addendum=addendum, projectName=_projectName) + func=name, + name=name, + obj_type=obj_type, + since=since, + removal=removal, + alternative=alternative, + addendum=addendum, + projectName=_projectName, + ) def warn_deprecated( - since, message='', name='', alternative='', pending=False, - obj_type='attribute', addendum='', removal=''): + since, + message="", + name="", + alternative="", + pending=False, + obj_type="attribute", + addendum="", + removal="", +): """ Used to display deprecation in a standard way. Parameters @@ -205,16 +229,29 @@ def warn_deprecated( warn_deprecated('1.4.0', name='matplotlib.name_of_module', obj_type='module') """ - message = '\n' + _generate_deprecation_message( - since, message, name, alternative, pending, obj_type, addendum, - removal=removal) - category = (PendingDeprecationWarning if pending - else _projectWarning) + message = "\n" + _generate_deprecation_message( + since, + message, + name, + alternative, + pending, + obj_type, + addendum, + removal=removal, + ) + category = PendingDeprecationWarning if pending else _projectWarning warnings.warn(message, category, stacklevel=2) -def deprecated(since, message='', name='', alternative='', pending=False, - addendum='', removal=''): +def deprecated( + since, + message="", + name="", + alternative="", + pending=False, + addendum="", + removal="", +): """ Decorator to mark a function or a class as deprecated. Parameters @@ -259,9 +296,14 @@ def the_function_to_deprecate(): pass """ - def deprecate(obj, message=message, name=name, alternative=alternative, - pending=pending, addendum=addendum): - + def deprecate( + obj, + message=message, + name=name, + alternative=alternative, + pending=pending, + addendum=addendum, + ): if not name: name = obj.__name__ @@ -294,24 +336,31 @@ def finalize(wrapper, new_doc): return wrapper message = _generate_deprecation_message( - since, message, name, alternative, pending, obj_type, addendum, - removal=removal) - category = (PendingDeprecationWarning if pending - else _projectWarning) + since, + message, + name, + alternative, + pending, + obj_type, + addendum, + removal=removal, + ) + category = PendingDeprecationWarning if pending else _projectWarning def wrapper(*args, **kwargs): warnings.warn(message, category, stacklevel=2) return func(*args, **kwargs) - old_doc = textwrap.dedent(old_doc or '').strip('\n') + old_doc = textwrap.dedent(old_doc or "").strip("\n") message = message.strip() - new_doc = (('\n.. deprecated:: %(since)s' - '\n %(message)s\n\n' % - {'since': since, 'message': message}) + old_doc) + new_doc = ( + "\n.. deprecated:: %(since)s" + "\n %(message)s\n\n" % {"since": since, "message": message} + ) + old_doc if not old_doc: # This is to prevent a spurious 'unexected unindent' warning from # docutils when the original docstring was blank. - new_doc += r'\ ' + new_doc += r"\ " return finalize(wrapper, new_doc) diff --git a/pvlib/albedo.py b/pvlib/albedo.py index 2dca47ef3d..707f13b3aa 100644 --- a/pvlib/albedo.py +++ b/pvlib/albedo.py @@ -10,43 +10,47 @@ # Sources of for the albedo values are provided in # pvlib.irradiance.get_ground_diffuse. SURFACE_ALBEDOS = { - 'urban': 0.18, - 'grass': 0.20, - 'fresh grass': 0.26, - 'soil': 0.17, - 'sand': 0.40, - 'snow': 0.65, - 'fresh snow': 0.75, - 'asphalt': 0.12, - 'concrete': 0.30, - 'aluminum': 0.85, - 'copper': 0.74, - 'fresh steel': 0.35, - 'dirty steel': 0.08, - 'sea': 0.06, + "urban": 0.18, + "grass": 0.20, + "fresh grass": 0.26, + "soil": 0.17, + "sand": 0.40, + "snow": 0.65, + "fresh snow": 0.75, + "asphalt": 0.12, + "concrete": 0.30, + "aluminum": 0.85, + "copper": 0.74, + "fresh steel": 0.35, + "dirty steel": 0.08, + "sea": 0.06, } WATER_COLOR_COEFFS = { - 'clear_water_no_waves': 0.13, - 'clear_water_ripples_up_to_2.5cm': 0.16, - 'clear_water_ripples_larger_than_2.5cm_occasional_whitecaps': 0.23, - 'clear_water_frequent_whitecaps': 0.3, - 'green_water_ripples_up_to_2.5cm': 0.22, - 'muddy_water_no_waves': 0.19 + "clear_water_no_waves": 0.13, + "clear_water_ripples_up_to_2.5cm": 0.16, + "clear_water_ripples_larger_than_2.5cm_occasional_whitecaps": 0.23, + "clear_water_frequent_whitecaps": 0.3, + "green_water_ripples_up_to_2.5cm": 0.22, + "muddy_water_no_waves": 0.19, } WATER_ROUGHNESS_COEFFS = { - 'clear_water_no_waves': 0.29, - 'clear_water_ripples_up_to_2.5cm': 0.7, - 'clear_water_ripples_larger_than_2.5cm_occasional_whitecaps': 1.25, - 'clear_water_frequent_whitecaps': 2, - 'green_water_ripples_up_to_2.5cm': 0.7, - 'muddy_water_no_waves': 0.29 + "clear_water_no_waves": 0.29, + "clear_water_ripples_up_to_2.5cm": 0.7, + "clear_water_ripples_larger_than_2.5cm_occasional_whitecaps": 1.25, + "clear_water_frequent_whitecaps": 2, + "green_water_ripples_up_to_2.5cm": 0.7, + "muddy_water_no_waves": 0.29, } -def inland_water_dvoracek(solar_elevation, surface_condition=None, - color_coeff=None, wave_roughness_coeff=None): +def inland_water_dvoracek( + solar_elevation, + surface_condition=None, + color_coeff=None, + wave_roughness_coeff=None, +): r""" Estimation of albedo for inland water bodies. @@ -154,13 +158,16 @@ def inland_water_dvoracek(solar_elevation, surface_condition=None, raise ValueError( "Either a `surface_condition` has to be chosen or" " a combination of `color_coeff` and" - " `wave_roughness_coeff`.") + " `wave_roughness_coeff`." + ) - solar_elevation_positive = np.where(solar_elevation < 0, 0, - solar_elevation) + solar_elevation_positive = np.where( + solar_elevation < 0, 0, solar_elevation + ) - albedo = color_coeff ** (wave_roughness_coeff * - sind(solar_elevation_positive) + 1) + albedo = color_coeff ** ( + wave_roughness_coeff * sind(solar_elevation_positive) + 1 + ) if isinstance(solar_elevation, pd.Series): albedo = pd.Series(albedo, index=solar_elevation.index) diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 4603fb7e11..32741e37f6 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -10,14 +10,19 @@ from pvlib._deprecation import deprecated -APPARENT_ZENITH_MODELS = ('simple', 'kasten1966', 'kastenyoung1989', - 'gueymard1993', 'pickering2002') -TRUE_ZENITH_MODELS = ('youngirvine1967', 'young1994') +APPARENT_ZENITH_MODELS = ( + "simple", + "kasten1966", + "kastenyoung1989", + "gueymard1993", + "pickering2002", +) +TRUE_ZENITH_MODELS = ("youngirvine1967", "young1994") AIRMASS_MODELS = APPARENT_ZENITH_MODELS + TRUE_ZENITH_MODELS def pres2alt(pressure): - ''' + """ Determine altitude from site pressure. Parameters @@ -49,7 +54,7 @@ def pres2alt(pressure): ----------- .. [1] "A Quick Derivation relating altitude to air pressure" from Portland State Aerospace Society, Version 1.03, 12/22/2004. - ''' + """ alt = 44331.5 - 4946.62 * pressure ** (0.190263) @@ -57,7 +62,7 @@ def pres2alt(pressure): def alt2pres(altitude): - ''' + """ Determine site pressure from altitude. Parameters @@ -89,15 +94,15 @@ def alt2pres(altitude): ----------- .. [1] "A Quick Derivation relating altitude to air pressure" from Portland State Aerospace Society, Version 1.03, 12/22/2004. - ''' + """ press = 100 * ((44331.514 - altitude) / 11880.516) ** (1 / 0.1902632) return press -def get_absolute_airmass(airmass_relative, pressure=101325.): - r''' +def get_absolute_airmass(airmass_relative, pressure=101325.0): + r""" Determine absolute (pressure-adjusted) airmass from relative airmass and pressure. @@ -127,15 +132,15 @@ def get_absolute_airmass(airmass_relative, pressure=101325.): .. [1] C. Gueymard, "Critical analysis and performance assessment of clear sky solar irradiance models using theoretical and measured data," Solar Energy, vol. 51, pp. 121-138, 1993. - ''' + """ - airmass_absolute = airmass_relative * pressure / 101325. + airmass_absolute = airmass_relative * pressure / 101325.0 return airmass_absolute -def get_relative_airmass(zenith, model='kastenyoung1989'): - ''' +def get_relative_airmass(zenith, model="kastenyoung1989"): + """ Calculate relative (not pressure-adjusted) airmass at sea level. Parameter ``model`` allows selection of different airmass models. @@ -212,7 +217,7 @@ def get_relative_airmass(zenith, model='kastenyoung1989'): .. [9] Matthew J. Reno, Clifford W. Hansen and Joshua S. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis" Sandia Report, (2012). - ''' + """ # set zenith values greater than 90 to nans z = np.where(zenith > 90, np.nan, zenith) @@ -220,33 +225,44 @@ def get_relative_airmass(zenith, model='kastenyoung1989'): model = model.lower() - if 'kastenyoung1989' == model: - am = (1.0 / (np.cos(zenith_rad) + - 0.50572*((6.07995 + (90 - z)) ** - 1.6364))) - elif 'kasten1966' == model: - am = 1.0 / (np.cos(zenith_rad) + 0.15*((93.885 - z) ** - 1.253)) - elif 'simple' == model: + if "kastenyoung1989" == model: + am = 1.0 / ( + np.cos(zenith_rad) + 0.50572 * ((6.07995 + (90 - z)) ** -1.6364) + ) + elif "kasten1966" == model: + am = 1.0 / (np.cos(zenith_rad) + 0.15 * ((93.885 - z) ** -1.253)) + elif "simple" == model: am = 1.0 / np.cos(zenith_rad) - elif 'pickering2002' == model: - am = (1.0 / (np.sin(np.radians(90 - z + - 244.0 / (165 + 47.0 * (90 - z) ** 1.1))))) - elif 'youngirvine1967' == model: + elif "pickering2002" == model: + am = 1.0 / ( + np.sin(np.radians(90 - z + 244.0 / (165 + 47.0 * (90 - z) ** 1.1))) + ) + elif "youngirvine1967" == model: sec_zen = 1.0 / np.cos(zenith_rad) am = sec_zen * (1 - 0.0012 * (sec_zen * sec_zen - 1)) - elif 'young1994' == model: - am = ((1.002432*((np.cos(zenith_rad)) ** 2) + - 0.148386*(np.cos(zenith_rad)) + 0.0096467) / - (np.cos(zenith_rad) ** 3 + - 0.149864*(np.cos(zenith_rad) ** 2) + - 0.0102963*(np.cos(zenith_rad)) + 0.000303978)) - elif 'gueymard1993' == model: - am = (1.0 / (np.cos(zenith_rad) + - 0.00176759*(z)*((94.37515 - z) ** - 1.21563))) - elif 'gueymard2003' == model: - am = (1.0 / (np.cos(zenith_rad) + - 0.48353*(z**0.095846)/(96.741 - z)**1.754)) + elif "young1994" == model: + am = ( + 1.002432 * ((np.cos(zenith_rad)) ** 2) + + 0.148386 * (np.cos(zenith_rad)) + + 0.0096467 + ) / ( + np.cos(zenith_rad) ** 3 + + 0.149864 * (np.cos(zenith_rad) ** 2) + + 0.0102963 * (np.cos(zenith_rad)) + + 0.000303978 + ) + elif "gueymard1993" == model: + am = 1.0 / ( + np.cos(zenith_rad) + + 0.00176759 * (z) * ((94.37515 - z) ** -1.21563) + ) + elif "gueymard2003" == model: + am = 1.0 / ( + np.cos(zenith_rad) + + 0.48353 * (z**0.095846) / (96.741 - z) ** 1.754 + ) else: - raise ValueError('%s is not a valid model for relativeairmass', model) + raise ValueError("%s is not a valid model for relativeairmass", model) if isinstance(zenith, pd.Series): am = pd.Series(am, index=zenith.index) @@ -321,16 +337,30 @@ def gueymard94_pw(temp_air, relative_humidity): """ T = temp_air + 273.15 # Convert to Kelvin # noqa: N806 - RH = relative_humidity # noqa: N806 + RH = relative_humidity # noqa: N806 theta = T / 273.15 # Eq. 1 from Keogh and Blakers pw = ( - 0.1 * - (0.4976 + 1.5265*theta + np.exp(13.6897*theta - 14.9188*(theta)**3)) * - (216.7*RH/(100*T)*np.exp(22.330 - 49.140*(100/T) - - 10.922*(100/T)**2 - 0.39015*T/100))) + 0.1 + * ( + 0.4976 + + 1.5265 * theta + + np.exp(13.6897 * theta - 14.9188 * (theta) ** 3) + ) + * ( + 216.7 + * RH + / (100 * T) + * np.exp( + 22.330 + - 49.140 * (100 / T) + - 10.922 * (100 / T) ** 2 + - 0.39015 * T / 100 + ) + ) + ) pw = np.maximum(pw, 0.1) @@ -405,9 +435,8 @@ def tdew_from_rh(temp_air, relative_humidity, coeff=(6.112, 17.62, 243.12)): # Substituting the Magnus equation and solving for dewpoint # First calculate ln(es/A) - ln_term = ( - (coeff[1] * temp_air) / (coeff[2] + temp_air) - + np.log(relative_humidity/100) + ln_term = (coeff[1] * temp_air) / (coeff[2] + temp_air) + np.log( + relative_humidity / 100 ) # Then solve for dewpoint @@ -417,8 +446,7 @@ def tdew_from_rh(temp_air, relative_humidity, coeff=(6.112, 17.62, 243.12)): first_solar_spectral_correction = deprecated( - since='0.10.0', - alternative='pvlib.spectrum.spectral_factor_firstsolar' + since="0.10.0", alternative="pvlib.spectrum.spectral_factor_firstsolar" )(pvlib.spectrum.spectral_factor_firstsolar) @@ -533,16 +561,18 @@ def kasten96_lt(airmass_absolute, precipitable_water, aod_bb): # the optical air mass. The precision of these fits is better than 1% when # compared with Modtran simulations in the range 1 < am < 5 and # 0 < pwat < 5 cm at sea level" - P. Ineichen (2008) - delta_w = 0.112 * airmass_absolute ** (-0.55) * precipitable_water ** 0.34 + delta_w = 0.112 * airmass_absolute ** (-0.55) * precipitable_water**0.34 # broadband AOD delta_a = aod_bb # "Then using the Kasten pyrheliometric formula (1980, 1996), the Linke # turbidity at am = 2 can be written. The extension of the Linke turbidity # coefficient to other values of air mass was published by Ineichen and # Perez (2002)" - P. Ineichen (2008) - lt = -(9.4 + 0.9 * airmass_absolute) * np.log( - np.exp(-airmass_absolute * (delta_cda + delta_w + delta_a)) - ) / airmass_absolute + lt = ( + -(9.4 + 0.9 * airmass_absolute) + * np.log(np.exp(-airmass_absolute * (delta_cda + delta_w + delta_a))) + / airmass_absolute + ) # filter out of extrapolated values return lt @@ -612,26 +642,30 @@ def angstrom_alpha(aod1, lambda1, aod2, lambda2): -------- pvlib.atmosphere.angstrom_aod_at_lambda """ - return - np.log(aod1 / aod2) / np.log(lambda1 / lambda2) + return -np.log(aod1 / aod2) / np.log(lambda1 / lambda2) # Values of the Hellmann exponent HELLMANN_SURFACE_EXPONENTS = { - 'unstable_air_above_open_water_surface': 0.06, - 'neutral_air_above_open_water_surface': 0.10, - 'stable_air_above_open_water_surface': 0.27, - 'unstable_air_above_flat_open_coast': 0.11, - 'neutral_air_above_flat_open_coast': 0.16, - 'stable_air_above_flat_open_coast': 0.40, - 'unstable_air_above_human_inhabited_areas': 0.27, - 'neutral_air_above_human_inhabited_areas': 0.34, - 'stable_air_above_human_inhabited_areas': 0.60, + "unstable_air_above_open_water_surface": 0.06, + "neutral_air_above_open_water_surface": 0.10, + "stable_air_above_open_water_surface": 0.27, + "unstable_air_above_flat_open_coast": 0.11, + "neutral_air_above_flat_open_coast": 0.16, + "stable_air_above_flat_open_coast": 0.40, + "unstable_air_above_human_inhabited_areas": 0.27, + "neutral_air_above_human_inhabited_areas": 0.34, + "stable_air_above_human_inhabited_areas": 0.60, } -def windspeed_powerlaw(wind_speed_reference, height_reference, - height_desired, exponent=None, - surface_type=None): +def windspeed_powerlaw( + wind_speed_reference, + height_reference, + height_desired, + exponent=None, + surface_type=None, +): r""" Estimate wind speed for different heights. @@ -755,14 +789,17 @@ def windspeed_powerlaw(wind_speed_reference, height_reference, pass else: raise ValueError( - "Either a 'surface_type' or an 'exponent' parameter must be given") + "Either a 'surface_type' or an 'exponent' parameter must be given" + ) wind_speed = wind_speed_reference * ( - (height_desired / height_reference) ** exponent) + (height_desired / height_reference) ** exponent + ) # if wind speed is negative or complex return NaN - wind_speed = np.where(np.iscomplex(wind_speed) | (wind_speed < 0), - np.nan, wind_speed) + wind_speed = np.where( + np.iscomplex(wind_speed) | (wind_speed < 0), np.nan, wind_speed + ) if isinstance(wind_speed_reference, pd.Series): wind_speed = pd.Series(wind_speed, index=wind_speed_reference.index) diff --git a/pvlib/bifacial/__init__.py b/pvlib/bifacial/__init__.py index e166c55108..cf76993bdb 100644 --- a/pvlib/bifacial/__init__.py +++ b/pvlib/bifacial/__init__.py @@ -7,7 +7,7 @@ from .loss_models import power_mismatch_deline # noqa: F401 pvfactors_timeseries = deprecated( - since='0.9.1', - name='pvlib.bifacial.pvfactors_timeseries', - alternative='pvlib.bifacial.pvfactors.pvfactors_timeseries' + since="0.9.1", + name="pvlib.bifacial.pvfactors_timeseries", + alternative="pvlib.bifacial.pvfactors.pvfactors_timeseries", )(pvfactors.pvfactors_timeseries) diff --git a/pvlib/bifacial/infinite_sheds.py b/pvlib/bifacial/infinite_sheds.py index 9f8a3787ae..8b36f7fc34 100644 --- a/pvlib/bifacial/infinite_sheds.py +++ b/pvlib/bifacial/infinite_sheds.py @@ -35,7 +35,7 @@ def _poa_ground_shadows(poa_ground, f_gnd_beam, df, vf_gnd_sky): ground. [W/m^2] """ - return poa_ground * (f_gnd_beam*(1 - df) + df*vf_gnd_sky) + return poa_ground * (f_gnd_beam * (1 - df) + df * vf_gnd_sky) def _poa_sky_diffuse_pv(dhi, gcr, surface_tilt): @@ -88,7 +88,7 @@ def _poa_sky_diffuse_pv(dhi, gcr, surface_tilt): poa_sky_diffuse_pv : numeric Total sky diffuse irradiance incident on the PV surface. [W/m^2] """ - vf_integ = utils.vf_row_sky_2d_integ(surface_tilt, gcr, 0., 1.) + vf_integ = utils.vf_row_sky_2d_integ(surface_tilt, gcr, 0.0, 1.0) return dhi * vf_integ @@ -115,12 +115,13 @@ def _poa_ground_pv(poa_ground, gcr, surface_tilt): numeric Ground diffuse irradiance on the row plane. [W/m^2] """ - vf_integ = utils.vf_row_ground_2d_integ(surface_tilt, gcr, 0., 1.) + vf_integ = utils.vf_row_ground_2d_integ(surface_tilt, gcr, 0.0, 1.0) return poa_ground * vf_integ -def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt, - surface_azimuth, gcr): +def _shaded_fraction( + solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr +): """ Calculate fraction (from the bottom) of row slant height that is shaded from direct irradiance by the row in front toward the sun. @@ -167,22 +168,37 @@ def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt, https://www.nrel.gov/docs/fy20osti/76626.pdf """ tan_phi = utils._solar_projection_tangent( - solar_zenith, solar_azimuth, surface_azimuth) + solar_zenith, solar_azimuth, surface_azimuth + ) # length of shadow behind a row as a fraction of pitch x = gcr * (sind(surface_tilt) * tan_phi + cosd(surface_tilt)) - f_x = 1 - 1. / x + f_x = 1 - 1.0 / x # set f_x to be 1 when sun is behind the array ao = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) - f_x = np.where(ao < 90, f_x, 1.) + f_x = np.where(ao < 90, f_x, 1.0) # when x < 1, the shadow is not long enough to fall on the row surface - f_x = np.where(x > 1., f_x, 0.) + f_x = np.where(x > 1.0, f_x, 0.0) return f_x -def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, - solar_azimuth, gcr, height, pitch, ghi, dhi, dni, - albedo, model='isotropic', dni_extra=None, iam=1.0, - npoints=100, vectorize=False): +def get_irradiance_poa( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + model="isotropic", + dni_extra=None, + iam=1.0, + npoints=100, + vectorize=False, +): r""" Calculate plane-of-array (POA) irradiance on one side of a row of modules. @@ -290,23 +306,36 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, -------- get_irradiance """ - if model == 'haydavies': + if model == "haydavies": if dni_extra is None: - raise ValueError(f'must supply dni_extra for {model} model') + raise ValueError(f"must supply dni_extra for {model} model") # Call haydavies first time within the horizontal plane - to subtract # circumsolar_horizontal from DHI - sky_diffuse_comps_horizontal = haydavies(0, 180, dhi, dni, dni_extra, - solar_zenith, solar_azimuth, - return_components=True) - circumsolar_horizontal = sky_diffuse_comps_horizontal['circumsolar'] + sky_diffuse_comps_horizontal = haydavies( + 0, + 180, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + return_components=True, + ) + circumsolar_horizontal = sky_diffuse_comps_horizontal["circumsolar"] # Call haydavies a second time where circumsolar_normal is facing # directly towards sun, and can be added to DNI - sky_diffuse_comps_normal = haydavies(solar_zenith, solar_azimuth, dhi, - dni, dni_extra, solar_zenith, - solar_azimuth, - return_components=True) - circumsolar_normal = sky_diffuse_comps_normal['circumsolar'] + sky_diffuse_comps_normal = haydavies( + solar_zenith, + solar_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + return_components=True, + ) + circumsolar_normal = sky_diffuse_comps_normal["circumsolar"] dhi = dhi - circumsolar_horizontal dni = dni + circumsolar_normal @@ -319,17 +348,19 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, # fraction of ground between rows that is illuminated accounting for # shade from panels. [1], Eq. 4 f_gnd_beam = utils._unshaded_ground_fraction( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr) + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr + ) # integrated view factor from the ground to the sky, integrated between # adjacent rows interior to the array # method differs from [1], Eq. 7 and Eq. 8; height is defined at row # center rather than at row lower edge as in [1]. vf_gnd_sky = utils.vf_ground_sky_2d_integ( - surface_tilt, gcr, height, pitch, max_rows, npoints, - vectorize) + surface_tilt, gcr, height, pitch, max_rows, npoints, vectorize + ) # fraction of row slant height that is shaded from direct irradiance - f_x = _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt, - surface_azimuth, gcr) + f_x = _shaded_fraction( + solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr + ) # Total sky diffuse received by both shaded and unshaded portions poa_sky_pv = _poa_sky_diffuse_pv(dhi, gcr, surface_tilt) @@ -341,15 +372,16 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, ground_diffuse = ghi * albedo # diffuse fraction - diffuse_fraction = np.clip(dhi / ghi, 0., 1.) + diffuse_fraction = np.clip(dhi / ghi, 0.0, 1.0) # make diffuse fraction 0 when ghi is small - diffuse_fraction = np.where(ghi < 0.0001, 0., diffuse_fraction) + diffuse_fraction = np.where(ghi < 0.0001, 0.0, diffuse_fraction) # Reduce ground-reflected irradiance because other rows in the array # block irradiance from reaching the ground. # [2], Eq. 9 ground_diffuse = _poa_ground_shadows( - ground_diffuse, f_gnd_beam, diffuse_fraction, vf_gnd_sky) + ground_diffuse, f_gnd_beam, diffuse_fraction, vf_gnd_sky + ) # Ground-reflected irradiance on the row surface accounting for # the view to the ground. This deviates from [1], Eq. 10, 11 and @@ -363,25 +395,49 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith, # component poa_diffuse = poa_gnd_pv + poa_sky_pv # beam on plane, make an array for consistency with poa_diffuse - poa_beam = np.atleast_1d(beam_component( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni)) + poa_beam = np.atleast_1d( + beam_component( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni + ) + ) poa_direct = poa_beam * (1 - f_x) * iam # direct only on the unshaded part poa_global = poa_direct + poa_diffuse output = { - 'poa_global': poa_global, 'poa_direct': poa_direct, - 'poa_diffuse': poa_diffuse, 'poa_ground_diffuse': poa_gnd_pv, - 'poa_sky_diffuse': poa_sky_pv, 'shaded_fraction': f_x} + "poa_global": poa_global, + "poa_direct": poa_direct, + "poa_diffuse": poa_diffuse, + "poa_ground_diffuse": poa_gnd_pv, + "poa_sky_diffuse": poa_sky_pv, + "shaded_fraction": f_x, + } if isinstance(poa_global, pd.Series): output = pd.DataFrame(output) return output -def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, - albedo, model='isotropic', dni_extra=None, iam_front=1.0, - iam_back=1.0, bifaciality=0.8, shade_factor=-0.02, - transmission_factor=0, npoints=100, vectorize=False): +def get_irradiance( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + model="isotropic", + dni_extra=None, + iam_front=1.0, + iam_back=1.0, + bifaciality=0.8, + shade_factor=-0.02, + transmission_factor=0, + npoints=100, + vectorize=False, +): """ Get front and rear irradiance using the infinite sheds model. @@ -534,34 +590,58 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, backside_tilt, backside_sysaz = _backside(surface_tilt, surface_azimuth) # front side POA irradiance irrad_front = get_irradiance_poa( - surface_tilt=surface_tilt, surface_azimuth=surface_azimuth, - solar_zenith=solar_zenith, solar_azimuth=solar_azimuth, - gcr=gcr, height=height, pitch=pitch, ghi=ghi, dhi=dhi, dni=dni, - albedo=albedo, model=model, dni_extra=dni_extra, iam=iam_front, - npoints=npoints, vectorize=vectorize) + surface_tilt=surface_tilt, + surface_azimuth=surface_azimuth, + solar_zenith=solar_zenith, + solar_azimuth=solar_azimuth, + gcr=gcr, + height=height, + pitch=pitch, + ghi=ghi, + dhi=dhi, + dni=dni, + albedo=albedo, + model=model, + dni_extra=dni_extra, + iam=iam_front, + npoints=npoints, + vectorize=vectorize, + ) # back side POA irradiance irrad_back = get_irradiance_poa( - surface_tilt=backside_tilt, surface_azimuth=backside_sysaz, - solar_zenith=solar_zenith, solar_azimuth=solar_azimuth, - gcr=gcr, height=height, pitch=pitch, ghi=ghi, dhi=dhi, dni=dni, - albedo=albedo, model=model, dni_extra=dni_extra, iam=iam_back, - npoints=npoints, vectorize=vectorize) + surface_tilt=backside_tilt, + surface_azimuth=backside_sysaz, + solar_zenith=solar_zenith, + solar_azimuth=solar_azimuth, + gcr=gcr, + height=height, + pitch=pitch, + ghi=ghi, + dhi=dhi, + dni=dni, + albedo=albedo, + model=model, + dni_extra=dni_extra, + iam=iam_back, + npoints=npoints, + vectorize=vectorize, + ) colmap_front = { - 'poa_global': 'poa_front', - 'poa_direct': 'poa_front_direct', - 'poa_diffuse': 'poa_front_diffuse', - 'poa_sky_diffuse': 'poa_front_sky_diffuse', - 'poa_ground_diffuse': 'poa_front_ground_diffuse', - 'shaded_fraction': 'shaded_fraction_front', + "poa_global": "poa_front", + "poa_direct": "poa_front_direct", + "poa_diffuse": "poa_front_diffuse", + "poa_sky_diffuse": "poa_front_sky_diffuse", + "poa_ground_diffuse": "poa_front_ground_diffuse", + "shaded_fraction": "shaded_fraction_front", } colmap_back = { - 'poa_global': 'poa_back', - 'poa_direct': 'poa_back_direct', - 'poa_diffuse': 'poa_back_diffuse', - 'poa_sky_diffuse': 'poa_back_sky_diffuse', - 'poa_ground_diffuse': 'poa_back_ground_diffuse', - 'shaded_fraction': 'shaded_fraction_back', + "poa_global": "poa_back", + "poa_direct": "poa_back_direct", + "poa_diffuse": "poa_back_diffuse", + "poa_sky_diffuse": "poa_back_sky_diffuse", + "poa_ground_diffuse": "poa_back_ground_diffuse", + "shaded_fraction": "shaded_fraction_back", } if isinstance(ghi, pd.Series): @@ -577,12 +657,13 @@ def get_irradiance(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, output = irrad_front effects = (1 + shade_factor) * (1 + transmission_factor) - output['poa_global'] = output['poa_front'] + \ - output['poa_back'] * bifaciality * effects + output["poa_global"] = ( + output["poa_front"] + output["poa_back"] * bifaciality * effects + ) return output def _backside(tilt, surface_azimuth): - backside_tilt = 180. - tilt - backside_sysaz = (180. + surface_azimuth) % 360. + backside_tilt = 180.0 - tilt + backside_sysaz = (180.0 + surface_azimuth) % 360.0 return backside_tilt, backside_sysaz diff --git a/pvlib/bifacial/pvfactors.py b/pvlib/bifacial/pvfactors.py index 05e3807658..2e1b8b77db 100644 --- a/pvlib/bifacial/pvfactors.py +++ b/pvlib/bifacial/pvfactors.py @@ -9,11 +9,24 @@ def pvfactors_timeseries( - solar_azimuth, solar_zenith, surface_azimuth, surface_tilt, - axis_azimuth, timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, - albedo, n_pvrows=3, index_observed_pvrow=1, - rho_front_pvrow=0.03, rho_back_pvrow=0.05, - horizon_band_angle=15.): + solar_azimuth, + solar_zenith, + surface_azimuth, + surface_tilt, + axis_azimuth, + timestamps, + dni, + dhi, + gcr, + pvrow_height, + pvrow_width, + albedo, + n_pvrows=3, + index_observed_pvrow=1, + rho_front_pvrow=0.03, + rho_back_pvrow=0.05, + horizon_band_angle=15.0, +): """ Calculate front and back surface plane-of-array irradiance on a fixed tilt or single-axis tracker PV array configuration using @@ -103,39 +116,57 @@ def pvfactors_timeseries( # Build up pv array configuration parameters pvarray_parameters = { - 'n_pvrows': n_pvrows, - 'axis_azimuth': axis_azimuth, - 'pvrow_height': pvrow_height, - 'pvrow_width': pvrow_width, - 'gcr': gcr + "n_pvrows": n_pvrows, + "axis_azimuth": axis_azimuth, + "pvrow_height": pvrow_height, + "pvrow_width": pvrow_width, + "gcr": gcr, } irradiance_model_params = { - 'rho_front': rho_front_pvrow, - 'rho_back': rho_back_pvrow, - 'horizon_band_angle': horizon_band_angle + "rho_front": rho_front_pvrow, + "rho_back": rho_back_pvrow, + "horizon_band_angle": horizon_band_angle, } # Create report function def fn_build_report(pvarray): - return {'total_inc_back': pvarray.ts_pvrows[index_observed_pvrow] - .back.get_param_weighted('qinc'), - 'total_inc_front': pvarray.ts_pvrows[index_observed_pvrow] - .front.get_param_weighted('qinc'), - 'total_abs_back': pvarray.ts_pvrows[index_observed_pvrow] - .back.get_param_weighted('qabs'), - 'total_abs_front': pvarray.ts_pvrows[index_observed_pvrow] - .front.get_param_weighted('qabs')} + return { + "total_inc_back": pvarray.ts_pvrows[ + index_observed_pvrow + ].back.get_param_weighted("qinc"), + "total_inc_front": pvarray.ts_pvrows[ + index_observed_pvrow + ].front.get_param_weighted("qinc"), + "total_abs_back": pvarray.ts_pvrows[ + index_observed_pvrow + ].back.get_param_weighted("qabs"), + "total_abs_front": pvarray.ts_pvrows[ + index_observed_pvrow + ].front.get_param_weighted("qabs"), + } # Run pvfactors calculations report = run_timeseries_engine( - fn_build_report, pvarray_parameters, - timestamps, dni, dhi, solar_zenith, solar_azimuth, - surface_tilt, surface_azimuth, albedo, - irradiance_model_params=irradiance_model_params) + fn_build_report, + pvarray_parameters, + timestamps, + dni, + dhi, + solar_zenith, + solar_azimuth, + surface_tilt, + surface_azimuth, + albedo, + irradiance_model_params=irradiance_model_params, + ) # Turn report into dataframe df_report = pd.DataFrame(report, index=timestamps) - return (df_report.total_inc_front, df_report.total_inc_back, - df_report.total_abs_front, df_report.total_abs_back) + return ( + df_report.total_inc_front, + df_report.total_inc_back, + df_report.total_abs_front, + df_report.total_abs_back, + ) diff --git a/pvlib/bifacial/utils.py b/pvlib/bifacial/utils.py index 6027155319..e03f694bf2 100644 --- a/pvlib/bifacial/utils.py +++ b/pvlib/bifacial/utils.py @@ -2,6 +2,7 @@ The bifacial.utils module contains functions that support bifacial irradiance modeling. """ + import numpy as np from pvlib.tools import sind, cosd, tand from scipy.integrate import trapezoid @@ -37,8 +38,14 @@ def _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth): return tan_phi -def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith, - solar_azimuth, gcr, max_zenith=87): +def _unshaded_ground_fraction( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + max_zenith=87, +): r""" Calculate the fraction of the ground with incident direct irradiance. @@ -83,11 +90,13 @@ def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith, Photovoltaic Specialists Conference (PVSC), 2019, pp. 1282-1287. :doi:`10.1109/PVSC40753.2019.8980572`. """ - tan_phi = _solar_projection_tangent(solar_zenith, solar_azimuth, - surface_azimuth) + tan_phi = _solar_projection_tangent( + solar_zenith, solar_azimuth, surface_azimuth + ) f_gnd_beam = 1.0 - np.minimum( - 1.0, gcr * np.abs(cosd(surface_tilt) + sind(surface_tilt) * tan_phi)) - np.where(solar_zenith > max_zenith, 0., f_gnd_beam) # [1], Eq. 4 + 1.0, gcr * np.abs(cosd(surface_tilt) + sind(surface_tilt) * tan_phi) + ) + np.where(solar_zenith > max_zenith, 0.0, f_gnd_beam) # [1], Eq. 4 return f_gnd_beam # 1 - min(1, abs()) < 1 always @@ -137,7 +146,7 @@ def vf_ground_sky_2d(rotation, gcr, x, pitch, height, max_rows=10): x = np.atleast_1d(x)[:, np.newaxis, np.newaxis] rotation = np.atleast_1d(rotation)[np.newaxis, :, np.newaxis] all_k = np.arange(-max_rows, max_rows + 1) - width = gcr * pitch / 2. + width = gcr * pitch / 2.0 distance_to_row_centers = (all_k - x) * pitch dy = width * sind(rotation) dx = width * cosd(rotation) @@ -168,13 +177,14 @@ def vf_ground_sky_2d(rotation, gcr, x, pitch, height, max_rows=10): # Note that the 0.5 view factor coefficient is applied after summing # as a minor speed optimization. np.subtract(next_edge, prev_edge, out=next_edge) - np.clip(next_edge, a_min=0., a_max=None, out=next_edge) + np.clip(next_edge, a_min=0.0, a_max=None, out=next_edge) vf = np.sum(next_edge, axis=-1) / 2 return vf -def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10, - npoints=100, vectorize=False): +def vf_ground_sky_2d_integ( + surface_tilt, gcr, height, pitch, max_rows=10, npoints=100, vectorize=False +): """ Integrated view factor to the sky from the ground underneath interior rows of the array. @@ -225,7 +235,7 @@ def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10, def _vf_poly(surface_tilt, gcr, x, delta): - r''' + r""" A term common to many 2D view factor calculations Parameters @@ -244,14 +254,14 @@ def _vf_poly(surface_tilt, gcr, x, delta): Returns ------- numeric - ''' + """ a = 1 / gcr c = cosd(surface_tilt) - return np.sqrt(a*a + 2*delta*a*c*x + x*x) + return np.sqrt(a * a + 2 * delta * a * c * x + x * x) def vf_row_sky_2d(surface_tilt, gcr, x): - r''' + r""" Calculate the view factor to the sky from a point x on a row surface. Assumes a PV system of infinitely long rows with uniform pitch on @@ -274,13 +284,13 @@ def vf_row_sky_2d(surface_tilt, gcr, x): vf : numeric Fraction of the sky dome visible from the point x. [unitless] - ''' + """ p = _vf_poly(surface_tilt, gcr, 1 - x, -1) - return 0.5*(1 + (1/gcr * cosd(surface_tilt) - (1 - x)) / p) + return 0.5 * (1 + (1 / gcr * cosd(surface_tilt) - (1 - x)) / p) def vf_row_sky_2d_integ(surface_tilt, gcr, x0=0, x1=1): - r''' + r""" Calculate the average view factor to the sky from a segment of the row surface between x0 and x1. @@ -309,20 +319,21 @@ def vf_row_sky_2d_integ(surface_tilt, gcr, x0=0, x1=1): Average fraction of the sky dome visible from points in the segment from x0 to x1. [unitless] - ''' + """ u = np.abs(x1 - x0) p0 = _vf_poly(surface_tilt, gcr, 1 - x0, -1) p1 = _vf_poly(surface_tilt, gcr, 1 - x1, -1) - with np.errstate(divide='ignore'): - result = np.where(u < 1e-6, - vf_row_sky_2d(surface_tilt, gcr, x0), - 0.5*(1 + 1/u * (p1 - p0)) - ) + with np.errstate(divide="ignore"): + result = np.where( + u < 1e-6, + vf_row_sky_2d(surface_tilt, gcr, x0), + 0.5 * (1 + 1 / u * (p1 - p0)), + ) return result def vf_row_ground_2d(surface_tilt, gcr, x): - r''' + r""" Calculate the view factor to the ground from a point x on a row surface. Assumes a PV system of infinitely long rows with uniform pitch on @@ -345,13 +356,13 @@ def vf_row_ground_2d(surface_tilt, gcr, x): vf : numeric View factor to the visible ground from the point x. [unitless] - ''' + """ p = _vf_poly(surface_tilt, gcr, x, 1) - return 0.5 * (1 - (1/gcr * cosd(surface_tilt) + x)/p) + return 0.5 * (1 - (1 / gcr * cosd(surface_tilt) + x) / p) def vf_row_ground_2d_integ(surface_tilt, gcr, x0=0, x1=1): - r''' + r""" Calculate the average view factor to the ground from a segment of the row surface between x0 and x1. @@ -380,13 +391,14 @@ def vf_row_ground_2d_integ(surface_tilt, gcr, x0=0, x1=1): Integrated view factor to the visible ground on the interval (x0, x1). [unitless] - ''' + """ u = np.abs(x1 - x0) p0 = _vf_poly(surface_tilt, gcr, x0, 1) p1 = _vf_poly(surface_tilt, gcr, x1, 1) - with np.errstate(divide='ignore'): - result = np.where(u < 1e-6, - vf_row_ground_2d(surface_tilt, gcr, x0), - 0.5*(1 - 1/u * (p1 - p0)) - ) + with np.errstate(divide="ignore"): + result = np.where( + u < 1e-6, + vf_row_ground_2d(surface_tilt, gcr, x0), + 0.5 * (1 - 1 / u * (p1 - p0)), + ) return result diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index be75ecd47a..036bc4e42c 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -16,9 +16,15 @@ from pvlib.tools import _degrees_to_index -def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, - altitude=0, dni_extra=1364., perez_enhancement=False): - ''' +def ineichen( + apparent_zenith, + airmass_absolute, + linke_turbidity, + altitude=0, + dni_extra=1364.0, + perez_enhancement=False, +): + """ Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model. Implements the Ineichen and Perez clear sky model for global @@ -84,7 +90,7 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, .. [5] J. Remund, et. al., "Worldwide Linke Turbidity Information", Proc. ISES Solar World Congress, June 2003. Goteborg, Sweden. - ''' # noqa: E501 + """ # noqa: E501 # ghi is calculated using either the equations in [1] by setting # perez_enhancement=False (default behavior) or using the model @@ -102,16 +108,16 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, tl = linke_turbidity - fh1 = np.exp(-altitude/8000.) - fh2 = np.exp(-altitude/1250.) + fh1 = np.exp(-altitude / 8000.0) + fh2 = np.exp(-altitude / 1250.0) cg1 = 5.09e-05 * altitude + 0.868 cg2 = 3.92e-05 * altitude + 0.0387 - ghi = np.exp(-cg2*airmass_absolute*(fh1 + fh2*(tl - 1))) + ghi = np.exp(-cg2 * airmass_absolute * (fh1 + fh2 * (tl - 1))) # https://github.com/pvlib/pvlib-python/issues/435 if perez_enhancement: - ghi *= np.exp(0.01*airmass_absolute**1.8) + ghi *= np.exp(0.01 * airmass_absolute**1.8) # use fmax to map airmass nans to 0s. multiply and divide by tl to # reinsert tl nans @@ -119,24 +125,23 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, # From [1] (Following [2] leads to 0.664 + 0.16268 / fh1) # See https://github.com/pvlib/pvlib-python/pull/808 - b = 0.664 + 0.163/fh1 + b = 0.664 + 0.163 / fh1 # BncI = "normal beam clear sky radiation" bnci = b * np.exp(-0.09 * airmass_absolute * (tl - 1)) bnci = dni_extra * np.fmax(bnci, 0) # "empirical correction" SE 73, 157 & SE 73, 312. - bnci_2 = ((1 - (0.1 - 0.2*np.exp(-tl))/(0.1 + 0.882/fh1)) / - cos_zenith) + bnci_2 = (1 - (0.1 - 0.2 * np.exp(-tl)) / (0.1 + 0.882 / fh1)) / cos_zenith bnci_2 = ghi * np.fmin(np.fmax(bnci_2, 0), 1e20) dni = np.minimum(bnci, bnci_2) - dhi = ghi - dni*cos_zenith + dhi = ghi - dni * cos_zenith irrads = OrderedDict() - irrads['ghi'] = ghi - irrads['dni'] = dni - irrads['dhi'] = dhi + irrads["ghi"] = ghi + irrads["dni"] = dni + irrads["dhi"] = dhi if isinstance(dni, pd.Series): irrads = pd.DataFrame.from_dict(irrads) @@ -144,8 +149,9 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, return irrads -def lookup_linke_turbidity(time, latitude, longitude, filepath=None, - interp_turbidity=True): +def lookup_linke_turbidity( + time, latitude, longitude, filepath=None, interp_turbidity=True +): """ Look up the Linke Turibidity from the ``LinkeTurbidities.h5`` data file supplied with pvlib. @@ -196,13 +202,13 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None, if filepath is None: pvlib_path = os.path.dirname(os.path.abspath(__file__)) - filepath = os.path.join(pvlib_path, 'data', 'LinkeTurbidities.h5') + filepath = os.path.join(pvlib_path, "data", "LinkeTurbidities.h5") - latitude_index = _degrees_to_index(latitude, coordinate='latitude') - longitude_index = _degrees_to_index(longitude, coordinate='longitude') + latitude_index = _degrees_to_index(latitude, coordinate="latitude") + longitude_index = _degrees_to_index(longitude, coordinate="longitude") - with h5py.File(filepath, 'r') as lt_h5_file: - lts = lt_h5_file['LinkeTurbidity'][latitude_index, longitude_index] + with h5py.File(filepath, "r") as lt_h5_file: + lts = lt_h5_file["LinkeTurbidity"][latitude_index, longitude_index] if interp_turbidity: linke_turbidity = _interpolate_turbidity(lts, time) @@ -210,7 +216,7 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None, months = tools._pandas_to_utc(time).month - 1 linke_turbidity = pd.Series(lts[months], index=time) - linke_turbidity /= 20. + linke_turbidity /= 20.0 return linke_turbidity @@ -226,8 +232,9 @@ def _is_leap_year(year): ------- isleap : array of bools """ - isleap = ((np.mod(year, 4) == 0) & - ((np.mod(year, 100) != 0) | (np.mod(year, 400) == 0))) + isleap = (np.mod(year, 4) == 0) & ( + (np.mod(year, 100) != 0) | (np.mod(year, 400) == 0) + ) return isleap @@ -284,14 +291,17 @@ def _calendar_month_middles(year): mdays[1] = mdays[1] + 1 ydays = 366 middles = np.concatenate( - [[-calendar.mdays[-1] / 2.0], # Dec last year - np.cumsum(mdays) - np.array(mdays) / 2., # this year - [ydays + calendar.mdays[1] / 2.0]]) # Jan next year + [ + [-calendar.mdays[-1] / 2.0], # Dec last year + np.cumsum(mdays) - np.array(mdays) / 2.0, # this year + [ydays + calendar.mdays[1] / 2.0], + ] + ) # Jan next year return middles def haurwitz(apparent_zenith): - ''' + """ Determine clear sky GHI using the Haurwitz model. Implements the Haurwitz clear sky model for global horizontal @@ -324,23 +334,31 @@ def haurwitz(apparent_zenith): .. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. - ''' + """ cos_zenith = tools.cosd(apparent_zenith.values) ghi_clear = np.zeros_like(apparent_zenith.values) cos_zen_gte_0 = cos_zenith > 0 - ghi_clear[cos_zen_gte_0] = (1098.0 * cos_zenith[cos_zen_gte_0] * - np.exp(-0.059/cos_zenith[cos_zen_gte_0])) + ghi_clear[cos_zen_gte_0] = ( + 1098.0 + * cos_zenith[cos_zen_gte_0] + * np.exp(-0.059 / cos_zenith[cos_zen_gte_0]) + ) - df_out = pd.DataFrame(index=apparent_zenith.index, - data=ghi_clear, - columns=['ghi']) + df_out = pd.DataFrame( + index=apparent_zenith.index, data=ghi_clear, columns=["ghi"] + ) return df_out -def simplified_solis(apparent_elevation, aod700=0.1, precipitable_water=1., - pressure=101325., dni_extra=1364.): +def simplified_solis( + apparent_elevation, + aod700=0.1, + precipitable_water=1.0, + pressure=101325.0, + dni_extra=1364.0, +): """ Calculate the clear sky GHI, DNI, and DHI according to the simplified Solis model. @@ -413,16 +431,16 @@ def simplified_solis(apparent_elevation, aod700=0.1, precipitable_water=1., # this prevents the creation of nans at night instead of 0s # it's also friendly to scalar and series inputs - sin_elev = np.maximum(1.e-30, np.sin(np.radians(apparent_elevation))) + sin_elev = np.maximum(1.0e-30, np.sin(np.radians(apparent_elevation))) - dni = i0p * np.exp(-taub/sin_elev**b) - ghi = i0p * np.exp(-taug/sin_elev**g) * sin_elev - dhi = i0p * np.exp(-taud/sin_elev**d) + dni = i0p * np.exp(-taub / sin_elev**b) + ghi = i0p * np.exp(-taug / sin_elev**g) * sin_elev + dhi = i0p * np.exp(-taud / sin_elev**d) irrads = OrderedDict() - irrads['ghi'] = ghi - irrads['dni'] = dni - irrads['dhi'] = dhi + irrads["ghi"] = ghi + irrads["dni"] = dni + irrads["dhi"] = dhi if isinstance(dni, pd.Series): irrads = pd.DataFrame.from_dict(irrads) @@ -432,23 +450,23 @@ def simplified_solis(apparent_elevation, aod700=0.1, precipitable_water=1., def _calc_i0p(i0, w, aod700, p): """Calculate the "enhanced extraterrestrial irradiance".""" - p0 = 101325. + p0 = 101325.0 io0 = 1.08 * w**0.0051 i01 = 0.97 * w**0.032 i02 = 0.12 * w**0.56 - i0p = i0 * (i02*aod700**2 + i01*aod700 + io0 + 0.071*np.log(p/p0)) + i0p = i0 * (i02 * aod700**2 + i01 * aod700 + io0 + 0.071 * np.log(p / p0)) return i0p def _calc_taub(w, aod700, p): """Calculate the taub coefficient""" - p0 = 101325. - tb1 = 1.82 + 0.056*np.log(w) + 0.0071*np.log(w)**2 - tb0 = 0.33 + 0.045*np.log(w) + 0.0096*np.log(w)**2 - tbp = 0.0089*w + 0.13 + p0 = 101325.0 + tb1 = 1.82 + 0.056 * np.log(w) + 0.0071 * np.log(w) ** 2 + tb0 = 0.33 + 0.045 * np.log(w) + 0.0096 * np.log(w) ** 2 + tbp = 0.0089 * w + 0.13 - taub = tb1*aod700 + tb0 + tbp*np.log(p/p0) + taub = tb1 * aod700 + tb0 + tbp * np.log(p / p0) return taub @@ -456,8 +474,8 @@ def _calc_taub(w, aod700, p): def _calc_b(w, aod700): """Calculate the b coefficient.""" - b1 = 0.00925*aod700**2 + 0.0148*aod700 - 0.0172 - b0 = -0.7565*aod700**2 + 0.5057*aod700 + 0.4557 + b1 = 0.00925 * aod700**2 + 0.0148 * aod700 - 0.0172 + b0 = -0.7565 * aod700**2 + 0.5057 * aod700 + 0.4557 b = b1 * np.log(w) + b0 @@ -466,11 +484,11 @@ def _calc_b(w, aod700): def _calc_taug(w, aod700, p): """Calculate the taug coefficient""" - p0 = 101325. - tg1 = 1.24 + 0.047*np.log(w) + 0.0061*np.log(w)**2 - tg0 = 0.27 + 0.043*np.log(w) + 0.0090*np.log(w)**2 - tgp = 0.0079*w + 0.1 - taug = tg1*aod700 + tg0 + tgp*np.log(p/p0) + p0 = 101325.0 + tg1 = 1.24 + 0.047 * np.log(w) + 0.0061 * np.log(w) ** 2 + tg0 = 0.27 + 0.043 * np.log(w) + 0.0090 * np.log(w) ** 2 + tgp = 0.0079 * w + 0.1 + taug = tg1 * aod700 + tg0 + tgp * np.log(p / p0) return taug @@ -478,7 +496,7 @@ def _calc_taug(w, aod700, p): def _calc_g(w, aod700): """Calculate the g coefficient.""" - g = -0.0147*np.log(w) - 0.3079*aod700**2 + 0.2846*aod700 + 0.3798 + g = -0.0147 * np.log(w) - 0.3079 * aod700**2 + 0.2846 * aod700 + 0.3798 return g @@ -499,24 +517,30 @@ def _calc_taud(w, aod700, p): aod700 = np.full_like(w, aod700) # set up nan-tolerant masks - aod700_lt_0p05 = np.full_like(aod700, False, dtype='bool') + aod700_lt_0p05 = np.full_like(aod700, False, dtype="bool") np.less(aod700, 0.05, where=~np.isnan(aod700), out=aod700_lt_0p05) aod700_mask = np.array([aod700_lt_0p05, ~aod700_lt_0p05], dtype=int) # create tuples of coefficients for # aod700 < 0.05, aod700 >= 0.05 - td4 = 86*w - 13800, -0.21*w + 11.6 - td3 = -3.11*w + 79.4, 0.27*w - 20.7 - td2 = -0.23*w + 74.8, -0.134*w + 15.5 - td1 = 0.092*w - 8.86, 0.0554*w - 5.71 - td0 = 0.0042*w + 3.12, 0.0057*w + 2.94 - tdp = -0.83*(1+aod700)**(-17.2), -0.71*(1+aod700)**(-15.0) + td4 = 86 * w - 13800, -0.21 * w + 11.6 + td3 = -3.11 * w + 79.4, 0.27 * w - 20.7 + td2 = -0.23 * w + 74.8, -0.134 * w + 15.5 + td1 = 0.092 * w - 8.86, 0.0554 * w - 5.71 + td0 = 0.0042 * w + 3.12, 0.0057 * w + 2.94 + tdp = -0.83 * (1 + aod700) ** (-17.2), -0.71 * (1 + aod700) ** (-15.0) tds = (np.array([td0, td1, td2, td3, td4, tdp]) * aod700_mask).sum(axis=1) - p0 = 101325. - taud = (tds[4]*aod700**4 + tds[3]*aod700**3 + tds[2]*aod700**2 + - tds[1]*aod700 + tds[0] + tds[5]*np.log(p/p0)) + p0 = 101325.0 + taud = ( + tds[4] * aod700**4 + + tds[3] * aod700**3 + + tds[2] * aod700**2 + + tds[1] * aod700 + + tds[0] + + tds[5] * np.log(p / p0) + ) # be polite about matching the output type to the input type(s) if len(taud) == 1: @@ -528,15 +552,15 @@ def _calc_taud(w, aod700, p): def _calc_d(aod700, p): """Calculate the d coefficient.""" - p0 = 101325. - dp = 1/(18 + 152*aod700) - d = -0.337*aod700**2 + 0.63*aod700 + 0.116 + dp*np.log(p/p0) + p0 = 101325.0 + dp = 1 / (18 + 152 * aod700) + d = -0.337 * aod700**2 + 0.63 * aod700 + 0.116 + dp * np.log(p / p0) return d def _calc_stats(data, samples_per_window, sample_interval, H): - """ Calculates statistics for each window, used by Reno-style clear + """Calculates statistics for each window, used by Reno-style clear sky detection functions. Does not return the line length statistic which is provided by _calc_windowed_stat and _line_length. @@ -588,34 +612,39 @@ def _calc_stats(data, samples_per_window, sample_interval, H): # shift to get forward difference, .diff() is backward difference instead data_diff = data.diff().shift(-1) data_slope = data_diff / sample_interval - data_slope_nstd = _slope_nstd_windowed(data_slope.values[:-1], data, H, - samples_per_window, sample_interval) + data_slope_nstd = _slope_nstd_windowed( + data_slope.values[:-1], data, H, samples_per_window, sample_interval + ) return data_mean, data_max, data_slope_nstd, data_slope def _slope_nstd_windowed(slopes, data, H, samples_per_window, sample_interval): - with np.errstate(divide='ignore', invalid='ignore'): - nstd = slopes[H[:-1, ]].std(ddof=1, axis=0) \ - / data.values[H].mean(axis=0) + with np.errstate(divide="ignore", invalid="ignore"): + nstd = slopes[H[:-1,]].std(ddof=1, axis=0) / data.values[H].mean( + axis=0 + ) return _to_centered_series(nstd, data.index, samples_per_window) def _max_diff_windowed(data, H, samples_per_window): raw = np.diff(data) - raw = np.abs(raw[H[:-1, ]]).max(axis=0) + raw = np.abs(raw[H[:-1,]]).max(axis=0) return _to_centered_series(raw, data.index, samples_per_window) -def _line_length_windowed(data, H, samples_per_window, - sample_interval): - raw = np.sqrt(np.diff(data)**2. + sample_interval**2.) - raw = np.sum(raw[H[:-1, ]], axis=0) +def _line_length_windowed(data, H, samples_per_window, sample_interval): + raw = np.sqrt(np.diff(data) ** 2.0 + sample_interval**2.0) + raw = np.sum(raw[H[:-1,]], axis=0) return _to_centered_series(raw, data.index, samples_per_window) def _to_centered_series(vals, idx, samples_per_window): - vals = np.pad(vals, ((0, len(idx) - len(vals)),), mode='constant', - constant_values=np.nan) + vals = np.pad( + vals, + ((0, len(idx) - len(vals)),), + mode="constant", + constant_values=np.nan, + ) shift = samples_per_window // 2 # align = 'center' only return pd.Series(index=idx, data=vals).shift(shift) @@ -641,7 +670,7 @@ def _clear_sample_index(clear_windows, samples_per_window, align, H): shift = -(samples_per_window // 2) idx = clear_windows.shift(shift) # drop rows at the end corresponding to windows past the end of data - idx = idx.drop(clear_windows.index[1 - samples_per_window:]) + idx = idx.drop(clear_windows.index[1 - samples_per_window :]) idx = idx.astype(bool) # shift changed type to object clear_samples = np.unique(H[:, idx]) return clear_samples @@ -659,29 +688,52 @@ def _clearsky_get_threshold(sample_interval): v209, p. 393-400, 2023. """ - if (sample_interval < 1 or sample_interval > 30): - raise ValueError("`infer_limits=True` can only be used for inputs \ - with time step from 1 to 30 minutes") + if sample_interval < 1 or sample_interval > 30: + raise ValueError( + "`infer_limits=True` can only be used for inputs \ + with time step from 1 to 30 minutes" + ) data_freq = np.array([1, 5, 15, 30]) window_length = np.interp(sample_interval, data_freq, [50, 60, 90, 120]) mean_diff = np.interp(sample_interval, data_freq, [75, 75, 75, 75]) max_diff = np.interp(sample_interval, data_freq, [60, 65, 75, 90]) - lower_line_length = np.interp(sample_interval, data_freq, [-45,-45,-45,-45]) + lower_line_length = np.interp( + sample_interval, data_freq, [-45, -45, -45, -45] + ) upper_line_length = np.interp(sample_interval, data_freq, [80, 80, 80, 80]) - var_diff = np.interp(sample_interval, data_freq, [0.005, 0.01, 0.032, 0.07]) + var_diff = np.interp( + sample_interval, data_freq, [0.005, 0.01, 0.032, 0.07] + ) slope_dev = np.interp(sample_interval, data_freq, [50, 60, 75, 96]) - return (window_length, mean_diff, max_diff, lower_line_length, - upper_line_length, var_diff, slope_dev) + return ( + window_length, + mean_diff, + max_diff, + lower_line_length, + upper_line_length, + var_diff, + slope_dev, + ) -def detect_clearsky(measured, clearsky, times=None, infer_limits=False, - window_length=10, mean_diff=75, max_diff=75, - lower_line_length=-5, upper_line_length=10, - var_diff=0.005, slope_dev=8, max_iterations=20, - return_components=False): +def detect_clearsky( + measured, + clearsky, + times=None, + infer_limits=False, + window_length=10, + mean_diff=75, + max_diff=75, + lower_line_length=-5, + upper_line_length=10, + var_diff=0.005, + slope_dev=8, + max_iterations=20, + return_components=False, +): """ Detects clear sky times using the algorithm developed by Reno and Hansen. @@ -815,44 +867,61 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, else: clear = clearsky - sample_interval, samples_per_window = \ - tools._get_sample_intervals(times, window_length) + sample_interval, samples_per_window = tools._get_sample_intervals( + times, window_length + ) if samples_per_window < 3: - raise ValueError(f"Samples per window of {samples_per_window}" - " found. Each window must contain at least 3 data" - " points." - f" Window length of {window_length} found; increase" - f" window length to {3*sample_interval} or longer.") + raise ValueError( + f"Samples per window of {samples_per_window}" + " found. Each window must contain at least 3 data" + " points." + f" Window length of {window_length} found; increase" + f" window length to {3*sample_interval} or longer." + ) # if infer_limits, find threshold values using the sample interval if infer_limits: - window_length, mean_diff, max_diff, lower_line_length, \ - upper_line_length, var_diff, slope_dev = \ - _clearsky_get_threshold(sample_interval) + ( + window_length, + mean_diff, + max_diff, + lower_line_length, + upper_line_length, + var_diff, + slope_dev, + ) = _clearsky_get_threshold(sample_interval) # recalculate samples_per_window using returned window_length - _, samples_per_window = \ - tools._get_sample_intervals(times, window_length) + _, samples_per_window = tools._get_sample_intervals( + times, window_length + ) # check that we have enough data to produce a nonempty hankel matrix if len(times) < samples_per_window: - raise ValueError(f"times has only {len(times)} entries, but it must \ - have at least {samples_per_window} entries") + raise ValueError( + f"times has only {len(times)} entries, but it must \ + have at least {samples_per_window} entries" + ) # generate matrix of integers for creating windows with indexing - H = hankel(np.arange(samples_per_window), - np.arange(samples_per_window-1, len(times))) + H = hankel( + np.arange(samples_per_window), + np.arange(samples_per_window - 1, len(times)), + ) # calculate measurement statistics meas_mean, meas_max, meas_slope_nstd, meas_slope = _calc_stats( - meas, samples_per_window, sample_interval, H) + meas, samples_per_window, sample_interval, H + ) meas_line_length = _line_length_windowed( - meas, H, samples_per_window, sample_interval) + meas, H, samples_per_window, sample_interval + ) # calculate clear sky statistics clear_mean, clear_max, _, clear_slope = _calc_stats( - clear, samples_per_window, sample_interval, H) + clear, samples_per_window, sample_interval, H + ) # find a scaling factor for the clear sky time series that minimizes the # RMSE between the clear times identified in the measured data and the @@ -864,14 +933,16 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, for iteration in range(max_iterations): scaled_clear = alpha * clear clear_line_length = _line_length_windowed( - scaled_clear, H, samples_per_window, sample_interval) + scaled_clear, H, samples_per_window, sample_interval + ) line_diff = meas_line_length - clear_line_length slope_max_diff = _max_diff_windowed( - meas - scaled_clear, H, samples_per_window) + meas - scaled_clear, H, samples_per_window + ) # evaluate comparison criteria - c1 = np.abs(meas_mean - alpha*clear_mean) < mean_diff - c2 = np.abs(meas_max - alpha*clear_max) < max_diff + c1 = np.abs(meas_mean - alpha * clear_mean) < mean_diff + c2 = np.abs(meas_max - alpha * clear_max) < max_diff c3 = (line_diff > lower_line_length) & (line_diff < upper_line_length) c4 = meas_slope_nstd < var_diff c5 = slope_max_diff < slope_dev @@ -879,10 +950,11 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, clear_windows = c1 & c2 & c3 & c4 & c5 & c6 # create array to return - clear_samples = np.full_like(meas, False, dtype='bool') + clear_samples = np.full_like(meas, False, dtype="bool") # find the samples contained in any window classified as clear - idx = _clear_sample_index(clear_windows, samples_per_window, 'center', - H) + idx = _clear_sample_index( + clear_windows, samples_per_window, "center", H + ) clear_samples[idx] = True # find a new alpha @@ -895,12 +967,16 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, if not (pd.isna(C) or C == 0): # safety check # only update alpha if C is strictly positive alpha = (clear_meas * clear_clear).sum() / C - if round(alpha*10000) == round(previous_alpha*10000): + if round(alpha * 10000) == round(previous_alpha * 10000): break else: import warnings - warnings.warn('rescaling failed to converge after %s iterations' - % max_iterations, RuntimeWarning) + + warnings.warn( + "rescaling failed to converge after %s iterations" + % max_iterations, + RuntimeWarning, + ) # be polite about returning the same type as was input if ispandas: @@ -908,28 +984,37 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, if return_components: components = OrderedDict() - components['mean_diff_flag'] = c1 - components['max_diff_flag'] = c2 - components['line_length_flag'] = c3 - components['slope_nstd_flag'] = c4 - components['slope_max_flag'] = c5 - components['mean_nan_flag'] = c6 - components['windows'] = clear_windows - - components['mean_diff'] = np.abs(meas_mean - alpha * clear_mean) - components['max_diff'] = np.abs(meas_max - alpha * clear_max) - components['line_length'] = meas_line_length - clear_line_length - components['slope_nstd'] = meas_slope_nstd - components['slope_max'] = slope_max_diff + components["mean_diff_flag"] = c1 + components["max_diff_flag"] = c2 + components["line_length_flag"] = c3 + components["slope_nstd_flag"] = c4 + components["slope_max_flag"] = c5 + components["mean_nan_flag"] = c6 + components["windows"] = clear_windows + + components["mean_diff"] = np.abs(meas_mean - alpha * clear_mean) + components["max_diff"] = np.abs(meas_max - alpha * clear_max) + components["line_length"] = meas_line_length - clear_line_length + components["slope_nstd"] = meas_slope_nstd + components["slope_max"] = slope_max_diff return clear_samples, components, alpha else: return clear_samples -def bird(zenith, airmass_relative, aod380, aod500, precipitable_water, - ozone=0.3, pressure=101325., dni_extra=1364., asymmetry=0.85, - albedo=0.2): +def bird( + zenith, + airmass_relative, + aod380, + aod500, + precipitable_water, + ozone=0.3, + pressure=101325.0, + dni_extra=1364.0, + asymmetry=0.85, + albedo=0.2, +): """ Bird Simple Clear Sky Broadband Solar Radiation Model @@ -1005,48 +1090,51 @@ def bird(zenith, airmass_relative, aod380, aod500, precipitable_water, airmass = airmass_relative # Bird clear sky model am_press = atmosphere.get_absolute_airmass(airmass, pressure) - t_rayleigh = ( - np.exp(-0.0903 * am_press ** 0.84 * ( - 1.0 + am_press - am_press ** 1.01 - )) + t_rayleigh = np.exp( + -0.0903 * am_press**0.84 * (1.0 + am_press - am_press**1.01) ) - am_o3 = ozone*airmass + am_o3 = ozone * airmass t_ozone = ( - 1.0 - 0.1611 * am_o3 * (1.0 + 139.48 * am_o3) ** -0.3034 - - 0.002715 * am_o3 / (1.0 + 0.044 * am_o3 + 0.0003 * am_o3 ** 2.0) + 1.0 + - 0.1611 * am_o3 * (1.0 + 139.48 * am_o3) ** -0.3034 + - 0.002715 * am_o3 / (1.0 + 0.044 * am_o3 + 0.0003 * am_o3**2.0) ) - t_gases = np.exp(-0.0127 * am_press ** 0.26) + t_gases = np.exp(-0.0127 * am_press**0.26) am_h2o = airmass * precipitable_water - t_water = ( - 1.0 - 2.4959 * am_h2o / ( - (1.0 + 79.034 * am_h2o) ** 0.6828 + 6.385 * am_h2o - ) + t_water = 1.0 - 2.4959 * am_h2o / ( + (1.0 + 79.034 * am_h2o) ** 0.6828 + 6.385 * am_h2o ) bird_huldstrom = atmosphere.bird_hulstrom80_aod_bb(aod380, aod500) t_aerosol = np.exp( - -(bird_huldstrom ** 0.873) * - (1.0 + bird_huldstrom - bird_huldstrom ** 0.7088) * airmass ** 0.9108 + -(bird_huldstrom**0.873) + * (1.0 + bird_huldstrom - bird_huldstrom**0.7088) + * airmass**0.9108 ) - taa = 1.0 - 0.1 * (1.0 - airmass + airmass ** 1.06) * (1.0 - t_aerosol) + taa = 1.0 - 0.1 * (1.0 - airmass + airmass**1.06) * (1.0 - t_aerosol) rs = 0.0685 + (1.0 - asymmetry) * (1.0 - t_aerosol / taa) id_ = 0.9662 * etr * t_aerosol * t_water * t_gases * t_ozone * t_rayleigh ze_cos = np.where(zenith < 90, np.cos(ze_rad), 0.0) id_nh = id_ * ze_cos ias = ( - etr * ze_cos * 0.79 * t_ozone * t_gases * t_water * taa * - (0.5 * (1.0 - t_rayleigh) + asymmetry * (1.0 - (t_aerosol / taa))) / ( - 1.0 - airmass + airmass ** 1.02 - ) + etr + * ze_cos + * 0.79 + * t_ozone + * t_gases + * t_water + * taa + * (0.5 * (1.0 - t_rayleigh) + asymmetry * (1.0 - (t_aerosol / taa))) + / (1.0 - airmass + airmass**1.02) ) gh = (id_nh + ias) / (1.0 - albedo * rs) diffuse_horiz = gh - id_nh # TODO: be DRY, use decorator to wrap methods that need to return either # OrderedDict or DataFrame instead of repeating this boilerplate code irrads = OrderedDict() - irrads['direct_horizontal'] = id_nh - irrads['ghi'] = gh - irrads['dni'] = id_ - irrads['dhi'] = diffuse_horiz - if isinstance(irrads['dni'], pd.Series): + irrads["direct_horizontal"] = id_nh + irrads["ghi"] = gh + irrads["dni"] = id_ + irrads["dhi"] = diffuse_horiz + if isinstance(irrads["dni"], pd.Series): irrads = pd.DataFrame.from_dict(irrads) return irrads diff --git a/pvlib/iam.py b/pvlib/iam.py index 161de84589..4050bc95f7 100644 --- a/pvlib/iam.py +++ b/pvlib/iam.py @@ -17,11 +17,11 @@ # a dict of required parameter names for each IAM model # keys are the function names for the IAM models _IAM_MODEL_PARAMS = { - 'ashrae': {'b'}, - 'physical': {'n', 'K', 'L'}, - 'martin_ruiz': {'a_r'}, - 'sapm': {'B0', 'B1', 'B2', 'B3', 'B4', 'B5'}, - 'interp': {'theta_ref', 'iam_ref'} + "ashrae": {"b"}, + "physical": {"n", "K", "L"}, + "martin_ruiz": {"a_r"}, + "sapm": {"B0", "B1", "B2", "B3", "B4", "B5"}, + "interp": {"theta_ref", "iam_ref"}, } @@ -81,7 +81,7 @@ def ashrae(aoi, b=0.05): """ iam = 1 - b * (1 / np.cos(np.radians(aoi)) - 1) - aoi_gte_90 = np.full_like(aoi, False, dtype='bool') + aoi_gte_90 = np.full_like(aoi, False, dtype="bool") np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90) iam = np.where(aoi_gte_90, 0, iam) iam = np.maximum(0, iam) @@ -176,11 +176,13 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None): n2costheta2 = n2 * costheta # reflectance of s-, p-polarized, and normal light by the first interface - with np.errstate(divide='ignore', invalid='ignore'): - rho12_s = \ - ((n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2)) ** 2 - rho12_p = \ - ((n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1)) ** 2 + with np.errstate(divide="ignore", invalid="ignore"): + rho12_s = ( + (n1costheta1 - n2costheta2) / (n1costheta1 + n2costheta2) + ) ** 2 + rho12_p = ( + (n1costheta2 - n2costheta1) / (n1costheta2 + n2costheta1) + ) ** 2 rho12_0 = ((n1 - n2) / (n1 + n2)) ** 2 @@ -213,7 +215,7 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None): tau_0 *= (1 - rho23_0) / (1 - rho23_0 * rho12_0) # transmittance after absorption in the glass - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): tau_s *= np.exp(-K * L / costheta) tau_p *= np.exp(-K * L / costheta) @@ -233,7 +235,7 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None): def martin_ruiz(aoi, a_r=0.16): - r''' + r""" Determine the incidence angle modifier (IAM) using the Martin and Ruiz incident angle model. @@ -291,7 +293,7 @@ def martin_ruiz(aoi, a_r=0.16): pvlib.iam.ashrae pvlib.iam.interp pvlib.iam.sapm - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 aoi_input = aoi @@ -302,7 +304,7 @@ def martin_ruiz(aoi, a_r=0.16): if np.any(np.less_equal(a_r, 0)): raise ValueError("The parameter 'a_r' cannot be zero or negative.") - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r)) iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam) @@ -313,7 +315,7 @@ def martin_ruiz(aoi, a_r=0.16): def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None): - ''' + """ Determine the incidence angle modifiers (iam) for diffuse sky and ground-reflected irradiance using the Martin and Ruiz incident angle model. @@ -377,7 +379,7 @@ def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None): pvlib.iam.ashrae pvlib.iam.interp pvlib.iam.sapm - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019 if isinstance(surface_tilt, pd.Series): @@ -403,25 +405,25 @@ def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None): cos = np.cos # avoid RuntimeWarnings for <, sin, and cos with nan - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): # because sin(pi) isn't exactly zero sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta)) trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta)) - trig_term_gnd = sin_beta + (beta - sin_beta) / (1 - cos(beta)) # noqa: E222 E261 E501 + trig_term_gnd = sin_beta + (beta - sin_beta) / (1 - cos(beta)) # noqa: E222 E261 E501 iam_sky = 1 - np.exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r) iam_gnd = 1 - np.exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r) if out_index is not None: - iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky') - iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground') + iam_sky = pd.Series(iam_sky, index=out_index, name="iam_sky") + iam_gnd = pd.Series(iam_gnd, index=out_index, name="iam_ground") return iam_sky, iam_gnd -def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): - r''' +def interp(aoi, theta_ref, iam_ref, method="linear", normalize=True): + r""" Determine the incidence angle modifier (IAM) by interpolating a set of reference values, which are usually measured values. @@ -467,24 +469,29 @@ def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True): pvlib.iam.ashrae pvlib.iam.martin_ruiz pvlib.iam.sapm - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 from scipy.interpolate import interp1d # Scipy doesn't give the clearest feedback, so check number of points here. - MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4} + MIN_REF_VALS = {"linear": 2, "quadratic": 3, "cubic": 4, 1: 2, 2: 3, 3: 4} if len(theta_ref) < MIN_REF_VALS.get(method, 2): - raise ValueError("Too few reference points defined " - "for interpolation method '%s'." % method) + raise ValueError( + "Too few reference points defined " + "for interpolation method '%s'." % method + ) if np.any(np.less(iam_ref, 0)): - raise ValueError("Negative value(s) found in 'iam_ref'. " - "This is not physically possible.") - - interpolator = interp1d(theta_ref, iam_ref, kind=method, - fill_value='extrapolate') + raise ValueError( + "Negative value(s) found in 'iam_ref'. " + "This is not physically possible." + ) + + interpolator = interp1d( + theta_ref, iam_ref, kind=method, fill_value="extrapolate" + ) aoi_input = aoi aoi = np.asanyarray(aoi) @@ -552,13 +559,19 @@ def sapm(aoi, module, upper=None): pvlib.iam.interp """ - aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'], - module['B1'], module['B0']] + aoi_coeff = [ + module["B5"], + module["B4"], + module["B3"], + module["B2"], + module["B1"], + module["B0"], + ] iam = np.polyval(aoi_coeff, aoi) iam = np.clip(iam, 0, upper) # nan tolerant masking - aoi_lt_0 = np.full_like(aoi, False, dtype='bool') + aoi_lt_0 = np.full_like(aoi, False, dtype="bool") np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0) iam = np.where(aoi_lt_0, 0, iam) @@ -624,21 +637,21 @@ def marion_diffuse(model, surface_tilt, **kwargs): """ models = { - 'physical': physical, - 'ashrae': ashrae, - 'sapm': sapm, - 'martin_ruiz': martin_ruiz, - 'schlick': schlick, + "physical": physical, + "ashrae": ashrae, + "sapm": sapm, + "martin_ruiz": martin_ruiz, + "schlick": schlick, } try: iam_model = models[model] except KeyError: - raise ValueError('model must be one of: ' + str(list(models.keys()))) + raise ValueError("model must be one of: " + str(list(models.keys()))) iam_function = functools.partial(iam_model, **kwargs) iam = {} - for region in ['sky', 'horizon', 'ground']: + for region in ["sky", "horizon", "ground"]: iam[region] = marion_integrate(iam_function, surface_tilt, region) return iam @@ -709,34 +722,34 @@ def marion_integrate(function, surface_tilt, region, num=None): """ if num is None: - if region in ['sky', 'ground']: + if region in ["sky", "ground"]: num = 180 - elif region == 'horizon': + elif region == "horizon": num = 1800 else: - raise ValueError(f'Invalid region: {region}') + raise ValueError(f"Invalid region: {region}") beta = np.radians(surface_tilt) if isinstance(beta, pd.Series): # convert Series to np array for broadcasting later beta = beta.values - ai = np.pi/num # angular increment + ai = np.pi / num # angular increment phi_range = np.linspace(0, np.pi, num, endpoint=False) - psi_range = np.linspace(0, 2*np.pi, 2*num, endpoint=False) + psi_range = np.linspace(0, 2 * np.pi, 2 * num, endpoint=False) # the pseudocode in [1] do these checks at the end, but it's # faster to do this criteria check up front instead of later. - if region == 'sky': - mask = phi_range + ai <= np.pi/2 - elif region == 'horizon': - lo = 89.5 * np.pi/180 - hi = np.pi/2 + if region == "sky": + mask = phi_range + ai <= np.pi / 2 + elif region == "horizon": + lo = 89.5 * np.pi / 180 + hi = np.pi / 2 mask = (lo <= phi_range) & (phi_range + ai <= hi) - elif region == 'ground': - mask = (phi_range >= np.pi/2) + elif region == "ground": + mask = phi_range >= np.pi / 2 else: - raise ValueError(f'Invalid region: {region}') + raise ValueError(f"Invalid region: {region}") phi_range = phi_range[mask] # fast Cartesian product of phi and psi @@ -747,8 +760,8 @@ def marion_integrate(function, surface_tilt, region, num=None): psi_1 = angles[:, [1]] phi_2 = phi_1 + ai # psi_2 = psi_1 + ai # not needed - phi_avg = phi_1 + 0.5*ai - psi_avg = psi_1 + 0.5*ai + phi_avg = phi_1 + 0.5 * ai + psi_avg = psi_1 + 0.5 * ai term_1 = np.cos(beta) * np.cos(phi_avg) # The AOI formula includes a term based on the difference between # panel azimuth and the photon azimuth, but because we assume each class @@ -765,18 +778,18 @@ def marion_integrate(function, surface_tilt, region, num=None): dAs = ai * (np.cos(phi_1) - np.cos(phi_2)) cosaoi_dAs = cosaoi * dAs # apply the final AOI check, zeroing out non-passing points - mask = aoi < np.pi/2 + mask = aoi < np.pi / 2 cosaoi_dAs = np.where(mask, cosaoi_dAs, 0) numerator = np.sum(function(np.degrees(aoi)) * cosaoi_dAs, axis=0) denominator = np.sum(cosaoi_dAs, axis=0) - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): # in some cases, no points pass the criteria # (e.g. region='ground', surface_tilt=0), so we override the division # by zero to set Fd=0. Also, preserve nans in beta. - Fd = np.where((denominator != 0) | ~np.isfinite(beta), - numerator / denominator, - 0) + Fd = np.where( + (denominator != 0) | ~np.isfinite(beta), numerator / denominator, 0 + ) # preserve input type if np.isscalar(surface_tilt): @@ -921,13 +934,18 @@ def schlick_diffuse(surface_tilt): cosB = cosd(surface_tilt) sinB = sind(surface_tilt) cuk = (2 / (np.pi * (1 + cosB))) * ( - (30/7)*np.pi - (160/21)*np.radians(surface_tilt) - (10/3)*np.pi*cosB - + (160/21)*cosB*sinB - (5/3)*np.pi*cosB*sinB**2 + (20/7)*cosB*sinB**3 - - (5/16)*np.pi*cosB*sinB**4 + (16/105)*cosB*sinB**5 + (30 / 7) * np.pi + - (160 / 21) * np.radians(surface_tilt) + - (10 / 3) * np.pi * cosB + + (160 / 21) * cosB * sinB + - (5 / 3) * np.pi * cosB * sinB**2 + + (20 / 7) * cosB * sinB**3 + - (5 / 16) * np.pi * cosB * sinB**4 + + (16 / 105) * cosB * sinB**5 ) # Eq 4 in [2] # relative transmittance of ground-reflected radiation by PV cover: - with np.errstate(divide='ignore', invalid='ignore'): # Eq 6 in [2] + with np.errstate(divide="ignore", invalid="ignore"): # Eq 6 in [2] cug = 40 / (21 * (1 - cosB)) - (1 + cosB) / (1 - cosB) * cuk cug = np.where(surface_tilt < 1e-6, 0, cug) @@ -945,13 +963,17 @@ def schlick_diffuse(surface_tilt): def _get_model(model_name): # check that model is implemented - model_dict = {'ashrae': ashrae, 'martin_ruiz': martin_ruiz, - 'physical': physical} + model_dict = { + "ashrae": ashrae, + "martin_ruiz": martin_ruiz, + "physical": physical, + } try: model = model_dict[model_name] except KeyError: - raise NotImplementedError(f"The {model_name} model has not been " - "implemented") + raise NotImplementedError( + f"The {model_name} model has not been " "implemented" + ) return model @@ -961,17 +983,18 @@ def _check_params(model_name, params): # belong to the model exp_params = _IAM_MODEL_PARAMS[model_name] if set(params.keys()) != exp_params: - raise ValueError(f"The {model_name} model was expecting to be passed " - "{', '.join(list(exp_params))}, but " - "was handed {', '.join(list(params.keys()))}") + raise ValueError( + f"The {model_name} model was expecting to be passed " + "{', '.join(list(exp_params))}, but " + "was handed {', '.join(list(params.keys()))}" + ) def _sin_weight(aoi): return 1 - sind(aoi) -def _residual(aoi, source_iam, target, target_params, - weight=_sin_weight): +def _residual(aoi, source_iam, target, target_params, weight=_sin_weight): # computes a sum of weighted differences between the source model # and target model, using the provided weight function @@ -1054,17 +1077,23 @@ def residual_function(target_params): def _minimize(residual_function, guess, bounds, xtol): if xtol is not None: - options = {'xtol': xtol} + options = {"xtol": xtol} else: options = None - with np.errstate(invalid='ignore'): - optimize_result = minimize(residual_function, guess, method="powell", - bounds=bounds, options=options) + with np.errstate(invalid="ignore"): + optimize_result = minimize( + residual_function, + guess, + method="powell", + bounds=bounds, + options=options, + ) if not optimize_result.success: try: - message = "Optimizer exited unsuccessfully:" \ - + optimize_result.message + message = ( + "Optimizer exited unsuccessfully:" + optimize_result.message + ) except AttributeError: message = "Optimizer exited unsuccessfully: \ No message explaining the failure was returned. \ @@ -1078,23 +1107,29 @@ def _minimize(residual_function, guess, bounds, xtol): def _process_return(target_name, optimize_result): if target_name == "ashrae": - target_params = {'b': optimize_result.x.item()} + target_params = {"b": optimize_result.x.item()} elif target_name == "martin_ruiz": - target_params = {'a_r': optimize_result.x.item()} + target_params = {"a_r": optimize_result.x.item()} elif target_name == "physical": L, n = optimize_result.x # have to unpack order because search order may be different if L > n: L, n = n, L - target_params = {'n': n, 'K': 4, 'L': L} + target_params = {"n": n, "K": 4, "L": L} return target_params -def convert(source_name, source_params, target_name, weight=_sin_weight, - fix_n=True, xtol=None): +def convert( + source_name, + source_params, + target_name, + weight=_sin_weight, + fix_n=True, + xtol=None, +): """ Convert a source IAM model to a target IAM model. @@ -1190,13 +1225,13 @@ def convert(source_name, source_params, target_name, weight=_sin_weight, # we can do some special set-up to improve the fit when the # target model is physical if source_name == "ashrae": - residual_function, guess, bounds = \ - _ashrae_to_physical(aoi, source_iam, weight, fix_n, - source_params['b']) + residual_function, guess, bounds = _ashrae_to_physical( + aoi, source_iam, weight, fix_n, source_params["b"] + ) elif source_name == "martin_ruiz": - residual_function, guess, bounds = \ - _martin_ruiz_to_physical(aoi, source_iam, weight, - source_params['a_r']) + residual_function, guess, bounds = _martin_ruiz_to_physical( + aoi, source_iam, weight, source_params["a_r"] + ) else: # otherwise, target model is ashrae or martin_ruiz, and scipy @@ -1207,8 +1242,7 @@ def convert(source_name, source_params, target_name, weight=_sin_weight, def residual_function(target_param): return _residual(aoi, source_iam, target, target_param, weight) - optimize_result = _minimize(residual_function, guess, bounds, - xtol=xtol) + optimize_result = _minimize(residual_function, guess, bounds, xtol=xtol) return _process_return(target_name, optimize_result) @@ -1281,12 +1315,13 @@ def fit(measured_aoi, measured_iam, model_name, weight=_sin_weight, xtol=None): if model_name == "physical": bounds = [(0, 0.08), (1, 2)] - guess = [0.002, 1+1e-08] + guess = [0.002, 1 + 1e-08] def residual_function(target_params): L, n = target_params - return _residual(measured_aoi, measured_iam, target, [n, 4, L], - weight) + return _residual( + measured_aoi, measured_iam, target, [n, 4, L], weight + ) # otherwise, target_name is martin_ruiz or ashrae else: @@ -1294,8 +1329,9 @@ def residual_function(target_params): guess = [0.05] def residual_function(target_param): - return _residual(measured_aoi, measured_iam, target, - target_param, weight) + return _residual( + measured_aoi, measured_iam, target, target_param, weight + ) optimize_result = _minimize(residual_function, guess, bounds, xtol) diff --git a/pvlib/inverter.py b/pvlib/inverter.py index 8207e6bba9..968da63291 100644 --- a/pvlib/inverter.py +++ b/pvlib/inverter.py @@ -16,29 +16,29 @@ def _sandia_eff(v_dc, p_dc, inverter): - r''' + r""" Calculate the inverter AC power without clipping - ''' - Paco = inverter['Paco'] - Pdco = inverter['Pdco'] - Vdco = inverter['Vdco'] - C0 = inverter['C0'] - C1 = inverter['C1'] - C2 = inverter['C2'] - C3 = inverter['C3'] - Pso = inverter['Pso'] + """ + Paco = inverter["Paco"] + Pdco = inverter["Pdco"] + Vdco = inverter["Vdco"] + C0 = inverter["C0"] + C1 = inverter["C1"] + C2 = inverter["C2"] + C3 = inverter["C3"] + Pso = inverter["Pso"] A = Pdco * (1 + C1 * (v_dc - Vdco)) B = Pso * (1 + C2 * (v_dc - Vdco)) C = C0 * (1 + C3 * (v_dc - Vdco)) - return (Paco / (A - B) - C * (A - B)) * (p_dc - B) + C * (p_dc - B)**2 + return (Paco / (A - B) - C * (A - B)) * (p_dc - B) + C * (p_dc - B) ** 2 def _sandia_limits(power_ac, p_dc, Paco, Pnt, Pso): - r''' + r""" Applies minimum and maximum power limits to `power_ac` - ''' + """ power_ac = np.minimum(Paco, power_ac) min_ac_power = -1.0 * abs(Pnt) below_limit = p_dc < Pso @@ -51,7 +51,7 @@ def _sandia_limits(power_ac, p_dc, Paco, Pnt, Pso): def sandia(v_dc, p_dc, inverter): - r''' + r""" Convert DC power and voltage to AC power using Sandia's Grid-Connected PV Inverter model. @@ -122,11 +122,11 @@ def sandia(v_dc, p_dc, inverter): See also -------- pvlib.pvsystem.retrieve_sam - ''' + """ - Paco = inverter['Paco'] - Pnt = inverter['Pnt'] - Pso = inverter['Pso'] + Paco = inverter["Paco"] + Pnt = inverter["Pnt"] + Pso = inverter["Pso"] power_ac = _sandia_eff(v_dc, p_dc, inverter) power_ac = _sandia_limits(power_ac, p_dc, Paco, Pnt, Pso) @@ -138,7 +138,7 @@ def sandia(v_dc, p_dc, inverter): def sandia_multi(v_dc, p_dc, inverter): - r''' + r""" Convert DC power and voltage to AC power for an inverter with multiple MPPT inputs. @@ -185,22 +185,23 @@ def sandia_multi(v_dc, p_dc, inverter): See also -------- pvlib.inverter.sandia - ''' + """ if len(p_dc) != len(v_dc): - raise ValueError('p_dc and v_dc have different lengths') + raise ValueError("p_dc and v_dc have different lengths") power_dc = sum(p_dc) - power_ac = 0. * power_dc + power_ac = 0.0 * power_dc for vdc, pdc in zip(v_dc, p_dc): power_ac += pdc / power_dc * _sandia_eff(vdc, power_dc, inverter) - return _sandia_limits(power_ac, power_dc, inverter['Paco'], - inverter['Pnt'], inverter['Pso']) + return _sandia_limits( + power_ac, power_dc, inverter["Paco"], inverter["Pnt"], inverter["Pso"] + ) def adr(v_dc, p_dc, inverter, vtol=0.10): - r''' + r""" Converts DC power and voltage to AC power using Anton Driesse's grid-connected inverter efficiency model. @@ -277,18 +278,18 @@ def adr(v_dc, p_dc, inverter, vtol=0.10): -------- pvlib.inverter.sandia pvlib.pvsystem.retrieve_sam - ''' - - p_nom = inverter['Pnom'] - v_nom = inverter['Vnom'] - pac_max = inverter['Pacmax'] - p_nt = inverter['Pnt'] - ce_list = inverter['ADRCoefficients'] - v_max = inverter['Vmax'] - v_min = inverter['Vmin'] - vdc_max = inverter['Vdcmax'] - mppt_hi = inverter['MPPTHi'] - mppt_low = inverter['MPPTLow'] + """ + + p_nom = inverter["Pnom"] + v_nom = inverter["Vnom"] + pac_max = inverter["Pacmax"] + p_nt = inverter["Pnt"] + ce_list = inverter["ADRCoefficients"] + v_max = inverter["Vmax"] + v_min = inverter["Vmin"] + vdc_max = inverter["Vdcmax"] + mppt_hi = inverter["MPPTHi"] + mppt_low = inverter["MPPTLow"] v_lim_upper = float(np.nanmax([v_max, vdc_max, mppt_hi]) * (1 + vtol)) v_lim_lower = float(np.nanmax([v_min, mppt_low]) * (1 - vtol)) @@ -297,23 +298,27 @@ def adr(v_dc, p_dc, inverter, vtol=0.10): vdc = v_dc / v_nom # zero voltage will lead to division by zero, but since power is # set to night time value later, these errors can be safely ignored - with np.errstate(invalid='ignore', divide='ignore'): - poly = np.array([pdc**0, # replace with np.ones_like? - pdc, - pdc**2, - vdc - 1, - pdc * (vdc - 1), - pdc**2 * (vdc - 1), - 1. / vdc - 1, # divide by 0 - pdc * (1. / vdc - 1), # invalid 0./0. --> nan - pdc**2 * (1. / vdc - 1)]) # divide by 0 + with np.errstate(invalid="ignore", divide="ignore"): + poly = np.array( + [ + pdc**0, # replace with np.ones_like? + pdc, + pdc**2, + vdc - 1, + pdc * (vdc - 1), + pdc**2 * (vdc - 1), + 1.0 / vdc - 1, # divide by 0 + pdc * (1.0 / vdc - 1), # invalid 0./0. --> nan + pdc**2 * (1.0 / vdc - 1), + ] + ) # divide by 0 p_loss = np.dot(np.array(ce_list), poly) power_ac = p_nom * (pdc - p_loss) p_nt = -1 * np.absolute(p_nt) # set output to nan where input is outside of limits # errstate silences case where input is nan - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): invalid = (v_lim_upper < v_dc) | (v_dc < v_lim_lower) power_ac = np.where(invalid, np.nan, power_ac) @@ -398,13 +403,19 @@ def pvwatts(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): # eta < 0 if zeta < 0.006. power_ac is forced to be >= 0 below. GH 541 # In some published versions of [1] the parentheses are missing - eta = eta_inv_nom / eta_inv_ref * ( - -0.0162 * zeta - np.divide(0.0059, zeta, out=eta, where=pdc_neq_0) - + 0.9858) # noQA: W503 + eta = ( + eta_inv_nom + / eta_inv_ref + * ( + -0.0162 * zeta + - np.divide(0.0059, zeta, out=eta, where=pdc_neq_0) + + 0.9858 + ) + ) # noQA: W503 power_ac = eta * pdc power_ac = np.minimum(pac0, power_ac) - power_ac = np.maximum(0, power_ac) # GH 541 + power_ac = np.maximum(0, power_ac) # GH 541 return power_ac @@ -443,7 +454,7 @@ def pvwatts_multi(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): def fit_sandia(ac_power, dc_power, dc_voltage, dc_voltage_level, p_ac_0, p_nt): - r''' + r""" Determine parameters for the Sandia inverter model. Parameters @@ -494,25 +505,31 @@ def fit_sandia(ac_power, dc_power, dc_voltage, dc_voltage_level, p_ac_0, p_nt): .. [3] W. Bower, et al., "Performance Test Protocol for Evaluating Inverters Used in Grid-Connected Photovoltaic Systems", available at https://www.energy.ca.gov/sites/default/files/2020-06/2004-11-22_Sandia_Test_Protocol_ada.pdf - ''' # noqa: E501 + """ # noqa: E501 - voltage_levels = ['Vmin', 'Vnom', 'Vmax'] + voltage_levels = ["Vmin", "Vnom", "Vmax"] # average dc input voltage at each voltage level v_d = np.array( - [dc_voltage[dc_voltage_level == 'Vmin'].mean(), - dc_voltage[dc_voltage_level == 'Vnom'].mean(), - dc_voltage[dc_voltage_level == 'Vmax'].mean()]) + [ + dc_voltage[dc_voltage_level == "Vmin"].mean(), + dc_voltage[dc_voltage_level == "Vnom"].mean(), + dc_voltage[dc_voltage_level == "Vmax"].mean(), + ] + ) v_nom = v_d[1] # model parameter # independent variable for regressions, x_d x_d = v_d - v_nom # empty dataframe to contain intermediate variables - coeffs = pd.DataFrame(index=voltage_levels, - columns=['a', 'b', 'c', 'p_dc', 'p_s0'], data=np.nan) + coeffs = pd.DataFrame( + index=voltage_levels, + columns=["a", "b", "c", "p_dc", "p_s0"], + data=np.nan, + ) def solve_quad(a, b, c): - return (-b + (b**2 - 4 * a * c)**.5) / (2 * a) + return (-b + (b**2 - 4 * a * c) ** 0.5) / (2 * a) # [2] STEP 3E, fit a line to (DC voltage, model_coefficient) def extract_c(x_d, add): @@ -532,18 +549,27 @@ def extract_c(x_d, add): p_s0 = solve_quad(a, b, c) # Add values to dataframe at index d - coeffs.loc[d, 'a'] = a - coeffs.loc[d, 'p_dc'] = p_dc - coeffs.loc[d, 'p_s0'] = p_s0 + coeffs.loc[d, "a"] = a + coeffs.loc[d, "p_dc"] = p_dc + coeffs.loc[d, "p_s0"] = p_s0 - b_dc0, b_dc1, c1 = extract_c(x_d, coeffs['p_dc']) - b_s0, b_s1, c2 = extract_c(x_d, coeffs['p_s0']) - b_c0, b_c1, c3 = extract_c(x_d, coeffs['a']) + b_dc0, b_dc1, c1 = extract_c(x_d, coeffs["p_dc"]) + b_s0, b_s1, c2 = extract_c(x_d, coeffs["p_s0"]) + b_c0, b_c1, c3 = extract_c(x_d, coeffs["a"]) p_dc0 = b_dc0 p_s0 = b_s0 c0 = b_c0 # prepare dict and return - return {'Paco': p_ac_0, 'Pdco': p_dc0, 'Vdco': v_nom, 'Pso': p_s0, - 'C0': c0, 'C1': c1, 'C2': c2, 'C3': c3, 'Pnt': p_nt} + return { + "Paco": p_ac_0, + "Pdco": p_dc0, + "Vdco": v_nom, + "Pso": p_s0, + "C0": c0, + "C1": c1, + "C2": c2, + "C3": c3, + "Pnt": p_nt, + } diff --git a/pvlib/iotools/acis.py b/pvlib/iotools/acis.py index 3be16cfa4c..deb357e072 100644 --- a/pvlib/iotools/acis.py +++ b/pvlib/iotools/acis.py @@ -5,21 +5,20 @@ VARIABLE_MAP = { # time series names - 'pcpn': 'precipitation', - 'maxt': 'temp_air_max', - 'avgt': 'temp_air_average', - 'obst': 'temp_air_observation', - 'mint': 'temp_air_min', - 'cdd': 'cooling_degree_days', - 'hdd': 'heating_degree_days', - 'gdd': 'growing_degree_days', - 'snow': 'snowfall', - 'snwd': 'snowdepth', - + "pcpn": "precipitation", + "maxt": "temp_air_max", + "avgt": "temp_air_average", + "obst": "temp_air_observation", + "mint": "temp_air_min", + "cdd": "cooling_degree_days", + "hdd": "heating_degree_days", + "gdd": "growing_degree_days", + "snow": "snowfall", + "snwd": "snowdepth", # metadata names - 'lat': 'latitude', - 'lon': 'longitude', - 'elev': 'altitude', + "lat": "latitude", + "lon": "longitude", + "elev": "altitude", } @@ -29,39 +28,41 @@ def _get_acis(start, end, params, map_variables, url, **kwargs): """ params = { # use pd.to_datetime so that strings (e.g. '2021-01-01') are accepted - 'sdate': pd.to_datetime(start).strftime('%Y-%m-%d'), - 'edate': pd.to_datetime(end).strftime('%Y-%m-%d'), - 'output': 'json', + "sdate": pd.to_datetime(start).strftime("%Y-%m-%d"), + "edate": pd.to_datetime(end).strftime("%Y-%m-%d"), + "output": "json", **params, # endpoint-specific parameters } - response = requests.post(url, - json=params, - headers={"Content-Type": "application/json"}, - **kwargs) + response = requests.post( + url, + json=params, + headers={"Content-Type": "application/json"}, + **kwargs, + ) response.raise_for_status() payload = response.json() # somewhat inconveniently, the ACIS API tends to return errors as "valid" # responses instead of using proper HTTP error codes: if "error" in payload: - raise requests.HTTPError(payload['error'], response=response) + raise requests.HTTPError(payload["error"], response=response) - columns = ['date'] + [e['name'] for e in params['elems']] - df = pd.DataFrame(payload['data'], columns=columns) - df = df.set_index('date') + columns = ["date"] + [e["name"] for e in params["elems"]] + df = pd.DataFrame(payload["data"], columns=columns) + df = df.set_index("date") df.index = pd.to_datetime(df.index) df.index.name = None - metadata = payload['meta'] + metadata = payload["meta"] try: - # for StnData endpoint, unpack combination "ll" into lat, lon - metadata['lon'], metadata['lat'] = metadata.pop('ll') + # for StnData endpoint, unpack combination "ll" into lat, lon + metadata["lon"], metadata["lat"] = metadata.pop("ll") except KeyError: pass try: - metadata['elev'] = metadata['elev'] * 0.3048 # feet to meters + metadata["elev"] = metadata["elev"] * 0.3048 # feet to meters except KeyError: # some queries don't return elevation pass @@ -76,8 +77,15 @@ def _get_acis(start, end, params, map_variables, url, **kwargs): return df, metadata -def get_acis_prism(latitude, longitude, start, end, map_variables=True, - url="https://data.rcc-acis.org/GridData", **kwargs): +def get_acis_prism( + latitude, + longitude, + start, + end, + map_variables=True, + url="https://data.rcc-acis.org/GridData", + **kwargs, +): """ Retrieve estimated daily precipitation and temperature data from PRISM via the Applied Climate Information System (ACIS). @@ -149,18 +157,26 @@ def get_acis_prism(latitude, longitude, start, end, map_variables=True, {"name": "gdd", "interval": "dly", "units": "degreeC"}, ] params = { - 'loc': f"{longitude},{latitude}", - 'grid': "21", - 'elems': elems, - 'meta': ["ll", "elev"], + "loc": f"{longitude},{latitude}", + "grid": "21", + "elems": elems, + "meta": ["ll", "elev"], } df, meta = _get_acis(start, end, params, map_variables, url, **kwargs) df = df.replace(-999, np.nan) return df, meta -def get_acis_nrcc(latitude, longitude, start, end, grid, map_variables=True, - url="https://data.rcc-acis.org/GridData", **kwargs): +def get_acis_nrcc( + latitude, + longitude, + start, + end, + grid, + map_variables=True, + url="https://data.rcc-acis.org/GridData", + **kwargs, +): """ Retrieve estimated daily precipitation and temperature data from the Northeast Regional Climate Center via the Applied Climate @@ -234,19 +250,25 @@ def get_acis_nrcc(latitude, longitude, start, end, grid, map_variables=True, {"name": "gdd", "interval": "dly", "units": "degreeC"}, ] params = { - 'loc': f"{longitude},{latitude}", - 'grid': grid, - 'elems': elems, - 'meta': ["ll", "elev"], + "loc": f"{longitude},{latitude}", + "grid": grid, + "elems": elems, + "meta": ["ll", "elev"], } df, meta = _get_acis(start, end, params, map_variables, url, **kwargs) df = df.replace(-999, np.nan) return df, meta - -def get_acis_mpe(latitude, longitude, start, end, map_variables=True, - url="https://data.rcc-acis.org/GridData", **kwargs): +def get_acis_mpe( + latitude, + longitude, + start, + end, + map_variables=True, + url="https://data.rcc-acis.org/GridData", + **kwargs, +): """ Retrieve estimated daily Multi-sensor Precipitation Estimates via the Applied Climate Information System (ACIS). @@ -312,19 +334,25 @@ def get_acis_mpe(latitude, longitude, start, end, map_variables=True, {"name": "pcpn", "interval": "dly", "units": "mm"}, ] params = { - 'loc': f"{longitude},{latitude}", - 'grid': "2", - 'elems': elems, - 'meta': ["ll"], # "elev" is not supported for this dataset + "loc": f"{longitude},{latitude}", + "grid": "2", + "elems": elems, + "meta": ["ll"], # "elev" is not supported for this dataset } df, meta = _get_acis(start, end, params, map_variables, url, **kwargs) df = df.replace(-999, np.nan) return df, meta -def get_acis_station_data(station, start, end, trace_val=0.001, - map_variables=True, - url="https://data.rcc-acis.org/StnData", **kwargs): +def get_acis_station_data( + station, + start, + end, + trace_val=0.001, + map_variables=True, + url="https://data.rcc-acis.org/StnData", + **kwargs, +): """ Retrieve weather station climate records via the Applied Climate Information System (ACIS). @@ -407,10 +435,12 @@ def get_acis_station_data(station, start, end, trace_val=0.001, {"name": "gdd", "interval": "dly", "units": "degreeC"}, ] params = { - 'sid': str(station), - 'elems': elems, - 'meta': ('name,state,sids,sid_dates,ll,elev,uid,county,' - 'climdiv,valid_daterange,tzo,network') + "sid": str(station), + "elems": elems, + "meta": ( + "name,state,sids,sid_dates,ll,elev,uid,county," + "climdiv,valid_daterange,tzo,network" + ), } df, metadata = _get_acis(start, end, params, map_variables, url, **kwargs) df = df.replace("M", np.nan) @@ -419,10 +449,14 @@ def get_acis_station_data(station, start, end, trace_val=0.001, return df, metadata -def get_acis_available_stations(latitude_range, longitude_range, - start=None, end=None, - url="https://data.rcc-acis.org/StnMeta", - **kwargs): +def get_acis_available_stations( + latitude_range, + longitude_range, + start=None, + end=None, + url="https://data.rcc-acis.org/StnMeta", + **kwargs, +): """ List weather stations in a given area available from the Applied Climate Information System (ACIS). @@ -453,7 +487,7 @@ def get_acis_available_stations(latitude_range, longitude_range, ------- stations : pandas.DataFrame A dataframe of station metadata, one row per station. - The ``sids`` column contains IDs that can be used with + The ``sids`` column contains IDs that can be used with :py:func:`get_acis_station_data`. Raises @@ -488,29 +522,41 @@ def get_acis_available_stations(latitude_range, longitude_range, ) params = { "bbox": bbox, - "meta": ("name,state,sids,sid_dates,ll,elev," - "uid,county,climdiv,tzo,network"), + "meta": ( + "name,state,sids,sid_dates,ll,elev," + "uid,county,climdiv,tzo,network" + ), } if start is not None and end is not None: - params['elems'] = ['maxt', 'mint', 'avgt', 'obst', - 'pcpn', 'snow', 'snwd'] - params['sdate'] = pd.to_datetime(start).strftime('%Y-%m-%d') - params['edate'] = pd.to_datetime(end).strftime('%Y-%m-%d') - - response = requests.post(url, - json=params, - headers={"Content-Type": "application/json"}, - **kwargs) + params["elems"] = [ + "maxt", + "mint", + "avgt", + "obst", + "pcpn", + "snow", + "snwd", + ] + params["sdate"] = pd.to_datetime(start).strftime("%Y-%m-%d") + params["edate"] = pd.to_datetime(end).strftime("%Y-%m-%d") + + response = requests.post( + url, + json=params, + headers={"Content-Type": "application/json"}, + **kwargs, + ) response.raise_for_status() payload = response.json() if "error" in payload: - raise requests.HTTPError(payload['error'], response=response) + raise requests.HTTPError(payload["error"], response=response) - metadata = payload['meta'] + metadata = payload["meta"] for station_record in metadata: - station_record['altitude'] = station_record.pop('elev') - station_record['longitude'], station_record['latitude'] = \ - station_record.pop('ll') + station_record["altitude"] = station_record.pop("elev") + station_record["longitude"], station_record["latitude"] = ( + station_record.pop("ll") + ) df = pd.DataFrame(metadata) return df diff --git a/pvlib/iotools/bsrn.py b/pvlib/iotools/bsrn.py index 43fdbe919f..47137a3096 100644 --- a/pvlib/iotools/bsrn.py +++ b/pvlib/iotools/bsrn.py @@ -11,44 +11,138 @@ BSRN_FTP_URL = "ftp.bsrn.awi.de" -BSRN_LR0100_COL_SPECS = [(0, 3), (4, 9), (10, 16), (16, 22), (22, 27), - (27, 32), (32, 39), (39, 45), (45, 50), (50, 55), - (55, 64), (64, 70), (70, 75)] - -BSRN_LR0300_COL_SPECS = [(1, 3), (4, 9), (10, 16), (16, 22), (22, 27), - (27, 31), (31, 38), (38, 44), (44, 49), (49, 54), - (54, 61), (61, 67), (67, 72), (72, 78)] - -BSRN_LR0500_COL_SPECS = [(0, 3), (3, 8), (8, 14), (14, 20), (20, 26), (26, 32), - (32, 38), (38, 44), (44, 50), (50, 56), (56, 62), - (62, 68), (68, 74), (74, 80)] - -BSRN_LR0100_COLUMNS = ['day', 'minute', - 'ghi', 'ghi_std', 'ghi_min', 'ghi_max', - 'dni', 'dni_std', 'dni_min', 'dni_max', - 'empty', 'empty', 'empty', 'empty', 'empty', - 'dhi', 'dhi_std', 'dhi_min', 'dhi_max', - 'lwd', 'lwd_std', 'lwd_min', 'lwd_max', - 'temp_air', 'relative_humidity', 'pressure'] - -BSRN_LR0300_COLUMNS = ['day', 'minute', 'gri', 'gri_std', 'gri_min', 'gri_max', - 'lwu', 'lwu_std', 'lwu_min', 'lwu_max', 'net_radiation', - 'net_radiation_std', 'net_radiation_min', - 'net_radiation_max'] - -BSRN_LR0500_COLUMNS = ['day', 'minute', 'uva_global', 'uva_global_std', - 'uva_global_min', 'uva_global_max', 'uvb_direct', - 'uvb_direct_std', 'uvb_direct_min', 'uvb_direct_max', - 'empty', 'empty', 'empty', 'empty', - 'uvb_global', 'uvb_global_std', 'uvb_global_min', - 'uvb_global_max', 'uvb_diffuse', 'uvb_diffuse_std', - 'uvb_diffuse', 'uvb_diffuse_std', - 'uvb_diffuse_min', 'uvb_diffuse_max', - 'uvb_reflected', 'uvb_reflected_std', - 'uvb_reflected_min', 'uvb_reflected_max'] - -BSRN_COLUMNS = {'0100': BSRN_LR0100_COLUMNS, '0300': BSRN_LR0300_COLUMNS, - '0500': BSRN_LR0500_COLUMNS} +BSRN_LR0100_COL_SPECS = [ + (0, 3), + (4, 9), + (10, 16), + (16, 22), + (22, 27), + (27, 32), + (32, 39), + (39, 45), + (45, 50), + (50, 55), + (55, 64), + (64, 70), + (70, 75), +] + +BSRN_LR0300_COL_SPECS = [ + (1, 3), + (4, 9), + (10, 16), + (16, 22), + (22, 27), + (27, 31), + (31, 38), + (38, 44), + (44, 49), + (49, 54), + (54, 61), + (61, 67), + (67, 72), + (72, 78), +] + +BSRN_LR0500_COL_SPECS = [ + (0, 3), + (3, 8), + (8, 14), + (14, 20), + (20, 26), + (26, 32), + (32, 38), + (38, 44), + (44, 50), + (50, 56), + (56, 62), + (62, 68), + (68, 74), + (74, 80), +] + +BSRN_LR0100_COLUMNS = [ + "day", + "minute", + "ghi", + "ghi_std", + "ghi_min", + "ghi_max", + "dni", + "dni_std", + "dni_min", + "dni_max", + "empty", + "empty", + "empty", + "empty", + "empty", + "dhi", + "dhi_std", + "dhi_min", + "dhi_max", + "lwd", + "lwd_std", + "lwd_min", + "lwd_max", + "temp_air", + "relative_humidity", + "pressure", +] + +BSRN_LR0300_COLUMNS = [ + "day", + "minute", + "gri", + "gri_std", + "gri_min", + "gri_max", + "lwu", + "lwu_std", + "lwu_min", + "lwu_max", + "net_radiation", + "net_radiation_std", + "net_radiation_min", + "net_radiation_max", +] + +BSRN_LR0500_COLUMNS = [ + "day", + "minute", + "uva_global", + "uva_global_std", + "uva_global_min", + "uva_global_max", + "uvb_direct", + "uvb_direct_std", + "uvb_direct_min", + "uvb_direct_max", + "empty", + "empty", + "empty", + "empty", + "uvb_global", + "uvb_global_std", + "uvb_global_min", + "uvb_global_max", + "uvb_diffuse", + "uvb_diffuse_std", + "uvb_diffuse", + "uvb_diffuse_std", + "uvb_diffuse_min", + "uvb_diffuse_max", + "uvb_reflected", + "uvb_reflected_std", + "uvb_reflected_min", + "uvb_reflected_max", +] + +BSRN_COLUMNS = { + "0100": BSRN_LR0100_COLUMNS, + "0300": BSRN_LR0300_COLUMNS, + "0500": BSRN_LR0500_COLUMNS, +} def _empty_dataframe_from_logical_records(logical_records): @@ -57,12 +151,19 @@ def _empty_dataframe_from_logical_records(logical_records): columns = [] for lr in logical_records: columns += BSRN_COLUMNS[lr][2:] - columns = [c for c in columns if c != 'empty'] + columns = [c for c in columns if c != "empty"] return pd.DataFrame(columns=columns) -def get_bsrn(station, start, end, username, password, - logical_records=('0100',), save_path=None): +def get_bsrn( + station, + start, + end, + username, + password, + logical_records=("0100",), + save_path=None, +): """ Retrieve ground measured irradiance data from the BSRN FTP server. @@ -159,43 +260,50 @@ def get_bsrn(station, start, end, username, password, end = pd.to_datetime(end) # Generate list files to download based on start/end (SSSMMYY.dat.gz) - filenames = pd.date_range( - start, end.replace(day=1) + pd.DateOffset(months=1), freq='1M')\ - .strftime(f"{station}%m%y.dat.gz").tolist() + filenames = ( + pd.date_range( + start, end.replace(day=1) + pd.DateOffset(months=1), freq="1M" + ) + .strftime(f"{station}%m%y.dat.gz") + .tolist() + ) # Create FTP connection with ftplib.FTP(BSRN_FTP_URL, username, password) as ftp: # Change to station sub-directory (checks that the station exists) try: - ftp.cwd(f'/{station}') + ftp.cwd(f"/{station}") except ftplib.error_perm as e: - raise KeyError('Station sub-directory does not exist. Specified ' - 'station is probably not a proper three letter ' - 'station abbreviation.') from e + raise KeyError( + "Station sub-directory does not exist. Specified " + "station is probably not a proper three letter " + "station abbreviation." + ) from e dfs = [] # Initialize list for monthly dataframes non_existing_files = [] # Initilize list of files that were not found for filename in filenames: try: bio = io.BytesIO() # Initialize BytesIO object # Retrieve binary file from server and write to BytesIO object - response = ftp.retrbinary(f'RETR {filename}', bio.write) + response = ftp.retrbinary(f"RETR {filename}", bio.write) # Check that transfer was successfull - if not response.startswith('226 Transfer complete'): + if not response.startswith("226 Transfer complete"): raise ftplib.Error(response) # Save file locally if save_path is specified if save_path is not None: # Create local file - with open(os.path.join(save_path, filename), 'wb') as f: + with open(os.path.join(save_path, filename), "wb") as f: f.write(bio.getbuffer()) # Write local file # Open gzip file and convert to StringIO bio.seek(0) # reset buffer to start of file - gzip_file = io.TextIOWrapper(gzip.GzipFile(fileobj=bio), - encoding='latin1') + gzip_file = io.TextIOWrapper( + gzip.GzipFile(fileobj=bio), encoding="latin1" + ) dfi, metadata = parse_bsrn(gzip_file, logical_records) dfs.append(dfi) # FTP client raises an error if the file does not exist on server except ftplib.error_perm as e: - if str(e) == '550 Failed to open file.': + if str(e) == "550 Failed to open file.": non_existing_files.append(filename) else: raise ftplib.error_perm(e) @@ -203,13 +311,15 @@ def get_bsrn(station, start, end, username, password, # Raise user warnings if not dfs: # If no files were found - warnings.warn('No files were available for the specified timeframe.') + warnings.warn("No files were available for the specified timeframe.") elif non_existing_files: # If only some files were missing - warnings.warn(f'The following files were not found: {non_existing_files}') # noqa: E501 + warnings.warn( + f"The following files were not found: {non_existing_files}" + ) # noqa: E501 # Concatenate monthly dataframes to one dataframe if len(dfs): - data = pd.concat(dfs, axis='rows') + data = pd.concat(dfs, axis="rows") else: # Return empty dataframe data = _empty_dataframe_from_logical_records(logical_records) metadata = {} @@ -217,7 +327,7 @@ def get_bsrn(station, start, end, username, password, return data, metadata -def parse_bsrn(fbuf, logical_records=('0100',)): +def parse_bsrn(fbuf, logical_records=("0100",)): """ Parse a file-like buffer of a BSRN station-to-archive file. @@ -246,46 +356,51 @@ def parse_bsrn(fbuf, logical_records=('0100',)): # Parse metadata fbuf.readline() # first line should be *U0001, so read it and discard date_line = fbuf.readline() # second line contains important metadata - start_date = pd.Timestamp(year=int(date_line[7:11]), - month=int(date_line[3:6]), day=1, - tz='UTC') # BSRN timestamps are UTC + start_date = pd.Timestamp( + year=int(date_line[7:11]), month=int(date_line[3:6]), day=1, tz="UTC" + ) # BSRN timestamps are UTC metadata = {} # Initilize dictionary containing metadata - metadata['start date'] = start_date - metadata['station identification number'] = int(date_line[:3]) - metadata['version of data'] = int(date_line.split()[-1]) + metadata["start date"] = start_date + metadata["station identification number"] = int(date_line[:3]) + metadata["version of data"] = int(date_line.split()[-1]) for line in fbuf: - if line[2:6] == '0004': # stop once LR0004 has been reached + if line[2:6] == "0004": # stop once LR0004 has been reached break - elif line == '': - raise ValueError('Mandatory record LR0004 not found.') - metadata['date when station description changed'] = fbuf.readline().strip() - metadata['surface type'] = int(fbuf.readline(3)) - metadata['topography type'] = int(fbuf.readline()) - metadata['address'] = fbuf.readline().strip() - metadata['telephone no. of station'] = fbuf.readline(20).strip() - metadata['FAX no. of station'] = fbuf.readline().strip() - metadata['TCP/IP no. of station'] = fbuf.readline(15).strip() - metadata['e-mail address of station'] = fbuf.readline().strip() - metadata['latitude_bsrn'] = float(fbuf.readline(8)) # BSRN convention - metadata['latitude'] = metadata['latitude_bsrn'] - 90 # ISO 19115 - metadata['longitude_bsrn'] = float(fbuf.readline(8)) # BSRN convention - metadata['longitude'] = metadata['longitude_bsrn'] - 180 # ISO 19115 - metadata['altitude'] = int(fbuf.readline(5)) + elif line == "": + raise ValueError("Mandatory record LR0004 not found.") + metadata["date when station description changed"] = fbuf.readline().strip() + metadata["surface type"] = int(fbuf.readline(3)) + metadata["topography type"] = int(fbuf.readline()) + metadata["address"] = fbuf.readline().strip() + metadata["telephone no. of station"] = fbuf.readline(20).strip() + metadata["FAX no. of station"] = fbuf.readline().strip() + metadata["TCP/IP no. of station"] = fbuf.readline(15).strip() + metadata["e-mail address of station"] = fbuf.readline().strip() + metadata["latitude_bsrn"] = float(fbuf.readline(8)) # BSRN convention + metadata["latitude"] = metadata["latitude_bsrn"] - 90 # ISO 19115 + metadata["longitude_bsrn"] = float(fbuf.readline(8)) # BSRN convention + metadata["longitude"] = metadata["longitude_bsrn"] - 180 # ISO 19115 + metadata["altitude"] = int(fbuf.readline(5)) metadata['identification of "SYNOP" station'] = fbuf.readline().strip() - metadata['date when horizon changed'] = fbuf.readline().strip() + metadata["date when horizon changed"] = fbuf.readline().strip() # Pass last section of LR0004 containing the horizon elevation data horizon = [] # list for raw horizon elevation data while True: line = fbuf.readline() - if ('*' in line) | (line == ''): + if ("*" in line) | (line == ""): break else: horizon += [int(i) for i in line.split()] - horizon = pd.Series(horizon[1::2], horizon[::2], name='horizon_elevation', - dtype=int).drop(-1, errors='ignore').sort_index() - horizon.index.name = 'azimuth' - metadata['horizon'] = horizon + horizon = ( + pd.Series( + horizon[1::2], horizon[::2], name="horizon_elevation", dtype=int + ) + .drop(-1, errors="ignore") + .sort_index() + ) + horizon.index.name = "azimuth" + metadata["horizon"] = horizon # Read file and store the starting line number and number of lines for # each logical record (LR) @@ -293,7 +408,7 @@ def parse_bsrn(fbuf, logical_records=('0100',)): lr_startrow = {} # Dictionary of starting line number for each LR lr_nrows = {} # Dictionary of end line number for each LR for num, line in enumerate(fbuf): - if line.startswith('*'): # Find start of all logical records + if line.startswith("*"): # Find start of all logical records if len(lr_startrow) >= 1: lr_nrows[lr] = num - lr_startrow[lr] - 1 # noqa: F821 lr = line[2:6] # string of 4 digit LR number @@ -301,70 +416,93 @@ def parse_bsrn(fbuf, logical_records=('0100',)): lr_nrows[lr] = num - lr_startrow[lr] for lr in logical_records: - if lr not in ['0100', '0300', '0500']: - raise ValueError(f"Logical record {lr} not in " - "['0100', '0300','0500'].") + if lr not in ["0100", "0300", "0500"]: + raise ValueError( + f"Logical record {lr} not in " "['0100', '0300','0500']." + ) dfs = [] # Initialize empty list for dataframe # Parse LR0100 - basic measurements including GHI, DNI, DHI and temperature - if ('0100' in lr_startrow.keys()) & ('0100' in logical_records): + if ("0100" in lr_startrow.keys()) & ("0100" in logical_records): fbuf.seek(0) # reset buffer to start of file - LR_0100 = pd.read_fwf(fbuf, skiprows=lr_startrow['0100'] + 1, - nrows=lr_nrows['0100'], header=None, - colspecs=BSRN_LR0100_COL_SPECS, - na_values=[-999.0, -99.9]) + LR_0100 = pd.read_fwf( + fbuf, + skiprows=lr_startrow["0100"] + 1, + nrows=lr_nrows["0100"], + header=None, + colspecs=BSRN_LR0100_COL_SPECS, + na_values=[-999.0, -99.9], + ) # Create multi-index and unstack, resulting in 1 col for each variable LR_0100 = LR_0100.set_index([LR_0100.index // 2, LR_0100.index % 2]) - LR_0100 = LR_0100.unstack(level=1).swaplevel(i=0, j=1, axis='columns') + LR_0100 = LR_0100.unstack(level=1).swaplevel(i=0, j=1, axis="columns") # Sort columns to match original order and assign column names - LR_0100 = LR_0100.reindex(sorted(LR_0100.columns), axis='columns') + LR_0100 = LR_0100.reindex(sorted(LR_0100.columns), axis="columns") LR_0100.columns = BSRN_LR0100_COLUMNS # Set datetime index - LR_0100.index = (start_date+pd.to_timedelta(LR_0100['day']-1, unit='d') - + pd.to_timedelta(LR_0100['minute'], unit='minutes')) + LR_0100.index = ( + start_date + + pd.to_timedelta(LR_0100["day"] - 1, unit="d") + + pd.to_timedelta(LR_0100["minute"], unit="minutes") + ) # Drop empty, minute, and day columns - LR_0100 = LR_0100.drop(columns=['empty', 'day', 'minute']) + LR_0100 = LR_0100.drop(columns=["empty", "day", "minute"]) dfs.append(LR_0100) # Parse LR0300 - other time series data, including upward and net radiation - if ('0300' in lr_startrow.keys()) & ('0300' in logical_records): + if ("0300" in lr_startrow.keys()) & ("0300" in logical_records): fbuf.seek(0) # reset buffer to start of file - LR_0300 = pd.read_fwf(fbuf, skiprows=lr_startrow['0300']+1, - nrows=lr_nrows['0300'], header=None, - na_values=[-999.0, -99.9], - colspecs=BSRN_LR0300_COL_SPECS, - names=BSRN_LR0300_COLUMNS) - LR_0300.index = (start_date+pd.to_timedelta(LR_0300['day']-1, unit='d') - + pd.to_timedelta(LR_0300['minute'], unit='minutes')) - LR_0300 = LR_0300.drop(columns=['day', 'minute']).astype(float) + LR_0300 = pd.read_fwf( + fbuf, + skiprows=lr_startrow["0300"] + 1, + nrows=lr_nrows["0300"], + header=None, + na_values=[-999.0, -99.9], + colspecs=BSRN_LR0300_COL_SPECS, + names=BSRN_LR0300_COLUMNS, + ) + LR_0300.index = ( + start_date + + pd.to_timedelta(LR_0300["day"] - 1, unit="d") + + pd.to_timedelta(LR_0300["minute"], unit="minutes") + ) + LR_0300 = LR_0300.drop(columns=["day", "minute"]).astype(float) dfs.append(LR_0300) # Parse LR0500 - UV measurements - if ('0500' in lr_startrow.keys()) & ('0500' in logical_records): + if ("0500" in lr_startrow.keys()) & ("0500" in logical_records): fbuf.seek(0) # reset buffer to start of file - LR_0500 = pd.read_fwf(fbuf, skiprows=lr_startrow['0500']+1, - nrows=lr_nrows['0500'], na_values=[-99.9], - header=None, colspecs=BSRN_LR0500_COL_SPECS) + LR_0500 = pd.read_fwf( + fbuf, + skiprows=lr_startrow["0500"] + 1, + nrows=lr_nrows["0500"], + na_values=[-99.9], + header=None, + colspecs=BSRN_LR0500_COL_SPECS, + ) # Create multi-index and unstack, resulting in 1 col for each variable LR_0500 = LR_0500.set_index([LR_0500.index // 2, LR_0500.index % 2]) - LR_0500 = LR_0500.unstack(level=1).swaplevel(i=0, j=1, axis='columns') + LR_0500 = LR_0500.unstack(level=1).swaplevel(i=0, j=1, axis="columns") # Sort columns to match original order and assign column names - LR_0500 = LR_0500.reindex(sorted(LR_0500.columns), axis='columns') + LR_0500 = LR_0500.reindex(sorted(LR_0500.columns), axis="columns") LR_0500.columns = BSRN_LR0500_COLUMNS - LR_0500.index = (start_date+pd.to_timedelta(LR_0500['day']-1, unit='d') - + pd.to_timedelta(LR_0500['minute'], unit='minutes')) - LR_0500 = LR_0500.drop(columns=['empty', 'day', 'minute']) + LR_0500.index = ( + start_date + + pd.to_timedelta(LR_0500["day"] - 1, unit="d") + + pd.to_timedelta(LR_0500["minute"], unit="minutes") + ) + LR_0500 = LR_0500.drop(columns=["empty", "day", "minute"]) dfs.append(LR_0500) if len(dfs): - data = pd.concat(dfs, axis='columns') + data = pd.concat(dfs, axis="columns") else: data = _empty_dataframe_from_logical_records(logical_records) metadata = {} return data, metadata -def read_bsrn(filename, logical_records=('0100',)): +def read_bsrn(filename, logical_records=("0100",)): """ Read a BSRN station-to-archive file into a DataFrame. @@ -454,10 +592,10 @@ def read_bsrn(filename, logical_records=('0100',)): .. [4] `BSRN Data Release Guidelines `_ """ # noqa: E501 - if str(filename).endswith('.gz'): # check if file is a gzipped (.gz) file - open_func, mode = gzip.open, 'rt' + if str(filename).endswith(".gz"): # check if file is a gzipped (.gz) file + open_func, mode = gzip.open, "rt" else: - open_func, mode = open, 'r' + open_func, mode = open, "r" with open_func(filename, mode) as f: content = parse_bsrn(f, logical_records) return content diff --git a/pvlib/iotools/crn.py b/pvlib/iotools/crn.py index 90e1d6d6b4..754a9af667 100644 --- a/pvlib/iotools/crn.py +++ b/pvlib/iotools/crn.py @@ -1,43 +1,62 @@ -"""Functions to read data from the US Climate Reference Network (CRN). -""" +"""Functions to read data from the US Climate Reference Network (CRN).""" import pandas as pd import numpy as np HEADERS = [ - 'WBANNO', 'UTC_DATE', 'UTC_TIME', 'LST_DATE', 'LST_TIME', 'CRX_VN', - 'LONGITUDE', 'LATITUDE', 'AIR_TEMPERATURE', 'PRECIPITATION', - 'SOLAR_RADIATION', 'SR_FLAG', 'SURFACE_TEMPERATURE', 'ST_TYPE', 'ST_FLAG', - 'RELATIVE_HUMIDITY', 'RH_FLAG', 'SOIL_MOISTURE_5', 'SOIL_TEMPERATURE_5', - 'WETNESS', 'WET_FLAG', 'WIND_1_5', 'WIND_FLAG'] + "WBANNO", + "UTC_DATE", + "UTC_TIME", + "LST_DATE", + "LST_TIME", + "CRX_VN", + "LONGITUDE", + "LATITUDE", + "AIR_TEMPERATURE", + "PRECIPITATION", + "SOLAR_RADIATION", + "SR_FLAG", + "SURFACE_TEMPERATURE", + "ST_TYPE", + "ST_FLAG", + "RELATIVE_HUMIDITY", + "RH_FLAG", + "SOIL_MOISTURE_5", + "SOIL_TEMPERATURE_5", + "WETNESS", + "WET_FLAG", + "WIND_1_5", + "WIND_FLAG", +] VARIABLE_MAP = { - 'LONGITUDE': 'longitude', - 'LATITUDE': 'latitude', - 'AIR_TEMPERATURE': 'temp_air', - 'SOLAR_RADIATION': 'ghi', - 'SR_FLAG': 'ghi_flag', - 'RELATIVE_HUMIDITY': 'relative_humidity', - 'RH_FLAG': 'relative_humidity_flag', - 'WIND_1_5': 'wind_speed', - 'WIND_FLAG': 'wind_speed_flag' + "LONGITUDE": "longitude", + "LATITUDE": "latitude", + "AIR_TEMPERATURE": "temp_air", + "SOLAR_RADIATION": "ghi", + "SR_FLAG": "ghi_flag", + "RELATIVE_HUMIDITY": "relative_humidity", + "RH_FLAG": "relative_humidity_flag", + "WIND_1_5": "wind_speed", + "WIND_FLAG": "wind_speed_flag", } NAN_DICT = { - 'CRX_VN': -99999, - 'AIR_TEMPERATURE': -9999, - 'PRECIPITATION': -9999, - 'SOLAR_RADIATION': -99999, - 'SURFACE_TEMPERATURE': -9999, - 'RELATIVE_HUMIDITY': -9999, - 'SOIL_MOISTURE_5': -99, - 'SOIL_TEMPERATURE_5': -9999, - 'WETNESS': -9999, - 'WIND_1_5': -99} + "CRX_VN": -99999, + "AIR_TEMPERATURE": -9999, + "PRECIPITATION": -9999, + "SOLAR_RADIATION": -99999, + "SURFACE_TEMPERATURE": -9999, + "RELATIVE_HUMIDITY": -9999, + "SOIL_MOISTURE_5": -99, + "SOIL_TEMPERATURE_5": -9999, + "WETNESS": -9999, + "WIND_1_5": -99, +} # Add NUL characters to possible NaN values for all columns -NAN_DICT = {k: [v, '\x00\x00\x00\x00\x00\x00'] for k, v in NAN_DICT.items()} +NAN_DICT = {k: [v, "\x00\x00\x00\x00\x00\x00"] for k, v in NAN_DICT.items()} # as specified in CRN README.txt file. excludes 1 space between columns WIDTHS = [5, 8, 4, 8, 4, 6, 7, 7, 7, 7, 6, 1, 7, 1, 1, 5, 1, 7, 7, 5, 1, 6, 1] @@ -48,10 +67,29 @@ # specify dtypes for potentially problematic values DTYPES = [ - 'int64', 'int64', 'int64', 'int64', 'int64', 'str', 'float64', 'float64', - 'float64', 'float64', 'float64', 'int64', 'float64', 'O', 'int64', - 'float64', 'int64', 'float64', 'float64', 'int64', 'int64', 'float64', - 'int64' + "int64", + "int64", + "int64", + "int64", + "int64", + "str", + "float64", + "float64", + "float64", + "float64", + "float64", + "int64", + "float64", + "O", + "int64", + "float64", + "int64", + "float64", + "float64", + "int64", + "int64", + "float64", + "int64", ] @@ -113,11 +151,12 @@ def read_crn(filename, map_variables=True): # when our minimum pandas >= 1.2.0 (skip_blank_lines bug for <1.2.0). # As a workaround, parse all values as strings, then drop NaN, then cast # to the appropriate dtypes, and mask "sentinal" NaN (e.g. -9999.0) - data = pd.read_fwf(filename, header=None, names=HEADERS, widths=WIDTHS, - dtype=str) + data = pd.read_fwf( + filename, header=None, names=HEADERS, widths=WIDTHS, dtype=str + ) # drop empty (bad) lines - data = data.dropna(axis=0, how='all') + data = data.dropna(axis=0, how="all") # can't set dtypes in read_fwf because int cols can't contain NaN, so # do it here instead @@ -129,9 +168,12 @@ def read_crn(filename, map_variables=True): # set index # UTC_TIME does not have leading 0s, so must zfill(4) to comply # with %H%M format - dts = data[['UTC_DATE', 'UTC_TIME']].astype(str) - dtindex = pd.to_datetime(dts['UTC_DATE'] + dts['UTC_TIME'].str.zfill(4), - format='%Y%m%d%H%M', utc=True) + dts = data[["UTC_DATE", "UTC_TIME"]].astype(str) + dtindex = pd.to_datetime( + dts["UTC_DATE"] + dts["UTC_TIME"].str.zfill(4), + format="%Y%m%d%H%M", + utc=True, + ) data = data.set_index(dtindex) if map_variables: diff --git a/pvlib/iotools/epw.py b/pvlib/iotools/epw.py index a777b69911..e1251d8dfe 100644 --- a/pvlib/iotools/epw.py +++ b/pvlib/iotools/epw.py @@ -8,7 +8,7 @@ def read_epw(filename, coerce_year=None): - r''' + r""" Read an EPW file in to a pandas dataframe. Note that values contained in the metadata dictionary are unchanged @@ -215,25 +215,30 @@ def read_epw(filename, coerce_year=None): .. [1] `EnergyPlus documentation, Auxiliary Programs `_ - ''' + """ - if str(filename).startswith('http'): + if str(filename).startswith("http"): # Attempts to download online EPW file # See comments above for possible online sources - request = Request(filename, headers={'User-Agent': ( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 ' - 'Safari/537.36')}) + request = Request( + filename, + headers={ + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 " + "Safari/537.36" + ) + }, + ) response = urlopen(request) - with io.StringIO(response.read().decode(errors='ignore')) as csvdata: + with io.StringIO(response.read().decode(errors="ignore")) as csvdata: data, meta = parse_epw(csvdata, coerce_year) else: # Assume it's accessible via the file system - with open(str(filename), 'r') as csvdata: + with open(str(filename), "r") as csvdata: data, meta = parse_epw(csvdata, coerce_year) - return data, meta @@ -272,26 +277,62 @@ def parse_epw(csvdata, coerce_year=None): # Read line with metadata firstline = csvdata.readline() - head = ['loc', 'city', 'state-prov', 'country', 'data_type', 'WMO_code', - 'latitude', 'longitude', 'TZ', 'altitude'] - meta = dict(zip(head, firstline.rstrip('\n').split(","))) - - meta['altitude'] = float(meta['altitude']) - meta['latitude'] = float(meta['latitude']) - meta['longitude'] = float(meta['longitude']) - meta['TZ'] = float(meta['TZ']) - - colnames = ['year', 'month', 'day', 'hour', 'minute', 'data_source_unct', - 'temp_air', 'temp_dew', 'relative_humidity', - 'atmospheric_pressure', 'etr', 'etrn', 'ghi_infrared', 'ghi', - 'dni', 'dhi', 'global_hor_illum', 'direct_normal_illum', - 'diffuse_horizontal_illum', 'zenith_luminance', - 'wind_direction', 'wind_speed', 'total_sky_cover', - 'opaque_sky_cover', 'visibility', 'ceiling_height', - 'present_weather_observation', 'present_weather_codes', - 'precipitable_water', 'aerosol_optical_depth', 'snow_depth', - 'days_since_last_snowfall', 'albedo', - 'liquid_precipitation_depth', 'liquid_precipitation_quantity'] + head = [ + "loc", + "city", + "state-prov", + "country", + "data_type", + "WMO_code", + "latitude", + "longitude", + "TZ", + "altitude", + ] + meta = dict(zip(head, firstline.rstrip("\n").split(","))) + + meta["altitude"] = float(meta["altitude"]) + meta["latitude"] = float(meta["latitude"]) + meta["longitude"] = float(meta["longitude"]) + meta["TZ"] = float(meta["TZ"]) + + colnames = [ + "year", + "month", + "day", + "hour", + "minute", + "data_source_unct", + "temp_air", + "temp_dew", + "relative_humidity", + "atmospheric_pressure", + "etr", + "etrn", + "ghi_infrared", + "ghi", + "dni", + "dhi", + "global_hor_illum", + "direct_normal_illum", + "diffuse_horizontal_illum", + "zenith_luminance", + "wind_direction", + "wind_speed", + "total_sky_cover", + "opaque_sky_cover", + "visibility", + "ceiling_height", + "present_weather_observation", + "present_weather_codes", + "precipitable_water", + "aerosol_optical_depth", + "snow_depth", + "days_since_last_snowfall", + "albedo", + "liquid_precipitation_depth", + "liquid_precipitation_quantity", + ] # We only have to skip 6 rows instead of 7 because we have already used # the realine call above. @@ -302,11 +343,11 @@ def parse_epw(csvdata, coerce_year=None): data["year"] = coerce_year # create index that supplies correct date and time zone information - dts = data[['month', 'day']].astype(str).apply(lambda x: x.str.zfill(2)) - hrs = (data['hour'] - 1).astype(str).str.zfill(2) - dtscat = data['year'].astype(str) + dts['month'] + dts['day'] + hrs - idx = pd.to_datetime(dtscat, format='%Y%m%d%H') - idx = idx.dt.tz_localize(int(meta['TZ'] * 3600)) + dts = data[["month", "day"]].astype(str).apply(lambda x: x.str.zfill(2)) + hrs = (data["hour"] - 1).astype(str).str.zfill(2) + dtscat = data["year"].astype(str) + dts["month"] + dts["day"] + hrs + idx = pd.to_datetime(dtscat, format="%Y%m%d%H") + idx = idx.dt.tz_localize(int(meta["TZ"] * 3600)) data.index = idx return data, meta diff --git a/pvlib/iotools/midc.py b/pvlib/iotools/midc.py index c0dfd370eb..2d1f05501c 100644 --- a/pvlib/iotools/midc.py +++ b/pvlib/iotools/midc.py @@ -1,5 +1,5 @@ -"""Functions to read NREL MIDC data. -""" +"""Functions to read NREL MIDC data.""" + import io @@ -18,92 +18,103 @@ # https://midcdmz.nrel.gov/apps/daily.pl?site=&live=1 # Where id is the key found in this dictionary MIDC_VARIABLE_MAP = { - 'BMS': { - 'Global CMP22 (vent/cor) [W/m^2]': 'ghi', - 'Direct CHP1-1 [W/m^2]': 'dni_chp1', + "BMS": { + "Global CMP22 (vent/cor) [W/m^2]": "ghi", + "Direct CHP1-1 [W/m^2]": "dni_chp1", # NIP was mapped to dni for pvlib<=0.10.5 - 'Direct NIP [W/m^2]': 'dni_nip', - 'Diffuse CM22-1 (vent/cor) [W/m^2]': 'dhi', - 'Avg Wind Speed @ 6ft [m/s]': 'wind_speed', - 'Tower Dry Bulb Temp [deg C]': 'temp_air', - 'Tower RH [%]': 'relative_humidity'}, - 'UOSMRL': { - 'Global CMP22 [W/m^2]': 'ghi', - 'Direct CHP1 [W/m^2]': 'dni_chp1', - 'Diffuse [W/m^2]': 'dhi', + "Direct NIP [W/m^2]": "dni_nip", + "Diffuse CM22-1 (vent/cor) [W/m^2]": "dhi", + "Avg Wind Speed @ 6ft [m/s]": "wind_speed", + "Tower Dry Bulb Temp [deg C]": "temp_air", + "Tower RH [%]": "relative_humidity", + }, + "UOSMRL": { + "Global CMP22 [W/m^2]": "ghi", + "Direct CHP1 [W/m^2]": "dni_chp1", + "Diffuse [W/m^2]": "dhi", # NIP was mapped to dni for pvlib<=0.10.5 - 'Direct NIP [W/m^2]': 'dni_nip', + "Direct NIP [W/m^2]": "dni_nip", # Schenk was mapped to dhi for pvlib<=0.10.5 # 'Diffuse Schenk [W/m^2]': 'dhi', - 'Air Temperature [deg C]': 'temp_air', - 'Relative Humidity [%]': 'relative_humidity', - 'Avg Wind Speed @ 10m [m/s]': 'wind_speed'}, - 'HSU': { - 'Global Horiz [W/m^2]': 'ghi', - 'Direct Normal (calc) [W/m^2]': 'dni', - 'Diffuse Horiz (band_corr) [W/m^2]': 'dhi'}, - 'UTPASRL': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horizontal [W/m^2]': 'dhi', - 'CHP1 Temp [deg C]': 'temp_air'}, - 'UAT': { - 'Global Horiz (platform) [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horiz [W/m^2]': 'dhi', - 'Air Temperature [deg C]': 'temp_air', - 'Rel Humidity [%]': 'relative_humidity', - 'Avg Wind Speed @ 3m [m/s]': 'wind_speed'}, - 'STAC': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horizontal [W/m^2]': 'dhi', - 'Avg Wind Speed @ 10m [m/s]': 'wind_speed', - 'Air Temperature [deg C]': 'temp_air', - 'Rel Humidity [%]': 'relative_humidity'}, - 'UNLV': { - 'Global Horiz [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horiz (calc) [W/m^2]': 'dhi', - 'Dry Bulb Temp [deg C]': 'temp_air', - 'Avg Wind Speed @ 30ft [m/s]': 'wind_speed'}, - 'ORNL': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horizontal [W/m^2]': 'dhi', - 'Air Temperature [deg C]': 'temp_air', - 'Rel Humidity [%]': 'relative_humidity', - 'Avg Wind Speed @ 42ft [m/s]': 'wind_speed'}, - 'NELHA': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Air Temperature [W/m^2]': 'temp_air', - 'Avg Wind Speed @ 10m [m/s]': 'wind_speed', - 'Rel Humidity [%]': 'relative_humidity'}, - 'ULL': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horizontal [W/m^2]': 'dhi', - 'Air Temperature [deg C]': 'temp_air', - 'Rel Humidity [%]': 'relative_humidity', - 'Avg Wind Speed @ 3m [m/s]': 'wind_speed'}, - 'NWTC': { - 'Global Horizontal [W/m^2]': 'ghi', - 'Direct Normal [W/m^2]': 'dni', - 'Diffuse Horizontal [W/m^2]': 'dhi', + "Air Temperature [deg C]": "temp_air", + "Relative Humidity [%]": "relative_humidity", + "Avg Wind Speed @ 10m [m/s]": "wind_speed", + }, + "HSU": { + "Global Horiz [W/m^2]": "ghi", + "Direct Normal (calc) [W/m^2]": "dni", + "Diffuse Horiz (band_corr) [W/m^2]": "dhi", + }, + "UTPASRL": { + "Global Horizontal [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horizontal [W/m^2]": "dhi", + "CHP1 Temp [deg C]": "temp_air", + }, + "UAT": { + "Global Horiz (platform) [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horiz [W/m^2]": "dhi", + "Air Temperature [deg C]": "temp_air", + "Rel Humidity [%]": "relative_humidity", + "Avg Wind Speed @ 3m [m/s]": "wind_speed", + }, + "STAC": { + "Global Horizontal [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horizontal [W/m^2]": "dhi", + "Avg Wind Speed @ 10m [m/s]": "wind_speed", + "Air Temperature [deg C]": "temp_air", + "Rel Humidity [%]": "relative_humidity", + }, + "UNLV": { + "Global Horiz [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horiz (calc) [W/m^2]": "dhi", + "Dry Bulb Temp [deg C]": "temp_air", + "Avg Wind Speed @ 30ft [m/s]": "wind_speed", + }, + "ORNL": { + "Global Horizontal [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horizontal [W/m^2]": "dhi", + "Air Temperature [deg C]": "temp_air", + "Rel Humidity [%]": "relative_humidity", + "Avg Wind Speed @ 42ft [m/s]": "wind_speed", + }, + "NELHA": { + "Global Horizontal [W/m^2]": "ghi", + "Air Temperature [W/m^2]": "temp_air", + "Avg Wind Speed @ 10m [m/s]": "wind_speed", + "Rel Humidity [%]": "relative_humidity", + }, + "ULL": { + "Global Horizontal [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horizontal [W/m^2]": "dhi", + "Air Temperature [deg C]": "temp_air", + "Rel Humidity [%]": "relative_humidity", + "Avg Wind Speed @ 3m [m/s]": "wind_speed", + }, + "NWTC": { + "Global Horizontal [W/m^2]": "ghi", + "Direct Normal [W/m^2]": "dni", + "Diffuse Horizontal [W/m^2]": "dhi", # PSP instrument was removed Feb. 2021 # PSP was mapped to ghi for pvlib<=0.10.5 # 'Global PSP [W/m^2]': 'ghi', - 'Temperature @ 2m [deg C]': 'temp_air', - 'Avg Wind Speed @ 2m [m/s]': 'wind_speed', - 'Relative Humidity [%]': 'relative_humidity'}, + "Temperature @ 2m [deg C]": "temp_air", + "Avg Wind Speed @ 2m [m/s]": "wind_speed", + "Relative Humidity [%]": "relative_humidity", + }, } # Maps problematic timezones to 'Etc/GMT' for parsing. TZ_MAP = { - 'PST': 'Etc/GMT+8', - 'CST': 'Etc/GMT+6', + "PST": "Etc/GMT+8", + "CST": "Etc/GMT+6", } @@ -124,8 +135,8 @@ def _format_index(data): """ tz_raw = data.columns[1] timezone = TZ_MAP.get(tz_raw, tz_raw) - datetime = data['DATE (MM/DD/YYYY)'] + data[tz_raw] - datetime = pd.to_datetime(datetime, format='%m/%d/%Y%H:%M') + datetime = data["DATE (MM/DD/YYYY)"] + data[tz_raw] + datetime = pd.to_datetime(datetime, format="%m/%d/%Y%H:%M") data = data.set_index(datetime) data = data.tz_localize(timezone) return data @@ -149,8 +160,8 @@ def _format_index_raw(data): tz_raw = data.columns[3] timezone = TZ_MAP.get(tz_raw, tz_raw) year = data.Year.apply(str) - jday = data.DOY.apply(lambda x: '{:03d}'.format(x)) - time = data[tz_raw].apply(lambda x: '{:04d}'.format(x)) + jday = data.DOY.apply(lambda x: "{:03d}".format(x)) + time = data[tz_raw].apply(lambda x: "{:04d}".format(x)) index = pd.to_datetime(year + jday + time, format="%Y%j%H%M") data = data.set_index(index) data = data.tz_localize(timezone) @@ -212,8 +223,9 @@ def read_midc(filename, variable_map={}, raw_data=False, **kwargs): return data -def read_midc_raw_data_from_nrel(site, start, end, variable_map={}, - timeout=30): +def read_midc_raw_data_from_nrel( + site, start, end, variable_map={}, timeout=30 +): """Request and read MIDC data directly from the raw data api. Parameters @@ -251,10 +263,12 @@ def read_midc_raw_data_from_nrel(site, start, end, variable_map={}, `here `_ for more details and considerations. """ - args = {'site': site, - 'begin': pd.to_datetime(start).strftime('%Y%m%d'), - 'end': pd.to_datetime(end).strftime('%Y%m%d')} - url = 'https://midcdmz.nrel.gov/apps/data_api.pl' + args = { + "site": site, + "begin": pd.to_datetime(start).strftime("%Y%m%d"), + "end": pd.to_datetime(end).strftime("%Y%m%d"), + } + url = "https://midcdmz.nrel.gov/apps/data_api.pl" # NOTE: just use requests.get(url, params=args) to build querystring # number of header columns and data columns do not always match, # so first parse the header to determine the number of data columns @@ -265,5 +279,9 @@ def read_midc_raw_data_from_nrel(site, start, end, variable_map={}, first_row = pd.read_csv(raw_csv, nrows=0) col_length = len(first_row.columns) raw_csv.seek(0) - return read_midc(raw_csv, variable_map=variable_map, raw_data=True, - usecols=range(col_length)) + return read_midc( + raw_csv, + variable_map=variable_map, + raw_data=True, + usecols=range(col_length), + ) diff --git a/pvlib/iotools/panond.py b/pvlib/iotools/panond.py index 2bc8363674..d780c9b156 100644 --- a/pvlib/iotools/panond.py +++ b/pvlib/iotools/panond.py @@ -7,7 +7,7 @@ def _num_type(value): """ Determine if a value is float, int or a string """ - if '.' in value: + if "." in value: try: # Detect float value_out = float(value) return value_out @@ -17,7 +17,6 @@ def _num_type(value): return value_out else: - try: # Detect int value_out = int(value) return value_out @@ -31,10 +30,10 @@ def _element_type(element): """ Determine if an element is a list then pass to _num_type() """ - if ',' in element: # Detect a list. + if "," in element: # Detect a list. # .pan/.ond don't use ',' to indicate 1000. If that changes, # a new method of list detection needs to be found. - values = element.split(',') + values = element.split(",") element_out = [] for val in values: # Determine datatype of each value element_out.append(_num_type(val)) @@ -67,15 +66,15 @@ def _parse_panond(fbuf): lines = fbuf.read().splitlines() for i in range(0, len(lines) - 1): - if lines[i] == '': # Skipping blank lines + if lines[i] == "": # Skipping blank lines continue # Reading blank lines. Stopping one short to avoid index error. # Last line never contains important data. # Creating variables to assist new level in dictionary creation logic - indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(' '))) // 2 - indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(' '))) // 2 + indent_lvl_1 = (len(lines[i]) - len(lines[i].lstrip(" "))) // 2 + indent_lvl_2 = (len(lines[i + 1]) - len(lines[i + 1].lstrip(" "))) // 2 # Split the line into key/value pair - line_data = lines[i].split('=') + line_data = lines[i].split("=") key = line_data[0].strip() # Logical to make sure there is a value to extract if len(line_data) > 1: diff --git a/pvlib/iotools/psm3.py b/pvlib/iotools/psm3.py index 34199b4c34..93f746907e 100644 --- a/pvlib/iotools/psm3.py +++ b/pvlib/iotools/psm3.py @@ -7,8 +7,6 @@ import requests import pandas as pd from json import JSONDecodeError -import warnings -from pvlib._deprecation import pvlibDeprecationWarning NSRDB_API_BASE = "https://developer.nrel.gov" PSM_URL = NSRDB_API_BASE + "/api/nsrdb/v2/solar/psm3-2-2-download.csv" @@ -16,55 +14,74 @@ PSM5MIN_URL = NSRDB_API_BASE + "/api/nsrdb/v2/solar/psm3-5min-download.csv" ATTRIBUTES = ( - 'air_temperature', 'dew_point', 'dhi', 'dni', 'ghi', 'surface_albedo', - 'surface_pressure', 'wind_direction', 'wind_speed') -PVLIB_PYTHON = 'pvlib python' + "air_temperature", + "dew_point", + "dhi", + "dni", + "ghi", + "surface_albedo", + "surface_pressure", + "wind_direction", + "wind_speed", +) +PVLIB_PYTHON = "pvlib python" # Dictionary mapping PSM3 response names to pvlib names VARIABLE_MAP = { - 'GHI': 'ghi', - 'DHI': 'dhi', - 'DNI': 'dni', - 'Clearsky GHI': 'ghi_clear', - 'Clearsky DHI': 'dhi_clear', - 'Clearsky DNI': 'dni_clear', - 'Solar Zenith Angle': 'solar_zenith', - 'Temperature': 'temp_air', - 'Dew Point': 'temp_dew', - 'Relative Humidity': 'relative_humidity', - 'Pressure': 'pressure', - 'Wind Speed': 'wind_speed', - 'Wind Direction': 'wind_direction', - 'Surface Albedo': 'albedo', - 'Precipitable Water': 'precipitable_water', + "GHI": "ghi", + "DHI": "dhi", + "DNI": "dni", + "Clearsky GHI": "ghi_clear", + "Clearsky DHI": "dhi_clear", + "Clearsky DNI": "dni_clear", + "Solar Zenith Angle": "solar_zenith", + "Temperature": "temp_air", + "Dew Point": "temp_dew", + "Relative Humidity": "relative_humidity", + "Pressure": "pressure", + "Wind Speed": "wind_speed", + "Wind Direction": "wind_direction", + "Surface Albedo": "albedo", + "Precipitable Water": "precipitable_water", } # Dictionary mapping pvlib names to PSM3 request names # Note, PSM3 uses different names for the same variables in the # response and the request REQUEST_VARIABLE_MAP = { - 'ghi': 'ghi', - 'dhi': 'dhi', - 'dni': 'dni', - 'ghi_clear': 'clearsky_ghi', - 'dhi_clear': 'clearsky_dhi', - 'dni_clear': 'clearsky_dni', - 'zenith': 'solar_zenith_angle', - 'temp_air': 'air_temperature', - 'temp_dew': 'dew_point', - 'relative_humidity': 'relative_humidity', - 'pressure': 'surface_pressure', - 'wind_speed': 'wind_speed', - 'wind_direction': 'wind_direction', - 'albedo': 'surface_albedo', - 'precipitable_water': 'total_precipitable_water', + "ghi": "ghi", + "dhi": "dhi", + "dni": "dni", + "ghi_clear": "clearsky_ghi", + "dhi_clear": "clearsky_dhi", + "dni_clear": "clearsky_dni", + "zenith": "solar_zenith_angle", + "temp_air": "air_temperature", + "temp_dew": "dew_point", + "relative_humidity": "relative_humidity", + "pressure": "surface_pressure", + "wind_speed": "wind_speed", + "wind_direction": "wind_direction", + "albedo": "surface_albedo", + "precipitable_water": "total_precipitable_water", } -def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60, - attributes=ATTRIBUTES, leap_day=True, full_name=PVLIB_PYTHON, - affiliation=PVLIB_PYTHON, map_variables=True, url=None, - timeout=30): +def get_psm3( + latitude, + longitude, + api_key, + email, + names="tmy", + interval=60, + attributes=ATTRIBUTES, + leap_day=True, + full_name=PVLIB_PYTHON, + affiliation=PVLIB_PYTHON, + map_variables=True, + url=None, + timeout=30, +): """ Retrieve NSRDB PSM3 timeseries weather data from the PSM3 API. The NSRDB is described in [1]_ and the PSM3 API is described in [2]_, [3]_, and [4]_. @@ -169,8 +186,8 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60, # The well know text (WKT) representation of geometry notation is strict. # A POINT object is a string with longitude first, then the latitude, with # four decimals each, and exactly one space between them. - longitude = ('%9.4f' % longitude).strip() - latitude = ('%8.4f' % latitude).strip() + longitude = ("%9.4f" % longitude).strip() + latitude = ("%8.4f" % latitude).strip() # TODO: make format_WKT(object_type, *args) in tools.py # convert to string to accomodate integer years being passed in @@ -181,23 +198,23 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60, # required query-string parameters for request to PSM3 API params = { - 'api_key': api_key, - 'full_name': full_name, - 'email': email, - 'affiliation': affiliation, - 'reason': PVLIB_PYTHON, - 'mailing_list': 'false', - 'wkt': 'POINT(%s %s)' % (longitude, latitude), - 'names': names, - 'attributes': ','.join(attributes), - 'leap_day': str(leap_day).lower(), - 'utc': 'false', - 'interval': interval + "api_key": api_key, + "full_name": full_name, + "email": email, + "affiliation": affiliation, + "reason": PVLIB_PYTHON, + "mailing_list": "false", + "wkt": "POINT(%s %s)" % (longitude, latitude), + "names": names, + "attributes": ",".join(attributes), + "leap_day": str(leap_day).lower(), + "utc": "false", + "interval": interval, } # request CSV download from NREL PSM3 if url is None: # determine the endpoint that suits the user inputs - if any(prefix in names for prefix in ('tmy', 'tgy', 'tdy')): + if any(prefix in names for prefix in ("tmy", "tgy", "tdy")): url = TMY_URL elif interval in (5, 15): url = PSM5MIN_URL @@ -209,13 +226,13 @@ def get_psm3(latitude, longitude, api_key, email, names='tmy', interval=60, # if the API key is rejected, then the response status will be 403 # Forbidden, and then the error is in the content and there is no JSON try: - errors = response.json()['errors'] + errors = response.json()["errors"] except JSONDecodeError: - errors = response.content.decode('utf-8') + errors = response.content.decode("utf-8") raise requests.HTTPError(errors, response=response) # the CSV is in the response content as a UTF-8 bytestring # to use pandas we need to create a file buffer from the response - fbuf = io.StringIO(response.content.decode('utf-8')) + fbuf = io.StringIO(response.content.decode("utf-8")) return parse_psm3(fbuf, map_variables) @@ -317,42 +334,47 @@ def parse_psm3(fbuf, map_variables=True): `_ """ # The first 2 lines of the response are headers with metadata - metadata_fields = fbuf.readline().split(',') + metadata_fields = fbuf.readline().split(",") metadata_fields[-1] = metadata_fields[-1].strip() # strip trailing newline - metadata_values = fbuf.readline().split(',') + metadata_values = fbuf.readline().split(",") metadata_values[-1] = metadata_values[-1].strip() # strip trailing newline metadata = dict(zip(metadata_fields, metadata_values)) # the response is all strings, so set some metadata types to numbers - metadata['Local Time Zone'] = int(metadata['Local Time Zone']) - metadata['Time Zone'] = int(metadata['Time Zone']) - metadata['Latitude'] = float(metadata['Latitude']) - metadata['Longitude'] = float(metadata['Longitude']) - metadata['Elevation'] = int(metadata['Elevation']) + metadata["Local Time Zone"] = int(metadata["Local Time Zone"]) + metadata["Time Zone"] = int(metadata["Time Zone"]) + metadata["Latitude"] = float(metadata["Latitude"]) + metadata["Longitude"] = float(metadata["Longitude"]) + metadata["Elevation"] = int(metadata["Elevation"]) # get the column names so we can set the dtypes - columns = fbuf.readline().split(',') + columns = fbuf.readline().split(",") columns[-1] = columns[-1].strip() # strip trailing newline # Since the header has so many columns, excel saves blank cols in the # data below the header lines. - columns = [col for col in columns if col != ''] + columns = [col for col in columns if col != ""] dtypes = dict.fromkeys(columns, float) # all floats except datevec dtypes.update(Year=int, Month=int, Day=int, Hour=int, Minute=int) - dtypes['Cloud Type'] = int - dtypes['Fill Flag'] = int + dtypes["Cloud Type"] = int + dtypes["Fill Flag"] = int data = pd.read_csv( - fbuf, header=None, names=columns, usecols=columns, dtype=dtypes, - delimiter=',', lineterminator='\n') # skip carriage returns \r + fbuf, + header=None, + names=columns, + usecols=columns, + dtype=dtypes, + delimiter=",", + lineterminator="\n", + ) # skip carriage returns \r # the response 1st 5 columns are a date vector, convert to datetime - dtidx = pd.to_datetime( - data[['Year', 'Month', 'Day', 'Hour', 'Minute']]) + dtidx = pd.to_datetime(data[["Year", "Month", "Day", "Hour", "Minute"]]) # in USA all timezones are integers - tz = 'Etc/GMT%+d' % -metadata['Time Zone'] + tz = "Etc/GMT%+d" % -metadata["Time Zone"] data.index = pd.DatetimeIndex(dtidx).tz_localize(tz) if map_variables: data = data.rename(columns=VARIABLE_MAP) - metadata['latitude'] = metadata.pop('Latitude') - metadata['longitude'] = metadata.pop('Longitude') - metadata['altitude'] = metadata.pop('Elevation') + metadata["latitude"] = metadata.pop("Latitude") + metadata["longitude"] = metadata.pop("Longitude") + metadata["altitude"] = metadata.pop("Elevation") return data, metadata @@ -394,6 +416,6 @@ def read_psm3(filename, map_variables=True): .. [2] `Standard Time Series Data File Format `_ """ - with open(str(filename), 'r') as fbuf: + with open(str(filename), "r") as fbuf: content = parse_psm3(fbuf, map_variables) return content diff --git a/pvlib/iotools/pvgis.py b/pvlib/iotools/pvgis.py index 3ddc3f6a91..4dbcfa8730 100644 --- a/pvlib/iotools/pvgis.py +++ b/pvlib/iotools/pvgis.py @@ -14,6 +14,7 @@ * `monthly radiation `_ """ + import io import json from pathlib import Path @@ -23,36 +24,50 @@ import pytz from pvlib.iotools import read_epw, parse_epw -URL = 'https://re.jrc.ec.europa.eu/api/' +URL = "https://re.jrc.ec.europa.eu/api/" # Dictionary mapping PVGIS names to pvlib names VARIABLE_MAP = { - 'G(h)': 'ghi', - 'Gb(n)': 'dni', - 'Gd(h)': 'dhi', - 'G(i)': 'poa_global', - 'Gb(i)': 'poa_direct', - 'Gd(i)': 'poa_sky_diffuse', - 'Gr(i)': 'poa_ground_diffuse', - 'H_sun': 'solar_elevation', - 'T2m': 'temp_air', - 'RH': 'relative_humidity', - 'SP': 'pressure', - 'WS10m': 'wind_speed', - 'WD10m': 'wind_direction', + "G(h)": "ghi", + "Gb(n)": "dni", + "Gd(h)": "dhi", + "G(i)": "poa_global", + "Gb(i)": "poa_direct", + "Gd(i)": "poa_sky_diffuse", + "Gr(i)": "poa_ground_diffuse", + "H_sun": "solar_elevation", + "T2m": "temp_air", + "RH": "relative_humidity", + "SP": "pressure", + "WS10m": "wind_speed", + "WD10m": "wind_direction", } -def get_pvgis_hourly(latitude, longitude, start=None, end=None, - raddatabase=None, components=True, - surface_tilt=0, surface_azimuth=180, - outputformat='json', - usehorizon=True, userhorizon=None, - pvcalculation=False, - peakpower=None, pvtechchoice='crystSi', - mountingplace='free', loss=0, trackingtype=0, - optimal_surface_tilt=False, optimalangles=False, - url=URL, map_variables=True, timeout=30): +def get_pvgis_hourly( + latitude, + longitude, + start=None, + end=None, + raddatabase=None, + components=True, + surface_tilt=0, + surface_azimuth=180, + outputformat="json", + usehorizon=True, + userhorizon=None, + pvcalculation=False, + peakpower=None, + pvtechchoice="crystSi", + mountingplace="free", + loss=0, + trackingtype=0, + optimal_surface_tilt=False, + optimalangles=False, + url=URL, + map_variables=True, + timeout=30, +): """Get hourly solar irradiation and modeled PV power output from PVGIS. PVGIS data is freely available at [1]_. @@ -203,28 +218,40 @@ def get_pvgis_hourly(latitude, longitude, start=None, end=None, `_ """ # noqa: E501 # use requests to format the query string by passing params dictionary - params = {'lat': latitude, 'lon': longitude, 'outputformat': outputformat, - 'angle': surface_tilt, 'aspect': surface_azimuth-180, - 'pvcalculation': int(pvcalculation), - 'pvtechchoice': pvtechchoice, 'mountingplace': mountingplace, - 'trackingtype': trackingtype, 'components': int(components), - 'usehorizon': int(usehorizon), - 'optimalangles': int(optimalangles), - 'optimalinclination': int(optimal_surface_tilt), 'loss': loss} + params = { + "lat": latitude, + "lon": longitude, + "outputformat": outputformat, + "angle": surface_tilt, + "aspect": surface_azimuth - 180, + "pvcalculation": int(pvcalculation), + "pvtechchoice": pvtechchoice, + "mountingplace": mountingplace, + "trackingtype": trackingtype, + "components": int(components), + "usehorizon": int(usehorizon), + "optimalangles": int(optimalangles), + "optimalinclination": int(optimal_surface_tilt), + "loss": loss, + } # pvgis only takes 0 for False, and 1 for True, not strings if userhorizon is not None: - params['userhorizon'] = ','.join(str(x) for x in userhorizon) + params["userhorizon"] = ",".join(str(x) for x in userhorizon) if raddatabase is not None: - params['raddatabase'] = raddatabase + params["raddatabase"] = raddatabase if start is not None: - params['startyear'] = start if isinstance(start, int) else pd.to_datetime(start).year # noqa: E501 + params["startyear"] = ( + start if isinstance(start, int) else pd.to_datetime(start).year + ) # noqa: E501 if end is not None: - params['endyear'] = end if isinstance(end, int) else pd.to_datetime(end).year # noqa: E501 + params["endyear"] = ( + end if isinstance(end, int) else pd.to_datetime(end).year + ) # noqa: E501 if peakpower is not None: - params['peakpower'] = peakpower + params["peakpower"] = peakpower # The url endpoint for hourly radiation is 'seriescalc' - res = requests.get(url + 'seriescalc', params=params, timeout=timeout) + res = requests.get(url + "seriescalc", params=params, timeout=timeout) # PVGIS returns really well formatted error messages in JSON for HTTP/1.1 # 400 BAD REQUEST so try to return that if possible, otherwise raise the # HTTP/1.1 error caught by requests @@ -234,19 +261,22 @@ def get_pvgis_hourly(latitude, longitude, start=None, end=None, except Exception: res.raise_for_status() else: - raise requests.HTTPError(err_msg['message']) + raise requests.HTTPError(err_msg["message"]) - return read_pvgis_hourly(io.StringIO(res.text), pvgis_format=outputformat, - map_variables=map_variables) + return read_pvgis_hourly( + io.StringIO(res.text), + pvgis_format=outputformat, + map_variables=map_variables, + ) def _parse_pvgis_hourly_json(src, map_variables): - inputs = src['inputs'] - metadata = src['meta'] - data = pd.DataFrame(src['outputs']['hourly']) - data.index = pd.to_datetime(data['time'], format='%Y%m%d:%H%M', utc=True) - data = data.drop('time', axis=1) - data = data.astype(dtype={'Int': 'int'}) # The 'Int' column to be integer + inputs = src["inputs"] + metadata = src["meta"] + data = pd.DataFrame(src["outputs"]["hourly"]) + data.index = pd.to_datetime(data["time"], format="%Y%m%d:%H%M", utc=True) + data = data.drop("time", axis=1) + data = data.astype(dtype={"Int": "int"}) # The 'Int' column to be integer if map_variables: data = data.rename(columns=VARIABLE_MAP) return data, inputs, metadata @@ -256,49 +286,51 @@ def _parse_pvgis_hourly_csv(src, map_variables): # The first 4 rows are latitude, longitude, elevation, radiation database inputs = {} # 'Latitude (decimal degrees): 45.000\r\n' - inputs['latitude'] = float(src.readline().split(':')[1]) + inputs["latitude"] = float(src.readline().split(":")[1]) # 'Longitude (decimal degrees): 8.000\r\n' - inputs['longitude'] = float(src.readline().split(':')[1]) + inputs["longitude"] = float(src.readline().split(":")[1]) # Elevation (m): 1389.0\r\n - inputs['elevation'] = float(src.readline().split(':')[1]) + inputs["elevation"] = float(src.readline().split(":")[1]) # 'Radiation database: \tPVGIS-SARAH\r\n' - inputs['radiation_database'] = src.readline().split(':')[1].strip() + inputs["radiation_database"] = src.readline().split(":")[1].strip() # Parse through the remaining metadata section (the number of lines for # this section depends on the requested parameters) while True: line = src.readline() - if line.startswith('time,'): # The data header starts with 'time,' + if line.startswith("time,"): # The data header starts with 'time,' # The last line of the metadata section contains the column names - names = line.strip().split(',') + names = line.strip().split(",") break # Only retrieve metadata from non-empty lines - elif line.strip() != '': - inputs[line.split(':')[0]] = line.split(':')[1].strip() - elif line == '': # If end of file is reached - raise ValueError('No data section was detected. File has probably ' - 'been modified since being downloaded from PVGIS') + elif line.strip() != "": + inputs[line.split(":")[0]] = line.split(":")[1].strip() + elif line == "": # If end of file is reached + raise ValueError( + "No data section was detected. File has probably " + "been modified since being downloaded from PVGIS" + ) # Save the entries from the data section to a list, until an empty line is # reached an empty line. The length of the section depends on the request data_lines = [] while True: line = src.readline() - if line.strip() == '': + if line.strip() == "": break else: - data_lines.append(line.strip().split(',')) + data_lines.append(line.strip().split(",")) data = pd.DataFrame(data_lines, columns=names) - data.index = pd.to_datetime(data['time'], format='%Y%m%d:%H%M', utc=True) - data = data.drop('time', axis=1) + data.index = pd.to_datetime(data["time"], format="%Y%m%d:%H%M", utc=True) + data = data.drop("time", axis=1) if map_variables: data = data.rename(columns=VARIABLE_MAP) # All columns should have the dtype=float, except 'Int' which should be # integer. It is necessary to convert to float, before converting to int - data = data.astype(float).astype(dtype={'Int': 'int'}) + data = data.astype(float).astype(dtype={"Int": "int"}) # Generate metadata dictionary containing description of parameters metadata = {} for line in src.readlines(): - if ':' in line: - metadata[line.split(':')[0]] = line.split(':')[1].strip() + if ":" in line: + metadata[line.split(":")[0]] = line.split(":")[1].strip() return data, inputs, metadata @@ -364,29 +396,31 @@ def read_pvgis_hourly(filename, pvgis_format=None, map_variables=True): # JSON: use Python built-in json module to convert file contents to a # Python dictionary, and pass the dictionary to the # _parse_pvgis_hourly_json() function from this module - if outputformat == 'json': + if outputformat == "json": try: src = json.load(filename) except AttributeError: # str/path has no .read() attribute - with open(str(filename), 'r') as fbuf: + with open(str(filename), "r") as fbuf: src = json.load(fbuf) return _parse_pvgis_hourly_json(src, map_variables=map_variables) # CSV: use _parse_pvgis_hourly_csv() - if outputformat == 'csv': + if outputformat == "csv": try: pvgis_data = _parse_pvgis_hourly_csv( - filename, map_variables=map_variables) + filename, map_variables=map_variables + ) except AttributeError: # str/path has no .read() attribute - with open(str(filename), 'r') as fbuf: + with open(str(filename), "r") as fbuf: pvgis_data = _parse_pvgis_hourly_csv( - fbuf, map_variables=map_variables) + fbuf, map_variables=map_variables + ) return pvgis_data # raise exception if pvgis format isn't in ['csv', 'json'] err_msg = ( - "pvgis format '{:s}' was unknown, must be either 'json' or 'csv'")\ - .format(outputformat) + "pvgis format '{:s}' was unknown, must be either 'json' or 'csv'" + ).format(outputformat) raise ValueError(err_msg) @@ -398,25 +432,39 @@ def _coerce_and_roll_tmy(tmy_data, tz, year): re-interpreted as zero / UTC. """ if tz: - tzname = pytz.timezone(f'Etc/GMT{-tz:+d}') + tzname = pytz.timezone(f"Etc/GMT{-tz:+d}") else: tz = 0 - tzname = pytz.timezone('UTC') - new_index = pd.DatetimeIndex([ - timestamp.replace(year=year, tzinfo=tzname) - for timestamp in tmy_data.index], - name=f'time({tzname})') + tzname = pytz.timezone("UTC") + new_index = pd.DatetimeIndex( + [ + timestamp.replace(year=year, tzinfo=tzname) + for timestamp in tmy_data.index + ], + name=f"time({tzname})", + ) new_tmy_data = pd.DataFrame( np.roll(tmy_data, tz, axis=0), columns=tmy_data.columns, - index=new_index) + index=new_index, + ) return new_tmy_data -def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True, - userhorizon=None, startyear=None, endyear=None, - map_variables=True, url=URL, timeout=30, - roll_utc_offset=None, coerce_year=None): +def get_pvgis_tmy( + latitude, + longitude, + outputformat="json", + usehorizon=True, + userhorizon=None, + startyear=None, + endyear=None, + map_variables=True, + url=URL, + timeout=30, + roll_utc_offset=None, + coerce_year=None, +): """ Get TMY data from PVGIS. @@ -486,18 +534,18 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True, `_ """ # use requests to format the query string by passing params dictionary - params = {'lat': latitude, 'lon': longitude, 'outputformat': outputformat} + params = {"lat": latitude, "lon": longitude, "outputformat": outputformat} # pvgis only likes 0 for False, and 1 for True, not strings, also the # default for usehorizon is already 1 (ie: True), so only set if False if not usehorizon: - params['usehorizon'] = 0 + params["usehorizon"] = 0 if userhorizon is not None: - params['userhorizon'] = ','.join(str(x) for x in userhorizon) + params["userhorizon"] = ",".join(str(x) for x in userhorizon) if startyear is not None: - params['startyear'] = startyear + params["startyear"] = startyear if endyear is not None: - params['endyear'] = endyear - res = requests.get(url + 'tmy', params=params, timeout=timeout) + params["endyear"] = endyear + res = requests.get(url + "tmy", params=params, timeout=timeout) # PVGIS returns really well formatted error messages in JSON for HTTP/1.1 # 400 BAD REQUEST so try to return that if possible, otherwise raise the # HTTP/1.1 error caught by requests @@ -507,20 +555,20 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True, except Exception: res.raise_for_status() else: - raise requests.HTTPError(err_msg['message']) + raise requests.HTTPError(err_msg["message"]) # initialize data to None in case API fails to respond to bad outputformat data = None, None, None, None - if outputformat == 'json': + if outputformat == "json": src = res.json() data, months_selected, inputs, meta = _parse_pvgis_tmy_json(src) - elif outputformat == 'csv': + elif outputformat == "csv": with io.BytesIO(res.content) as src: data, months_selected, inputs, meta = _parse_pvgis_tmy_csv(src) - elif outputformat == 'basic': + elif outputformat == "basic": with io.BytesIO(res.content) as src: data, months_selected, inputs, meta = _parse_pvgis_tmy_basic(src) - elif outputformat == 'epw': - with io.StringIO(res.content.decode('utf-8')) as src: + elif outputformat == "epw": + with io.StringIO(res.content.decode("utf-8")) as src: data, meta = parse_epw(src) months_selected, inputs = None, None else: @@ -540,13 +588,14 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True, def _parse_pvgis_tmy_json(src): - inputs = src['inputs'] - meta = src['meta'] - months_selected = src['outputs']['months_selected'] - data = pd.DataFrame(src['outputs']['tmy_hourly']) + inputs = src["inputs"] + meta = src["meta"] + months_selected = src["outputs"]["months_selected"] + data = pd.DataFrame(src["outputs"]["tmy_hourly"]) data.index = pd.to_datetime( - data['time(UTC)'], format='%Y%m%d:%H%M', utc=True) - data = data.drop('time(UTC)', axis=1) + data["time(UTC)"], format="%Y%m%d:%H%M", utc=True + ) + data = data.drop("time(UTC)", axis=1) return data, months_selected, inputs, meta @@ -554,39 +603,42 @@ def _parse_pvgis_tmy_csv(src): # the first 3 rows are latitude, longitude, elevation inputs = {} # 'Latitude (decimal degrees): 45.000\r\n' - inputs['latitude'] = float(src.readline().split(b':')[1]) + inputs["latitude"] = float(src.readline().split(b":")[1]) # 'Longitude (decimal degrees): 8.000\r\n' - inputs['longitude'] = float(src.readline().split(b':')[1]) + inputs["longitude"] = float(src.readline().split(b":")[1]) # Elevation (m): 1389.0\r\n - inputs['elevation'] = float(src.readline().split(b':')[1]) + inputs["elevation"] = float(src.readline().split(b":")[1]) # then there's a 13 row comma separated table with two columns: month, year # which contains the year used for that month in the src.readline() # get "month,year\r\n" months_selected = [] for month in range(12): months_selected.append( - {'month': month+1, 'year': int(src.readline().split(b',')[1])}) + {"month": month + 1, "year": int(src.readline().split(b",")[1])} + ) # then there's the TMY (typical meteorological year) data # first there's a header row: # time(UTC),T2m,RH,G(h),Gb(n),Gd(h),IR(h),WS10m,WD10m,SP - headers = [h.decode('utf-8').strip() for h in src.readline().split(b',')] + headers = [h.decode("utf-8").strip() for h in src.readline().split(b",")] data = pd.DataFrame( - [src.readline().split(b',') for _ in range(8760)], columns=headers) - dtidx = data['time(UTC)'].apply(lambda dt: dt.decode('utf-8')) - dtidx = pd.to_datetime(dtidx, format='%Y%m%d:%H%M', utc=True) - data = data.drop('time(UTC)', axis=1) + [src.readline().split(b",") for _ in range(8760)], columns=headers + ) + dtidx = data["time(UTC)"].apply(lambda dt: dt.decode("utf-8")) + dtidx = pd.to_datetime(dtidx, format="%Y%m%d:%H%M", utc=True) + data = data.drop("time(UTC)", axis=1) data = pd.DataFrame(data, dtype=float) data.index = dtidx # finally there's some meta data - meta = [line.decode('utf-8').strip() for line in src.readlines()] + meta = [line.decode("utf-8").strip() for line in src.readlines()] return data, months_selected, inputs, meta def _parse_pvgis_tmy_basic(src): data = pd.read_csv(src) data.index = pd.to_datetime( - data['time(UTC)'], format='%Y%m%d:%H%M', utc=True) - data = data.drop('time(UTC)', axis=1) + data["time(UTC)"], format="%Y%m%d:%H%M", utc=True + ) + data = data.drop("time(UTC)", axis=1) return data, None, None, None @@ -648,7 +700,7 @@ def read_pvgis_tmy(filename, pvgis_format=None, map_variables=True): # 'csv', or 'basic' # EPW: use the EPW parser from the pvlib.iotools epw.py module - if outputformat == 'epw': + if outputformat == "epw": try: data, meta = parse_epw(filename) except AttributeError: # str/path has no .read() attribute @@ -661,32 +713,33 @@ def read_pvgis_tmy(filename, pvgis_format=None, map_variables=True): # JSON: use Python built-in json module to convert file contents to a # Python dictionary, and pass the dictionary to the _parse_pvgis_tmy_json() # function from this module - elif outputformat == 'json': + elif outputformat == "json": try: src = json.load(filename) except AttributeError: # str/path has no .read() attribute - with open(str(filename), 'r') as fbuf: + with open(str(filename), "r") as fbuf: src = json.load(fbuf) data, months_selected, inputs, meta = _parse_pvgis_tmy_json(src) # CSV or basic: use the correct parser from this module # eg: _parse_pvgis_tmy_csv() or _parse_pvgist_tmy_basic() - elif outputformat in ['csv', 'basic']: + elif outputformat in ["csv", "basic"]: # get the correct parser function for this output format from globals() - pvgis_parser = globals()['_parse_pvgis_tmy_{:s}'.format(outputformat)] + pvgis_parser = globals()["_parse_pvgis_tmy_{:s}".format(outputformat)] # NOTE: pvgis_parse() is a pvgis parser function from this module, # either _parse_pvgis_tmy_csv() or _parse_pvgist_tmy_basic() try: data, months_selected, inputs, meta = pvgis_parser(filename) except AttributeError: # str/path has no .read() attribute - with open(str(filename), 'rb') as fbuf: + with open(str(filename), "rb") as fbuf: data, months_selected, inputs, meta = pvgis_parser(fbuf) else: # raise exception if pvgis format isn't in ['csv','basic','epw','json'] err_msg = ( "pvgis format '{:s}' was unknown, must be either 'epw', 'json', " - "'csv', or 'basic'").format(outputformat) + "'csv', or 'basic'" + ).format(outputformat) raise ValueError(err_msg) if map_variables: @@ -728,22 +781,22 @@ def get_pvgis_horizon(latitude, longitude, url=URL, **kwargs): .. [1] `PVGIS horizon profile tool `_ """ - params = {'lat': latitude, 'lon': longitude, 'outputformat': 'json'} - res = requests.get(url + 'printhorizon', params=params, **kwargs) + params = {"lat": latitude, "lon": longitude, "outputformat": "json"} + res = requests.get(url + "printhorizon", params=params, **kwargs) if not res.ok: try: err_msg = res.json() except Exception: res.raise_for_status() else: - raise requests.HTTPError(err_msg['message']) + raise requests.HTTPError(err_msg["message"]) json_output = res.json() - metadata = json_output['meta'] - data = pd.DataFrame(json_output['outputs']['horizon_profile']) - data.columns = ['horizon_azimuth', 'horizon_elevation'] + metadata = json_output["meta"] + data = pd.DataFrame(json_output["outputs"]["horizon_profile"]) + data.columns = ["horizon_azimuth", "horizon_elevation"] # Convert azimuth to pvlib convention (north=0, south=180) - data['horizon_azimuth'] += 180 - data.set_index('horizon_azimuth', inplace=True) - data = data['horizon_elevation'] # convert to pd.Series + data["horizon_azimuth"] += 180 + data.set_index("horizon_azimuth", inplace=True) + data = data["horizon_elevation"] # convert to pd.Series data = data[data.index < 360] # remove duplicate north point (0 and 360) return data, metadata diff --git a/pvlib/iotools/sodapro.py b/pvlib/iotools/sodapro.py index b9922af4b8..29b0923182 100644 --- a/pvlib/iotools/sodapro.py +++ b/pvlib/iotools/sodapro.py @@ -9,44 +9,75 @@ import warnings -URL = 'api.soda-solardata.com' +URL = "api.soda-solardata.com" CAMS_INTEGRATED_COLUMNS = [ - 'TOA', 'Clear sky GHI', 'Clear sky BHI', 'Clear sky DHI', 'Clear sky BNI', - 'GHI', 'BHI', 'DHI', 'BNI', - 'GHI no corr', 'BHI no corr', 'DHI no corr', 'BNI no corr'] + "TOA", + "Clear sky GHI", + "Clear sky BHI", + "Clear sky DHI", + "Clear sky BNI", + "GHI", + "BHI", + "DHI", + "BNI", + "GHI no corr", + "BHI no corr", + "DHI no corr", + "BNI no corr", +] # Dictionary mapping CAMS Radiation and McClear variables to pvlib names VARIABLE_MAP = { - 'TOA': 'ghi_extra', - 'Clear sky GHI': 'ghi_clear', - 'Clear sky BHI': 'bhi_clear', - 'Clear sky DHI': 'dhi_clear', - 'Clear sky BNI': 'dni_clear', - 'GHI': 'ghi', - 'BHI': 'bhi', - 'DHI': 'dhi', - 'BNI': 'dni', - 'sza': 'solar_zenith', + "TOA": "ghi_extra", + "Clear sky GHI": "ghi_clear", + "Clear sky BHI": "bhi_clear", + "Clear sky DHI": "dhi_clear", + "Clear sky BNI": "dni_clear", + "GHI": "ghi", + "BHI": "bhi", + "DHI": "dhi", + "BNI": "dni", + "sza": "solar_zenith", } # Dictionary mapping time steps to CAMS time step format -TIME_STEPS_MAP = {'1min': 'PT01M', '15min': 'PT15M', '1h': 'PT01H', - '1d': 'P01D', '1M': 'P01M'} +TIME_STEPS_MAP = { + "1min": "PT01M", + "15min": "PT15M", + "1h": "PT01H", + "1d": "P01D", + "1M": "P01M", +} -TIME_STEPS_IN_HOURS = {'1min': 1/60, '15min': 15/60, '1h': 1, '1d': 24} +TIME_STEPS_IN_HOURS = {"1min": 1 / 60, "15min": 15 / 60, "1h": 1, "1d": 24} -SUMMATION_PERIOD_TO_TIME_STEP = {'0 year 0 month 0 day 0 h 1 min 0 s': '1min', - '0 year 0 month 0 day 0 h 15 min 0 s': '15min', # noqa - '0 year 0 month 0 day 1 h 0 min 0 s': '1h', - '0 year 0 month 1 day 0 h 0 min 0 s': '1d', - '0 year 1 month 0 day 0 h 0 min 0 s': '1M'} +SUMMATION_PERIOD_TO_TIME_STEP = { + "0 year 0 month 0 day 0 h 1 min 0 s": "1min", + "0 year 0 month 0 day 0 h 15 min 0 s": "15min", # noqa + "0 year 0 month 0 day 1 h 0 min 0 s": "1h", + "0 year 0 month 1 day 0 h 0 min 0 s": "1d", + "0 year 1 month 0 day 0 h 0 min 0 s": "1M", +} -def get_cams(latitude, longitude, start, end, email, identifier='mcclear', - altitude=None, time_step='1h', time_ref='UT', verbose=False, - integrated=False, label=None, map_variables=True, - server=URL, timeout=30): +def get_cams( + latitude, + longitude, + start, + end, + email, + identifier="mcclear", + altitude=None, + time_step="1h", + time_ref="UT", + verbose=False, + integrated=False, + label=None, + map_variables=True, + server=URL, + timeout=30, +): """Retrieve irradiance and clear-sky time series from CAMS. Time-series of radiation and/or clear-sky global, beam, and @@ -166,15 +197,17 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear', try: time_step_str = TIME_STEPS_MAP[time_step] except KeyError: - raise ValueError(f'Time step not recognized. Must be one of ' - f'{list(TIME_STEPS_MAP.keys())}') + raise ValueError( + f"Time step not recognized. Must be one of " + f"{list(TIME_STEPS_MAP.keys())}" + ) - if (verbose) and ((time_step != '1min') or (time_ref != 'UT')): + if (verbose) and ((time_step != "1min") or (time_ref != "UT")): verbose = False warnings.warn("Verbose mode only supports 1 min. UT time series!") - if identifier not in ['mcclear', 'cams_radiation']: - raise ValueError('Identifier must be either mcclear or cams_radiation') + if identifier not in ["mcclear", "cams_radiation"]: + raise ValueError("Identifier must be either mcclear or cams_radiation") # Format verbose variable to the required format: {'true', 'false'} verbose = str(verbose).lower() @@ -183,56 +216,64 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear', altitude = -999 # Start and end date should be in the format: yyyy-mm-dd - start = pd.to_datetime(start).strftime('%Y-%m-%d') - end = pd.to_datetime(end).strftime('%Y-%m-%d') + start = pd.to_datetime(start).strftime("%Y-%m-%d") + end = pd.to_datetime(end).strftime("%Y-%m-%d") - email = email.replace('@', '%2540') # Format email address - identifier = 'get_{}'.format(identifier.lower()) # Format identifier str + email = email.replace("@", "%2540") # Format email address + identifier = "get_{}".format(identifier.lower()) # Format identifier str base_url = f"https://{server}/service/wps" data_inputs_dict = { - 'latitude': latitude, - 'longitude': longitude, - 'altitude': altitude, - 'date_begin': start, - 'date_end': end, - 'time_ref': time_ref, - 'summarization': time_step_str, - 'username': email, - 'verbose': verbose} + "latitude": latitude, + "longitude": longitude, + "altitude": altitude, + "date_begin": start, + "date_end": end, + "time_ref": time_ref, + "summarization": time_step_str, + "username": email, + "verbose": verbose, + } # Manual formatting of the input parameters seperating each by a semicolon - data_inputs = ";".join([f"{key}={value}" for key, value in - data_inputs_dict.items()]) - - params = {'Service': 'WPS', - 'Request': 'Execute', - 'Identifier': identifier, - 'version': '1.0.0', - 'RawDataOutput': 'irradiation', - } + data_inputs = ";".join( + [f"{key}={value}" for key, value in data_inputs_dict.items()] + ) + + params = { + "Service": "WPS", + "Request": "Execute", + "Identifier": identifier, + "version": "1.0.0", + "RawDataOutput": "irradiation", + } # The DataInputs parameter of the URL has to be manually formatted and # added to the base URL as it contains sub-parameters seperated by # semi-colons, which gets incorrectly formatted by the requests function # if passed using the params argument. - res = requests.get(base_url + '?DataInputs=' + data_inputs, params=params, - timeout=timeout) + res = requests.get( + base_url + "?DataInputs=" + data_inputs, params=params, timeout=timeout + ) # Response from CAMS follows the status and reason format of PyWPS4 # If an error occurs on server side, it will return error 400 - bad request # Additional information is available in the response text, so it is added # to the error displayed to facilitate users effort to fix their request if not res.ok: - errors = res.text.split('ows:ExceptionText')[1][1:-2] + errors = res.text.split("ows:ExceptionText")[1][1:-2] res.reason = "%s: <%s>" % (res.reason, errors) res.raise_for_status() # Successful requests returns a csv data file else: - fbuf = io.StringIO(res.content.decode('utf-8')) - data, metadata = parse_cams(fbuf, integrated=integrated, label=label, - map_variables=map_variables) + fbuf = io.StringIO(res.content.decode("utf-8")) + data, metadata = parse_cams( + fbuf, + integrated=integrated, + label=label, + map_variables=map_variables, + ) return data, metadata @@ -274,61 +315,65 @@ def parse_cams(fbuf, integrated=False, label=None, map_variables=True): metadata = {} # Initial lines starting with # contain metadata while True: - line = fbuf.readline().rstrip('\n') - if line.startswith('# Observation period'): + line = fbuf.readline().rstrip("\n") + if line.startswith("# Observation period"): # The last line of the metadata section contains the column names - names = line.lstrip('# ').split(';') + names = line.lstrip("# ").split(";") break # End of metadata section has been reached - elif ': ' in line: - metadata[line.split(': ')[0].lstrip('# ')] = line.split(': ')[1] + elif ": " in line: + metadata[line.split(": ")[0].lstrip("# ")] = line.split(": ")[1] # Convert latitude, longitude, and altitude values from strings to floats for k_old in list(metadata.keys()): - k_new = k_old.lstrip().split(' ')[0].lower() - if k_new in ['latitude', 'longitude', 'altitude']: + k_new = k_old.lstrip().split(" ")[0].lower() + if k_new in ["latitude", "longitude", "altitude"]: metadata[k_new] = float(metadata.pop(k_old)) - metadata['radiation_unit'] = \ - {True: 'Wh/m^2', False: 'W/m^2'}[integrated] + metadata["radiation_unit"] = {True: "Wh/m^2", False: "W/m^2"}[integrated] # Determine the time_step from the metadata dictionary time_step = SUMMATION_PERIOD_TO_TIME_STEP[ - metadata['Summarization (integration) period']] - metadata['time_step'] = time_step + metadata["Summarization (integration) period"] + ] + metadata["time_step"] = time_step - data = pd.read_csv(fbuf, sep=';', comment='#', header=None, names=names) + data = pd.read_csv(fbuf, sep=";", comment="#", header=None, names=names) - obs_period = data['Observation period'].str.split('/') + obs_period = data["Observation period"].str.split("/") # Set index as the start observation time (left) and localize to UTC - if (label == 'left') | ((label is None) & (time_step != '1M')): + if (label == "left") | ((label is None) & (time_step != "1M")): data.index = pd.to_datetime(obs_period.str[0], utc=True) # Set index as the stop observation time (right) and localize to UTC # default label for monthly data is 'right' following Pandas' convention - elif (label == 'right') | ((label is None) & (time_step == '1M')): + elif (label == "right") | ((label is None) & (time_step == "1M")): data.index = pd.to_datetime(obs_period.str[1], utc=True) # For time_steps '1d' and '1M', drop timezone and round to nearest midnight - if (time_step == '1d') | (time_step == '1M'): + if (time_step == "1d") | (time_step == "1M"): data.index = pd.DatetimeIndex(data.index.date) # For monthly data with 'right' label, the index should be the last # date of the month and not the first date of the following month - if (time_step == '1M') & (label != 'left'): + if (time_step == "1M") & (label != "left"): data.index = data.index - pd.Timedelta(days=1) if not integrated: # Convert radiation values from Wh/m2 to W/m2 - integrated_cols = [c for c in CAMS_INTEGRATED_COLUMNS - if c in data.columns] - - if time_step == '1M': - time_delta = (pd.to_datetime(obs_period.str[1]) - - pd.to_datetime(obs_period.str[0])) - hours = time_delta.dt.total_seconds()/60/60 - data[integrated_cols] = data[integrated_cols].\ - divide(hours.tolist(), axis='rows') + integrated_cols = [ + c for c in CAMS_INTEGRATED_COLUMNS if c in data.columns + ] + + if time_step == "1M": + time_delta = pd.to_datetime(obs_period.str[1]) - pd.to_datetime( + obs_period.str[0] + ) + hours = time_delta.dt.total_seconds() / 60 / 60 + data[integrated_cols] = data[integrated_cols].divide( + hours.tolist(), axis="rows" + ) else: - data[integrated_cols] = (data[integrated_cols] / - TIME_STEPS_IN_HOURS[time_step]) + data[integrated_cols] = ( + data[integrated_cols] / TIME_STEPS_IN_HOURS[time_step] + ) data.index.name = None # Set index name to None if map_variables: data = data.rename(columns=VARIABLE_MAP) @@ -373,6 +418,6 @@ def read_cams(filename, integrated=False, label=None, map_variables=True): .. [1] `CAMS solar radiation documentation `_ """ - with open(str(filename), 'r') as fbuf: + with open(str(filename), "r") as fbuf: content = parse_cams(fbuf, integrated, label, map_variables) return content diff --git a/pvlib/iotools/solaranywhere.py b/pvlib/iotools/solaranywhere.py index dfa7420ccc..4416e5a4fb 100644 --- a/pvlib/iotools/solaranywhere.py +++ b/pvlib/iotools/solaranywhere.py @@ -6,50 +6,65 @@ import time import json -URL = 'https://service.solaranywhere.com/api/v2' +URL = "https://service.solaranywhere.com/api/v2" # Dictionary mapping SolarAnywhere names to standard pvlib names # Names with spaces are used in SolarAnywhere files, and names without spaces # are used by the SolarAnywhere API VARIABLE_MAP = { - 'Global Horizontal Irradiance (GHI) W/m2': 'ghi', - 'GlobalHorizontalIrradiance_WattsPerMeterSquared': 'ghi', - 'DirectNormalIrradiance_WattsPerMeterSquared': 'dni', - 'Direct Normal Irradiance (DNI) W/m2': 'dni', - 'Diffuse Horizontal Irradiance (DIF) W/m2': 'dhi', - 'DiffuseHorizontalIrradiance_WattsPerMeterSquared': 'dhi', - 'AmbientTemperature (deg C)': 'temp_air', - 'AmbientTemperature_DegreesC': 'temp_air', - 'WindSpeed (m/s)': 'wind_speed', - 'WindSpeed_MetersPerSecond': 'wind_speed', - 'Relative Humidity (%)': 'relative_humidity', - 'RelativeHumidity_Percent': 'relative_humidity', - 'Clear Sky GHI': 'ghi_clear', - 'ClearSkyGHI_WattsPerMeterSquared': 'ghi_clear', - 'Clear Sky DNI': 'dni_clear', - 'ClearSkyDNI_WattsPerMeterSquared': 'dni_clear', - 'Clear Sky DHI': 'dhi_clear', - 'ClearSkyDHI_WattsPerMeterSquared': 'dhi_clear', - 'Albedo': 'albedo', - 'Albedo_Unitless': 'albedo', + "Global Horizontal Irradiance (GHI) W/m2": "ghi", + "GlobalHorizontalIrradiance_WattsPerMeterSquared": "ghi", + "DirectNormalIrradiance_WattsPerMeterSquared": "dni", + "Direct Normal Irradiance (DNI) W/m2": "dni", + "Diffuse Horizontal Irradiance (DIF) W/m2": "dhi", + "DiffuseHorizontalIrradiance_WattsPerMeterSquared": "dhi", + "AmbientTemperature (deg C)": "temp_air", + "AmbientTemperature_DegreesC": "temp_air", + "WindSpeed (m/s)": "wind_speed", + "WindSpeed_MetersPerSecond": "wind_speed", + "Relative Humidity (%)": "relative_humidity", + "RelativeHumidity_Percent": "relative_humidity", + "Clear Sky GHI": "ghi_clear", + "ClearSkyGHI_WattsPerMeterSquared": "ghi_clear", + "Clear Sky DNI": "dni_clear", + "ClearSkyDNI_WattsPerMeterSquared": "dni_clear", + "Clear Sky DHI": "dhi_clear", + "ClearSkyDHI_WattsPerMeterSquared": "dhi_clear", + "Albedo": "albedo", + "Albedo_Unitless": "albedo", } DEFAULT_VARIABLES = [ - 'StartTime', 'ObservationTime', 'EndTime', - 'GlobalHorizontalIrradiance_WattsPerMeterSquared', - 'DirectNormalIrradiance_WattsPerMeterSquared', - 'DiffuseHorizontalIrradiance_WattsPerMeterSquared', - 'AmbientTemperature_DegreesC', 'WindSpeed_MetersPerSecond', - 'Albedo_Unitless', 'DataVersion' + "StartTime", + "ObservationTime", + "EndTime", + "GlobalHorizontalIrradiance_WattsPerMeterSquared", + "DirectNormalIrradiance_WattsPerMeterSquared", + "DiffuseHorizontalIrradiance_WattsPerMeterSquared", + "AmbientTemperature_DegreesC", + "WindSpeed_MetersPerSecond", + "Albedo_Unitless", + "DataVersion", ] -def get_solaranywhere(latitude, longitude, api_key, start=None, end=None, - source='SolarAnywhereLatest', time_resolution=60, - spatial_resolution=0.01, true_dynamics=False, - probability_of_exceedance=None, - variables=DEFAULT_VARIABLES, missing_data='FillAverage', - url=URL, map_variables=True, timeout=300): +def get_solaranywhere( + latitude, + longitude, + api_key, + start=None, + end=None, + source="SolarAnywhereLatest", + time_resolution=60, + spatial_resolution=0.01, + true_dynamics=False, + probability_of_exceedance=None, + variables=DEFAULT_VARIABLES, + missing_data="FillAverage", + url=URL, + map_variables=True, + timeout=300, +): """Retrieve historical irradiance time series data from SolarAnywhere. The SolarAnywhere API is described in [1]_ and [2]_. A detailed list of @@ -135,15 +150,14 @@ def get_solaranywhere(latitude, longitude, api_key, start=None, end=None, .. [4] `SolarAnywhere variable definitions `_ """ # noqa: E501 - headers = {'content-type': "application/json; charset=utf-8", - 'X-Api-Key': api_key, - 'Accept': "application/json"} + headers = { + "content-type": "application/json; charset=utf-8", + "X-Api-Key": api_key, + "Accept": "application/json", + } payload = { - "Sites": [{ - "Latitude": latitude, - "Longitude": longitude - }], + "Sites": [{"Latitude": latitude, "Longitude": longitude}], "Options": { "OutputFields": variables, "SummaryOutputFields": [], # Do not request summary/monthly data @@ -151,17 +165,18 @@ def get_solaranywhere(latitude, longitude, api_key, start=None, end=None, "TimeResolution_Minutes": time_resolution, "WeatherDataSource": source, "MissingDataHandling": missing_data, - } + }, } if true_dynamics: - payload['Options']['ApplyTrueDynamics'] = True + payload["Options"]["ApplyTrueDynamics"] = True if probability_of_exceedance is not None: if not isinstance(probability_of_exceedance, int): - raise ValueError('`probability_of_exceedance` must be an integer') - payload['Options']['ProbabilityOfExceedance'] = \ + raise ValueError("`probability_of_exceedance` must be an integer") + payload["Options"]["ProbabilityOfExceedance"] = ( probability_of_exceedance + ) # Add start/end time if requesting non-TMY data if (start is not None) or (end is not None): @@ -170,19 +185,21 @@ def get_solaranywhere(latitude, longitude, api_key, start=None, end=None, end = pd.to_datetime(end) # start/end are required to have an associated time zone if start.tz is None: - start = start.tz_localize('UTC') + start = start.tz_localize("UTC") if end.tz is None: - end = end.tz_localize('UTC') - payload['Options']["StartTime"] = start.isoformat() - payload['Options']["EndTime"] = end.isoformat() + end = end.tz_localize("UTC") + payload["Options"]["StartTime"] = start.isoformat() + payload["Options"]["EndTime"] = end.isoformat() # Convert the payload dictionary to a JSON string (uses double quotes) payload = json.dumps(payload) # Make data request - request = requests.post(url+'/WeatherData', data=payload, headers=headers) + request = requests.post( + url + "/WeatherData", data=payload, headers=headers + ) # Raise error if request is not OK if request.ok is False: - raise ValueError(request.json()['Message']) + raise ValueError(request.json()["Message"]) # Retrieve weather request ID weather_request_id = request.json()["WeatherRequestId"] @@ -191,35 +208,47 @@ def get_solaranywhere(latitude, longitude, api_key, start=None, end=None, start_time = time.time() # Current time in seconds since the Epoch # Attempt to retrieve results until the max response time has been exceeded while True: - results = requests.get(url+'/WeatherDataResult/'+weather_request_id, headers=headers) # noqa: E501 + results = requests.get( + url + "/WeatherDataResult/" + weather_request_id, headers=headers + ) # noqa: E501 results_json = results.json() - if results_json.get('Status') == 'Done': - if results_json['WeatherDataResults'][0]['Status'] == 'Failure': - raise RuntimeError(results_json['WeatherDataResults'][0]['ErrorMessages'][0]['Message']) # noqa: E501 + if results_json.get("Status") == "Done": + if results_json["WeatherDataResults"][0]["Status"] == "Failure": + raise RuntimeError( + results_json["WeatherDataResults"][0]["ErrorMessages"][0][ + "Message" + ] + ) # noqa: E501 break - elif (time.time()-start_time) > timeout: - raise TimeoutError('Time exceeded the `timeout`.') + elif (time.time() - start_time) > timeout: + raise TimeoutError("Time exceeded the `timeout`.") time.sleep(5) # Sleep for 5 seconds before each data retrieval attempt # Extract time series data - data = pd.DataFrame(results_json['WeatherDataResults'][0]['WeatherDataPeriods']['WeatherDataPeriods']) # noqa: E501 + data = pd.DataFrame( + results_json["WeatherDataResults"][0]["WeatherDataPeriods"][ + "WeatherDataPeriods" + ] + ) # noqa: E501 # Set datetime index - data.index = pd.to_datetime(data['ObservationTime']) + data.index = pd.to_datetime(data["ObservationTime"]) if map_variables: data = data.rename(columns=VARIABLE_MAP) # Parse metadata - meta = results_json['WeatherDataResults'][0]['WeatherSourceInformation'] - meta['time_resolution'] = results_json['WeatherDataResults'][0]['WeatherDataPeriods']['TimeResolution_Minutes'] # noqa: E501 - meta['spatial_resolution'] = spatial_resolution + meta = results_json["WeatherDataResults"][0]["WeatherSourceInformation"] + meta["time_resolution"] = results_json["WeatherDataResults"][0][ + "WeatherDataPeriods" + ]["TimeResolution_Minutes"] # noqa: E501 + meta["spatial_resolution"] = spatial_resolution # Rename and convert applicable metadata parameters to floats - meta['latitude'] = float(meta.pop('Latitude')) - meta['longitude'] = float(meta.pop('Longitude')) - meta['altitude'] = float(meta.pop('Elevation_Meters')) + meta["latitude"] = float(meta.pop("Latitude")) + meta["longitude"] = float(meta.pop("Longitude")) + meta["altitude"] = float(meta.pop("Elevation_Meters")) return data, meta -def read_solaranywhere(filename, map_variables=True, encoding='iso-8859-1'): +def read_solaranywhere(filename, map_variables=True, encoding="iso-8859-1"): """ Read a SolarAnywhere formatted file into a pandas DataFrame. @@ -254,40 +283,41 @@ def read_solaranywhere(filename, map_variables=True, encoding='iso-8859-1'): .. [1] `SolarAnywhere historical data file formats `_ """ - with open(str(filename), 'r', encoding=encoding) as fbuf: + with open(str(filename), "r", encoding=encoding) as fbuf: # Extract first line of file which contains the metadata - firstline = fbuf.readline().strip().split(',') + firstline = fbuf.readline().strip().split(",") # Read remaining part of file which contains the time series data data = pd.read_csv(fbuf) # Parse metadata meta = {} - meta['USAF'] = int(firstline.pop(0)) - meta['name'] = firstline.pop(0) - meta['state'] = firstline.pop(0) - meta['TZ'] = float(firstline.pop(0)) - meta['latitude'] = float(firstline.pop(0)) - meta['longitude'] = float(firstline.pop(0)) - meta['altitude'] = float(firstline.pop(0)) + meta["USAF"] = int(firstline.pop(0)) + meta["name"] = firstline.pop(0) + meta["state"] = firstline.pop(0) + meta["TZ"] = float(firstline.pop(0)) + meta["latitude"] = float(firstline.pop(0)) + meta["longitude"] = float(firstline.pop(0)) + meta["altitude"] = float(firstline.pop(0)) # SolarAnywhere files contain additional metadata than the TMY3 format. # The additional metadata is specified as key-value pairs, where each entry # is separated by a slash, and the key-value pairs are separated by a # colon. E.g., 'Data Version: 3.4 / Type: Typical Year / ...' - for i in ','.join(firstline).replace('"', '').split('/'): - if ':' in i: - k, v = i.split(':') + for i in ",".join(firstline).replace('"', "").split("/"): + if ":" in i: + k, v = i.split(":") meta[k.strip()] = v.strip() - meta['LatLon Resolution'] = float(meta['LatLon Resolution']) + meta["LatLon Resolution"] = float(meta["LatLon Resolution"]) # Set index - data.index = pd.to_datetime(data['ObservationTime(LST)'], - format='%m/%d/%Y %H:%M') + data.index = pd.to_datetime( + data["ObservationTime(LST)"], format="%m/%d/%Y %H:%M" + ) # Set timezone - data = data.tz_localize(int(meta['TZ'] * 3600)) + data = data.tz_localize(int(meta["TZ"] * 3600)) # Remove notion of LST in case the index is later converted to another tz - data.index.name = data.index.name.replace('(LST)', '') + data.index.name = data.index.name.replace("(LST)", "") # Missing values can be represented as: blanks, 'NaN', or -999 data = data.replace(-999, np.nan) diff --git a/pvlib/iotools/solargis.py b/pvlib/iotools/solargis.py index 375c7ed3e8..f62551f524 100644 --- a/pvlib/iotools/solargis.py +++ b/pvlib/iotools/solargis.py @@ -5,14 +5,26 @@ from dataclasses import dataclass import io -URL = 'https://solargis.info/ws/rest/datadelivery/request' +URL = "https://solargis.info/ws/rest/datadelivery/request" TIME_RESOLUTION_MAP = { - 5: 'MIN_5', 10: 'MIN_10', 15: 'MIN_15', 30: 'MIN_30', 60: 'HOURLY', - 'PT05M': 'MIN_5', 'PT5M': 'MIN_5', 'PT10M': 'MIN_10', 'PT15M': 'MIN_15', - 'PT30': 'MIN_30', 'PT60M': 'HOURLY', 'PT1H': 'HOURLY', 'P1D': 'DAILY', - 'P1M': 'MONTHLY', 'P1Y': 'YEARLY'} + 5: "MIN_5", + 10: "MIN_10", + 15: "MIN_15", + 30: "MIN_30", + 60: "HOURLY", + "PT05M": "MIN_5", + "PT5M": "MIN_5", + "PT10M": "MIN_10", + "PT15M": "MIN_15", + "PT30": "MIN_30", + "PT60M": "HOURLY", + "PT1H": "HOURLY", + "P1D": "DAILY", + "P1M": "MONTHLY", + "P1Y": "YEARLY", +} @dataclass @@ -25,48 +37,77 @@ class ParameterMap: # define the conventions between Solargis and pvlib nomenclature and units VARIABLE_MAP = [ # Irradiance (unit varies based on time resolution) - ParameterMap('GHI', 'ghi'), - ParameterMap('GHI_C', 'ghi_clear'), # this is stated in documentation - ParameterMap('GHIc', 'ghi_clear'), # this is used in practice - ParameterMap('DNI', 'dni'), - ParameterMap('DNI_C', 'dni_clear'), - ParameterMap('DNIc', 'dni_clear'), - ParameterMap('DIF', 'dhi'), - ParameterMap('GTI', 'poa_global'), - ParameterMap('GTI_C', 'poa_global_clear'), - ParameterMap('GTIc', 'poa_global_clear'), + ParameterMap("GHI", "ghi"), + ParameterMap("GHI_C", "ghi_clear"), # this is stated in documentation + ParameterMap("GHIc", "ghi_clear"), # this is used in practice + ParameterMap("DNI", "dni"), + ParameterMap("DNI_C", "dni_clear"), + ParameterMap("DNIc", "dni_clear"), + ParameterMap("DIF", "dhi"), + ParameterMap("GTI", "poa_global"), + ParameterMap("GTI_C", "poa_global_clear"), + ParameterMap("GTIc", "poa_global_clear"), # Solar position - ParameterMap('SE', 'solar_elevation'), + ParameterMap("SE", "solar_elevation"), # SA -> solar_azimuth (degrees) (different convention) ParameterMap("SA", "solar_azimuth", lambda x: x + 180), # Weather / atmospheric parameters - ParameterMap('TEMP', 'temp_air'), - ParameterMap('TD', 'temp_dew'), + ParameterMap("TEMP", "temp_air"), + ParameterMap("TD", "temp_dew"), # surface_pressure (hPa) -> pressure (Pa) - ParameterMap('AP', 'pressure', lambda x: x*100), - ParameterMap('RH', 'relative_humidity'), - ParameterMap('WS', 'wind_speed'), - ParameterMap('WD', 'wind_direction'), - ParameterMap('INC', 'aoi'), # angle of incidence of direct irradiance + ParameterMap("AP", "pressure", lambda x: x * 100), + ParameterMap("RH", "relative_humidity"), + ParameterMap("WS", "wind_speed"), + ParameterMap("WD", "wind_direction"), + ParameterMap("INC", "aoi"), # angle of incidence of direct irradiance # precipitable_water (kg/m2) -> precipitable_water (cm) - ParameterMap('PWAT', 'precipitable_water', lambda x: x/10), + ParameterMap("PWAT", "precipitable_water", lambda x: x / 10), ] METADATA_FIELDS = [ - 'issued', 'site name', 'latitude', 'longitude', 'elevation', - 'summarization type', 'summarization period' + "issued", + "site name", + "latitude", + "longitude", + "elevation", + "summarization type", + "summarization period", ] # Variables that use "-9" as nan values -NA_9_COLUMNS = ['GHI', 'GHIc', 'DNI', 'DNIc', 'DIF', 'GTI', 'GIc', 'KT', 'PAR', - 'PREC', 'PWAT', 'SDWE', 'SFWE'] +NA_9_COLUMNS = [ + "GHI", + "GHIc", + "DNI", + "DNIc", + "DIF", + "GTI", + "GIc", + "KT", + "PAR", + "PREC", + "PWAT", + "SDWE", + "SFWE", +] -def get_solargis(latitude, longitude, start, end, variables, api_key, - time_resolution, timestamp_type='center', tz='GMT+00', - terrain_shading=True, url=URL, map_variables=True, - timeout=30): +def get_solargis( + latitude, + longitude, + start, + end, + variables, + api_key, + time_resolution, + timestamp_type="center", + tz="GMT+00", + terrain_shading=True, + url=URL, + map_variables=True, + timeout=30, +): """ Retrieve irradiance time series data from Solargis. @@ -141,13 +182,13 @@ def get_solargis(latitude, longitude, start, end, variables, api_key, start = pd.to_datetime(start) end = pd.to_datetime(end) - headers = {'Content-Type': 'application/xml'} + headers = {"Content-Type": "application/xml"} # Solargis recommends creating a unique site_id for each location request. # The site_id does not impact the data retrieval and is used for debugging. site_id = f"latitude_{latitude}_longitude_{longitude}" - request_xml = f'''{timestamp_type.upper()} {tz} - ''' # noqa: E501 + """ # noqa: E501 - response = requests.post(url + "?key=" + api_key, headers=headers, - data=request_xml.encode('utf8'), timeout=timeout) + response = requests.post( + url + "?key=" + api_key, + headers=headers, + data=request_xml.encode("utf8"), + timeout=timeout, + ) if response.ok is False: raise requests.HTTPError(response.json()) # Parse metadata - header = pd.read_xml(io.StringIO(response.text), parser='etree') - meta_lines = header['metadata'].iloc[0].split('#') + header = pd.read_xml(io.StringIO(response.text), parser="etree") + meta_lines = header["metadata"].iloc[0].split("#") meta_lines = [line.strip() for line in meta_lines] meta = {} for line in meta_lines: - if ':' in line: - key = line.split(':')[0].lower() + if ":" in line: + key = line.split(":")[0].lower() if key in METADATA_FIELDS: - meta[key] = ':'.join(line.split(':')[1:]) - meta['latitude'] = float(meta['latitude']) - meta['longitude'] = float(meta['longitude']) - meta['altitude'] = float(meta.pop('elevation').replace('m a.s.l.', '')) + meta[key] = ":".join(line.split(":")[1:]) + meta["latitude"] = float(meta["latitude"]) + meta["longitude"] = float(meta["longitude"]) + meta["altitude"] = float(meta.pop("elevation").replace("m a.s.l.", "")) # Parse data - data = pd.read_xml(io.StringIO(response.text), xpath='.//doc:row', - namespaces={'doc': 'http://geomodel.eu/schema/ws/data'}, - parser='etree') - data.index = pd.to_datetime(data['dateTime']) + data = pd.read_xml( + io.StringIO(response.text), + xpath=".//doc:row", + namespaces={"doc": "http://geomodel.eu/schema/ws/data"}, + parser="etree", + ) + data.index = pd.to_datetime(data["dateTime"]) # when requesting one variable, it is necessary to convert dataframe to str - data = data['values'].astype(str).str.split(' ', expand=True) + data = data["values"].astype(str).str.split(" ", expand=True) data = data.astype(float) - data.columns = header['columns'].iloc[0].split() + data.columns = header["columns"].iloc[0].split() # Replace "-9" with nan values for specific columns for variable in data.columns: @@ -206,9 +254,10 @@ def get_solargis(latitude, longitude, start, end, variables, api_key, if variable.solargis_name in data.columns: data.rename( columns={variable.solargis_name: variable.pvlib_name}, - inplace=True + inplace=True, + ) + data[variable.pvlib_name] = data[variable.pvlib_name].apply( + variable.conversion ) - data[variable.pvlib_name] = data[ - variable.pvlib_name].apply(variable.conversion) return data, meta diff --git a/pvlib/iotools/solcast.py b/pvlib/iotools/solcast.py index 71c5c42ffa..892513d7e8 100644 --- a/pvlib/iotools/solcast.py +++ b/pvlib/iotools/solcast.py @@ -1,5 +1,4 @@ -""" Functions to access data from the Solcast API. -""" +"""Functions to access data from the Solcast API.""" import requests import pandas as pd @@ -21,7 +20,7 @@ class ParameterMap: # air_temp -> temp_air (deg C) ParameterMap("air_temp", "temp_air"), # surface_pressure (hPa) -> pressure (Pa) - ParameterMap("surface_pressure", "pressure", lambda x: x*100), + ParameterMap("surface_pressure", "pressure", lambda x: x * 100), # dewpoint_temp -> temp_dew (deg C) ParameterMap("dewpoint_temp", "temp_dew"), # gti (W/m^2) -> poa_global (W/m^2) @@ -31,18 +30,16 @@ class ParameterMap: # wind_direction_10m (deg) -> wind_direction (deg) ParameterMap("wind_direction_10m", "wind_direction"), # azimuth -> solar_azimuth (degrees) (different convention) - ParameterMap( - "azimuth", "solar_azimuth", lambda x: -x % 360 - ), + ParameterMap("azimuth", "solar_azimuth", lambda x: -x % 360), # precipitable_water (kg/m2) -> precipitable_water (cm) - ParameterMap("precipitable_water", "precipitable_water", lambda x: x/10), + ParameterMap("precipitable_water", "precipitable_water", lambda x: x / 10), # zenith -> solar_zenith ParameterMap("zenith", "solar_zenith"), # clearsky ParameterMap("clearsky_dhi", "dhi_clear"), ParameterMap("clearsky_dni", "dni_clear"), ParameterMap("clearsky_ghi", "ghi_clear"), - ParameterMap("clearsky_gti", "poa_global_clear") + ParameterMap("clearsky_gti", "poa_global_clear"), ] @@ -113,17 +110,14 @@ def get_solcast_tmy( """ params = dict( - latitude=latitude, - longitude=longitude, - format="json", - **kwargs + latitude=latitude, longitude=longitude, format="json", **kwargs ) data = _get_solcast( endpoint="tmy/radiation_and_weather", params=params, api_key=api_key, - map_variables=map_variables + map_variables=map_variables, ) return data, {"latitude": latitude, "longitude": longitude} @@ -137,7 +131,7 @@ def get_solcast_historic( end=None, duration=None, map_variables=True, - **kwargs + **kwargs, ): """Get historical irradiance and weather estimates. @@ -222,14 +216,14 @@ def get_solcast_historic( duration=duration, api_key=api_key, format="json", - **kwargs + **kwargs, ) data = _get_solcast( endpoint="historic/radiation_and_weather", params=params, api_key=api_key, - map_variables=map_variables + map_variables=map_variables, ) return data, {"latitude": latitude, "longitude": longitude} @@ -297,17 +291,14 @@ def get_solcast_forecast( """ params = dict( - latitude=latitude, - longitude=longitude, - format="json", - **kwargs + latitude=latitude, longitude=longitude, format="json", **kwargs ) data = _get_solcast( endpoint="forecast/radiation_and_weather", params=params, api_key=api_key, - map_variables=map_variables + map_variables=map_variables, ) return data, {"latitude": latitude, "longitude": longitude} @@ -384,17 +375,14 @@ def get_solcast_live( """ params = dict( - latitude=latitude, - longitude=longitude, - format="json", - **kwargs + latitude=latitude, longitude=longitude, format="json", **kwargs ) data = _get_solcast( endpoint="live/radiation_and_weather", params=params, api_key=api_key, - map_variables=map_variables + map_variables=map_variables, ) return data, {"latitude": latitude, "longitude": longitude} @@ -414,8 +402,10 @@ def _solcast2pvlib(data): """ # move from period_end to period_middle as per pvlib convention - data["period_mid"] = pd.to_datetime( - data.period_end) - pd.to_timedelta(data.period.values) / 2 + data["period_mid"] = ( + pd.to_datetime(data.period_end) + - pd.to_timedelta(data.period.values) / 2 + ) data = data.set_index("period_mid").drop(columns=["period_end", "period"]) # rename and convert variables @@ -423,19 +413,15 @@ def _solcast2pvlib(data): if variable.solcast_name in data.columns: data.rename( columns={variable.solcast_name: variable.pvlib_name}, - inplace=True + inplace=True, + ) + data[variable.pvlib_name] = data[variable.pvlib_name].apply( + variable.conversion ) - data[variable.pvlib_name] = data[ - variable.pvlib_name].apply(variable.conversion) return data -def _get_solcast( - endpoint, - params, - api_key, - map_variables -): +def _get_solcast(endpoint, params, api_key, map_variables): """Retrieve weather, irradiance and power data from the Solcast API. Parameters @@ -467,9 +453,9 @@ def _get_solcast( """ response = requests.get( - url='/'.join([BASE_URL, endpoint]), + url="/".join([BASE_URL, endpoint]), params=params, - headers={"Authorization": f"Bearer {api_key}"} + headers={"Authorization": f"Bearer {api_key}"}, ) if response.status_code == 200: diff --git a/pvlib/iotools/solrad.py b/pvlib/iotools/solrad.py index 90bf5b666e..f22e45af33 100644 --- a/pvlib/iotools/solrad.py +++ b/pvlib/iotools/solrad.py @@ -7,26 +7,51 @@ # pvlib conventions BASE_HEADERS = ( - 'year', 'julian_day', 'month', 'day', 'hour', 'minute', 'decimal_time', - 'solar_zenith', 'ghi', 'ghi_flag', 'dni', 'dni_flag', 'dhi', 'dhi_flag', - 'uvb', 'uvb_flag', 'uvb_temp', 'uvb_temp_flag' + "year", + "julian_day", + "month", + "day", + "hour", + "minute", + "decimal_time", + "solar_zenith", + "ghi", + "ghi_flag", + "dni", + "dni_flag", + "dhi", + "dhi_flag", + "uvb", + "uvb_flag", + "uvb_temp", + "uvb_temp_flag", ) # following README_SOLRAD.txt variable names for remaining -STD_HEADERS = ('std_dw_psp', 'std_direct', 'std_diffuse', 'std_uvb') +STD_HEADERS = ("std_dw_psp", "std_direct", "std_diffuse", "std_uvb") HEADERS = BASE_HEADERS + STD_HEADERS -DPIR_HEADERS = ('dpir', 'dpir_flag', 'dpirc', 'dpirc_flag', 'dpird', - 'dpird_flag') +DPIR_HEADERS = ( + "dpir", + "dpir_flag", + "dpirc", + "dpirc_flag", + "dpird", + "dpird_flag", +) -MADISON_HEADERS = BASE_HEADERS + DPIR_HEADERS + STD_HEADERS + ( - 'std_dpir', 'std_dpirc', 'std_dpird') +MADISON_HEADERS = ( + BASE_HEADERS + + DPIR_HEADERS + + STD_HEADERS + + ("std_dpir", "std_dpirc", "std_dpird") +) # as specified in README_SOLRAD.txt file. excludes 1 space between columns -WIDTHS = [4, 3] + 4*[2] + [6, 6] + 5*[7, 1] + 4*[9] -MADISON_WIDTHS = [4, 3] + 4*[2] + [6, 6] + 8*[7, 1] + 7*[9] +WIDTHS = [4, 3] + 4 * [2] + [6, 6] + 5 * [7, 1] + 4 * [9] +MADISON_WIDTHS = [4, 3] + 4 * [2] + [6, 6] + 8 * [7, 1] + 7 * [9] # add 1 to make fields contiguous (required by pandas.read_fwf) WIDTHS = [w + 1 for w in WIDTHS] MADISON_WIDTHS = [w + 1 for w in MADISON_WIDTHS] @@ -35,17 +60,63 @@ MADISON_WIDTHS[-1] -= 1 DTYPES = [ - 'int64', 'int64', 'int64', 'int64', 'int64', 'int64', 'float64', - 'float64', 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', - 'float64', 'int64', 'float64', 'int64', 'float64', 'float64', - 'float64', 'float64'] + "int64", + "int64", + "int64", + "int64", + "int64", + "int64", + "float64", + "float64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", +] MADISON_DTYPES = [ - 'int64', 'int64', 'int64', 'int64', 'int64', 'int64', 'float64', 'float64', - 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', 'float64', - 'int64', 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', - 'float64', 'int64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64'] + "int64", + "int64", + "int64", + "int64", + "int64", + "int64", + "float64", + "float64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", +] def read_solrad(filename): @@ -96,7 +167,7 @@ def read_solrad(filename): Program. Bull. Amer. Meteor. Soc., 77, 2857-2864. :doi:`10.1175/1520-0477(1996)077<2857:TNISIS>2.0.CO;2` """ - if 'msn' in str(filename): + if "msn" in str(filename): names = MADISON_HEADERS widths = MADISON_WIDTHS dtypes = MADISON_DTYPES @@ -107,44 +178,60 @@ def read_solrad(filename): meta = {} - if str(filename).startswith('ftp') or str(filename).startswith('http'): + if str(filename).startswith("ftp") or str(filename).startswith("http"): response = requests.get(filename) response.raise_for_status() file_buffer = io.StringIO(response.content.decode()) else: - with open(str(filename), 'r') as file_buffer: + with open(str(filename), "r") as file_buffer: file_buffer = io.StringIO(file_buffer.read()) # The first line has the name of the station, and the second gives the # station's latitude, longitude, elevation above mean sea level in meters, # and the displacement in hours from local standard time. - meta['station_name'] = file_buffer.readline().strip() + meta["station_name"] = file_buffer.readline().strip() meta_line = file_buffer.readline().split() - meta['latitude'] = float(meta_line[0]) - meta['longitude'] = float(meta_line[1]) - meta['altitude'] = float(meta_line[2]) - meta['TZ'] = int(meta_line[3]) + meta["latitude"] = float(meta_line[0]) + meta["longitude"] = float(meta_line[1]) + meta["altitude"] = float(meta_line[2]) + meta["TZ"] = int(meta_line[3]) # read in data - data = pd.read_fwf(file_buffer, header=None, names=names, - widths=widths, na_values=-9999.9, dtypes=dtypes) + data = pd.read_fwf( + file_buffer, + header=None, + names=names, + widths=widths, + na_values=-9999.9, + dtypes=dtypes, + ) # set index # columns do not have leading 0s, so must zfill(2) to comply # with %m%d%H%M format - dts = data[['month', 'day', 'hour', 'minute']].astype(str).apply( - lambda x: x.str.zfill(2)) + dts = ( + data[["month", "day", "hour", "minute"]] + .astype(str) + .apply(lambda x: x.str.zfill(2)) + ) dtindex = pd.to_datetime( - data['year'].astype(str) + dts['month'] + dts['day'] + dts['hour'] + - dts['minute'], format='%Y%m%d%H%M', utc=True) + data["year"].astype(str) + + dts["month"] + + dts["day"] + + dts["hour"] + + dts["minute"], + format="%Y%m%d%H%M", + utc=True, + ) data = data.set_index(dtindex) return data, meta -def get_solrad(station, start, end, - url="https://gml.noaa.gov/aftp/data/radiation/solrad/"): +def get_solrad( + station, start, end, url="https://gml.noaa.gov/aftp/data/radiation/solrad/" +): """Request data from NOAA SOLRAD and read it into a Dataframe. A list of stations and their descriptions can be found in [1]_, @@ -195,7 +282,7 @@ def get_solrad(station, start, end, end = pd.to_datetime(end) # Generate list of filenames - dates = pd.date_range(start.floor('d'), end, freq='d') + dates = pd.date_range(start.floor("d"), end, freq="d") station = station.lower() filenames = [ f"{station}/{d.year}/{station}{d.strftime('%y')}{d.dayofyear:03}.dat" @@ -210,12 +297,14 @@ def get_solrad(station, start, end, except requests.exceptions.HTTPError: warnings.warn(f"The following file was not found: {f}") - data = pd.concat(dfs, axis='rows') + data = pd.concat(dfs, axis="rows") - meta = {'station': station, - 'filenames': filenames, - # all file should have the same metadata, so just merge in the - # metadata from the last file - **file_metadata} + meta = { + "station": station, + "filenames": filenames, + # all file should have the same metadata, so just merge in the + # metadata from the last file + **file_metadata, + } return data, meta diff --git a/pvlib/iotools/srml.py b/pvlib/iotools/srml.py index 728c3a7093..8b092e8e32 100644 --- a/pvlib/iotools/srml.py +++ b/pvlib/iotools/srml.py @@ -1,6 +1,7 @@ """Collection of functions to operate on data from University of Oregon Solar Radiation Monitoring Laboratory (SRML) data. """ + import numpy as np import pandas as pd import urllib @@ -13,15 +14,15 @@ # numbers `here. `_ VARIABLE_MAP = { - '100': 'ghi', - '201': 'dni', - '300': 'dhi', - '920': 'wind_direction', - '921': 'wind_speed', - '930': 'temp_air', - '931': 'temp_dew', - '933': 'relative_humidity', - '937': 'temp_cell', + "100": "ghi", + "201": "dni", + "300": "dhi", + "920": "wind_direction", + "921": "wind_speed", + "930": "temp_air", + "931": "temp_dew", + "933": "relative_humidity", + "937": "temp_cell", } @@ -62,7 +63,7 @@ def read_srml(filename, map_variables=True): .. [2] `Archival (short interval) data files `_ """ - tsv_data = pd.read_csv(filename, delimiter='\t') + tsv_data = pd.read_csv(filename, delimiter="\t") data = _format_index(tsv_data) # Drop day of year and time columns data = data[data.columns[2:]] @@ -83,13 +84,15 @@ def read_srml(filename, map_variables=True): # '0.2': 'temp_air_2'} # columns = data.columns - flag_label_map = {flag: columns[columns.get_loc(flag) - 1] + '_flag' - for flag in columns[1::2]} + flag_label_map = { + flag: columns[columns.get_loc(flag) - 1] + "_flag" + for flag in columns[1::2] + } data = data.rename(columns=flag_label_map) # Mask data marked with quality flag 99 (bad or missing data) for col in columns[::2]: - missing = data[col + '_flag'] == 99 + missing = data[col + "_flag"] == 99 data[col] = data[col].where(~(missing), np.nan) return data @@ -108,7 +111,7 @@ def _map_columns(col): The pvlib label if it was found in the mapping, else the original label. """ - if col.startswith('7'): + if col.startswith("7"): # spectral data try: return VARIABLE_MAP[col] @@ -117,7 +120,7 @@ def _map_columns(col): try: variable_name = VARIABLE_MAP[col[:3]] variable_number = col[3:] - return variable_name + '_' + variable_number + return variable_name + "_" + variable_number except KeyError: return col @@ -161,17 +164,24 @@ def _format_index(df): # to correct to valid times. old_hours = df_time % 100 > 60 times = df_time.where(~old_hours, df_time - 40) - times = times.apply(lambda x: '{:04.0f}'.format(x)) - doy = df_doy.apply(lambda x: '{:03.0f}'.format(x)) - dts = pd.to_datetime(str(year) + '-' + doy + '-' + times, - format='%Y-%j-%H%M') + times = times.apply(lambda x: "{:04.0f}".format(x)) + doy = df_doy.apply(lambda x: "{:03.0f}".format(x)) + dts = pd.to_datetime( + str(year) + "-" + doy + "-" + times, format="%Y-%j-%H%M" + ) df.index = dts - df = df.tz_localize('Etc/GMT+8') + df = df.tz_localize("Etc/GMT+8") return df -def get_srml(station, start, end, filetype='PO', map_variables=True, - url="http://solardata.uoregon.edu/download/Archive/"): +def get_srml( + station, + start, + end, + filetype="PO", + map_variables=True, + url="http://solardata.uoregon.edu/download/Archive/", +): """Request data from UoO SRML and read it into a Dataframe. The University of Oregon Solar Radiation Monitoring Laboratory (SRML) is @@ -237,8 +247,9 @@ def get_srml(station, start, end, filetype='PO', map_variables=True, # Generate list of months months = pd.date_range( - start, end.replace(day=1) + pd.DateOffset(months=1), freq='1M') - months_str = months.strftime('%y%m') + start, end.replace(day=1) + pd.DateOffset(months=1), freq="1M" + ) + months_str = months.strftime("%y%m") # Generate list of filenames filenames = [f"{station}{filetype}{m}.txt" for m in months_str] @@ -251,10 +262,8 @@ def get_srml(station, start, end, filetype='PO', map_variables=True, except urllib.error.HTTPError: warnings.warn(f"The following file was not found: {f}") - data = pd.concat(dfs, axis='rows') + data = pd.concat(dfs, axis="rows") - meta = {'filetype': filetype, - 'station': station, - 'filenames': filenames} + meta = {"filetype": filetype, "station": station, "filenames": filenames} return data, meta diff --git a/pvlib/iotools/surfrad.py b/pvlib/iotools/surfrad.py index 77d9833034..f907fe8783 100644 --- a/pvlib/iotools/surfrad.py +++ b/pvlib/iotools/surfrad.py @@ -1,39 +1,80 @@ """ Import functions for NOAA SURFRAD Data. """ + import io from urllib.request import urlopen, Request import pandas as pd import numpy as np SURFRAD_COLUMNS = [ - 'year', 'jday', 'month', 'day', 'hour', 'minute', 'dt', 'zen', - 'dw_solar', 'dw_solar_flag', 'uw_solar', 'uw_solar_flag', 'direct_n', - 'direct_n_flag', 'diffuse', 'diffuse_flag', 'dw_ir', 'dw_ir_flag', - 'dw_casetemp', 'dw_casetemp_flag', 'dw_dometemp', 'dw_dometemp_flag', - 'uw_ir', 'uw_ir_flag', 'uw_casetemp', 'uw_casetemp_flag', 'uw_dometemp', - 'uw_dometemp_flag', 'uvb', 'uvb_flag', 'par', 'par_flag', 'netsolar', - 'netsolar_flag', 'netir', 'netir_flag', 'totalnet', 'totalnet_flag', - 'temp', 'temp_flag', 'rh', 'rh_flag', 'windspd', 'windspd_flag', - 'winddir', 'winddir_flag', 'pressure', 'pressure_flag'] + "year", + "jday", + "month", + "day", + "hour", + "minute", + "dt", + "zen", + "dw_solar", + "dw_solar_flag", + "uw_solar", + "uw_solar_flag", + "direct_n", + "direct_n_flag", + "diffuse", + "diffuse_flag", + "dw_ir", + "dw_ir_flag", + "dw_casetemp", + "dw_casetemp_flag", + "dw_dometemp", + "dw_dometemp_flag", + "uw_ir", + "uw_ir_flag", + "uw_casetemp", + "uw_casetemp_flag", + "uw_dometemp", + "uw_dometemp_flag", + "uvb", + "uvb_flag", + "par", + "par_flag", + "netsolar", + "netsolar_flag", + "netir", + "netir_flag", + "totalnet", + "totalnet_flag", + "temp", + "temp_flag", + "rh", + "rh_flag", + "windspd", + "windspd_flag", + "winddir", + "winddir_flag", + "pressure", + "pressure_flag", +] # Dictionary mapping surfrad variables to pvlib names VARIABLE_MAP = { - 'zen': 'solar_zenith', - 'dw_solar': 'ghi', - 'dw_solar_flag': 'ghi_flag', - 'direct_n': 'dni', - 'direct_n_flag': 'dni_flag', - 'diffuse': 'dhi', - 'diffuse_flag': 'dhi_flag', - 'temp': 'temp_air', - 'temp_flag': 'temp_air_flag', - 'windspd': 'wind_speed', - 'windspd_flag': 'wind_speed_flag', - 'winddir': 'wind_direction', - 'winddir_flag': 'wind_direction_flag', - 'rh': 'relative_humidity', - 'rh_flag': 'relative_humidity_flag' + "zen": "solar_zenith", + "dw_solar": "ghi", + "dw_solar_flag": "ghi_flag", + "direct_n": "dni", + "direct_n_flag": "dni_flag", + "diffuse": "dhi", + "diffuse_flag": "dhi_flag", + "temp": "temp_air", + "temp_flag": "temp_air_flag", + "windspd": "wind_speed", + "windspd_flag": "wind_speed_flag", + "winddir": "wind_direction", + "winddir_flag": "wind_direction_flag", + "rh": "relative_humidity", + "rh_flag": "relative_humidity_flag", } @@ -126,12 +167,12 @@ def read_surfrad(filename, map_variables=True): .. [3] `NOAA SURFRAD HTTP Index `_ """ - if str(filename).startswith('ftp') or str(filename).startswith('http'): + if str(filename).startswith("ftp") or str(filename).startswith("http"): req = Request(filename) response = urlopen(req) - file_buffer = io.StringIO(response.read().decode(errors='ignore')) + file_buffer = io.StringIO(response.read().decode(errors="ignore")) else: - file_buffer = open(str(filename), 'r') + file_buffer = open(str(filename), "r") # Read and parse the first two lines to build the metadata dict. station = file_buffer.readline() @@ -139,15 +180,16 @@ def read_surfrad(filename, map_variables=True): metadata_list = file_metadata.split() metadata = {} - metadata['name'] = station.strip() - metadata['latitude'] = float(metadata_list[0]) - metadata['longitude'] = float(metadata_list[1]) - metadata['elevation'] = float(metadata_list[2]) - metadata['surfrad_version'] = int(metadata_list[-1]) - metadata['tz'] = 'UTC' - - data = pd.read_csv(file_buffer, sep=r'\s+', - header=None, names=SURFRAD_COLUMNS) + metadata["name"] = station.strip() + metadata["latitude"] = float(metadata_list[0]) + metadata["longitude"] = float(metadata_list[1]) + metadata["elevation"] = float(metadata_list[2]) + metadata["surfrad_version"] = int(metadata_list[-1]) + metadata["tz"] = "UTC" + + data = pd.read_csv( + file_buffer, sep=r"\s+", header=None, names=SURFRAD_COLUMNS + ) file_buffer.close() data = _format_index(data) @@ -174,10 +216,10 @@ def _format_index(data): Dataframe with a DatetimeIndex localized to UTC. """ year = data.year.apply(str) - jday = data.jday.apply(lambda x: '{:03d}'.format(x)) - hours = data.hour.apply(lambda x: '{:02d}'.format(x)) - minutes = data.minute.apply(lambda x: '{:02d}'.format(x)) + jday = data.jday.apply(lambda x: "{:03d}".format(x)) + hours = data.hour.apply(lambda x: "{:02d}".format(x)) + minutes = data.minute.apply(lambda x: "{:02d}".format(x)) index = pd.to_datetime(year + jday + hours + minutes, format="%Y%j%H%M") data.index = index - data = data.tz_localize('UTC') + data = data.tz_localize("UTC") return data diff --git a/pvlib/iotools/tmy.py b/pvlib/iotools/tmy.py index fde96ee679..051c10cd09 100644 --- a/pvlib/iotools/tmy.py +++ b/pvlib/iotools/tmy.py @@ -8,24 +8,29 @@ # Dictionary mapping TMY3 names to pvlib names VARIABLE_MAP = { - 'GHI (W/m^2)': 'ghi', - 'ETR (W/m^2)': 'ghi_extra', - 'DNI (W/m^2)': 'dni', - 'ETRN (W/m^2)': 'dni_extra', - 'DHI (W/m^2)': 'dhi', - 'Pressure (mbar)': 'pressure', - 'Wdir (degrees)': 'wind_direction', - 'Wspd (m/s)': 'wind_speed', - 'Dry-bulb (C)': 'temp_air', - 'Dew-point (C)': 'temp_dew', - 'RHum (%)': 'relative_humidity', - 'Alb (unitless)': 'albedo', - 'Pwat (cm)': 'precipitable_water' + "GHI (W/m^2)": "ghi", + "ETR (W/m^2)": "ghi_extra", + "DNI (W/m^2)": "dni", + "ETRN (W/m^2)": "dni_extra", + "DHI (W/m^2)": "dhi", + "Pressure (mbar)": "pressure", + "Wdir (degrees)": "wind_direction", + "Wspd (m/s)": "wind_speed", + "Dry-bulb (C)": "temp_air", + "Dew-point (C)": "temp_dew", + "RHum (%)": "relative_humidity", + "Alb (unitless)": "albedo", + "Pwat (cm)": "precipitable_water", } -def read_tmy3(filename, coerce_year=None, map_variables=None, recolumn=None, - encoding=None): +def read_tmy3( + filename, + coerce_year=None, + map_variables=None, + recolumn=None, + encoding=None, +): """Read a TMY3 file into a pandas dataframe. Note that values contained in the metadata dictionary are unchanged @@ -191,32 +196,34 @@ def read_tmy3(filename, coerce_year=None, map_variables=None, recolumn=None, .. [3] `SolarAnywhere file formats `_ """ # noqa: E501 - head = ['USAF', 'Name', 'State', 'TZ', 'latitude', 'longitude', 'altitude'] + head = ["USAF", "Name", "State", "TZ", "latitude", "longitude", "altitude"] - with open(str(filename), 'r', encoding=encoding) as fbuf: + with open(str(filename), "r", encoding=encoding) as fbuf: # header information on the 1st line (0 indexing) firstline = fbuf.readline() # use pandas to read the csv file buffer # header is actually the second line, but tell pandas to look for data = pd.read_csv(fbuf, header=0) - meta = dict(zip(head, firstline.rstrip('\n').split(","))) + meta = dict(zip(head, firstline.rstrip("\n").split(","))) # convert metadata strings to numeric types - meta['altitude'] = float(meta['altitude']) - meta['latitude'] = float(meta['latitude']) - meta['longitude'] = float(meta['longitude']) - meta['TZ'] = float(meta['TZ']) - meta['USAF'] = int(meta['USAF']) + meta["altitude"] = float(meta["altitude"]) + meta["latitude"] = float(meta["latitude"]) + meta["longitude"] = float(meta["longitude"]) + meta["TZ"] = float(meta["TZ"]) + meta["USAF"] = int(meta["USAF"]) # get the date column as a pd.Series of numpy datetime64 - data_ymd = pd.to_datetime(data['Date (MM/DD/YYYY)'], format='%m/%d/%Y') + data_ymd = pd.to_datetime(data["Date (MM/DD/YYYY)"], format="%m/%d/%Y") # extract minutes - minutes = data['Time (HH:MM)'].str.split(':').str[1].astype(int) + minutes = data["Time (HH:MM)"].str.split(":").str[1].astype(int) # shift the time column so that midnite is 00:00 instead of 24:00 - shifted_hour = data['Time (HH:MM)'].str.split(':').str[0].astype(int) % 24 + shifted_hour = data["Time (HH:MM)"].str.split(":").str[0].astype(int) % 24 # shift the dates at midnight (24:00) so they correspond to the next day. # If midnight is specified as 00:00 do not shift date. - data_ymd[data['Time (HH:MM)'].str[:2] == '24'] += datetime.timedelta(days=1) # noqa: E501 + data_ymd[data["Time (HH:MM)"].str[:2] == "24"] += datetime.timedelta( + days=1 + ) # noqa: E501 # NOTE: as of pandas>=0.24 the pd.Series.array has a month attribute, but # in pandas-0.18.1, only DatetimeIndex has month, but indices are immutable # so we need to continue to work with the panda series of dates `data_ymd` @@ -228,33 +235,39 @@ def read_tmy3(filename, coerce_year=None, map_variables=None, recolumn=None, # timedeltas if coerce_year is not None: data_ymd = data_ymd.map(lambda dt: dt.replace(year=coerce_year)) - data_ymd.iloc[-1] = data_ymd.iloc[-1].replace(year=coerce_year+1) + data_ymd.iloc[-1] = data_ymd.iloc[-1].replace(year=coerce_year + 1) # NOTE: as of pvlib-0.6.3, min req is pandas-0.18.1, so pd.to_timedelta # unit must be in (D,h,m,s,ms,us,ns), but pandas>=0.24 allows unit='hour' - data.index = data_ymd + pd.to_timedelta(shifted_hour, unit='h') \ - + pd.to_timedelta(minutes, unit='min') + data.index = ( + data_ymd + + pd.to_timedelta(shifted_hour, unit="h") + + pd.to_timedelta(minutes, unit="min") + ) # shouldnt' specify both recolumn and map_variables if recolumn is not None and map_variables is not None: msg = "`map_variables` and `recolumn` cannot both be specified" raise ValueError(msg) elif map_variables is None and recolumn is not None: warnings.warn( - 'The recolumn parameter is deprecated and will be removed in ' - 'pvlib 0.11.0. Use `map_variables` instead, although note that ' - 'its behavior is different from `recolumn`.', - pvlibDeprecationWarning) + "The recolumn parameter is deprecated and will be removed in " + "pvlib 0.11.0. Use `map_variables` instead, although note that " + "its behavior is different from `recolumn`.", + pvlibDeprecationWarning, + ) elif map_variables is None and recolumn is None: warnings.warn( - 'TMY3 variable names will be renamed to pvlib conventions by ' - 'default starting in pvlib 0.11.0. Specify map_variables=True ' - 'to enable that behavior now, or specify map_variables=False ' - 'to hide this warning.', pvlibDeprecationWarning) + "TMY3 variable names will be renamed to pvlib conventions by " + "default starting in pvlib 0.11.0. Specify map_variables=True " + "to enable that behavior now, or specify map_variables=False " + "to hide this warning.", + pvlibDeprecationWarning, + ) if map_variables: data = data.rename(columns=VARIABLE_MAP) elif recolumn or (recolumn is None and map_variables is None): data = _recolumn(data) - data = data.tz_localize(int(meta['TZ'] * 3600)) + data = data.tz_localize(int(meta["TZ"] * 3600)) return data, meta @@ -274,29 +287,81 @@ def _recolumn(tmy3_dataframe): Recolumned DataFrame. """ # paste in the header as one long line - raw_columns = 'ETR (W/m^2),ETRN (W/m^2),GHI (W/m^2),GHI source,GHI uncert (%),DNI (W/m^2),DNI source,DNI uncert (%),DHI (W/m^2),DHI source,DHI uncert (%),GH illum (lx),GH illum source,Global illum uncert (%),DN illum (lx),DN illum source,DN illum uncert (%),DH illum (lx),DH illum source,DH illum uncert (%),Zenith lum (cd/m^2),Zenith lum source,Zenith lum uncert (%),TotCld (tenths),TotCld source,TotCld uncert (code),OpqCld (tenths),OpqCld source,OpqCld uncert (code),Dry-bulb (C),Dry-bulb source,Dry-bulb uncert (code),Dew-point (C),Dew-point source,Dew-point uncert (code),RHum (%),RHum source,RHum uncert (code),Pressure (mbar),Pressure source,Pressure uncert (code),Wdir (degrees),Wdir source,Wdir uncert (code),Wspd (m/s),Wspd source,Wspd uncert (code),Hvis (m),Hvis source,Hvis uncert (code),CeilHgt (m),CeilHgt source,CeilHgt uncert (code),Pwat (cm),Pwat source,Pwat uncert (code),AOD (unitless),AOD source,AOD uncert (code),Alb (unitless),Alb source,Alb uncert (code),Lprecip depth (mm),Lprecip quantity (hr),Lprecip source,Lprecip uncert (code),PresWth (METAR code),PresWth source,PresWth uncert (code)' # noqa: E501 + raw_columns = "ETR (W/m^2),ETRN (W/m^2),GHI (W/m^2),GHI source,GHI uncert (%),DNI (W/m^2),DNI source,DNI uncert (%),DHI (W/m^2),DHI source,DHI uncert (%),GH illum (lx),GH illum source,Global illum uncert (%),DN illum (lx),DN illum source,DN illum uncert (%),DH illum (lx),DH illum source,DH illum uncert (%),Zenith lum (cd/m^2),Zenith lum source,Zenith lum uncert (%),TotCld (tenths),TotCld source,TotCld uncert (code),OpqCld (tenths),OpqCld source,OpqCld uncert (code),Dry-bulb (C),Dry-bulb source,Dry-bulb uncert (code),Dew-point (C),Dew-point source,Dew-point uncert (code),RHum (%),RHum source,RHum uncert (code),Pressure (mbar),Pressure source,Pressure uncert (code),Wdir (degrees),Wdir source,Wdir uncert (code),Wspd (m/s),Wspd source,Wspd uncert (code),Hvis (m),Hvis source,Hvis uncert (code),CeilHgt (m),CeilHgt source,CeilHgt uncert (code),Pwat (cm),Pwat source,Pwat uncert (code),AOD (unitless),AOD source,AOD uncert (code),Alb (unitless),Alb source,Alb uncert (code),Lprecip depth (mm),Lprecip quantity (hr),Lprecip source,Lprecip uncert (code),PresWth (METAR code),PresWth source,PresWth uncert (code)" # noqa: E501 new_columns = [ - 'ETR', 'ETRN', 'GHI', 'GHISource', 'GHIUncertainty', - 'DNI', 'DNISource', 'DNIUncertainty', 'DHI', 'DHISource', - 'DHIUncertainty', 'GHillum', 'GHillumSource', 'GHillumUncertainty', - 'DNillum', 'DNillumSource', 'DNillumUncertainty', 'DHillum', - 'DHillumSource', 'DHillumUncertainty', 'Zenithlum', - 'ZenithlumSource', 'ZenithlumUncertainty', 'TotCld', 'TotCldSource', - 'TotCldUncertainty', 'OpqCld', 'OpqCldSource', 'OpqCldUncertainty', - 'DryBulb', 'DryBulbSource', 'DryBulbUncertainty', 'DewPoint', - 'DewPointSource', 'DewPointUncertainty', 'RHum', 'RHumSource', - 'RHumUncertainty', 'Pressure', 'PressureSource', - 'PressureUncertainty', 'Wdir', 'WdirSource', 'WdirUncertainty', - 'Wspd', 'WspdSource', 'WspdUncertainty', 'Hvis', 'HvisSource', - 'HvisUncertainty', 'CeilHgt', 'CeilHgtSource', 'CeilHgtUncertainty', - 'Pwat', 'PwatSource', 'PwatUncertainty', 'AOD', 'AODSource', - 'AODUncertainty', 'Alb', 'AlbSource', 'AlbUncertainty', - 'Lprecipdepth', 'Lprecipquantity', 'LprecipSource', - 'LprecipUncertainty', 'PresWth', 'PresWthSource', - 'PresWthUncertainty'] - - mapping = dict(zip(raw_columns.split(','), new_columns)) + "ETR", + "ETRN", + "GHI", + "GHISource", + "GHIUncertainty", + "DNI", + "DNISource", + "DNIUncertainty", + "DHI", + "DHISource", + "DHIUncertainty", + "GHillum", + "GHillumSource", + "GHillumUncertainty", + "DNillum", + "DNillumSource", + "DNillumUncertainty", + "DHillum", + "DHillumSource", + "DHillumUncertainty", + "Zenithlum", + "ZenithlumSource", + "ZenithlumUncertainty", + "TotCld", + "TotCldSource", + "TotCldUncertainty", + "OpqCld", + "OpqCldSource", + "OpqCldUncertainty", + "DryBulb", + "DryBulbSource", + "DryBulbUncertainty", + "DewPoint", + "DewPointSource", + "DewPointUncertainty", + "RHum", + "RHumSource", + "RHumUncertainty", + "Pressure", + "PressureSource", + "PressureUncertainty", + "Wdir", + "WdirSource", + "WdirUncertainty", + "Wspd", + "WspdSource", + "WspdUncertainty", + "Hvis", + "HvisSource", + "HvisUncertainty", + "CeilHgt", + "CeilHgtSource", + "CeilHgtUncertainty", + "Pwat", + "PwatSource", + "PwatUncertainty", + "AOD", + "AODSource", + "AODUncertainty", + "Alb", + "AlbSource", + "AlbUncertainty", + "Lprecipdepth", + "Lprecipquantity", + "LprecipSource", + "LprecipUncertainty", + "PresWth", + "PresWthSource", + "PresWthUncertainty", + ] + + mapping = dict(zip(raw_columns.split(","), new_columns)) return tmy3_dataframe.rename(columns=mapping) @@ -431,9 +496,9 @@ def read_tmy2(filename): :doi:`10.2172/87130` """ # noqa: E501 # paste in the column info as one long line - string = '%2d%2d%2d%2d%4d%4d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%2d%1s%1d%2d%1s%1d%4d%1s%1d%4d%1s%1d%3d%1s%1d%4d%1s%1d%3d%1s%1d%3d%1s%1d%4d%1s%1d%5d%1s%1d%10d%3d%1s%1d%3d%1s%1d%3d%1s%1d%2d%1s%1d' # noqa: E501 - columns = 'year,month,day,hour,ETR,ETRN,GHI,GHISource,GHIUncertainty,DNI,DNISource,DNIUncertainty,DHI,DHISource,DHIUncertainty,GHillum,GHillumSource,GHillumUncertainty,DNillum,DNillumSource,DNillumUncertainty,DHillum,DHillumSource,DHillumUncertainty,Zenithlum,ZenithlumSource,ZenithlumUncertainty,TotCld,TotCldSource,TotCldUncertainty,OpqCld,OpqCldSource,OpqCldUncertainty,DryBulb,DryBulbSource,DryBulbUncertainty,DewPoint,DewPointSource,DewPointUncertainty,RHum,RHumSource,RHumUncertainty,Pressure,PressureSource,PressureUncertainty,Wdir,WdirSource,WdirUncertainty,Wspd,WspdSource,WspdUncertainty,Hvis,HvisSource,HvisUncertainty,CeilHgt,CeilHgtSource,CeilHgtUncertainty,PresentWeather,Pwat,PwatSource,PwatUncertainty,AOD,AODSource,AODUncertainty,SnowDepth,SnowDepthSource,SnowDepthUncertainty,LastSnowfall,LastSnowfallSource,LastSnowfallUncertaint' # noqa: E501 - hdr_columns = 'WBAN,City,State,TZ,latitude,longitude,altitude' + string = "%2d%2d%2d%2d%4d%4d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%4d%1s%1d%2d%1s%1d%2d%1s%1d%4d%1s%1d%4d%1s%1d%3d%1s%1d%4d%1s%1d%3d%1s%1d%3d%1s%1d%4d%1s%1d%5d%1s%1d%10d%3d%1s%1d%3d%1s%1d%3d%1s%1d%2d%1s%1d" # noqa: E501 + columns = "year,month,day,hour,ETR,ETRN,GHI,GHISource,GHIUncertainty,DNI,DNISource,DNIUncertainty,DHI,DHISource,DHIUncertainty,GHillum,GHillumSource,GHillumUncertainty,DNillum,DNillumSource,DNillumUncertainty,DHillum,DHillumSource,DHillumUncertainty,Zenithlum,ZenithlumSource,ZenithlumUncertainty,TotCld,TotCldSource,TotCldUncertainty,OpqCld,OpqCldSource,OpqCldUncertainty,DryBulb,DryBulbSource,DryBulbUncertainty,DewPoint,DewPointSource,DewPointUncertainty,RHum,RHumSource,RHumUncertainty,Pressure,PressureSource,PressureUncertainty,Wdir,WdirSource,WdirUncertainty,Wspd,WspdSource,WspdUncertainty,Hvis,HvisSource,HvisUncertainty,CeilHgt,CeilHgtSource,CeilHgtUncertainty,PresentWeather,Pwat,PwatSource,PwatUncertainty,AOD,AODSource,AODUncertainty,SnowDepth,SnowDepthSource,SnowDepthUncertainty,LastSnowfall,LastSnowfallSource,LastSnowfallUncertaint" # noqa: E501 + hdr_columns = "WBAN,City,State,TZ,latitude,longitude,altitude" tmy2, tmy2_meta = _read_tmy2(string, columns, hdr_columns, str(filename)) @@ -460,17 +525,19 @@ def _parsemeta_tmy2(columns, line): meta = rawmeta[:3] # take the first string entries meta.append(int(rawmeta[3])) # Convert to decimal notation with S negative - longitude = ( - float(rawmeta[5]) + float(rawmeta[6])/60) * (2*(rawmeta[4] == 'N') - 1) + longitude = (float(rawmeta[5]) + float(rawmeta[6]) / 60) * ( + 2 * (rawmeta[4] == "N") - 1 + ) # Convert to decimal notation with W negative - latitude = ( - float(rawmeta[8]) + float(rawmeta[9])/60) * (2*(rawmeta[7] == 'E') - 1) + latitude = (float(rawmeta[8]) + float(rawmeta[9]) / 60) * ( + 2 * (rawmeta[7] == "E") - 1 + ) meta.append(longitude) meta.append(latitude) meta.append(float(rawmeta[10])) # Creates a dictionary of metadata - meta_dict = dict(zip(columns.split(','), meta)) + meta_dict = dict(zip(columns.split(","), meta)) return meta_dict @@ -488,36 +555,42 @@ def _read_tmy2(string, columns, hdr_columns, fname): # Reset the cursor and array for each line cursor = 1 part = [] - for marker in string.split('%'): + for marker in string.split("%"): # Skip the first line of markers - if marker == '': + if marker == "": continue # Read the next increment from the marker list - increment = int(re.findall(r'\d+', marker)[0]) + increment = int(re.findall(r"\d+", marker)[0]) next_cursor = cursor + increment # Extract the value from the line in the file - val = (line[cursor:next_cursor]) + val = line[cursor:next_cursor] # increment the cursor by the length of the read value cursor = next_cursor # Determine the datatype from the marker string - if marker[-1] == 'd': + if marker[-1] == "d": try: val = float(val) except ValueError: - raise ValueError('WARNING: In {} Read value is not an ' - 'integer " {} " '.format(fname, val)) - elif marker[-1] == 's': + raise ValueError( + "WARNING: In {} Read value is not an " + 'integer " {} " '.format(fname, val) + ) + elif marker[-1] == "s": try: val = str(val) except ValueError: - raise ValueError('WARNING: In {} Read value is not a ' - 'string " {} " '.format(fname, val)) + raise ValueError( + "WARNING: In {} Read value is not a " + 'string " {} " '.format(fname, val) + ) else: - raise Exception('WARNING: In {} Improper column DataFrame ' - '" %{} " '.format(__name__, marker)) + raise Exception( + "WARNING: In {} Improper column DataFrame " + '" %{} " '.format(__name__, marker) + ) part.append(val) @@ -529,13 +602,17 @@ def _read_tmy2(string, columns, hdr_columns, fname): axes.append(part) # Create datetime objects from read data - date.append(datetime.datetime(year=int(year), - month=int(part[1]), - day=int(part[2]), - hour=(int(part[3]) - 1))) + date.append( + datetime.datetime( + year=int(year), + month=int(part[1]), + day=int(part[2]), + hour=(int(part[3]) - 1), + ) + ) data = pd.DataFrame( - axes, index=date, - columns=columns.split(',')).tz_localize(int(meta['TZ'] * 3600)) + axes, index=date, columns=columns.split(",") + ).tz_localize(int(meta["TZ"] * 3600)) return data, meta diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py index 7fbb1ea985..e1fb14f58c 100644 --- a/pvlib/irradiance.py +++ b/pvlib/irradiance.py @@ -22,16 +22,23 @@ # Deprecation warning based on https://peps.python.org/pep-0562/ def __getattr__(attr): - if attr == 'SURFACE_ALBEDOS': - warnings.warn(f"{attr} has been moved to the albedo module as of " - "v0.11.0. Please use pvlib.albedo.SURFACE_ALBEDOS.", - pvlibDeprecationWarning) + if attr == "SURFACE_ALBEDOS": + warnings.warn( + f"{attr} has been moved to the albedo module as of " + "v0.11.0. Please use pvlib.albedo.SURFACE_ALBEDOS.", + pvlibDeprecationWarning, + ) return pvlib.albedo.SURFACE_ALBEDOS raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") -def get_extra_radiation(datetime_or_doy, solar_constant=1366.1, - method='spencer', epoch_year=2014, **kwargs): +def get_extra_radiation( + datetime_or_doy, + solar_constant=1366.1, + method="spencer", + epoch_year=2014, + **kwargs, +): """ Determine extraterrestrial radiation from day of year. @@ -83,28 +90,36 @@ def get_extra_radiation(datetime_or_doy, solar_constant=1366.1, Civil Engineers, Ed. R. G. Allen et al. """ - to_doy, to_datetimeindex, to_output = \ - _handle_extra_radiation_types(datetime_or_doy, epoch_year) + to_doy, to_datetimeindex, to_output = _handle_extra_radiation_types( + datetime_or_doy, epoch_year + ) # consider putting asce and spencer methods in their own functions method = method.lower() - if method == 'asce': - B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy), - offset=0) + if method == "asce": + B = solarposition._calculate_simple_day_angle( + to_doy(datetime_or_doy), offset=0 + ) RoverR0sqrd = 1 + 0.033 * np.cos(B) - elif method == 'spencer': + elif method == "spencer": B = solarposition._calculate_simple_day_angle(to_doy(datetime_or_doy)) - RoverR0sqrd = (1.00011 + 0.034221 * np.cos(B) + 0.00128 * np.sin(B) + - 0.000719 * np.cos(2 * B) + 7.7e-05 * np.sin(2 * B)) - elif method == 'pyephem': + RoverR0sqrd = ( + 1.00011 + + 0.034221 * np.cos(B) + + 0.00128 * np.sin(B) + + 0.000719 * np.cos(2 * B) + + 7.7e-05 * np.sin(2 * B) + ) + elif method == "pyephem": times = to_datetimeindex(datetime_or_doy) RoverR0sqrd = solarposition.pyephem_earthsun_distance(times) ** (-2) - elif method == 'nrel': + elif method == "nrel": times = to_datetimeindex(datetime_or_doy) - RoverR0sqrd = \ - solarposition.nrel_earthsun_distance(times, **kwargs) ** (-2) + RoverR0sqrd = solarposition.nrel_earthsun_distance( + times, **kwargs + ) ** (-2) else: - raise ValueError('Invalid method: %s', method) + raise ValueError("Invalid method: %s", method) Ea = solar_constant * RoverR0sqrd @@ -122,28 +137,38 @@ def _handle_extra_radiation_types(datetime_or_doy, epoch_year): # a better way to do it. if isinstance(datetime_or_doy, pd.DatetimeIndex): to_doy = tools._pandas_to_doy # won't be evaluated unless necessary - def to_datetimeindex(x): return x # noqa: E306 + + def to_datetimeindex(x): + return x # noqa: E306 + to_output = partial(pd.Series, index=datetime_or_doy) elif isinstance(datetime_or_doy, pd.Timestamp): to_doy = tools._pandas_to_doy - to_datetimeindex = \ - tools._datetimelike_scalar_to_datetimeindex + to_datetimeindex = tools._datetimelike_scalar_to_datetimeindex to_output = tools._scalar_out - elif isinstance(datetime_or_doy, - (datetime.date, datetime.datetime, np.datetime64)): + elif isinstance( + datetime_or_doy, (datetime.date, datetime.datetime, np.datetime64) + ): to_doy = tools._datetimelike_scalar_to_doy - to_datetimeindex = \ - tools._datetimelike_scalar_to_datetimeindex + to_datetimeindex = tools._datetimelike_scalar_to_datetimeindex to_output = tools._scalar_out elif np.isscalar(datetime_or_doy): # ints and floats of various types - def to_doy(x): return x # noqa: E306 - to_datetimeindex = partial(tools._doy_to_datetimeindex, - epoch_year=epoch_year) + + def to_doy(x): + return x # noqa: E306 + + to_datetimeindex = partial( + tools._doy_to_datetimeindex, epoch_year=epoch_year + ) to_output = tools._scalar_out else: # assume that we have an array-like object of doy - def to_doy(x): return x # noqa: E306 - to_datetimeindex = partial(tools._doy_to_datetimeindex, - epoch_year=epoch_year) + + def to_doy(x): + return x # noqa: E306 + + to_datetimeindex = partial( + tools._doy_to_datetimeindex, epoch_year=epoch_year + ) to_output = tools._array_out return to_doy, to_datetimeindex, to_output @@ -176,16 +201,17 @@ def aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth): Dot product of panel normal and solar angle. """ - projection = ( - tools.cosd(surface_tilt) * tools.cosd(solar_zenith) + - tools.sind(surface_tilt) * tools.sind(solar_zenith) * - tools.cosd(solar_azimuth - surface_azimuth)) + projection = tools.cosd(surface_tilt) * tools.cosd( + solar_zenith + ) + tools.sind(surface_tilt) * tools.sind(solar_zenith) * tools.cosd( + solar_azimuth - surface_azimuth + ) # GH 1185 projection = np.clip(projection, -1, 1) try: - projection.name = 'aoi_projection' + projection.name = "aoi_projection" except AttributeError: pass @@ -216,20 +242,22 @@ def aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth): Angle of incidence in degrees. """ - projection = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + projection = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) aoi_value = np.rad2deg(np.arccos(projection)) try: - aoi_value.name = 'aoi' + aoi_value.name = "aoi" except AttributeError: pass return aoi_value -def beam_component(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - dni): +def beam_component( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni +): """ Calculates the beam component of the plane of array irradiance. @@ -251,19 +279,29 @@ def beam_component(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, beam : numeric Beam component """ - beam = dni * aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + beam = dni * aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) beam = np.maximum(beam, 0) return beam -def get_total_irradiance(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - dni, ghi, dhi, dni_extra=None, airmass=None, - albedo=0.25, surface_type=None, - model='isotropic', - model_perez='allsitescomposite1990'): +def get_total_irradiance( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=None, + airmass=None, + albedo=0.25, + surface_type=None, + model="isotropic", + model_perez="allsitescomposite1990", +): r""" Determine total in-plane irradiance and its beam, sky diffuse and ground reflected components, using the specified sky diffuse irradiance model. @@ -331,22 +369,40 @@ def get_total_irradiance(surface_tilt, surface_azimuth, """ poa_sky_diffuse = get_sky_diffuse( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, model=model, - model_perez=model_perez) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=dni_extra, + airmass=airmass, + model=model, + model_perez=model_perez, + ) - poa_ground_diffuse = get_ground_diffuse(surface_tilt, ghi, albedo, - surface_type) + poa_ground_diffuse = get_ground_diffuse( + surface_tilt, ghi, albedo, surface_type + ) aoi_ = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) irrads = poa_components(aoi_, dni, poa_sky_diffuse, poa_ground_diffuse) return irrads -def get_sky_diffuse(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - dni, ghi, dhi, dni_extra=None, airmass=None, - model='isotropic', - model_perez='allsitescomposite1990'): +def get_sky_diffuse( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=None, + airmass=None, + model="isotropic", + model_perez="allsitescomposite1990", +): r""" Determine in-plane sky diffuse irradiance component using the specified sky diffuse irradiance model. @@ -416,41 +472,82 @@ def get_sky_diffuse(surface_tilt, surface_azimuth, model = model.lower() - if dni_extra is None and model in {'haydavies', 'reindl', - 'perez', 'perez-driesse'}: - raise ValueError(f'dni_extra is required for model {model}') + if dni_extra is None and model in { + "haydavies", + "reindl", + "perez", + "perez-driesse", + }: + raise ValueError(f"dni_extra is required for model {model}") - if model == 'isotropic': + if model == "isotropic": sky = isotropic(surface_tilt, dhi) - elif model == 'klucher': - sky = klucher(surface_tilt, surface_azimuth, dhi, ghi, - solar_zenith, solar_azimuth) - elif model == 'haydavies': - sky = haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith, solar_azimuth) - elif model == 'reindl': - sky = reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, - solar_zenith, solar_azimuth) - elif model == 'king': + elif model == "klucher": + sky = klucher( + surface_tilt, + surface_azimuth, + dhi, + ghi, + solar_zenith, + solar_azimuth, + ) + elif model == "haydavies": + sky = haydavies( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + ) + elif model == "reindl": + sky = reindl( + surface_tilt, + surface_azimuth, + dhi, + dni, + ghi, + dni_extra, + solar_zenith, + solar_azimuth, + ) + elif model == "king": sky = king(surface_tilt, dhi, ghi, solar_zenith) - elif model == 'perez': + elif model == "perez": if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - sky = perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith, solar_azimuth, airmass, - model=model_perez) - elif model == 'perez-driesse': + sky = perez( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + airmass, + model=model_perez, + ) + elif model == "perez-driesse": # perez_driesse will calculate its own airmass if needed - sky = perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith, solar_azimuth, airmass) + sky = perez_driesse( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + airmass, + ) else: - raise ValueError(f'invalid model selection {model}') + raise ValueError(f"invalid model selection {model}") return sky def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse): - r''' + r""" Determine in-plane irradiance components. Combines DNI with sky diffuse and ground-reflected irradiance to calculate @@ -490,18 +587,18 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse): ------ Negative beam irradiation due to aoi :math:`> 90^{\circ}` or AOI :math:`< 0^{\circ}` is set to zero. - ''' + """ poa_direct = np.maximum(dni * np.cos(np.radians(aoi)), 0) poa_diffuse = poa_sky_diffuse + poa_ground_diffuse poa_global = poa_direct + poa_diffuse irrads = OrderedDict() - irrads['poa_global'] = poa_global - irrads['poa_direct'] = poa_direct - irrads['poa_diffuse'] = poa_diffuse - irrads['poa_sky_diffuse'] = poa_sky_diffuse - irrads['poa_ground_diffuse'] = poa_ground_diffuse + irrads["poa_global"] = poa_global + irrads["poa_direct"] = poa_direct + irrads["poa_diffuse"] = poa_diffuse + irrads["poa_sky_diffuse"] = poa_sky_diffuse + irrads["poa_ground_diffuse"] = poa_ground_diffuse if isinstance(poa_direct, pd.Series): irrads = pd.DataFrame(irrads) @@ -509,8 +606,8 @@ def poa_components(aoi, dni, poa_sky_diffuse, poa_ground_diffuse): return irrads -def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None): - r''' +def get_ground_diffuse(surface_tilt, ghi, albedo=0.25, surface_type=None): + r""" Estimate diffuse irradiance on a tilted surface from ground reflections. Ground diffuse irradiance is calculated as @@ -563,7 +660,7 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None): .. [4] Payne, R. E. "Albedo of the Sea Surface". J. Atmos. Sci., 29, pp. 959–970, 1972. :doi:`10.1175/1520-0469(1972)029<0959:AOTSS>2.0.CO;2` - ''' + """ if surface_type is not None: albedo = pvlib.albedo.SURFACE_ALBEDOS[surface_type] @@ -571,7 +668,7 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None): diffuse_irrad = ghi * albedo * (1 - np.cos(np.radians(surface_tilt))) * 0.5 try: - diffuse_irrad.name = 'diffuse_ground' + diffuse_irrad.name = "diffuse_ground" except AttributeError: pass @@ -579,7 +676,7 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None): def isotropic(surface_tilt, dhi): - r''' + r""" Determine diffuse irradiance from the sky on a tilted surface using the isotropic sky model. @@ -620,15 +717,16 @@ def isotropic(surface_tilt, dhi): meaning, and significance of the isotropic sky model" 2020, Solar Energy vol. 201. pp. 8-12 :doi:`10.1016/j.solener.2020.02.067` - ''' + """ sky_diffuse = dhi * (1 + tools.cosd(surface_tilt)) * 0.5 return sky_diffuse -def klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, - solar_azimuth): - r''' +def klucher( + surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth +): + r""" Determine diffuse irradiance from the sky on a tilted surface using the Klucher (1979) model. @@ -698,15 +796,16 @@ def klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 :doi:`10.1016/j.solener.2006.03.009` - ''' + """ # zenith angle with respect to panel normal. - cos_tt = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + cos_tt = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) cos_tt = np.maximum(cos_tt, 0) # GH 526 # silence warning from 0 / 0 - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): F = 1 - ((dhi / ghi) ** 2) try: @@ -717,17 +816,25 @@ def klucher(surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, term1 = 0.5 * (1 + tools.cosd(surface_tilt)) term2 = 1 + F * (tools.sind(0.5 * surface_tilt) ** 3) - term3 = 1 + F * (cos_tt ** 2) * (tools.sind(solar_zenith) ** 3) + term3 = 1 + F * (cos_tt**2) * (tools.sind(solar_zenith) ** 3) sky_diffuse = dhi * term1 * term2 * term3 return sky_diffuse -def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith=None, solar_azimuth=None, projection_ratio=None, - return_components=False): - r''' +def haydavies( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith=None, + solar_azimuth=None, + projection_ratio=None, + return_components=False, +): + r""" Determine diffuse irradiance from the sky on a tilted surface using the Hay and Davies (1980) model. @@ -826,12 +933,13 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 :doi:`10.1016/j.solener.2006.03.009` - ''' + """ # if necessary, calculate ratio of titled and horizontal beam irradiance if projection_ratio is None: - cos_tt = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + cos_tt = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) cos_tt = np.maximum(cos_tt, 0) # GH 526 cos_solar_zenith = tools.cosd(solar_zenith) Rb = cos_tt / np.maximum(cos_solar_zenith, 0.01745) # GH 432 @@ -851,13 +959,14 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, if return_components: diffuse_components = OrderedDict() - diffuse_components['sky_diffuse'] = sky_diffuse + diffuse_components["sky_diffuse"] = sky_diffuse # Calculate the individual components - diffuse_components['isotropic'] = poa_isotropic - diffuse_components['circumsolar'] = poa_circumsolar - diffuse_components['horizon'] = np.where( - np.isnan(diffuse_components['isotropic']), np.nan, 0.) + diffuse_components["isotropic"] = poa_isotropic + diffuse_components["circumsolar"] = poa_circumsolar + diffuse_components["horizon"] = np.where( + np.isnan(diffuse_components["isotropic"]), np.nan, 0.0 + ) if isinstance(sky_diffuse, pd.Series): diffuse_components = pd.DataFrame(diffuse_components) @@ -866,9 +975,17 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, return sky_diffuse -def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, - solar_zenith, solar_azimuth): - r''' +def reindl( + surface_tilt, + surface_azimuth, + dhi, + dni, + ghi, + dni_extra, + solar_zenith, + solar_azimuth, +): + r""" Determine the diffuse irradiance from the sky on a tilted surface using the Reindl (1990) model. @@ -953,10 +1070,11 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, compute solar irradiance on inclined surfaces for building energy simulation. Solar Energy 81(2), 254-267 :doi:`10.1016/j.solener.2006.03.009` - ''' + """ - cos_tt = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + cos_tt = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) cos_tt = np.maximum(cos_tt, 0) # GH 526 # do not apply cos(zen) limit here (needed for HB below) @@ -975,9 +1093,9 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, # these are the () and [] sub-terms of the second term of eqn 8 term1 = 1 - AI term2 = 0.5 * (1 + tools.cosd(surface_tilt)) - with np.errstate(invalid='ignore', divide='ignore'): + with np.errstate(invalid="ignore", divide="ignore"): hb_to_ghi = np.where(ghi == 0, 0, np.divide(HB, ghi)) - term3 = 1 + np.sqrt(hb_to_ghi) * (tools.sind(0.5 * surface_tilt)**3) + term3 = 1 + np.sqrt(hb_to_ghi) * (tools.sind(0.5 * surface_tilt) ** 3) sky_diffuse = dhi * (AI * Rb + term1 * term2 * term3) sky_diffuse = np.maximum(sky_diffuse, 0) @@ -985,7 +1103,7 @@ def reindl(surface_tilt, surface_azimuth, dhi, dni, ghi, dni_extra, def king(surface_tilt, dhi, ghi, solar_zenith): - ''' + """ Determine diffuse irradiance from the sky on a tilted surface using the King model. @@ -1016,20 +1134,33 @@ def king(surface_tilt, dhi, ghi, solar_zenith): -------- poa_sky_diffuse : numeric The diffuse component of the solar radiation. - ''' + """ - sky_diffuse = (dhi * (1 + tools.cosd(surface_tilt)) / 2 + ghi * - (0.012 * solar_zenith - 0.04) * - (1 - tools.cosd(surface_tilt)) / 2) + sky_diffuse = ( + dhi * (1 + tools.cosd(surface_tilt)) / 2 + + ghi + * (0.012 * solar_zenith - 0.04) + * (1 - tools.cosd(surface_tilt)) + / 2 + ) sky_diffuse = np.maximum(sky_diffuse, 0) return sky_diffuse -def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith, solar_azimuth, airmass, - model='allsitescomposite1990', return_components=False): - ''' +def perez( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + airmass, + model="allsitescomposite1990", + return_components=False, +): + """ Determine diffuse irradiance from the sky on a tilted surface using one of the Perez models. @@ -1141,7 +1272,7 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, .. [4] Perez, R. et. al 1988. "The Development and Verification of the Perez Diffuse Radiation Model". SAND88-7030 - ''' + """ kappa = 1.041 # for solar_zenith in radians z = np.radians(solar_zenith) # convert to radians @@ -1150,8 +1281,8 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, delta = dhi * airmass / dni_extra # epsilon is the sky's "clearness" - with np.errstate(invalid='ignore'): - eps = ((dhi + dni) / dhi + kappa * (z ** 3)) / (1 + kappa * (z ** 3)) + with np.errstate(invalid="ignore"): + eps = ((dhi + dni) / dhi + kappa * (z**3)) / (1 + kappa * (z**3)) # numpy indexing below will not work with a Series if isinstance(eps, pd.Series): @@ -1161,7 +1292,7 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, # rules. 1 = overcast ... 8 = clear (these names really only make # sense for small zenith angles, but...) these values will # eventually be used as indicies for coeffecient look ups - ebin = np.digitize(eps, (0., 1.065, 1.23, 1.5, 1.95, 2.8, 4.5, 6.2)) + ebin = np.digitize(eps, (0.0, 1.065, 1.23, 1.5, 1.95, 2.8, 4.5, 6.2)) ebin = np.array(ebin) # GH 642 ebin[np.isnan(eps)] = 0 @@ -1178,13 +1309,14 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, F1c = np.vstack((F1c, nans)) F2c = np.vstack((F2c, nans)) - F1 = (F1c[ebin, 0] + F1c[ebin, 1] * delta + F1c[ebin, 2] * z) + F1 = F1c[ebin, 0] + F1c[ebin, 1] * delta + F1c[ebin, 2] * z F1 = np.maximum(F1, 0) - F2 = (F2c[ebin, 0] + F2c[ebin, 1] * delta + F2c[ebin, 2] * z) + F2 = F2c[ebin, 0] + F2c[ebin, 1] * delta + F2c[ebin, 2] * z - A = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + A = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) A = np.maximum(A, 0) B = tools.cosd(solar_zenith) @@ -1205,12 +1337,12 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, if return_components: diffuse_components = OrderedDict() - diffuse_components['sky_diffuse'] = sky_diffuse + diffuse_components["sky_diffuse"] = sky_diffuse # Calculate the different components - diffuse_components['isotropic'] = dhi * term1 - diffuse_components['circumsolar'] = dhi * term2 - diffuse_components['horizon'] = dhi * term3 + diffuse_components["isotropic"] = dhi * term1 + diffuse_components["circumsolar"] = dhi * term2 + diffuse_components["horizon"] = dhi * term3 # Set values of components to 0 when sky_diffuse is 0 mask = sky_diffuse == 0 @@ -1218,44 +1350,46 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra, diffuse_components = pd.DataFrame(diffuse_components) diffuse_components.loc[mask] = 0 else: - diffuse_components = {k: np.where(mask, 0, v) for k, v in - diffuse_components.items()} + diffuse_components = { + k: np.where(mask, 0, v) for k, v in diffuse_components.items() + } return diffuse_components else: return sky_diffuse def _calc_delta(dhi, dni_extra, solar_zenith, airmass=None): - ''' + """ Compute the delta parameter, which represents sky dome "brightness" in the Perez and Perez-Driesse models. Helper function for perez_driesse transposition. - ''' + """ if airmass is None: # use the same airmass model as in the original perez work - airmass = atmosphere.get_relative_airmass(solar_zenith, - 'kastenyoung1989') + airmass = atmosphere.get_relative_airmass( + solar_zenith, "kastenyoung1989" + ) - max_airmass = atmosphere.get_relative_airmass(90, 'kastenyoung1989') + max_airmass = atmosphere.get_relative_airmass(90, "kastenyoung1989") airmass = np.where(solar_zenith >= 90, max_airmass, airmass) return dhi / (dni_extra / airmass) def _calc_zeta(dhi, dni, zenith): - ''' + """ Compute the zeta parameter, which represents sky dome "clearness" in the Perez-Driesse model. Helper function for perez_driesse transposition. - ''' + """ dhi = np.asarray(dhi) dni = np.asarray(dni) # first calculate what zeta would be without the kappa correction # using eq. 5 and eq. 13 - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): zeta = dni / (dhi + dni) zeta = np.where(dhi == 0, 0.0, zeta) @@ -1269,43 +1403,67 @@ def _calc_zeta(dhi, dni, zenith): def _f(i, j, zeta): - ''' + """ Evaluate the quadratic splines corresponding to the allsitescomposite1990 Perez model look-up table. Helper function for perez_driesse transposition. - ''' + """ knots = np.array( - [0.000, 0.000, 0.000, - 0.061, 0.187, 0.333, 0.487, 0.643, 0.778, 0.839, - 1.000, 1.000, 1.000]) + [ + 0.000, + 0.000, + 0.000, + 0.061, + 0.187, + 0.333, + 0.487, + 0.643, + 0.778, + 0.839, + 1.000, + 1.000, + 1.000, + ] + ) coefs = np.array( - [[-0.053, +0.529, -0.028, -0.071, +0.061, -0.019], - [-0.008, +0.588, -0.062, -0.060, +0.072, -0.022], - [+0.131, +0.770, -0.167, -0.026, +0.106, -0.032], - [+0.328, +0.471, -0.216, +0.069, -0.105, -0.028], - [+0.557, +0.241, -0.300, +0.086, -0.085, -0.012], - [+0.861, -0.323, -0.355, +0.240, -0.467, -0.008], - [+1.212, -1.239, -0.444, +0.305, -0.797, +0.047], - [+1.099, -1.847, -0.365, +0.275, -1.132, +0.124], - [+0.544, +0.157, -0.213, +0.118, -1.455, +0.292], - [+0.544, +0.157, -0.213, +0.118, -1.455, +0.292], - [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000], - [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000], - [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000]]) + [ + [-0.053, +0.529, -0.028, -0.071, +0.061, -0.019], + [-0.008, +0.588, -0.062, -0.060, +0.072, -0.022], + [+0.131, +0.770, -0.167, -0.026, +0.106, -0.032], + [+0.328, +0.471, -0.216, +0.069, -0.105, -0.028], + [+0.557, +0.241, -0.300, +0.086, -0.085, -0.012], + [+0.861, -0.323, -0.355, +0.240, -0.467, -0.008], + [+1.212, -1.239, -0.444, +0.305, -0.797, +0.047], + [+1.099, -1.847, -0.365, +0.275, -1.132, +0.124], + [+0.544, +0.157, -0.213, +0.118, -1.455, +0.292], + [+0.544, +0.157, -0.213, +0.118, -1.455, +0.292], + [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000], + [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000], + [+0.000, +0.000, +0.000, +0.000, +0.000, +0.000], + ] + ) coefs = coefs.T.reshape((2, 3, 13)) - tck = (knots, coefs[i-1, j-1], 2) + tck = (knots, coefs[i - 1, j - 1], 2) return splev(zeta, tck) -def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, - solar_zenith, solar_azimuth, airmass=None, - return_components=False): - ''' +def perez_driesse( + surface_tilt, + surface_azimuth, + dhi, + dni, + dni_extra, + solar_zenith, + solar_azimuth, + airmass=None, + return_components=False, +): + """ Determine diffuse irradiance from the sky on a tilted surface using the continuous Perez-Driesse model. @@ -1399,7 +1557,7 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, klucher reindl king - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct., 2023 delta = _calc_delta(dhi, dni_extra, solar_zenith, airmass) @@ -1416,8 +1574,9 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, # lines after this point are identical to the original perez function # with some checks removed - A = aoi_projection(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth) + A = aoi_projection( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) A = np.maximum(A, 0) B = tools.cosd(solar_zenith) @@ -1432,12 +1591,12 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, if return_components: diffuse_components = OrderedDict() - diffuse_components['sky_diffuse'] = sky_diffuse + diffuse_components["sky_diffuse"] = sky_diffuse # Calculate the different components - diffuse_components['isotropic'] = dhi * term1 - diffuse_components['circumsolar'] = dhi * term2 - diffuse_components['horizon'] = dhi * term3 + diffuse_components["isotropic"] = dhi * term1 + diffuse_components["circumsolar"] = dhi * term2 + diffuse_components["horizon"] = dhi * term3 if isinstance(sky_diffuse, pd.Series): diffuse_components = pd.DataFrame(diffuse_components) @@ -1447,42 +1606,62 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra, return sky_diffuse -def _poa_from_ghi(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - ghi, - dni_extra, airmass, albedo): - ''' +def _poa_from_ghi( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + ghi, + dni_extra, + airmass, + albedo, +): + """ Transposition function that includes decomposition of GHI using the continuous Erbs-Driesse model. Helper function for ghi_from_poa_driesse_2023. - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023 erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra) - dni = erbsout['dni'] - dhi = erbsout['dhi'] - - irrads = get_total_irradiance(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra, airmass, albedo, - model='perez-driesse') + dni = erbsout["dni"] + dhi = erbsout["dhi"] + + irrads = get_total_irradiance( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra, + airmass, + albedo, + model="perez-driesse", + ) - return irrads['poa_global'] + return irrads["poa_global"] -def _ghi_from_poa(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - poa_global, - dni_extra, airmass, albedo, - xtol=0.01): - ''' +def _ghi_from_poa( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + poa_global, + dni_extra, + airmass, + albedo, + xtol=0.01, +): + """ Reverse transposition function that uses the scalar bisection from scipy. Helper function for ghi_from_poa_driesse_2023. - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023 # propagate nans and zeros quickly @@ -1493,10 +1672,16 @@ def _ghi_from_poa(surface_tilt, surface_azimuth, # function whose root needs to be found def poa_error(ghi): - poa_hat = _poa_from_ghi(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - ghi, - dni_extra, airmass, albedo) + poa_hat = _poa_from_ghi( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + ghi, + dni_extra, + airmass, + albedo, + ) return poa_hat - poa_global # calculate an upper bound for ghi using clearness index 1.25 @@ -1504,14 +1689,15 @@ def poa_error(ghi): ghi_high = np.maximum(10, 1.25 * ghi_clear) try: - result = bisect(poa_error, - a=0, - b=ghi_high, - xtol=xtol, - maxiter=25, - full_output=True, - disp=False, - ) + result = bisect( + poa_error, + a=0, + b=ghi_high, + xtol=xtol, + maxiter=25, + full_output=True, + disp=False, + ) except ValueError: # this occurs when poa_error has the same sign at both end points ghi = np.nan @@ -1525,13 +1711,19 @@ def poa_error(ghi): return ghi, conv, niter -def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - poa_global, - dni_extra, airmass=None, albedo=0.25, - xtol=0.01, - full_output=False): - ''' +def ghi_from_poa_driesse_2023( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + poa_global, + dni_extra, + airmass=None, + albedo=0.25, + xtol=0.01, + full_output=False, +): + """ Estimate global horizontal irradiance (GHI) from global plane-of-array (POA) irradiance. This reverse transposition algorithm uses a bisection search together with the continuous Perez-Driesse transposition and @@ -1589,7 +1781,7 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, perez_driesse erbs_driesse gti_dirint - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023 if xtol <= 0: @@ -1597,11 +1789,17 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, ghi_from_poa_array = np.vectorize(_ghi_from_poa) - ghi, conv, niter = ghi_from_poa_array(surface_tilt, surface_azimuth, - solar_zenith, solar_azimuth, - poa_global, - dni_extra, airmass, albedo, - xtol=xtol) + ghi, conv, niter = ghi_from_poa_array( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + poa_global, + dni_extra, + airmass, + albedo, + xtol=xtol, + ) if isinstance(poa_global, pd.Series): ghi = pd.Series(ghi, poa_global.index) @@ -1615,10 +1813,11 @@ def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth, @renamed_kwarg_warning( - since='0.11.2', - old_param_name='clearsky_ghi', - new_param_name='ghi_clear', - removal="0.13.0") + since="0.11.2", + old_param_name="clearsky_ghi", + new_param_name="ghi_clear", + removal="0.13.0", +) def clearsky_index(ghi, ghi_clear, max_clearsky_index=2.0): """ Calculate the clearsky index. @@ -1648,8 +1847,7 @@ def clearsky_index(ghi, ghi_clear, max_clearsky_index=2.0): """ clearsky_index = ghi / ghi_clear # set +inf, -inf, and nans to zero - clearsky_index = np.where(~np.isfinite(clearsky_index), 0, - clearsky_index) + clearsky_index = np.where(~np.isfinite(clearsky_index), 0, clearsky_index) # but preserve nans in the input arrays input_is_nan = ~np.isfinite(ghi) | ~np.isfinite(ghi_clear) clearsky_index = np.where(input_is_nan, np.nan, clearsky_index) @@ -1664,8 +1862,13 @@ def clearsky_index(ghi, ghi_clear, max_clearsky_index=2.0): return clearsky_index -def clearness_index(ghi, solar_zenith, extra_radiation, min_cos_zenith=0.065, - max_clearness_index=2.0): +def clearness_index( + ghi, + solar_zenith, + extra_radiation, + min_cos_zenith=0.065, + max_clearness_index=2.0, +): """ Calculate the clearness index. @@ -1717,8 +1920,9 @@ def clearness_index(ghi, solar_zenith, extra_radiation, min_cos_zenith=0.065, return kt -def clearness_index_zenith_independent(clearness_index, airmass, - max_clearness_index=2.0): +def clearness_index_zenith_independent( + clearness_index, airmass, max_clearness_index=2.0 +): """ Calculate the zenith angle independent clearness index. @@ -1766,8 +1970,15 @@ def _kt_kt_prime_factor(airmass): return 1.031 * np.exp(-1.4 / (0.9 + 9.4 / airmass)) + 0.1 -def disc(ghi, solar_zenith, datetime_or_doy, pressure=101325, - min_cos_zenith=0.065, max_zenith=87, max_airmass=12): +def disc( + ghi, + solar_zenith, + datetime_or_doy, + pressure=101325, + min_cos_zenith=0.065, + max_zenith=87, + max_airmass=12, +): """ Estimate Direct Normal Irradiance from Global Horizontal Irradiance using the DISC model. @@ -1844,12 +2055,17 @@ def disc(ghi, solar_zenith, datetime_or_doy, pressure=101325, # this is the I0 calculation from the reference # SSC uses solar constant = 1367.0 (checked 2018 08 15) - I0 = get_extra_radiation(datetime_or_doy, 1370., 'spencer') + I0 = get_extra_radiation(datetime_or_doy, 1370.0, "spencer") - kt = clearness_index(ghi, solar_zenith, I0, min_cos_zenith=min_cos_zenith, - max_clearness_index=1) + kt = clearness_index( + ghi, + solar_zenith, + I0, + min_cos_zenith=min_cos_zenith, + max_clearness_index=1, + ) - am = atmosphere.get_relative_airmass(solar_zenith, model='kasten1966') + am = atmosphere.get_relative_airmass(solar_zenith, model="kasten1966") if pressure is not None: am = atmosphere.get_absolute_airmass(am, pressure) @@ -1860,9 +2076,9 @@ def disc(ghi, solar_zenith, datetime_or_doy, pressure=101325, dni = np.where(bad_values, 0, dni) output = OrderedDict() - output['dni'] = dni - output['kt'] = kt - output['airmass'] = am + output["dni"] = dni + output["kt"] = kt + output["airmass"] = am if isinstance(datetime_or_doy, pd.DatetimeIndex): output = pd.DataFrame(output, index=datetime_or_doy) @@ -1894,30 +2110,43 @@ def _disc_kn(clearness_index, airmass, max_airmass=12): am = np.minimum(am, max_airmass) # GH 450 - is_cloudy = (kt <= 0.6) + is_cloudy = kt <= 0.6 # Use Horner's method to compute polynomials efficiently a = np.where( is_cloudy, - 0.512 + kt*(-1.56 + kt*(2.286 - 2.222*kt)), - -5.743 + kt*(21.77 + kt*(-27.49 + 11.56*kt))) + 0.512 + kt * (-1.56 + kt * (2.286 - 2.222 * kt)), + -5.743 + kt * (21.77 + kt * (-27.49 + 11.56 * kt)), + ) b = np.where( is_cloudy, - 0.37 + 0.962*kt, - 41.4 + kt*(-118.5 + kt*(66.05 + 31.9*kt))) + 0.37 + 0.962 * kt, + 41.4 + kt * (-118.5 + kt * (66.05 + 31.9 * kt)), + ) c = np.where( is_cloudy, - -0.28 + kt*(0.932 - 2.048*kt), - -47.01 + kt*(184.2 + kt*(-222.0 + 73.81*kt))) + -0.28 + kt * (0.932 - 2.048 * kt), + -47.01 + kt * (184.2 + kt * (-222.0 + 73.81 * kt)), + ) - delta_kn = a + b * np.exp(c*am) + delta_kn = a + b * np.exp(c * am) - Knc = 0.866 + am*(-0.122 + am*(0.0121 + am*(-0.000653 + 1.4e-05*am))) + Knc = 0.866 + am * ( + -0.122 + am * (0.0121 + am * (-0.000653 + 1.4e-05 * am)) + ) Kn = Knc - delta_kn return Kn, am -def dirint(ghi, solar_zenith, times, pressure=101325., use_delta_kt_prime=True, - temp_dew=None, min_cos_zenith=0.065, max_zenith=87): +def dirint( + ghi, + solar_zenith, + times, + pressure=101325.0, + use_delta_kt_prime=True, + temp_dew=None, + min_cos_zenith=0.065, + max_zenith=87, +): """ Determine DNI from GHI using the DIRINT modification of the DISC model. @@ -1991,39 +2220,51 @@ def dirint(ghi, solar_zenith, times, pressure=101325., use_delta_kt_prime=True, SERI/TR-215-3087, Golden, CO: Solar Energy Research Institute, 1987. """ - disc_out = disc(ghi, solar_zenith, times, pressure=pressure, - min_cos_zenith=min_cos_zenith, max_zenith=max_zenith) - airmass = disc_out['airmass'] - kt = disc_out['kt'] + disc_out = disc( + ghi, + solar_zenith, + times, + pressure=pressure, + min_cos_zenith=min_cos_zenith, + max_zenith=max_zenith, + ) + airmass = disc_out["airmass"] + kt = disc_out["kt"] kt_prime = clearness_index_zenith_independent( - kt, airmass, max_clearness_index=1) - delta_kt_prime = _delta_kt_prime_dirint(kt_prime, use_delta_kt_prime, - times) + kt, airmass, max_clearness_index=1 + ) + delta_kt_prime = _delta_kt_prime_dirint( + kt_prime, use_delta_kt_prime, times + ) w = _temp_dew_dirint(temp_dew, times) - dirint_coeffs = _dirint_coeffs(times, kt_prime, solar_zenith, w, - delta_kt_prime) + dirint_coeffs = _dirint_coeffs( + times, kt_prime, solar_zenith, w, delta_kt_prime + ) # Perez eqn 5 - dni = disc_out['dni'] * dirint_coeffs + dni = disc_out["dni"] * dirint_coeffs return dni -def _dirint_from_dni_ktprime(dni, kt_prime, solar_zenith, use_delta_kt_prime, - temp_dew): +def _dirint_from_dni_ktprime( + dni, kt_prime, solar_zenith, use_delta_kt_prime, temp_dew +): """ Calculate DIRINT DNI from supplied DISC DNI and Kt'. Supports :py:func:`gti_dirint` """ times = dni.index - delta_kt_prime = _delta_kt_prime_dirint(kt_prime, use_delta_kt_prime, - times) + delta_kt_prime = _delta_kt_prime_dirint( + kt_prime, use_delta_kt_prime, times + ) w = _temp_dew_dirint(temp_dew, times) - dirint_coeffs = _dirint_coeffs(times, kt_prime, solar_zenith, w, - delta_kt_prime) + dirint_coeffs = _dirint_coeffs( + times, kt_prime, solar_zenith, w, delta_kt_prime + ) dni_dirint = dni * dirint_coeffs return dni_dirint @@ -2041,9 +2282,11 @@ def _delta_kt_prime_dirint(kt_prime, use_delta_kt_prime, times): # positions. Use kt_previous and kt_next to handle series of length 1 kt_next.iloc[-1] = kt_previous.iloc[-1] kt_previous.iloc[0] = kt_next.iloc[0] - delta_kt_prime = 0.5 * ((kt_prime - kt_next).abs().add( - (kt_prime - kt_previous).abs(), - fill_value=0)) + delta_kt_prime = 0.5 * ( + (kt_prime - kt_next) + .abs() + .add((kt_prime - kt_previous).abs(), fill_value=0) + ) else: # do not change unless also modifying _dirint_bins delta_kt_prime = pd.Series(-1, index=times) @@ -2082,21 +2325,28 @@ def _dirint_coeffs(times, kt_prime, solar_zenith, w, delta_kt_prime): ------- dirint_coeffs : array-like """ - kt_prime_bin, zenith_bin, w_bin, delta_kt_prime_bin = \ - _dirint_bins(times, kt_prime, solar_zenith, w, delta_kt_prime) + kt_prime_bin, zenith_bin, w_bin, delta_kt_prime_bin = _dirint_bins( + times, kt_prime, solar_zenith, w, delta_kt_prime + ) # get the coefficients coeffs = _get_dirint_coeffs() # subtract 1 to account for difference between MATLAB-style bin # assignment and Python-style array lookup. - dirint_coeffs = coeffs[kt_prime_bin-1, zenith_bin-1, - delta_kt_prime_bin-1, w_bin-1] + dirint_coeffs = coeffs[ + kt_prime_bin - 1, zenith_bin - 1, delta_kt_prime_bin - 1, w_bin - 1 + ] # convert unassigned bins to nan - dirint_coeffs = np.where((kt_prime_bin == 0) | (zenith_bin == 0) | - (w_bin == 0) | (delta_kt_prime_bin == 0), - np.nan, dirint_coeffs) + dirint_coeffs = np.where( + (kt_prime_bin == 0) + | (zenith_bin == 0) + | (w_bin == 0) + | (delta_kt_prime_bin == 0), + np.nan, + dirint_coeffs, + ) return dirint_coeffs @@ -2148,8 +2398,9 @@ def _dirint_bins(times, kt_prime, zenith, w, delta_kt_prime): # Create delta_kt_prime binning. delta_kt_prime_bin = pd.Series(0, index=times, dtype=np.int64) delta_kt_prime_bin[(delta_kt_prime >= 0) & (delta_kt_prime < 0.015)] = 1 - delta_kt_prime_bin[(delta_kt_prime >= 0.015) & - (delta_kt_prime < 0.035)] = 2 + delta_kt_prime_bin[ + (delta_kt_prime >= 0.015) & (delta_kt_prime < 0.035) + ] = 2 delta_kt_prime_bin[(delta_kt_prime >= 0.035) & (delta_kt_prime < 0.07)] = 3 delta_kt_prime_bin[(delta_kt_prime >= 0.07) & (delta_kt_prime < 0.15)] = 4 delta_kt_prime_bin[(delta_kt_prime >= 0.15) & (delta_kt_prime < 0.3)] = 5 @@ -2160,18 +2411,29 @@ def _dirint_bins(times, kt_prime, zenith, w, delta_kt_prime): @renamed_kwarg_warning( - since='0.11.2', - old_param_name='ghi_clearsky', - new_param_name='ghi_clear', - removal="0.13.0") + since="0.11.2", + old_param_name="ghi_clearsky", + new_param_name="ghi_clear", + removal="0.13.0", +) @renamed_kwarg_warning( - since='0.11.2', - old_param_name='dni_clearsky', - new_param_name='dni_clear', - removal="0.13.0") -def dirindex(ghi, ghi_clear, dni_clear, zenith, times, pressure=101325., - use_delta_kt_prime=True, temp_dew=None, min_cos_zenith=0.065, - max_zenith=87): + since="0.11.2", + old_param_name="dni_clearsky", + new_param_name="dni_clear", + removal="0.13.0", +) +def dirindex( + ghi, + ghi_clear, + dni_clear, + zenith, + times, + pressure=101325.0, + use_delta_kt_prime=True, + temp_dew=None, + min_cos_zenith=0.065, + max_zenith=87, +): """ Determine DNI from GHI using the DIRINDEX model. @@ -2251,30 +2513,52 @@ def dirindex(ghi, ghi_clear, dni_clear, zenith, times, pressure=101325., irradiances: description and validation. Solar Energy, 73(5), 307-317. """ - dni_dirint = dirint(ghi, zenith, times, pressure=pressure, - use_delta_kt_prime=use_delta_kt_prime, - temp_dew=temp_dew, min_cos_zenith=min_cos_zenith, - max_zenith=max_zenith) + dni_dirint = dirint( + ghi, + zenith, + times, + pressure=pressure, + use_delta_kt_prime=use_delta_kt_prime, + temp_dew=temp_dew, + min_cos_zenith=min_cos_zenith, + max_zenith=max_zenith, + ) - dni_dirint_clearsky = dirint(ghi_clear, zenith, times, - pressure=pressure, - use_delta_kt_prime=use_delta_kt_prime, - temp_dew=temp_dew, - min_cos_zenith=min_cos_zenith, - max_zenith=max_zenith) + dni_dirint_clearsky = dirint( + ghi_clear, + zenith, + times, + pressure=pressure, + use_delta_kt_prime=use_delta_kt_prime, + temp_dew=temp_dew, + min_cos_zenith=min_cos_zenith, + max_zenith=max_zenith, + ) dni_dirindex = dni_clear * dni_dirint / dni_dirint_clearsky - dni_dirindex[dni_dirindex < 0] = 0. + dni_dirindex[dni_dirindex < 0] = 0.0 return dni_dirindex -def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times, - surface_tilt, surface_azimuth, pressure=101325., - use_delta_kt_prime=True, temp_dew=None, albedo=.25, - model='perez', model_perez='allsitescomposite1990', - calculate_gt_90=True, max_iterations=30): +def gti_dirint( + poa_global, + aoi, + solar_zenith, + solar_azimuth, + times, + surface_tilt, + surface_azimuth, + pressure=101325.0, + use_delta_kt_prime=True, + temp_dew=None, + albedo=0.25, + model="perez", + model_perez="allsitescomposite1990", + calculate_gt_90=True, + max_iterations=30, +): """ Determine GHI, DNI, DHI from POA global using the GTI DIRINT model. @@ -2373,48 +2657,79 @@ def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times, # for AOI less than 90 degrees ghi, dni, dhi, kt_prime = _gti_dirint_lt_90( - poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, times, - surface_tilt, surface_azimuth, pressure=pressure, - use_delta_kt_prime=use_delta_kt_prime, temp_dew=temp_dew, - albedo=albedo, model=model, model_perez=model_perez, - max_iterations=max_iterations) + poa_global, + aoi, + aoi_lt_90, + solar_zenith, + solar_azimuth, + times, + surface_tilt, + surface_azimuth, + pressure=pressure, + use_delta_kt_prime=use_delta_kt_prime, + temp_dew=temp_dew, + albedo=albedo, + model=model, + model_perez=model_perez, + max_iterations=max_iterations, + ) # for AOI greater than or equal to 90 degrees if calculate_gt_90: ghi_gte_90, dni_gte_90, dhi_gte_90 = _gti_dirint_gte_90( - poa_global, aoi, solar_zenith, solar_azimuth, - surface_tilt, times, kt_prime, - pressure=pressure, temp_dew=temp_dew, albedo=albedo) + poa_global, + aoi, + solar_zenith, + solar_azimuth, + surface_tilt, + times, + kt_prime, + pressure=pressure, + temp_dew=temp_dew, + albedo=albedo, + ) else: ghi_gte_90, dni_gte_90, dhi_gte_90 = np.nan, np.nan, np.nan # put the AOI < 90 and AOI >= 90 conditions together output = OrderedDict() - output['ghi'] = ghi.where(aoi_lt_90, ghi_gte_90) - output['dni'] = dni.where(aoi_lt_90, dni_gte_90) - output['dhi'] = dhi.where(aoi_lt_90, dhi_gte_90) + output["ghi"] = ghi.where(aoi_lt_90, ghi_gte_90) + output["dni"] = dni.where(aoi_lt_90, dni_gte_90) + output["dhi"] = dhi.where(aoi_lt_90, dhi_gte_90) output = pd.DataFrame(output, index=times) return output -def _gti_dirint_lt_90(poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, - times, surface_tilt, surface_azimuth, pressure=101325., - use_delta_kt_prime=True, temp_dew=None, albedo=.25, - model='perez', model_perez='allsitescomposite1990', - max_iterations=30): +def _gti_dirint_lt_90( + poa_global, + aoi, + aoi_lt_90, + solar_zenith, + solar_azimuth, + times, + surface_tilt, + surface_azimuth, + pressure=101325.0, + use_delta_kt_prime=True, + temp_dew=None, + albedo=0.25, + model="perez", + model_perez="allsitescomposite1990", + max_iterations=30, +): """ GTI-DIRINT model for AOI < 90 degrees. See Marion 2015 Section 2.1. See gti_dirint signature for parameter details. """ - I0 = get_extra_radiation(times, 1370, 'spencer') + I0 = get_extra_radiation(times, 1370, "spencer") cos_zenith = tools.cosd(solar_zenith) # I0h as in Marion 2015 eqns 1, 3 I0h = I0 * np.maximum(0.065, cos_zenith) - airmass = atmosphere.get_relative_airmass(solar_zenith, model='kasten1966') + airmass = atmosphere.get_relative_airmass(solar_zenith, model="kasten1966") airmass = atmosphere.get_absolute_airmass(airmass, pressure) # these coeffs and diff variables and the loop below @@ -2438,7 +2753,6 @@ def _gti_dirint_lt_90(poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, poa_global_i = poa_global for iteration, coeff in enumerate(coeffs): - # test if difference between modeled GTI and # measured GTI (poa_global) is less than 1 Wm⁻² # only test for aoi less than 90 deg @@ -2453,13 +2767,14 @@ def _gti_dirint_lt_90(poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, disc_dni = np.maximum(_disc_kn(kt, airmass)[0] * I0, 0) kt_prime = clearness_index_zenith_independent(kt, airmass) # dirint DNI in Marion eqn 3 - dni = _dirint_from_dni_ktprime(disc_dni, kt_prime, solar_zenith, - use_delta_kt_prime, temp_dew) + dni = _dirint_from_dni_ktprime( + disc_dni, kt_prime, solar_zenith, use_delta_kt_prime, temp_dew + ) # calculate DHI using Marion eqn 3 (identify 1st term on RHS as GHI) # I0h has a minimum zenith projection, but multiplier of DNI does not - ghi = kt * I0h # Kt * I0 * max(0.065, cos(zen)) - dhi = ghi - dni * cos_zenith # no cos(zen) restriction here + ghi = kt * I0h # Kt * I0 * max(0.065, cos(zen)) + dhi = ghi - dni * cos_zenith # no cos(zen) restriction here # following SSC code dni = np.maximum(dni, 0) @@ -2470,11 +2785,21 @@ def _gti_dirint_lt_90(poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, # GTI-DIRINT uses perez transposition model, but we allow for # any model here all_irrad = get_total_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - dni, ghi, dhi, dni_extra=I0, airmass=airmass, - albedo=albedo, model=model, model_perez=model_perez) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=I0, + airmass=airmass, + albedo=albedo, + model=model, + model_perez=model_perez, + ) - gti_model = all_irrad['poa_global'] + gti_model = all_irrad["poa_global"] # calculate new diff diff = gti_model - poa_global @@ -2508,50 +2833,64 @@ def _gti_dirint_lt_90(poa_global, aoi, aoi_lt_90, solar_zenith, solar_azimuth, # therefore we have exceeded max_iterations failed_points = best_diff[aoi_lt_90][~best_diff_lte_1_lt_90] warnings.warn( - ('%s points failed to converge after %s iterations. best_diff:\n%s' - % (len(failed_points), max_iterations, failed_points)), - RuntimeWarning) + ( + "%s points failed to converge after %s iterations. best_diff:\n%s" + % (len(failed_points), max_iterations, failed_points) + ), + RuntimeWarning, + ) # return the best data, whether or not the solution converged return best_ghi, best_dni, best_dhi, best_kt_prime -def _gti_dirint_gte_90(poa_global, aoi, solar_zenith, solar_azimuth, - surface_tilt, times, kt_prime, - pressure=101325., temp_dew=None, albedo=.25): +def _gti_dirint_gte_90( + poa_global, + aoi, + solar_zenith, + solar_azimuth, + surface_tilt, + times, + kt_prime, + pressure=101325.0, + temp_dew=None, + albedo=0.25, +): """ GTI-DIRINT model for AOI >= 90 degrees. See Marion 2015 Section 2.2. See gti_dirint signature for parameter details. """ - kt_prime_gte_90 = _gti_dirint_gte_90_kt_prime(aoi, solar_zenith, - solar_azimuth, times, - kt_prime) + kt_prime_gte_90 = _gti_dirint_gte_90_kt_prime( + aoi, solar_zenith, solar_azimuth, times, kt_prime + ) - I0 = get_extra_radiation(times, 1370, 'spencer') - airmass = atmosphere.get_relative_airmass(solar_zenith, model='kasten1966') + I0 = get_extra_radiation(times, 1370, "spencer") + airmass = atmosphere.get_relative_airmass(solar_zenith, model="kasten1966") airmass = atmosphere.get_absolute_airmass(airmass, pressure) kt = kt_prime_gte_90 * _kt_kt_prime_factor(airmass) disc_dni = np.maximum(_disc_kn(kt, airmass)[0] * I0, 0) - dni_gte_90 = _dirint_from_dni_ktprime(disc_dni, kt_prime, solar_zenith, - False, temp_dew) + dni_gte_90 = _dirint_from_dni_ktprime( + disc_dni, kt_prime, solar_zenith, False, temp_dew + ) dni_gte_90_proj = dni_gte_90 * tools.cosd(solar_zenith) cos_surface_tilt = tools.cosd(surface_tilt) # isotropic sky plus ground diffuse dhi_gte_90 = ( - (2 * poa_global - dni_gte_90_proj * albedo * (1 - cos_surface_tilt)) / - (1 + cos_surface_tilt + albedo * (1 - cos_surface_tilt))) + 2 * poa_global - dni_gte_90_proj * albedo * (1 - cos_surface_tilt) + ) / (1 + cos_surface_tilt + albedo * (1 - cos_surface_tilt)) ghi_gte_90 = dni_gte_90_proj + dhi_gte_90 return ghi_gte_90, dni_gte_90, dhi_gte_90 -def _gti_dirint_gte_90_kt_prime(aoi, solar_zenith, solar_azimuth, times, - kt_prime): +def _gti_dirint_gte_90_kt_prime( + aoi, solar_zenith, solar_azimuth, times, kt_prime +): """ Determine kt' values to be used in GTI-DIRINT AOI >= 90 deg case. See Marion 2015 Section 2.2. @@ -2661,17 +3000,23 @@ def erbs(ghi, zenith, datetime_or_doy, min_cos_zenith=0.065, max_zenith=87): dni_extra = get_extra_radiation(datetime_or_doy) - kt = clearness_index(ghi, zenith, dni_extra, min_cos_zenith=min_cos_zenith, - max_clearness_index=1) + kt = clearness_index( + ghi, + zenith, + dni_extra, + min_cos_zenith=min_cos_zenith, + max_clearness_index=1, + ) # For Kt <= 0.22, set the diffuse fraction - df = 1 - 0.09*kt + df = 1 - 0.09 * kt # For Kt > 0.22 and Kt <= 0.8, set the diffuse fraction - df = np.where((kt > 0.22) & (kt <= 0.8), - 0.9511 - 0.1604*kt + 4.388*kt**2 - - 16.638*kt**3 + 12.336*kt**4, - df) + df = np.where( + (kt > 0.22) & (kt <= 0.8), + 0.9511 - 0.1604 * kt + 4.388 * kt**2 - 16.638 * kt**3 + 12.336 * kt**4, + df, + ) # For Kt > 0.8, set the diffuse fraction df = np.where(kt > 0.8, 0.165, df) @@ -2685,9 +3030,9 @@ def erbs(ghi, zenith, datetime_or_doy, min_cos_zenith=0.065, max_zenith=87): dhi = np.where(bad_values, ghi, dhi) data = OrderedDict() - data['dni'] = dni - data['dhi'] = dhi - data['kt'] = kt + data["dni"] = dni + data["dhi"] = dhi + data["kt"] = kt if isinstance(datetime_or_doy, pd.DatetimeIndex): data = pd.DataFrame(data, index=datetime_or_doy) @@ -2695,8 +3040,14 @@ def erbs(ghi, zenith, datetime_or_doy, min_cos_zenith=0.065, max_zenith=87): return data -def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, - min_cos_zenith=0.065, max_zenith=87): +def erbs_driesse( + ghi, + zenith, + datetime_or_doy=None, + dni_extra=None, + min_cos_zenith=0.065, + max_zenith=87, +): r""" Estimate DNI and DHI from GHI using the continuous Erbs-Driesse model. @@ -2779,15 +3130,18 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug., 2023 # central polynomial coefficients with float64 precision - p = [+12.26911439571261000, - -16.47050842469730700, - +04.24692671521831700, - -00.11390583806313881, - +00.94629663357100100] + p = [ + +12.26911439571261000, + -16.47050842469730700, + +04.24692671521831700, + -00.11390583806313881, + +00.94629663357100100, + ] if datetime_or_doy is None and dni_extra is None: - raise ValueError('Either datetime_or_doy or dni_extra ' - 'must be provided.') + raise ValueError( + "Either datetime_or_doy or dni_extra " "must be provided." + ) if dni_extra is None: dni_extra = get_extra_radiation(datetime_or_doy) @@ -2795,8 +3149,13 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, # negative ghi should not reach this point, but just in case ghi = np.maximum(0, ghi) - kt = clearness_index(ghi, zenith, dni_extra, min_cos_zenith=min_cos_zenith, - max_clearness_index=1) + kt = clearness_index( + ghi, + zenith, + dni_extra, + min_cos_zenith=min_cos_zenith, + max_clearness_index=1, + ) # For all Kt, set the default diffuse fraction df = 1 - 0.09 * kt @@ -2816,9 +3175,9 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, dhi = np.where(bad_values, ghi, dhi) data = OrderedDict() - data['dni'] = dni - data['dhi'] = dhi - data['kt'] = kt + data["dni"] = dni + data["dhi"] = dhi + data["kt"] = kt if isinstance(datetime_or_doy, pd.DatetimeIndex): data = pd.DataFrame(data, index=datetime_or_doy) @@ -2828,8 +3187,14 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None, return data -def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None, - min_cos_zenith=0.065, max_zenith=87): +def orgill_hollands( + ghi, + zenith, + datetime_or_doy, + dni_extra=None, + min_cos_zenith=0.065, + max_zenith=87, +): """Estimate DNI and DHI from GHI using the Orgill and Hollands model. The Orgill and Hollands model [1]_ estimates the diffuse fraction DF from @@ -2883,15 +3248,19 @@ def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None, if dni_extra is None: dni_extra = get_extra_radiation(datetime_or_doy) - kt = clearness_index(ghi, zenith, dni_extra, min_cos_zenith=min_cos_zenith, - max_clearness_index=1) + kt = clearness_index( + ghi, + zenith, + dni_extra, + min_cos_zenith=min_cos_zenith, + max_clearness_index=1, + ) # For Kt < 0.35, set the diffuse fraction - df = 1 - 0.249*kt + df = 1 - 0.249 * kt # For Kt >= 0.35 and Kt <= 0.75, set the diffuse fraction - df = np.where((kt >= 0.35) & (kt <= 0.75), - 1.557 - 1.84*kt, df) + df = np.where((kt >= 0.35) & (kt <= 0.75), 1.557 - 1.84 * kt, df) # For Kt > 0.75, set the diffuse fraction df = np.where(kt > 0.75, 0.177, df) @@ -2905,9 +3274,9 @@ def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None, dhi = np.where(bad_values, ghi, dhi) data = OrderedDict() - data['dni'] = dni - data['dhi'] = dhi - data['kt'] = kt + data["dni"] = dni + data["dhi"] = dhi + data["kt"] = kt if isinstance(datetime_or_doy, pd.DatetimeIndex): data = pd.DataFrame(data, index=datetime_or_doy) @@ -2915,8 +3284,15 @@ def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None, return data -def boland(ghi, solar_zenith, datetime_or_doy, a_coeff=8.645, b_coeff=0.613, - min_cos_zenith=0.065, max_zenith=87): +def boland( + ghi, + solar_zenith, + datetime_or_doy, + a_coeff=8.645, + b_coeff=0.613, + min_cos_zenith=0.065, + max_zenith=87, +): r""" Estimate DNI and DHI from GHI using the Boland clearness index model. @@ -2992,8 +3368,12 @@ def boland(ghi, solar_zenith, datetime_or_doy, a_coeff=8.645, b_coeff=0.613, dni_extra = get_extra_radiation(datetime_or_doy) kt = clearness_index( - ghi, solar_zenith, dni_extra, min_cos_zenith=min_cos_zenith, - max_clearness_index=1) + ghi, + solar_zenith, + dni_extra, + min_cos_zenith=min_cos_zenith, + max_clearness_index=1, + ) # Boland equation df = 1.0 / (1.0 + np.exp(a_coeff * (kt - b_coeff))) @@ -3010,9 +3390,9 @@ def boland(ghi, solar_zenith, datetime_or_doy, a_coeff=8.645, b_coeff=0.613, dhi = np.where(bad_values, ghi, dhi) data = OrderedDict() - data['dni'] = dni - data['dhi'] = dhi - data['kt'] = kt + data["dni"] = dni + data["dhi"] = dhi + data["kt"] = kt if isinstance(datetime_or_doy, pd.DatetimeIndex): data = pd.DataFrame(data, index=datetime_or_doy) @@ -3020,9 +3400,10 @@ def boland(ghi, solar_zenith, datetime_or_doy, a_coeff=8.645, b_coeff=0.613, return data -def campbell_norman(zenith, transmittance, pressure=101325.0, - dni_extra=1367.0): - ''' +def campbell_norman( + zenith, transmittance, pressure=101325.0, dni_extra=1367.0 +): + """ Determine DNI, DHI, GHI from extraterrestrial flux, transmittance, and atmospheric pressure. @@ -3052,21 +3433,21 @@ def campbell_norman(zenith, transmittance, pressure=101325.0, ---------- .. [1] Campbell, G. S., J. M. Norman (1998) An Introduction to Environmental Biophysics. 2nd Ed. New York: Springer. - ''' + """ tau = transmittance - airmass = atmosphere.get_relative_airmass(zenith, model='simple') + airmass = atmosphere.get_relative_airmass(zenith, model="simple") airmass = atmosphere.get_absolute_airmass(airmass, pressure=pressure) - dni = dni_extra*tau**airmass + dni = dni_extra * tau**airmass cos_zen = tools.cosd(zenith) dhi = 0.3 * (1.0 - tau**airmass) * dni_extra * cos_zen ghi = dhi + dni * cos_zen irrads = OrderedDict() - irrads['ghi'] = ghi - irrads['dni'] = dni - irrads['dhi'] = dhi + irrads["ghi"] = ghi + irrads["dni"] = dni + irrads["dhi"] = dhi if isinstance(ghi, pd.Series): irrads = pd.DataFrame(irrads) @@ -3075,7 +3456,7 @@ def campbell_norman(zenith, transmittance, pressure=101325.0, def _liujordan(zenith, transmittance, airmass, dni_extra=1367.0): - ''' + """ Determine DNI, DHI, GHI from extraterrestrial flux, transmittance, and optical air mass number. @@ -3113,18 +3494,18 @@ def _liujordan(zenith, transmittance, airmass, dni_extra=1367.0): .. [2] Liu, B. Y., R. C. Jordan, (1960). "The interrelationship and characteristic distribution of direct, diffuse, and total solar radiation". Solar Energy 4:1-19 - ''' + """ tau = transmittance - dni = dni_extra*tau**airmass + dni = dni_extra * tau**airmass dhi = 0.3 * (1.0 - tau**airmass) * dni_extra * np.cos(np.radians(zenith)) ghi = dhi + dni * np.cos(np.radians(zenith)) irrads = OrderedDict() - irrads['ghi'] = ghi - irrads['dni'] = dni - irrads['dhi'] = dhi + irrads["ghi"] = ghi + irrads["dni"] = dni + irrads["dhi"] = dhi if isinstance(ghi, pd.Series): irrads = pd.DataFrame(irrads) @@ -3133,7 +3514,7 @@ def _liujordan(zenith, transmittance, airmass, dni_extra=1367.0): def _get_perez_coefficients(perezmodel): - ''' + """ Find coefficients for the Perez model Parameters @@ -3182,107 +3563,119 @@ def _get_perez_coefficients(perezmodel): .. [4] Perez, R. et. al 1988. "The Development and Verification of the Perez Diffuse Radiation Model". SAND88-7030 - ''' + """ coeffdict = { - 'allsitescomposite1990': [ - [-0.0080, 0.5880, -0.0620, -0.0600, 0.0720, -0.0220], - [0.1300, 0.6830, -0.1510, -0.0190, 0.0660, -0.0290], - [0.3300, 0.4870, -0.2210, 0.0550, -0.0640, -0.0260], - [0.5680, 0.1870, -0.2950, 0.1090, -0.1520, -0.0140], - [0.8730, -0.3920, -0.3620, 0.2260, -0.4620, 0.0010], - [1.1320, -1.2370, -0.4120, 0.2880, -0.8230, 0.0560], - [1.0600, -1.6000, -0.3590, 0.2640, -1.1270, 0.1310], - [0.6780, -0.3270, -0.2500, 0.1560, -1.3770, 0.2510]], - 'allsitescomposite1988': [ - [-0.0180, 0.7050, -0.071, -0.0580, 0.1020, -0.0260], - [0.1910, 0.6450, -0.1710, 0.0120, 0.0090, -0.0270], - [0.4400, 0.3780, -0.2560, 0.0870, -0.1040, -0.0250], - [0.7560, -0.1210, -0.3460, 0.1790, -0.3210, -0.0080], - [0.9960, -0.6450, -0.4050, 0.2600, -0.5900, 0.0170], - [1.0980, -1.2900, -0.3930, 0.2690, -0.8320, 0.0750], - [0.9730, -1.1350, -0.3780, 0.1240, -0.2580, 0.1490], - [0.6890, -0.4120, -0.2730, 0.1990, -1.6750, 0.2370]], - 'sandiacomposite1988': [ - [-0.1960, 1.0840, -0.0060, -0.1140, 0.1800, -0.0190], - [0.2360, 0.5190, -0.1800, -0.0110, 0.0200, -0.0380], - [0.4540, 0.3210, -0.2550, 0.0720, -0.0980, -0.0460], - [0.8660, -0.3810, -0.3750, 0.2030, -0.4030, -0.0490], - [1.0260, -0.7110, -0.4260, 0.2730, -0.6020, -0.0610], - [0.9780, -0.9860, -0.3500, 0.2800, -0.9150, -0.0240], - [0.7480, -0.9130, -0.2360, 0.1730, -1.0450, 0.0650], - [0.3180, -0.7570, 0.1030, 0.0620, -1.6980, 0.2360]], - 'usacomposite1988': [ - [-0.0340, 0.6710, -0.0590, -0.0590, 0.0860, -0.0280], - [0.2550, 0.4740, -0.1910, 0.0180, -0.0140, -0.0330], - [0.4270, 0.3490, -0.2450, 0.0930, -0.1210, -0.0390], - [0.7560, -0.2130, -0.3280, 0.1750, -0.3040, -0.0270], - [1.0200, -0.8570, -0.3850, 0.2800, -0.6380, -0.0190], - [1.0500, -1.3440, -0.3480, 0.2800, -0.8930, 0.0370], - [0.9740, -1.5070, -0.3700, 0.1540, -0.5680, 0.1090], - [0.7440, -1.8170, -0.2560, 0.2460, -2.6180, 0.2300]], - 'france1988': [ - [0.0130, 0.7640, -0.1000, -0.0580, 0.1270, -0.0230], - [0.0950, 0.9200, -0.1520, 0, 0.0510, -0.0200], - [0.4640, 0.4210, -0.2800, 0.0640, -0.0510, -0.0020], - [0.7590, -0.0090, -0.3730, 0.2010, -0.3820, 0.0100], - [0.9760, -0.4000, -0.4360, 0.2710, -0.6380, 0.0510], - [1.1760, -1.2540, -0.4620, 0.2950, -0.9750, 0.1290], - [1.1060, -1.5630, -0.3980, 0.3010, -1.4420, 0.2120], - [0.9340, -1.5010, -0.2710, 0.4200, -2.9170, 0.2490]], - 'phoenix1988': [ - [-0.0030, 0.7280, -0.0970, -0.0750, 0.1420, -0.0430], - [0.2790, 0.3540, -0.1760, 0.0300, -0.0550, -0.0540], - [0.4690, 0.1680, -0.2460, 0.0480, -0.0420, -0.0570], - [0.8560, -0.5190, -0.3400, 0.1760, -0.3800, -0.0310], - [0.9410, -0.6250, -0.3910, 0.1880, -0.3600, -0.0490], - [1.0560, -1.1340, -0.4100, 0.2810, -0.7940, -0.0650], - [0.9010, -2.1390, -0.2690, 0.1180, -0.6650, 0.0460], - [0.1070, 0.4810, 0.1430, -0.1110, -0.1370, 0.2340]], - 'elmonte1988': [ - [0.0270, 0.7010, -0.1190, -0.0580, 0.1070, -0.0600], - [0.1810, 0.6710, -0.1780, -0.0790, 0.1940, -0.0350], - [0.4760, 0.4070, -0.2880, 0.0540, -0.0320, -0.0550], - [0.8750, -0.2180, -0.4030, 0.1870, -0.3090, -0.0610], - [1.1660, -1.0140, -0.4540, 0.2110, -0.4100, -0.0440], - [1.1430, -2.0640, -0.2910, 0.0970, -0.3190, 0.0530], - [1.0940, -2.6320, -0.2590, 0.0290, -0.4220, 0.1470], - [0.1550, 1.7230, 0.1630, -0.1310, -0.0190, 0.2770]], - 'osage1988': [ - [-0.3530, 1.4740, 0.0570, -0.1750, 0.3120, 0.0090], - [0.3630, 0.2180, -0.2120, 0.0190, -0.0340, -0.0590], - [-0.0310, 1.2620, -0.0840, -0.0820, 0.2310, -0.0170], - [0.6910, 0.0390, -0.2950, 0.0910, -0.1310, -0.0350], - [1.1820, -1.3500, -0.3210, 0.4080, -0.9850, -0.0880], - [0.7640, 0.0190, -0.2030, 0.2170, -0.2940, -0.1030], - [0.2190, 1.4120, 0.2440, 0.4710, -2.9880, 0.0340], - [3.5780, 22.2310, -10.7450, 2.4260, 4.8920, -5.6870]], - 'albuquerque1988': [ - [0.0340, 0.5010, -0.0940, -0.0630, 0.1060, -0.0440], - [0.2290, 0.4670, -0.1560, -0.0050, -0.0190, -0.0230], - [0.4860, 0.2410, -0.2530, 0.0530, -0.0640, -0.0220], - [0.8740, -0.3930, -0.3970, 0.1810, -0.3270, -0.0370], - [1.1930, -1.2960, -0.5010, 0.2810, -0.6560, -0.0450], - [1.0560, -1.7580, -0.3740, 0.2260, -0.7590, 0.0340], - [0.9010, -4.7830, -0.1090, 0.0630, -0.9700, 0.1960], - [0.8510, -7.0550, -0.0530, 0.0600, -2.8330, 0.3300]], - 'capecanaveral1988': [ - [0.0750, 0.5330, -0.1240, -0.0670, 0.0420, -0.0200], - [0.2950, 0.4970, -0.2180, -0.0080, 0.0030, -0.0290], - [0.5140, 0.0810, -0.2610, 0.0750, -0.1600, -0.0290], - [0.7470, -0.3290, -0.3250, 0.1810, -0.4160, -0.0300], - [0.9010, -0.8830, -0.2970, 0.1780, -0.4890, 0.0080], - [0.5910, -0.0440, -0.1160, 0.2350, -0.9990, 0.0980], - [0.5370, -2.4020, 0.3200, 0.1690, -1.9710, 0.3100], - [-0.8050, 4.5460, 1.0720, -0.2580, -0.9500, 0.7530]], - 'albany1988': [ - [0.0120, 0.5540, -0.0760, -0.0520, 0.0840, -0.0290], - [0.2670, 0.4370, -0.1940, 0.0160, 0.0220, -0.0360], - [0.4200, 0.3360, -0.2370, 0.0740, -0.0520, -0.0320], - [0.6380, -0.0010, -0.2810, 0.1380, -0.1890, -0.0120], - [1.0190, -1.0270, -0.3420, 0.2710, -0.6280, 0.0140], - [1.1490, -1.9400, -0.3310, 0.3220, -1.0970, 0.0800], - [1.4340, -3.9940, -0.4920, 0.4530, -2.3760, 0.1170], - [1.0070, -2.2920, -0.4820, 0.3900, -3.3680, 0.2290]], } + "allsitescomposite1990": [ + [-0.0080, 0.5880, -0.0620, -0.0600, 0.0720, -0.0220], + [0.1300, 0.6830, -0.1510, -0.0190, 0.0660, -0.0290], + [0.3300, 0.4870, -0.2210, 0.0550, -0.0640, -0.0260], + [0.5680, 0.1870, -0.2950, 0.1090, -0.1520, -0.0140], + [0.8730, -0.3920, -0.3620, 0.2260, -0.4620, 0.0010], + [1.1320, -1.2370, -0.4120, 0.2880, -0.8230, 0.0560], + [1.0600, -1.6000, -0.3590, 0.2640, -1.1270, 0.1310], + [0.6780, -0.3270, -0.2500, 0.1560, -1.3770, 0.2510], + ], + "allsitescomposite1988": [ + [-0.0180, 0.7050, -0.071, -0.0580, 0.1020, -0.0260], + [0.1910, 0.6450, -0.1710, 0.0120, 0.0090, -0.0270], + [0.4400, 0.3780, -0.2560, 0.0870, -0.1040, -0.0250], + [0.7560, -0.1210, -0.3460, 0.1790, -0.3210, -0.0080], + [0.9960, -0.6450, -0.4050, 0.2600, -0.5900, 0.0170], + [1.0980, -1.2900, -0.3930, 0.2690, -0.8320, 0.0750], + [0.9730, -1.1350, -0.3780, 0.1240, -0.2580, 0.1490], + [0.6890, -0.4120, -0.2730, 0.1990, -1.6750, 0.2370], + ], + "sandiacomposite1988": [ + [-0.1960, 1.0840, -0.0060, -0.1140, 0.1800, -0.0190], + [0.2360, 0.5190, -0.1800, -0.0110, 0.0200, -0.0380], + [0.4540, 0.3210, -0.2550, 0.0720, -0.0980, -0.0460], + [0.8660, -0.3810, -0.3750, 0.2030, -0.4030, -0.0490], + [1.0260, -0.7110, -0.4260, 0.2730, -0.6020, -0.0610], + [0.9780, -0.9860, -0.3500, 0.2800, -0.9150, -0.0240], + [0.7480, -0.9130, -0.2360, 0.1730, -1.0450, 0.0650], + [0.3180, -0.7570, 0.1030, 0.0620, -1.6980, 0.2360], + ], + "usacomposite1988": [ + [-0.0340, 0.6710, -0.0590, -0.0590, 0.0860, -0.0280], + [0.2550, 0.4740, -0.1910, 0.0180, -0.0140, -0.0330], + [0.4270, 0.3490, -0.2450, 0.0930, -0.1210, -0.0390], + [0.7560, -0.2130, -0.3280, 0.1750, -0.3040, -0.0270], + [1.0200, -0.8570, -0.3850, 0.2800, -0.6380, -0.0190], + [1.0500, -1.3440, -0.3480, 0.2800, -0.8930, 0.0370], + [0.9740, -1.5070, -0.3700, 0.1540, -0.5680, 0.1090], + [0.7440, -1.8170, -0.2560, 0.2460, -2.6180, 0.2300], + ], + "france1988": [ + [0.0130, 0.7640, -0.1000, -0.0580, 0.1270, -0.0230], + [0.0950, 0.9200, -0.1520, 0, 0.0510, -0.0200], + [0.4640, 0.4210, -0.2800, 0.0640, -0.0510, -0.0020], + [0.7590, -0.0090, -0.3730, 0.2010, -0.3820, 0.0100], + [0.9760, -0.4000, -0.4360, 0.2710, -0.6380, 0.0510], + [1.1760, -1.2540, -0.4620, 0.2950, -0.9750, 0.1290], + [1.1060, -1.5630, -0.3980, 0.3010, -1.4420, 0.2120], + [0.9340, -1.5010, -0.2710, 0.4200, -2.9170, 0.2490], + ], + "phoenix1988": [ + [-0.0030, 0.7280, -0.0970, -0.0750, 0.1420, -0.0430], + [0.2790, 0.3540, -0.1760, 0.0300, -0.0550, -0.0540], + [0.4690, 0.1680, -0.2460, 0.0480, -0.0420, -0.0570], + [0.8560, -0.5190, -0.3400, 0.1760, -0.3800, -0.0310], + [0.9410, -0.6250, -0.3910, 0.1880, -0.3600, -0.0490], + [1.0560, -1.1340, -0.4100, 0.2810, -0.7940, -0.0650], + [0.9010, -2.1390, -0.2690, 0.1180, -0.6650, 0.0460], + [0.1070, 0.4810, 0.1430, -0.1110, -0.1370, 0.2340], + ], + "elmonte1988": [ + [0.0270, 0.7010, -0.1190, -0.0580, 0.1070, -0.0600], + [0.1810, 0.6710, -0.1780, -0.0790, 0.1940, -0.0350], + [0.4760, 0.4070, -0.2880, 0.0540, -0.0320, -0.0550], + [0.8750, -0.2180, -0.4030, 0.1870, -0.3090, -0.0610], + [1.1660, -1.0140, -0.4540, 0.2110, -0.4100, -0.0440], + [1.1430, -2.0640, -0.2910, 0.0970, -0.3190, 0.0530], + [1.0940, -2.6320, -0.2590, 0.0290, -0.4220, 0.1470], + [0.1550, 1.7230, 0.1630, -0.1310, -0.0190, 0.2770], + ], + "osage1988": [ + [-0.3530, 1.4740, 0.0570, -0.1750, 0.3120, 0.0090], + [0.3630, 0.2180, -0.2120, 0.0190, -0.0340, -0.0590], + [-0.0310, 1.2620, -0.0840, -0.0820, 0.2310, -0.0170], + [0.6910, 0.0390, -0.2950, 0.0910, -0.1310, -0.0350], + [1.1820, -1.3500, -0.3210, 0.4080, -0.9850, -0.0880], + [0.7640, 0.0190, -0.2030, 0.2170, -0.2940, -0.1030], + [0.2190, 1.4120, 0.2440, 0.4710, -2.9880, 0.0340], + [3.5780, 22.2310, -10.7450, 2.4260, 4.8920, -5.6870], + ], + "albuquerque1988": [ + [0.0340, 0.5010, -0.0940, -0.0630, 0.1060, -0.0440], + [0.2290, 0.4670, -0.1560, -0.0050, -0.0190, -0.0230], + [0.4860, 0.2410, -0.2530, 0.0530, -0.0640, -0.0220], + [0.8740, -0.3930, -0.3970, 0.1810, -0.3270, -0.0370], + [1.1930, -1.2960, -0.5010, 0.2810, -0.6560, -0.0450], + [1.0560, -1.7580, -0.3740, 0.2260, -0.7590, 0.0340], + [0.9010, -4.7830, -0.1090, 0.0630, -0.9700, 0.1960], + [0.8510, -7.0550, -0.0530, 0.0600, -2.8330, 0.3300], + ], + "capecanaveral1988": [ + [0.0750, 0.5330, -0.1240, -0.0670, 0.0420, -0.0200], + [0.2950, 0.4970, -0.2180, -0.0080, 0.0030, -0.0290], + [0.5140, 0.0810, -0.2610, 0.0750, -0.1600, -0.0290], + [0.7470, -0.3290, -0.3250, 0.1810, -0.4160, -0.0300], + [0.9010, -0.8830, -0.2970, 0.1780, -0.4890, 0.0080], + [0.5910, -0.0440, -0.1160, 0.2350, -0.9990, 0.0980], + [0.5370, -2.4020, 0.3200, 0.1690, -1.9710, 0.3100], + [-0.8050, 4.5460, 1.0720, -0.2580, -0.9500, 0.7530], + ], + "albany1988": [ + [0.0120, 0.5540, -0.0760, -0.0520, 0.0840, -0.0290], + [0.2670, 0.4370, -0.1940, 0.0160, 0.0220, -0.0360], + [0.4200, 0.3360, -0.2370, 0.0740, -0.0520, -0.0320], + [0.6380, -0.0010, -0.2810, 0.1380, -0.1890, -0.0120], + [1.0190, -1.0270, -0.3420, 0.2710, -0.6280, 0.0140], + [1.1490, -1.9400, -0.3310, 0.3220, -1.0970, 0.0800], + [1.4340, -3.9940, -0.4920, 0.4530, -2.3760, 0.1170], + [1.0070, -2.2920, -0.4820, 0.3900, -3.3680, 0.2290], + ], + } array = np.array(coeffdict[perezmodel]) @@ -3315,7 +3708,8 @@ def _get_dirint_coeffs(): [0.830130, 0.830130, 0.171970, 0.841070, 0.457370], [0.548010, 0.548010, 0.478000, 0.966880, 1.036370], [0.548010, 0.548010, 1.000000, 3.012370, 1.976540], - [0.582690, 0.582690, 0.229720, 0.892710, 0.569950]] + [0.582690, 0.582690, 0.229720, 0.892710, 0.569950], + ] coeffs[1, 2, :, :] = [ [0.131280, 0.131280, 0.385460, 0.511070, 0.127940], @@ -3324,7 +3718,8 @@ def _get_dirint_coeffs(): [0.090100, 0.184580, 0.260500, 0.687480, 0.579440], [0.131530, 0.131530, 0.370190, 1.380350, 1.052270], [1.116250, 1.116250, 0.928030, 3.525490, 2.316920], - [0.090100, 0.237000, 0.300040, 0.812470, 0.664970]] + [0.090100, 0.237000, 0.300040, 0.812470, 0.664970], + ] coeffs[1, 3, :, :] = [ [0.587510, 0.130000, 0.400000, 0.537210, 0.832490], @@ -3333,7 +3728,8 @@ def _get_dirint_coeffs(): [0.421540, 0.753970, 0.750660, 3.706840, 0.983790], [0.706680, 0.373530, 1.245670, 0.864860, 1.992630], [4.864400, 0.117390, 0.265180, 0.359180, 3.310820], - [0.392080, 0.493290, 0.651560, 1.932780, 0.898730]] + [0.392080, 0.493290, 0.651560, 1.932780, 0.898730], + ] coeffs[1, 4, :, :] = [ [0.126970, 0.126970, 0.126970, 0.126970, 0.126970], @@ -3342,7 +3738,8 @@ def _get_dirint_coeffs(): [4.000000, 3.000000, 2.000000, 0.975430, 1.965570], [12.494170, 12.494170, 8.000000, 5.083520, 8.792390], [21.744240, 21.744240, 21.744240, 21.744240, 21.744240], - [3.241680, 12.494170, 1.620760, 1.375250, 2.331620]] + [3.241680, 12.494170, 1.620760, 1.375250, 2.331620], + ] coeffs[1, 5, :, :] = [ [0.126970, 0.126970, 0.126970, 0.126970, 0.126970], @@ -3351,7 +3748,8 @@ def _get_dirint_coeffs(): [4.000000, 3.000000, 2.000000, 0.975430, 1.965570], [12.494170, 12.494170, 8.000000, 5.083520, 8.792390], [21.744240, 21.744240, 21.744240, 21.744240, 21.744240], - [3.241680, 12.494170, 1.620760, 1.375250, 2.331620]] + [3.241680, 12.494170, 1.620760, 1.375250, 2.331620], + ] coeffs[1, 6, :, :] = [ [0.126970, 0.126970, 0.126970, 0.126970, 0.126970], @@ -3360,7 +3758,8 @@ def _get_dirint_coeffs(): [4.000000, 3.000000, 2.000000, 0.975430, 1.965570], [12.494170, 12.494170, 8.000000, 5.083520, 8.792390], [21.744240, 21.744240, 21.744240, 21.744240, 21.744240], - [3.241680, 12.494170, 1.620760, 1.375250, 2.331620]] + [3.241680, 12.494170, 1.620760, 1.375250, 2.331620], + ] coeffs[2, 1, :, :] = [ [0.337440, 0.337440, 0.969110, 1.097190, 1.116080], @@ -3369,7 +3768,8 @@ def _get_dirint_coeffs(): [0.584040, 0.584040, 0.847250, 0.914940, 1.289300], [0.337440, 0.337440, 0.310240, 1.435020, 1.852830], [0.337440, 0.337440, 1.015010, 1.097190, 2.117230], - [0.337440, 0.337440, 0.969110, 1.145730, 1.476400]] + [0.337440, 0.337440, 0.969110, 1.145730, 1.476400], + ] coeffs[2, 2, :, :] = [ [0.300000, 0.300000, 0.700000, 1.100000, 0.796940], @@ -3378,7 +3778,8 @@ def _get_dirint_coeffs(): [0.746730, 0.399830, 0.470970, 0.986530, 0.785370], [0.575420, 0.936700, 1.649200, 1.495840, 1.335590], [1.319670, 4.002570, 1.276390, 2.644550, 2.518670], - [0.665190, 0.678910, 1.012360, 1.199940, 0.986580]] + [0.665190, 0.678910, 1.012360, 1.199940, 0.986580], + ] coeffs[2, 3, :, :] = [ [0.378870, 0.974060, 0.500000, 0.491880, 0.665290], @@ -3387,7 +3788,8 @@ def _get_dirint_coeffs(): [0.119070, 0.365120, 0.560520, 0.793720, 0.802600], [0.781610, 0.837390, 1.270420, 1.537980, 1.292950], [1.152290, 1.152290, 1.492080, 1.245370, 2.177100], - [0.424660, 0.529550, 0.966910, 1.033460, 0.958730]] + [0.424660, 0.529550, 0.966910, 1.033460, 0.958730], + ] coeffs[2, 4, :, :] = [ [0.310590, 0.714410, 0.252450, 0.500000, 0.607600], @@ -3396,7 +3798,8 @@ def _get_dirint_coeffs(): [0.719280, 0.698620, 0.657770, 1.190840, 0.681110], [0.426240, 1.464840, 0.678550, 1.157730, 0.978430], [2.501120, 1.789130, 1.387090, 2.394180, 2.394180], - [0.491640, 0.677610, 0.685610, 1.082400, 0.735410]] + [0.491640, 0.677610, 0.685610, 1.082400, 0.735410], + ] coeffs[2, 5, :, :] = [ [0.597000, 0.500000, 0.300000, 0.310050, 0.413510], @@ -3405,7 +3808,8 @@ def _get_dirint_coeffs(): [0.401020, 0.559110, 0.403630, 1.016710, 0.671490], [0.400360, 0.750830, 0.842640, 1.802600, 1.023830], [3.315300, 1.510380, 2.443650, 1.638820, 2.133990], - [0.530790, 0.745850, 0.693050, 1.458040, 0.804500]] + [0.530790, 0.745850, 0.693050, 1.458040, 0.804500], + ] coeffs[2, 6, :, :] = [ [0.597000, 0.500000, 0.300000, 0.310050, 0.800920], @@ -3414,7 +3818,8 @@ def _get_dirint_coeffs(): [0.401020, 0.559110, 0.403630, 1.016710, 0.898570], [0.400360, 0.750830, 0.842640, 1.802600, 3.400390], [3.315300, 1.510380, 2.443650, 1.638820, 2.508780], - [0.204340, 1.157740, 2.003080, 2.622080, 1.409380]] + [0.204340, 1.157740, 2.003080, 2.622080, 1.409380], + ] coeffs[3, 1, :, :] = [ [1.242210, 1.242210, 1.242210, 1.242210, 1.242210], @@ -3423,7 +3828,8 @@ def _get_dirint_coeffs(): [1.053850, 1.053850, 1.399690, 1.084640, 1.233340], [1.151540, 1.151540, 1.118290, 1.531640, 1.411840], [1.494980, 1.494980, 1.700000, 1.800810, 1.671600], - [1.018450, 1.018450, 1.153600, 1.321890, 1.294670]] + [1.018450, 1.018450, 1.153600, 1.321890, 1.294670], + ] coeffs[3, 2, :, :] = [ [0.700000, 0.700000, 1.023460, 0.700000, 0.945830], @@ -3432,7 +3838,8 @@ def _get_dirint_coeffs(): [1.095300, 1.075060, 1.176490, 1.139470, 1.096110], [1.201660, 1.201660, 1.438200, 1.256280, 1.198060], [1.525850, 1.525850, 1.869160, 1.985410, 1.911590], - [1.288220, 1.082810, 1.286370, 1.166170, 1.119330]] + [1.288220, 1.082810, 1.286370, 1.166170, 1.119330], + ] coeffs[3, 3, :, :] = [ [0.600000, 1.029910, 0.859890, 0.550000, 0.813600], @@ -3441,7 +3848,8 @@ def _get_dirint_coeffs(): [0.526580, 0.932310, 0.908620, 0.983520, 0.988090], [1.036110, 1.100690, 0.848380, 1.035270, 1.042380], [1.048440, 1.652720, 0.900000, 2.350410, 1.082950], - [0.817410, 0.976160, 0.861300, 0.974780, 1.004580]] + [0.817410, 0.976160, 0.861300, 0.974780, 1.004580], + ] coeffs[3, 4, :, :] = [ [0.782110, 0.564280, 0.600000, 0.600000, 0.665740], @@ -3450,7 +3858,8 @@ def _get_dirint_coeffs(): [0.709310, 0.872780, 0.908480, 0.953290, 0.844350], [0.863920, 0.947770, 0.876220, 1.078750, 0.936910], [1.280350, 0.866720, 0.769790, 1.078750, 0.975130], - [0.725420, 0.869970, 0.868810, 0.951190, 0.829220]] + [0.725420, 0.869970, 0.868810, 0.951190, 0.829220], + ] coeffs[3, 5, :, :] = [ [0.791750, 0.654040, 0.483170, 0.409000, 0.597180], @@ -3459,7 +3868,8 @@ def _get_dirint_coeffs(): [0.637630, 0.767610, 0.925670, 0.990310, 0.847670], [0.736380, 0.946060, 1.117590, 1.029340, 0.947020], [1.180970, 0.850000, 1.050000, 0.950000, 0.888580], - [0.700560, 0.801440, 0.961970, 0.906140, 0.823880]] + [0.700560, 0.801440, 0.961970, 0.906140, 0.823880], + ] coeffs[3, 6, :, :] = [ [0.500000, 0.500000, 0.586770, 0.470550, 0.629790], @@ -3468,7 +3878,8 @@ def _get_dirint_coeffs(): [0.554710, 0.734730, 0.985820, 0.915640, 0.898260], [0.712510, 1.205990, 0.909510, 1.078260, 0.885610], [1.899260, 1.559710, 1.000000, 1.150000, 1.120390], - [0.653880, 0.793120, 0.903320, 0.944070, 0.796130]] + [0.653880, 0.793120, 0.903320, 0.944070, 0.796130], + ] coeffs[4, 1, :, :] = [ [1.000000, 1.000000, 1.050000, 1.170380, 1.178090], @@ -3477,7 +3888,8 @@ def _get_dirint_coeffs(): [1.201590, 1.201590, 0.993610, 1.109380, 1.126320], [1.065010, 1.065010, 0.828660, 0.939970, 1.017930], [1.065010, 1.065010, 0.623690, 1.119620, 1.132260], - [1.071570, 1.071570, 0.958070, 1.114130, 1.127110]] + [1.071570, 1.071570, 0.958070, 1.114130, 1.127110], + ] coeffs[4, 2, :, :] = [ [0.950000, 0.973390, 0.852520, 1.092200, 1.096590], @@ -3486,7 +3898,8 @@ def _get_dirint_coeffs(): [1.032980, 1.034540, 0.968460, 1.032080, 1.015780], [0.900000, 0.977210, 0.945960, 1.008840, 0.969960], [0.600000, 0.750000, 0.750000, 0.844710, 0.899100], - [0.926800, 0.965030, 0.968520, 1.044910, 1.032310]] + [0.926800, 0.965030, 0.968520, 1.044910, 1.032310], + ] coeffs[4, 3, :, :] = [ [0.850000, 1.029710, 0.961100, 1.055670, 1.009700], @@ -3495,7 +3908,8 @@ def _get_dirint_coeffs(): [0.775610, 0.909610, 0.927800, 0.987800, 0.952100], [1.000990, 0.881880, 0.875950, 0.949100, 0.893690], [0.902370, 0.875960, 0.807990, 0.942410, 0.917920], - [0.856580, 0.928270, 0.946820, 1.032260, 0.972990]] + [0.856580, 0.928270, 0.946820, 1.032260, 0.972990], + ] coeffs[4, 4, :, :] = [ [0.750000, 0.857930, 0.983800, 1.056540, 0.980240], @@ -3504,7 +3918,8 @@ def _get_dirint_coeffs(): [0.800000, 0.914550, 0.908570, 0.999190, 0.915230], [0.778540, 0.800590, 0.799070, 0.902180, 0.851560], [0.680190, 0.317410, 0.507680, 0.388910, 0.646710], - [0.794920, 0.912780, 0.960830, 1.057110, 0.947950]] + [0.794920, 0.912780, 0.960830, 1.057110, 0.947950], + ] coeffs[4, 5, :, :] = [ [0.750000, 0.833890, 0.867530, 1.059890, 0.932840], @@ -3513,7 +3928,8 @@ def _get_dirint_coeffs(): [0.802400, 0.955110, 0.911660, 1.045070, 0.944470], [0.884890, 0.766210, 0.885390, 0.859070, 0.818190], [0.615680, 0.700000, 0.850000, 0.624620, 0.669300], - [0.835570, 0.946150, 0.977090, 1.049350, 0.979970]] + [0.835570, 0.946150, 0.977090, 1.049350, 0.979970], + ] coeffs[4, 6, :, :] = [ [0.689220, 0.809600, 0.900000, 0.789500, 0.853990], @@ -3522,7 +3938,8 @@ def _get_dirint_coeffs(): [0.843620, 0.981300, 0.951590, 0.946100, 0.966330], [0.694740, 0.814690, 0.572650, 0.400000, 0.726830], [0.211370, 0.671780, 0.416340, 0.297290, 0.498050], - [0.843540, 0.882330, 0.911760, 0.898420, 0.960210]] + [0.843540, 0.882330, 0.911760, 0.898420, 0.960210], + ] coeffs[5, 1, :, :] = [ [1.054880, 1.075210, 1.068460, 1.153370, 1.069220], @@ -3531,7 +3948,8 @@ def _get_dirint_coeffs(): [0.920000, 0.950000, 0.978720, 1.020280, 0.984440], [0.850000, 0.908500, 0.839940, 0.985570, 0.962180], [0.800000, 0.800000, 0.810080, 0.950000, 0.961550], - [1.038590, 1.063200, 1.034440, 1.112780, 1.037800]] + [1.038590, 1.063200, 1.034440, 1.112780, 1.037800], + ] coeffs[5, 2, :, :] = [ [1.017610, 1.028360, 1.058960, 1.133180, 1.045620], @@ -3540,7 +3958,8 @@ def _get_dirint_coeffs(): [0.847160, 0.935300, 0.930540, 0.955050, 0.946560], [0.880260, 0.867110, 0.874130, 0.972650, 0.883420], [0.627150, 0.627150, 0.700000, 0.774070, 0.845130], - [0.973700, 1.006240, 1.026190, 1.071960, 1.017240]] + [0.973700, 1.006240, 1.026190, 1.071960, 1.017240], + ] coeffs[5, 3, :, :] = [ [1.028710, 1.017570, 1.025900, 1.081790, 1.024240], @@ -3549,7 +3968,8 @@ def _get_dirint_coeffs(): [0.900810, 0.901330, 0.928830, 0.979570, 0.913100], [0.761030, 0.845150, 0.805360, 0.936790, 0.853460], [0.626400, 0.546750, 0.730500, 0.850000, 0.689050], - [0.957630, 0.985480, 0.991790, 1.050220, 0.987900]] + [0.957630, 0.985480, 0.991790, 1.050220, 0.987900], + ] coeffs[5, 4, :, :] = [ [0.992730, 0.993880, 1.017150, 1.059120, 1.017450], @@ -3558,7 +3978,8 @@ def _get_dirint_coeffs(): [0.828750, 0.868090, 0.834920, 0.905510, 0.871530], [0.781540, 0.782470, 0.767910, 0.764140, 0.795890], [0.743460, 0.693390, 0.514870, 0.630150, 0.715660], - [0.934760, 0.957870, 0.959640, 0.972510, 0.981640]] + [0.934760, 0.957870, 0.959640, 0.972510, 0.981640], + ] coeffs[5, 5, :, :] = [ [0.965840, 0.941240, 0.987100, 1.022540, 1.011160], @@ -3567,7 +3988,8 @@ def _get_dirint_coeffs(): [0.811720, 0.869090, 0.812020, 0.850000, 0.821050], [0.682030, 0.679480, 0.632450, 0.746580, 0.738550], [0.668290, 0.445860, 0.500000, 0.678920, 0.696510], - [0.926940, 0.953350, 0.959050, 0.876210, 0.991490]] + [0.926940, 0.953350, 0.959050, 0.876210, 0.991490], + ] coeffs[5, 6, :, :] = [ [0.948940, 0.997760, 0.850000, 0.826520, 0.998470], @@ -3576,7 +3998,8 @@ def _get_dirint_coeffs(): [1.000000, 0.746140, 0.751740, 0.598390, 0.725230], [0.922210, 0.500000, 0.376800, 0.517110, 0.548630], [0.500000, 0.450000, 0.429970, 0.404490, 0.539940], - [0.960430, 0.881630, 0.775640, 0.596350, 0.937680]] + [0.960430, 0.881630, 0.775640, 0.596350, 0.937680], + ] coeffs[6, 1, :, :] = [ [1.030000, 1.040000, 1.000000, 1.000000, 1.049510], @@ -3585,7 +4008,8 @@ def _get_dirint_coeffs(): [1.050000, 0.790000, 0.880000, 0.820000, 0.951840], [1.000000, 0.530000, 0.440000, 0.710000, 0.928730], [0.540000, 0.470000, 0.500000, 0.550000, 0.773950], - [1.038270, 0.920180, 0.910930, 0.821140, 1.034560]] + [1.038270, 0.920180, 0.910930, 0.821140, 1.034560], + ] coeffs[6, 2, :, :] = [ [1.041020, 0.997520, 0.961600, 1.000000, 1.035780], @@ -3594,7 +4018,8 @@ def _get_dirint_coeffs(): [0.951870, 0.850000, 0.748770, 0.700000, 0.883850], [0.900000, 0.823190, 0.727450, 0.600000, 0.839870], [0.850000, 0.805020, 0.692310, 0.500000, 0.788410], - [1.010090, 0.895270, 0.773030, 0.816280, 1.011680]] + [1.010090, 0.895270, 0.773030, 0.816280, 1.011680], + ] coeffs[6, 3, :, :] = [ [1.022450, 1.004600, 0.983650, 1.000000, 1.032940], @@ -3603,7 +4028,8 @@ def _get_dirint_coeffs(): [0.816420, 0.885000, 0.644950, 0.817650, 0.865310], [0.742960, 0.765690, 0.561520, 0.700000, 0.827140], [0.643870, 0.596710, 0.474460, 0.600000, 0.651200], - [0.971740, 0.940560, 0.714880, 0.864380, 1.001650]] + [0.971740, 0.940560, 0.714880, 0.864380, 1.001650], + ] coeffs[6, 4, :, :] = [ [0.995260, 0.977010, 1.000000, 1.000000, 1.035250], @@ -3612,7 +4038,8 @@ def _get_dirint_coeffs(): [0.873480, 0.873450, 0.751470, 0.850000, 0.863040], [0.761470, 0.702360, 0.638770, 0.750000, 0.783120], [0.734080, 0.650000, 0.600000, 0.650000, 0.715660], - [0.942160, 0.919100, 0.770340, 0.731170, 0.995180]] + [0.942160, 0.919100, 0.770340, 0.731170, 0.995180], + ] coeffs[6, 5, :, :] = [ [0.952560, 0.916780, 0.920000, 0.900000, 1.005880], @@ -3621,7 +4048,8 @@ def _get_dirint_coeffs(): [0.868090, 0.807170, 0.823550, 0.600000, 0.844520], [0.769570, 0.719870, 0.650000, 0.550000, 0.733500], [0.580250, 0.650000, 0.600000, 0.500000, 0.628850], - [0.904770, 0.852650, 0.708370, 0.493730, 0.949030]] + [0.904770, 0.852650, 0.708370, 0.493730, 0.949030], + ] coeffs[6, 6, :, :] = [ [0.911970, 0.800000, 0.800000, 0.800000, 0.956320], @@ -3630,19 +4058,27 @@ def _get_dirint_coeffs(): [0.648440, 0.600000, 0.641120, 0.500000, 0.695780], [0.570000, 0.550000, 0.598800, 0.400000, 0.560150], [0.475230, 0.500000, 0.518640, 0.339970, 0.520230], - [0.743440, 0.592190, 0.603060, 0.316930, 0.794390]] + [0.743440, 0.592190, 0.603060, 0.316930, 0.794390], + ] return coeffs[1:, 1:, :, :] @renamed_kwarg_warning( - since='0.11.2', - old_param_name='clearsky_dni', - new_param_name='dni_clear', - removal="0.13.0") -def dni(ghi, dhi, zenith, dni_clear=None, clearsky_tolerance=1.1, - zenith_threshold_for_zero_dni=88.0, - zenith_threshold_for_clearsky_limit=80.0): + since="0.11.2", + old_param_name="clearsky_dni", + new_param_name="dni_clear", + removal="0.13.0", +) +def dni( + ghi, + dhi, + zenith, + dni_clear=None, + clearsky_tolerance=1.1, + zenith_threshold_for_zero_dni=88.0, + zenith_threshold_for_clearsky_limit=80.0, +): """ Determine DNI from GHI and DHI. @@ -3696,11 +4132,11 @@ def dni(ghi, dhi, zenith, dni_clear=None, clearsky_tolerance=1.1, dni = (ghi - dhi) / tools.cosd(zenith) # cutoff negative values - dni[dni < 0] = float('nan') + dni[dni < 0] = float("nan") # set non-zero DNI values for zenith angles >= # zenith_threshold_for_zero_dni to NaN - dni[(zenith >= zenith_threshold_for_zero_dni) & (dni != 0)] = float('nan') + dni[(zenith >= zenith_threshold_for_zero_dni) & (dni != 0)] = float("nan") # correct DNI values for zenith angles greater or equal to the # zenith_threshold_for_clearsky_limit and smaller than the @@ -3708,17 +4144,17 @@ def dni(ghi, dhi, zenith, dni_clear=None, clearsky_tolerance=1.1, # clearsky_tolerance) if dni_clear is not None: max_dni = dni_clear * clearsky_tolerance - dni[(zenith >= zenith_threshold_for_clearsky_limit) & - (zenith < zenith_threshold_for_zero_dni) & - (dni > max_dni)] = max_dni + dni[ + (zenith >= zenith_threshold_for_clearsky_limit) + & (zenith < zenith_threshold_for_zero_dni) + & (dni > max_dni) + ] = max_dni return dni -def complete_irradiance(solar_zenith, - ghi=None, - dhi=None, - dni=None, - dni_clear=None): +def complete_irradiance( + solar_zenith, ghi=None, dhi=None, dni=None, dni_clear=None +): r""" Use the component sum equations to calculate the missing series, using the other available time series. One of the three parameters (ghi, dhi, @@ -3758,13 +4194,13 @@ def complete_irradiance(solar_zenith, Pandas series of 'ghi', 'dhi', and 'dni' columns with datetime index """ if ghi is not None and dhi is not None and dni is None: - dni = pvlib.irradiance.dni(ghi, dhi, solar_zenith, - dni_clear=dni_clear, - clearsky_tolerance=1.1) + dni = pvlib.irradiance.dni( + ghi, dhi, solar_zenith, dni_clear=dni_clear, clearsky_tolerance=1.1 + ) elif dni is not None and dhi is not None and ghi is None: - ghi = (dhi + dni * tools.cosd(solar_zenith)) + ghi = dhi + dni * tools.cosd(solar_zenith) elif dni is not None and ghi is not None and dhi is None: - dhi = (ghi - dni * tools.cosd(solar_zenith)) + dhi = ghi - dni * tools.cosd(solar_zenith) else: raise ValueError( "Please check that exactly one of ghi, dhi and dni parameters " @@ -3772,9 +4208,7 @@ def complete_irradiance(solar_zenith, ) # Merge the outputs into a master dataframe containing 'ghi', 'dhi', # and 'dni' columns - component_sum_df = pd.DataFrame({'ghi': ghi, - 'dhi': dhi, - 'dni': dni}) + component_sum_df = pd.DataFrame({"ghi": ghi, "dhi": dhi, "dni": dni}) return component_sum_df @@ -3818,10 +4252,16 @@ def louche(ghi, solar_zenith, datetime_or_doy, max_zenith=90): Kt = clearness_index(ghi, solar_zenith, I0) - kb = -10.627*Kt**5 + 15.307*Kt**4 - 5.205 * \ - Kt**3 + 0.994*Kt**2 - 0.059*Kt + 0.002 - dni = kb*I0 - dhi = ghi - dni*tools.cosd(solar_zenith) + kb = ( + -10.627 * Kt**5 + + 15.307 * Kt**4 + - 5.205 * Kt**3 + + 0.994 * Kt**2 + - 0.059 * Kt + + 0.002 + ) + dni = kb * I0 + dhi = ghi - dni * tools.cosd(solar_zenith) bad_values = (solar_zenith > max_zenith) | (ghi < 0) | (dni < 0) dni = np.where(bad_values, 0, dni) @@ -3829,9 +4269,9 @@ def louche(ghi, solar_zenith, datetime_or_doy, max_zenith=90): dhi = np.where(bad_values, ghi, dhi) data = OrderedDict() - data['dni'] = dni - data['dhi'] = dhi - data['kt'] = Kt + data["dni"] = dni + data["dhi"] = dhi + data["kt"] = Kt if isinstance(datetime_or_doy, pd.DatetimeIndex): data = pd.DataFrame(data, index=datetime_or_doy) diff --git a/pvlib/ivtools/sde.py b/pvlib/ivtools/sde.py index 236e91e390..dba99b19cc 100644 --- a/pvlib/ivtools/sde.py +++ b/pvlib/ivtools/sde.py @@ -10,8 +10,9 @@ from pvlib.ivtools.utils import _schumaker_qspline -def fit_sandia_simple(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None, - vlim=0.2, ilim=0.1): +def fit_sandia_simple( + voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None, vlim=0.2, ilim=0.1 +): r""" Fits the single diode equation (SDE) to an IV curve. @@ -158,8 +159,9 @@ def fit_sandia_simple(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None, beta0, beta1 = _sandia_beta0_beta1(voltage, current, vlim, v_oc) # Find beta3 and beta4 from the exponential portion of the IV curve - beta3, beta4 = _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim, - i_sc) + beta3, beta4 = _sandia_beta3_beta4( + voltage, current, beta0, beta1, ilim, i_sc + ) # calculate single diode parameters from regression coefficients return _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc) @@ -207,8 +209,11 @@ def _sandia_beta0_beta1(v, i, vlim, v_oc): beta1 = -coef[0].item() break if any(np.isnan([beta0, beta1])): - raise RuntimeError("Parameter extraction failed: beta0={}, beta1={}" - .format(beta0, beta1)) + raise RuntimeError( + "Parameter extraction failed: beta0={}, beta1={}".format( + beta0, beta1 + ) + ) else: return beta0, beta1 @@ -219,14 +224,17 @@ def _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim, i_sc): y = beta0 - beta1 * voltage - current x = np.array([np.ones_like(voltage), voltage, current]).T # Select points where y > ilim * i_sc to regress log(y) onto x - idx = (y > ilim * i_sc) + idx = y > ilim * i_sc result = np.linalg.lstsq(x[idx], np.log(y[idx]), rcond=None) coef = result[0] beta3 = coef[1].item() beta4 = coef[2].item() if any(np.isnan([beta3, beta4])): - raise RuntimeError("Parameter extraction failed: beta3={}, beta4={}" - .format(beta3, beta4)) + raise RuntimeError( + "Parameter extraction failed: beta3={}, beta4={}".format( + beta3, beta4 + ) + ) else: return beta3, beta4 @@ -245,7 +253,7 @@ def _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc): raise RuntimeError("Parameter extraction failed: I0 is undetermined.") elif (io_vmp > 0) and (io_voc > 0): io = 0.5 * (io_vmp + io_voc) - elif (io_vmp > 0): + elif io_vmp > 0: io = io_vmp else: # io_voc > 0 io = io_voc @@ -253,8 +261,9 @@ def _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc): def _calc_I0(voltage, current, iph, gsh, rs, nNsVth): - return (iph - current - gsh * (voltage + rs * current)) / \ - np.expm1((voltage + rs * current) / nNsVth) + return (iph - current - gsh * (voltage + rs * current)) / np.expm1( + (voltage + rs * current) / nNsVth + ) def _fit_sandia_cocontent(voltage, current, nsvth): @@ -320,11 +329,11 @@ def _fit_sandia_cocontent(voltage, current, nsvth): """ if len(current) != len(voltage): - raise ValueError("voltage and current should have the same " - "length") + raise ValueError("voltage and current should have the same " "length") if len(voltage) < 6: - raise ValueError("at least 6 voltage points are required; ~50 are " - "recommended") + raise ValueError( + "at least 6 voltage points are required; ~50 are " "recommended" + ) isc = current[0] # short circuit current voc = voltage[-1] # open circuit voltage @@ -341,22 +350,28 @@ def _fit_sandia_cocontent(voltage, current, nsvth): # Extract five parameter values from regression coefficients. # Equation 11, [3] - betagp = beta[3] * 2. + betagp = beta[3] * 2.0 # Equation 12, [3] - betars = (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) / (4. * beta[3]) + betars = (np.sqrt(1.0 + 16.0 * beta[3] * beta[4]) - 1.0) / (4.0 * beta[3]) # Equation 13, [3] - betan = (beta[0] * (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) + 4. * - beta[1] * beta[3]) / (4. * beta[3] * nsvth) + betan = ( + beta[0] * (np.sqrt(1.0 + 16.0 * beta[3] * beta[4]) - 1.0) + + 4.0 * beta[1] * beta[3] + ) / (4.0 * beta[3] * nsvth) # Single diode equation at Voc, approximating Iph + Io by Isc betaio = (isc - voc * betagp) / (np.exp(voc / (betan * nsvth))) # Single diode equation at Isc, using Rsh, Rs, n and Io that were # determined above - betaiph = isc - betaio + betaio * np.exp(isc / (betan * nsvth)) + \ - isc * betars * betagp + betaiph = ( + isc + - betaio + + betaio * np.exp(isc / (betan * nsvth)) + + isc * betars * betagp + ) iph = betaiph io = betaio @@ -375,25 +390,31 @@ def _cocontent(v, c, isc, kflag): # with coefficients in input c, at the discrete sequence of knots in v xn = len(v) delx = v[1:] - v[:-1] - tmp = np.array([1. / 3., .5, 1.]) + tmp = np.array([1.0 / 3.0, 0.5, 1.0]) ss = np.tile(tmp, [xn - 1, 1]) cc = c * ss # cast coefficients to a convenient shape # compute integral on each interval - tmpint = np.sum(cc * np.array([delx ** 3, delx ** 2, delx]).T, 1) - tmpint = np.append(0., tmpint) + tmpint = np.sum(cc * np.array([delx**3, delx**2, delx]).T, 1) + tmpint = np.append(0.0, tmpint) # compute co-content = Int_0^V (Isc - I) dV scc = np.zeros(xn) # Use trapezoid rule for the first 5 intervals due to spline being # unreliable near the left endpoint scc[0:5] = isc * v[0:5] - np.cumsum(tmpint[0:5]) # by spline - scc[5:(xn - 5)] = isc * (v[5:(xn - 5)] - v[4]) - \ - np.cumsum(tmpint[5:(xn - 5)]) + scc[4] + scc[5 : (xn - 5)] = ( + isc * (v[5 : (xn - 5)] - v[4]) + - np.cumsum(tmpint[5 : (xn - 5)]) + + scc[4] + ) # Use trapezoid rule for the last 5 intervals due to spline being # unreliable near the right endpoint - scc[(xn - 5):xn] = isc * (v[(xn - 5):xn] - v[xn - 6]) - \ - np.cumsum(tmpint[(xn - 5):xn]) + scc[xn - 6] + scc[(xn - 5) : xn] = ( + isc * (v[(xn - 5) : xn] - v[xn - 6]) + - np.cumsum(tmpint[(xn - 5) : xn]) + + scc[xn - 6] + ) # For estimating diode equation parameters only use original data points, # not at any knots added by the quadratic spline fit @@ -414,8 +435,9 @@ def _cocontent_regress(v, i, voc, isc, cci): tmpx_mean = np.mean(tmpx, axis=0) tmpx_std = np.std(tmpx, axis=0, ddof=1) - tmpx_zscore = (tmpx - np.tile(tmpx_mean, [tmpx_length, 1])) / \ - np.tile(tmpx_std, [tmpx_length, 1]) + tmpx_zscore = (tmpx - np.tile(tmpx_mean, [tmpx_length, 1])) / np.tile( + tmpx_std, [tmpx_length, 1] + ) tmpx_d, tmpx_v = np.linalg.eig(np.cov(tmpx_zscore.T)) @@ -424,7 +446,7 @@ def _cocontent_regress(v, i, voc, isc, cci): ev1 = tmpx_v[:, idx[0]] # Second component set to be orthogonal and rotated counterclockwise by 90. - ev2 = np.dot(np.array([[0., -1.], [1., 0.]]), ev1) + ev2 = np.dot(np.array([[0.0, -1.0], [1.0, 0.0]]), ev1) r = np.array([ev1, ev2]) # principal components transformation s = np.dot(tmpx_zscore, r) @@ -435,8 +457,16 @@ def _cocontent_regress(v, i, voc, isc, cci): # predictors. Shifting makes a constant term necessary in the regression # model - sx = np.vstack((s[:, 0], s[:, 1], s[:, 0] * s[:, 1], s[:, 0] * s[:, 0], - s[:, 1] * s[:, 1], col1)).T + sx = np.vstack( + ( + s[:, 0], + s[:, 1], + s[:, 0] * s[:, 1], + s[:, 0] * s[:, 0], + s[:, 1] * s[:, 1], + col1, + ) + ).T gamma = np.linalg.lstsq(sx, scc, rcond=None)[0] # coefficients from regression in rotated coordinates @@ -446,24 +476,45 @@ def _cocontent_regress(v, i, voc, isc, cci): # between [V' I' V'I' V'^2 I'^2] and sx, where prime ' indicates shifted # and scaled data. Used to translate from regression coefficients in # rotated coordinates to coefficients in initial V, I coordinates. - mb = np.array([[r[0, 0], r[1, 0], 0., 0., 0.], [r[0, 1], r[1, 1], 0., 0., - 0.], - [0., 0., r[0, 0] * r[1, 1] + r[0, 1] * r[1, 0], 2. * - r[0, 0] * r[0, 1], 2. * r[1, 0] * r[1, 1]], - [0., 0., r[0, 0] * r[1, 0], r[0, 0] ** 2., r[1, 0] ** 2.], - [0., 0., r[0, 1] * r[1, 1], r[0, 1] ** 2., r[1, 1] ** 2.]]) + mb = np.array( + [ + [r[0, 0], r[1, 0], 0.0, 0.0, 0.0], + [r[0, 1], r[1, 1], 0.0, 0.0, 0.0], + [ + 0.0, + 0.0, + r[0, 0] * r[1, 1] + r[0, 1] * r[1, 0], + 2.0 * r[0, 0] * r[0, 1], + 2.0 * r[1, 0] * r[1, 1], + ], + [0.0, 0.0, r[0, 0] * r[1, 0], r[0, 0] ** 2.0, r[1, 0] ** 2.0], + [0.0, 0.0, r[0, 1] * r[1, 1], r[0, 1] ** 2.0, r[1, 1] ** 2.0], + ] + ) # matrix which is used to undo effect of shifting and scaling on regression # coefficients. - ma = np.array([[np.std(v, ddof=1), 0., np.std(v, ddof=1) * - np.mean(isc - i), 2. * np.std(v, ddof=1) * np.mean(v), - 0.], [0., np.std(isc - i, ddof=1), np.std(isc - i, ddof=1) - * np.mean(v), 0., - 2. * np.std(isc - i, ddof=1) * np.mean(isc - i)], - [0., 0., np.std(v, ddof=1) * np.std(isc - i, ddof=1), 0., - 0.], - [0., 0., 0., np.std(v, ddof=1) ** 2., 0.], - [0., 0., 0., 0., np.std(isc - i, ddof=1) ** 2.]]) + ma = np.array( + [ + [ + np.std(v, ddof=1), + 0.0, + np.std(v, ddof=1) * np.mean(isc - i), + 2.0 * np.std(v, ddof=1) * np.mean(v), + 0.0, + ], + [ + 0.0, + np.std(isc - i, ddof=1), + np.std(isc - i, ddof=1) * np.mean(v), + 0.0, + 2.0 * np.std(isc - i, ddof=1) * np.mean(isc - i), + ], + [0.0, 0.0, np.std(v, ddof=1) * np.std(isc - i, ddof=1), 0.0, 0.0], + [0.0, 0.0, 0.0, np.std(v, ddof=1) ** 2.0, 0.0], + [0.0, 0.0, 0.0, 0.0, np.std(isc - i, ddof=1) ** 2.0], + ] + ) # translate from coefficients in rotated space (gamma) to coefficients in # original coordinates (beta) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 9c72fbf7e5..644af6b808 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -21,11 +21,21 @@ from pvlib.tools import _first_order_centered_difference -CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} - - -def fit_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, - gamma_pmp, cells_in_series, temp_ref=25): +CONSTANTS = {"E0": 1000.0, "T0": 25.0, "k": constants.k, "q": constants.e} + + +def fit_cec_sam( + celltype, + v_mp, + i_mp, + v_oc, + i_sc, + alpha_sc, + beta_voc, + gamma_pmp, + cells_in_series, + temp_ref=25, +): """ Estimates parameters for the CEC single diode model (SDM) using the SAM SDK. @@ -101,26 +111,50 @@ def fit_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, try: from PySAM import PySSC except ImportError: - raise ImportError("Requires NREL's PySAM package at " - "https://pypi.org/project/NREL-PySAM/.") - - datadict = {'tech_model': '6parsolve', 'financial_model': None, - 'celltype': celltype, 'Vmp': v_mp, - 'Imp': i_mp, 'Voc': v_oc, 'Isc': i_sc, 'alpha_isc': alpha_sc, - 'beta_voc': beta_voc, 'gamma_pmp': gamma_pmp, - 'Nser': cells_in_series, 'Tref': temp_ref} + raise ImportError( + "Requires NREL's PySAM package at " + "https://pypi.org/project/NREL-PySAM/." + ) + + datadict = { + "tech_model": "6parsolve", + "financial_model": None, + "celltype": celltype, + "Vmp": v_mp, + "Imp": i_mp, + "Voc": v_oc, + "Isc": i_sc, + "alpha_isc": alpha_sc, + "beta_voc": beta_voc, + "gamma_pmp": gamma_pmp, + "Nser": cells_in_series, + "Tref": temp_ref, + } result = PySSC.ssc_sim_from_dict(datadict) - if result['cmod_success'] == 1: - return tuple([result[k] for k in ['Il', 'Io', 'Rs', 'Rsh', 'a', - 'Adj']]) + if result["cmod_success"] == 1: + return tuple( + [result[k] for k in ["Il", "Io", "Rs", "Rsh", "a", "Adj"]] + ) else: - raise RuntimeError('Parameter estimation failed') - - -def fit_desoto(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, cells_in_series, - EgRef=1.121, dEgdT=-0.0002677, temp_ref=25, irrad_ref=1000, - init_guess={}, root_kwargs={}): + raise RuntimeError("Parameter estimation failed") + + +def fit_desoto( + v_mp, + i_mp, + v_oc, + i_sc, + alpha_sc, + beta_voc, + cells_in_series, + EgRef=1.121, + dEgdT=-0.0002677, + temp_ref=25, + irrad_ref=1000, + init_guess={}, + root_kwargs={}, +): """ Calculates the parameters for the De Soto single diode model. @@ -215,55 +249,63 @@ def fit_desoto(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, cells_in_series, """ # Constants - k = constants.value('Boltzmann constant in eV/K') # in eV/K + k = constants.value("Boltzmann constant in eV/K") # in eV/K Tref = temp_ref + 273.15 # [K] # initial guesses of variables for computing convergence: # Default values are taken from [1], p753 - init_guess_keys = ['IL_0', 'Io_0', 'Rs_0', 'Rsh_0', 'a_0'] # order matters + init_guess_keys = ["IL_0", "Io_0", "Rs_0", "Rsh_0", "a_0"] # order matters init = {key: None for key in init_guess_keys} - init['IL_0'] = i_sc - init['a_0'] = 1.5*k*Tref*cells_in_series - init['Io_0'] = i_sc * np.exp(-v_oc/init['a_0']) - init['Rs_0'] = (init['a_0']*np.log1p((init['IL_0'] - i_mp)/init['Io_0']) - - v_mp) / i_mp - init['Rsh_0'] = 100.0 + init["IL_0"] = i_sc + init["a_0"] = 1.5 * k * Tref * cells_in_series + init["Io_0"] = i_sc * np.exp(-v_oc / init["a_0"]) + init["Rs_0"] = ( + init["a_0"] * np.log1p((init["IL_0"] - i_mp) / init["Io_0"]) - v_mp + ) / i_mp + init["Rsh_0"] = 100.0 # overwrite if optional init_guess is provided for key in init_guess: if key in init_guess_keys: init[key] = init_guess[key] else: - raise ValueError(f"'{key}' is not a valid name;" - f" allowed values are {init_guess_keys}") + raise ValueError( + f"'{key}' is not a valid name;" + f" allowed values are {init_guess_keys}" + ) # params_i : initial values vector params_i = np.array([init[k] for k in init_guess_keys]) # specs of module - specs = (i_sc, v_oc, i_mp, v_mp, beta_voc, alpha_sc, EgRef, dEgdT, - Tref, k) + specs = (i_sc, v_oc, i_mp, v_mp, beta_voc, alpha_sc, EgRef, dEgdT, Tref, k) # computing with system of equations described in [1] - optimize_result = optimize.root(_system_of_equations_desoto, x0=params_i, - args=(specs,), **root_kwargs) + optimize_result = optimize.root( + _system_of_equations_desoto, x0=params_i, args=(specs,), **root_kwargs + ) if optimize_result.success: sdm_params = optimize_result.x else: raise RuntimeError( - 'Parameter estimation failed:\n' + optimize_result.message) + "Parameter estimation failed:\n" + optimize_result.message + ) # results - return ({'I_L_ref': sdm_params[0], - 'I_o_ref': sdm_params[1], - 'R_s': sdm_params[2], - 'R_sh_ref': sdm_params[3], - 'a_ref': sdm_params[4], - 'alpha_sc': alpha_sc, - 'EgRef': EgRef, - 'dEgdT': dEgdT, - 'irrad_ref': irrad_ref, - 'temp_ref': temp_ref}, - optimize_result) + return ( + { + "I_L_ref": sdm_params[0], + "I_o_ref": sdm_params[1], + "R_s": sdm_params[2], + "R_sh_ref": sdm_params[3], + "a_ref": sdm_params[4], + "alpha_sc": alpha_sc, + "EgRef": EgRef, + "dEgdT": dEgdT, + "irrad_ref": irrad_ref, + "temp_ref": temp_ref, + }, + optimize_result, + ) def _system_of_equations_desoto(params, specs): @@ -301,14 +343,15 @@ def _system_of_equations_desoto(params, specs): y[1] = -IL + Io * np.expm1(Voc / a) + Voc / Rsh # 3rd equation - Imp & Vmp - eq(5) in [1] - y[2] = Imp - IL + Io * np.expm1((Vmp + Imp * Rs) / a) \ - + (Vmp + Imp * Rs) / Rsh + y[2] = ( + Imp - IL + Io * np.expm1((Vmp + Imp * Rs) / a) + (Vmp + Imp * Rs) / Rsh + ) # 4th equation - Pmp derivated=0 - eq23.2.6 in [2] # caution: eq(6) in [1] has a sign error - y[3] = Imp \ - - Vmp * ((Io / a) * np.exp((Vmp + Imp * Rs) / a) + 1.0 / Rsh) \ - / (1.0 + (Io * Rs / a) * np.exp((Vmp + Imp * Rs) / a) + Rs / Rsh) + y[3] = Imp - Vmp * ( + (Io / a) * np.exp((Vmp + Imp * Rs) / a) + 1.0 / Rsh + ) / (1.0 + (Io * Rs / a) * np.exp((Vmp + Imp * Rs) / a) + Rs / Rsh) # 5th equation - open-circuit T2 - eq (4) at temperature T2 in [1] T2 = Tref + 2 @@ -316,13 +359,15 @@ def _system_of_equations_desoto(params, specs): a2 = a * T2 / Tref # eq (8) in [1] IL2 = IL + alpha_sc * (T2 - Tref) # eq (11) in [1] Eg2 = EgRef * (1 + dEgdT * (T2 - Tref)) # eq (10) in [1] - Io2 = Io * (T2 / Tref)**3 * np.exp(1 / k * (EgRef/Tref - Eg2/T2)) # eq (9) + Io2 = ( + Io * (T2 / Tref) ** 3 * np.exp(1 / k * (EgRef / Tref - Eg2 / T2)) + ) # eq (9) y[4] = -IL2 + Io2 * np.expm1(Voc2 / a2) + Voc2 / Rsh # eq (4) at T2 return y -def fit_pvsyst_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): +def fit_pvsyst_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.0e-3): """ Estimate parameters for the PVsyst module performance model. @@ -443,18 +488,18 @@ def fit_pvsyst_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): if const is None: const = CONSTANTS - ee = ivcurves['ee'] - tc = ivcurves['tc'] + ee = ivcurves["ee"] + tc = ivcurves["tc"] tck = tc + 273.15 - isc = ivcurves['i_sc'] - voc = ivcurves['v_oc'] - imp = ivcurves['i_mp'] - vmp = ivcurves['v_mp'] + isc = ivcurves["i_sc"] + voc = ivcurves["v_oc"] + imp = ivcurves["i_mp"] + vmp = ivcurves["v_mp"] # Cell Thermal Voltage - vth = const['k'] / const['q'] * tck + vth = const["k"] / const["q"] * tck - n = len(ivcurves['v_oc']) + n = len(ivcurves["v_oc"]) # Initial estimate of Rsh used to obtain the diode factor gamma0 and diode # temperature coefficient mu_gamma. Rsh is estimated using the co-content @@ -462,48 +507,55 @@ def fit_pvsyst_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): rsh = np.ones(n) for j in range(n): - voltage, current = rectify_iv_curve(ivcurves['v'][j], ivcurves['i'][j]) + voltage, current = rectify_iv_curve(ivcurves["v"][j], ivcurves["i"][j]) # initial estimate of Rsh, from integral over voltage regression # [5] Step 3a; [6] Step 3a _, _, _, rsh[j], _ = _fit_sandia_cocontent( - voltage, current, vth[j] * specs['cells_in_series']) + voltage, current, vth[j] * specs["cells_in_series"] + ) - gamma_ref, mu_gamma = _fit_pvsyst_sandia_gamma(voc, isc, rsh, vth, tck, - specs, const) + gamma_ref, mu_gamma = _fit_pvsyst_sandia_gamma( + voc, isc, rsh, vth, tck, specs, const + ) - badgamma = np.isnan(gamma_ref) or np.isnan(mu_gamma) \ - or not np.isreal(gamma_ref) or not np.isreal(mu_gamma) + badgamma = ( + np.isnan(gamma_ref) + or np.isnan(mu_gamma) + or not np.isreal(gamma_ref) + or not np.isreal(mu_gamma) + ) if badgamma: raise RuntimeError( "Failed to estimate the diode (ideality) factor parameter;" - " aborting parameter estimation.") + " aborting parameter estimation." + ) - gamma = gamma_ref + mu_gamma * (tc - const['T0']) - nnsvth = gamma * (vth * specs['cells_in_series']) + gamma = gamma_ref + mu_gamma * (tc - const["T0"]) + nnsvth = gamma * (vth * specs["cells_in_series"]) # For each IV curve, sequentially determine initial values for Io, Rs, # and Iph [5] Step 3a; [6] Step 3 - iph, io, rs, u = _initial_iv_params(ivcurves, ee, voc, isc, rsh, - nnsvth) + iph, io, rs, u = _initial_iv_params(ivcurves, ee, voc, isc, rsh, nnsvth) # Update values for each IV curve to converge at vmp, imp, voc and isc - iph, io, rs, rsh, u = _update_iv_params(voc, isc, vmp, imp, ee, - iph, io, rs, rsh, nnsvth, u, - maxiter, eps1) + iph, io, rs, rsh, u = _update_iv_params( + voc, isc, vmp, imp, ee, iph, io, rs, rsh, nnsvth, u, maxiter, eps1 + ) # get single diode models from converged values for each IV curve - pvsyst = _extract_sdm_params(ee, tc, iph, io, rs, rsh, gamma, u, - specs, const, model='pvsyst') + pvsyst = _extract_sdm_params( + ee, tc, iph, io, rs, rsh, gamma, u, specs, const, model="pvsyst" + ) # Add parameters estimated in this function - pvsyst['gamma_ref'] = gamma_ref - pvsyst['mu_gamma'] = mu_gamma - pvsyst['cells_in_series'] = specs['cells_in_series'] + pvsyst["gamma_ref"] = gamma_ref + pvsyst["mu_gamma"] = mu_gamma + pvsyst["cells_in_series"] = specs["cells_in_series"] return pvsyst -def fit_desoto_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): +def fit_desoto_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.0e-3): """ Estimate parameters for the De Soto module performance model. @@ -614,16 +666,16 @@ def fit_desoto_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): if const is None: const = CONSTANTS - ee = ivcurves['ee'] - tc = ivcurves['tc'] + ee = ivcurves["ee"] + tc = ivcurves["tc"] tck = tc + 273.15 - isc = ivcurves['i_sc'] - voc = ivcurves['v_oc'] - imp = ivcurves['i_mp'] - vmp = ivcurves['v_mp'] + isc = ivcurves["i_sc"] + voc = ivcurves["v_oc"] + imp = ivcurves["i_mp"] + vmp = ivcurves["v_mp"] # Cell Thermal Voltage - vth = const['k'] / const['q'] * tck + vth = const["k"] / const["q"] * tck n = len(voc) @@ -633,11 +685,12 @@ def fit_desoto_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): rsh = np.ones(n) for j in range(n): - voltage, current = rectify_iv_curve(ivcurves['v'][j], ivcurves['i'][j]) + voltage, current = rectify_iv_curve(ivcurves["v"][j], ivcurves["i"][j]) # initial estimate of Rsh, from integral over voltage regression # [5] Step 3a; [6] Step 3a _, _, _, rsh[j], _ = _fit_sandia_cocontent( - voltage, current, vth[j] * specs['cells_in_series']) + voltage, current, vth[j] * specs["cells_in_series"] + ) n0 = _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const) @@ -646,27 +699,33 @@ def fit_desoto_sandia(ivcurves, specs, const=None, maxiter=5, eps1=1.e-3): if bad_n: raise RuntimeError( "Failed to estimate the diode (ideality) factor parameter;" - " aborting parameter estimation.") + " aborting parameter estimation." + ) - nnsvth = n0 * specs['cells_in_series'] * vth + nnsvth = n0 * specs["cells_in_series"] * vth # For each IV curve, sequentially determine initial values for Io, Rs, # and Iph [5] Step 3a; [6] Step 3 - iph, io, rs, u = _initial_iv_params(ivcurves, ee, voc, isc, rsh, - nnsvth) + iph, io, rs, u = _initial_iv_params(ivcurves, ee, voc, isc, rsh, nnsvth) # Update values for each IV curve to converge at vmp, imp, voc and isc - iph, io, rs, rsh, u = _update_iv_params(voc, isc, vmp, imp, ee, - iph, io, rs, rsh, nnsvth, u, - maxiter, eps1) + iph, io, rs, rsh, u = _update_iv_params( + voc, isc, vmp, imp, ee, iph, io, rs, rsh, nnsvth, u, maxiter, eps1 + ) # get single diode models from converged values for each IV curve - desoto = _extract_sdm_params(ee, tc, iph, io, rs, rsh, n0, u, - specs, const, model='desoto') + desoto = _extract_sdm_params( + ee, tc, iph, io, rs, rsh, n0, u, specs, const, model="desoto" + ) # Add parameters estimated in this function - desoto['a_ref'] = n0 * specs['cells_in_series'] * const['k'] / \ - const['q'] * (const['T0'] + 273.15) - desoto['cells_in_series'] = specs['cells_in_series'] + desoto["a_ref"] = ( + n0 + * specs["cells_in_series"] + * const["k"] + / const["q"] + * (const["T0"] + 273.15) + ) + desoto["cells_in_series"] = specs["cells_in_series"] return desoto @@ -675,17 +734,23 @@ def _fit_pvsyst_sandia_gamma(voc, isc, rsh, vth, tck, specs, const): # Estimate the diode factor gamma from Isc-Voc data. Method incorporates # temperature dependence by means of the equation for Io - y = np.log(isc - voc / rsh) - 3. * np.log(tck / (const['T0'] + 273.15)) - x1 = const['q'] / const['k'] * (1. / (const['T0'] + 273.15) - 1. / tck) - x2 = voc / (vth * specs['cells_in_series']) + y = np.log(isc - voc / rsh) - 3.0 * np.log(tck / (const["T0"] + 273.15)) + x1 = const["q"] / const["k"] * (1.0 / (const["T0"] + 273.15) - 1.0 / tck) + x2 = voc / (vth * specs["cells_in_series"]) uu = np.logical_or(np.isnan(y), np.isnan(x1), np.isnan(x2)) - x = np.vstack((np.ones(len(x1[~uu])), x1[~uu], -x1[~uu] * - (tck[~uu] - (const['T0'] + 273.15)), x2[~uu], - -x2[~uu] * (tck[~uu] - (const['T0'] + 273.15)))).T + x = np.vstack( + ( + np.ones(len(x1[~uu])), + x1[~uu], + -x1[~uu] * (tck[~uu] - (const["T0"] + 273.15)), + x2[~uu], + -x2[~uu] * (tck[~uu] - (const["T0"] + 273.15)), + ) + ).T alpha = np.linalg.lstsq(x, y[~uu], rcond=None)[0] - gamma_ref = 1. / alpha[3] + gamma_ref = 1.0 / alpha[3] mu_gamma = alpha[4] / alpha[3] ** 2 return gamma_ref, mu_gamma @@ -697,10 +762,11 @@ def _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const): import statsmodels.api as sm except ImportError: raise ImportError( - 'Parameter extraction using Sandia method requires statsmodels') + "Parameter extraction using Sandia method requires statsmodels" + ) - x = specs['cells_in_series'] * vth * np.log(ee / const['E0']) - y = voc - specs['beta_voc'] * (tc - const['T0']) + x = specs["cells_in_series"] * vth * np.log(ee / const["E0"]) + y = voc - specs["beta_voc"] * (tc - const["T0"]) new_x = sm.add_constant(x) res = sm.RLM(y, new_x).fit() return np.array(res.params)[1] @@ -709,42 +775,48 @@ def _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const): def _initial_iv_params(ivcurves, ee, voc, isc, rsh, nnsvth): # sets initial values for iph, io, rs and quality filter u. # Helper function for fit__sandia. - n = len(ivcurves['v_oc']) + n = len(ivcurves["v_oc"]) io = np.ones(n) iph = np.ones(n) rs = np.ones(n) for j in range(n): - if rsh[j] > 0: - volt, curr = rectify_iv_curve(ivcurves['v'][j], - ivcurves['i'][j]) + volt, curr = rectify_iv_curve(ivcurves["v"][j], ivcurves["i"][j]) # Initial estimate of Io, evaluate the single diode model at # voc and approximate Iph + Io = Isc [5] Step 3a; [6] Step 3b - io[j] = (isc[j] - voc[j] / rsh[j]) * np.exp(-voc[j] / - nnsvth[j]) + io[j] = (isc[j] - voc[j] / rsh[j]) * np.exp(-voc[j] / nnsvth[j]) # initial estimate of rs from dI/dV near Voc # [5] Step 3a; [6] Step 3c [didv, d2id2v] = _numdiff(volt, curr) - t3 = volt > .5 * voc[j] - t4 = volt < .9 * voc[j] - tmp = -rsh[j] * didv - 1. + t3 = volt > 0.5 * voc[j] + t4 = volt < 0.9 * voc[j] + tmp = -rsh[j] * didv - 1.0 with np.errstate(invalid="ignore"): # expect nan in didv - v = np.logical_and.reduce(np.array([t3, t4, ~np.isnan(tmp), - np.greater(tmp, 0)])) + v = np.logical_and.reduce( + np.array([t3, t4, ~np.isnan(tmp), np.greater(tmp, 0)]) + ) if np.any(v): - vtrs = (nnsvth[j] / isc[j] * ( - np.log(tmp[v] * nnsvth[j] / (rsh[j] * io[j])) - - volt[v] / nnsvth[j])) + vtrs = ( + nnsvth[j] + / isc[j] + * ( + np.log(tmp[v] * nnsvth[j] / (rsh[j] * io[j])) + - volt[v] / nnsvth[j] + ) + ) rs[j] = np.mean(vtrs[vtrs > 0], axis=0) else: - rs[j] = 0. + rs[j] = 0.0 # Initial estimate of Iph, evaluate the single diode model at # Isc [5] Step 3a; [6] Step 3d - iph[j] = isc[j] + io[j] * np.expm1(isc[j] / nnsvth[j]) \ + iph[j] = ( + isc[j] + + io[j] * np.expm1(isc[j] / nnsvth[j]) + isc[j] * rs[j] / rsh[j] + ) else: io[j] = np.nan @@ -766,27 +838,33 @@ def _initial_iv_params(ivcurves, ee, voc, isc, rsh, nnsvth): return iph, io, rs, u -def _update_iv_params(voc, isc, vmp, imp, ee, iph, io, rs, rsh, nnsvth, u, - maxiter, eps1): +def _update_iv_params( + voc, isc, vmp, imp, ee, iph, io, rs, rsh, nnsvth, u, maxiter, eps1 +): # Refine Rsh, Rs, Io and Iph in that order. # Helper function for fit__sandia. - counter = 1. # counter variable for parameter updating while loop, + counter = 1.0 # counter variable for parameter updating while loop, # counts iterations prevconvergeparams = {} - prevconvergeparams['state'] = 0.0 + prevconvergeparams["state"] = 0.0 not_converged = np.array([True]) while not_converged.any() and counter <= maxiter: # update rsh to match max power point using a fixed point method. - rsh[u] = _update_rsh_fixed_pt(vmp[u], imp[u], iph[u], io[u], rs[u], - rsh[u], nnsvth[u]) + rsh[u] = _update_rsh_fixed_pt( + vmp[u], imp[u], iph[u], io[u], rs[u], rsh[u], nnsvth[u] + ) # Calculate Rs to be consistent with Rsh and maximum power point - _, phi = _calc_theta_phi_exact(vmp[u], imp[u], iph[u], io[u], - rs[u], rsh[u], nnsvth[u]) - rs[u] = (iph[u] + io[u] - imp[u]) * rsh[u] / imp[u] - \ - nnsvth[u] * phi / imp[u] - vmp[u] / imp[u] + _, phi = _calc_theta_phi_exact( + vmp[u], imp[u], iph[u], io[u], rs[u], rsh[u], nnsvth[u] + ) + rs[u] = ( + (iph[u] + io[u] - imp[u]) * rsh[u] / imp[u] + - nnsvth[u] * phi / imp[u] + - vmp[u] / imp[u] + ) # Update filter for good parameters u = _filter_params(ee, isc, io, rs, rsh) @@ -806,43 +884,45 @@ def _update_iv_params(voc, isc, vmp, imp, ee, iph, io, rs, rsh, nnsvth, u, # check convergence criteria # [5] Step 3d convergeparams = _check_converge( - prevconvergeparams, result, vmp[u], imp[u], counter) + prevconvergeparams, result, vmp[u], imp[u], counter + ) prevconvergeparams = convergeparams - counter += 1. - t5 = prevconvergeparams['vmperrmeanchange'] >= eps1 - t6 = prevconvergeparams['imperrmeanchange'] >= eps1 - t7 = prevconvergeparams['pmperrmeanchange'] >= eps1 - t8 = prevconvergeparams['vmperrstdchange'] >= eps1 - t9 = prevconvergeparams['imperrstdchange'] >= eps1 - t10 = prevconvergeparams['pmperrstdchange'] >= eps1 - t11 = prevconvergeparams['vmperrabsmaxchange'] >= eps1 - t12 = prevconvergeparams['imperrabsmaxchange'] >= eps1 - t13 = prevconvergeparams['pmperrabsmaxchange'] >= eps1 - not_converged = np.logical_or.reduce(np.array([t5, t6, t7, t8, t9, - t10, t11, t12, t13])) + counter += 1.0 + t5 = prevconvergeparams["vmperrmeanchange"] >= eps1 + t6 = prevconvergeparams["imperrmeanchange"] >= eps1 + t7 = prevconvergeparams["pmperrmeanchange"] >= eps1 + t8 = prevconvergeparams["vmperrstdchange"] >= eps1 + t9 = prevconvergeparams["imperrstdchange"] >= eps1 + t10 = prevconvergeparams["pmperrstdchange"] >= eps1 + t11 = prevconvergeparams["vmperrabsmaxchange"] >= eps1 + t12 = prevconvergeparams["imperrabsmaxchange"] >= eps1 + t13 = prevconvergeparams["pmperrabsmaxchange"] >= eps1 + not_converged = np.logical_or.reduce( + np.array([t5, t6, t7, t8, t9, t10, t11, t12, t13]) + ) return iph, io, rs, rsh, u -def _extract_sdm_params(ee, tc, iph, io, rs, rsh, n, u, specs, const, - model): +def _extract_sdm_params(ee, tc, iph, io, rs, rsh, n, u, specs, const, model): # Get single diode model parameters from five parameters iph, io, rs, rsh # and n vs. effective irradiance and temperature try: import statsmodels.api as sm except ImportError: raise ImportError( - 'Parameter extraction using Sandia method requires statsmodels') + "Parameter extraction using Sandia method requires statsmodels" + ) tck = tc + 273.15 - tok = const['T0'] + 273.15 # convert to to K + tok = const["T0"] + 273.15 # convert to to K params = {} - if model == 'pvsyst': + if model == "pvsyst": # Estimate I_o_ref and EgRef - x_for_io = const['q'] / const['k'] * (1. / tok - 1. / tck[u]) / n[u] + x_for_io = const["q"] / const["k"] * (1.0 / tok - 1.0 / tck[u]) / n[u] # Estimate R_sh_0, R_sh_ref and R_sh_exp # Initial guesses. R_sh_0 is value at ee=0. @@ -867,33 +947,44 @@ def fun_rsh(x, rshexp, ee, e0, rsh): x0 = np.array([grsh0, grshref]) beta = optimize.least_squares( - fun_rsh, x0, args=(R_sh_exp, ee[u], const['E0'], rsh[u]), - bounds=np.array([[1., 1.], [1.e7, 1.e6]]), verbose=2) + fun_rsh, + x0, + args=(R_sh_exp, ee[u], const["E0"], rsh[u]), + bounds=np.array([[1.0, 1.0], [1.0e7, 1.0e6]]), + verbose=2, + ) # Extract PVsyst parameter values R_sh_0 = beta.x[0] R_sh_ref = beta.x[1] # parameters unique to PVsyst - params['R_sh_0'] = R_sh_0 - params['R_sh_exp'] = R_sh_exp + params["R_sh_0"] = R_sh_0 + params["R_sh_exp"] = R_sh_exp - elif model == 'desoto': + elif model == "desoto": dEgdT = -0.0002677 - x_for_io = const['q'] / const['k'] * ( - 1. / tok - 1. / tck[u] + dEgdT * (tc[u] - const['T0']) / tck[u]) + x_for_io = ( + const["q"] + / const["k"] + * ( + 1.0 / tok + - 1.0 / tck[u] + + dEgdT * (tc[u] - const["T0"]) / tck[u] + ) + ) # Estimate R_sh_ref nans = np.isnan(rsh) - x = const['E0'] / ee[np.logical_and(u, ee > 400, ~nans)] + x = const["E0"] / ee[np.logical_and(u, ee > 400, ~nans)] y = rsh[np.logical_and(u, ee > 400, ~nans)] new_x = sm.add_constant(x) beta = sm.RLM(y, new_x).fit() R_sh_ref = beta.params[1] - params['dEgdT'] = dEgdT + params["dEgdT"] = dEgdT # Estimate I_o_ref and EgRef - y = np.log(io[u]) - 3. * np.log(tck[u] / tok) + y = np.log(io[u]) - 3.0 * np.log(tck[u] / tok) new_x = sm.add_constant(x_for_io) res = sm.RLM(y, new_x).fit() beta = res.params @@ -901,27 +992,27 @@ def fun_rsh(x, rshexp, ee, e0, rsh): EgRef = beta[1] # Estimate I_L_ref - x = tc[u] - const['T0'] - y = iph[u] * (const['E0'] / ee[u]) + x = tc[u] - const["T0"] + y = iph[u] * (const["E0"] / ee[u]) # average over non-NaN values of Y and X - nans = np.isnan(y - specs['alpha_sc'] * x) - I_L_ref = np.mean(y[~nans] - specs['alpha_sc'] * x[~nans]) + nans = np.isnan(y - specs["alpha_sc"] * x) + I_L_ref = np.mean(y[~nans] - specs["alpha_sc"] * x[~nans]) # Estimate R_s nans = np.isnan(rs) R_s = np.mean(rs[np.logical_and(u, ee > 400, ~nans)]) - params['I_L_ref'] = I_L_ref - params['I_o_ref'] = I_o_ref - params['EgRef'] = EgRef - params['R_sh_ref'] = R_sh_ref - params['R_s'] = R_s + params["I_L_ref"] = I_L_ref + params["I_o_ref"] = I_o_ref + params["EgRef"] = EgRef + params["R_sh_ref"] = R_sh_ref + params["R_s"] = R_s # save values for each IV curve - params['iph'] = iph - params['io'] = io - params['rsh'] = rsh - params['rs'] = rs - params['u'] = u + params["iph"] = iph + params["io"] = io + params["rsh"] = rsh + params["rs"] = rs + params["u"] = u return params @@ -971,19 +1062,19 @@ def _update_io(voc, iph, io, rs, rsh, nnsvth): while maxerr > eps and k < niter: # Predict Voc - pvoc = v_from_i(0., iph, tio, rs, rsh, nnsvth) + pvoc = v_from_i(0.0, iph, tio, rs, rsh, nnsvth) # Difference in Voc dvoc = pvoc - voc # Update Io with np.errstate(invalid="ignore", divide="ignore"): - new_io = tio * (1. + (2. * dvoc) / (2. * nnsvth - dvoc)) + new_io = tio * (1.0 + (2.0 * dvoc) / (2.0 * nnsvth - dvoc)) # Calculate Maximum Percent Difference - maxerr = np.max(np.abs(new_io - tio) / tio) * 100. + maxerr = np.max(np.abs(new_io - tio) / tio) * 100.0 tio = new_io - k += 1. + k += 1.0 return new_io @@ -997,7 +1088,8 @@ def _rsh_pvsyst(x, rshexp, g, go): rshref = x[1] rshb = np.maximum( - (rshref - rsho * np.exp(-rshexp)) / (1. - np.exp(-rshexp)), 0.) + (rshref - rsho * np.exp(-rshexp)) / (1.0 - np.exp(-rshexp)), 0.0 + ) rsh = rshb + (rsho - rshb) * np.exp(-rshexp * g / go) return rsh @@ -1008,23 +1100,24 @@ def _filter_params(ee, isc, io, rs, rsh): # where effective irradiance Ee differs by more than 5% from a linear fit # to Isc vs. Ee - badrsh = np.logical_or(rsh < 0., np.isnan(rsh)) - negrs = rs < 0. + badrsh = np.logical_or(rsh < 0.0, np.isnan(rsh)) + negrs = rs < 0.0 badrs = np.logical_or(rs > rsh, np.isnan(rs)) imagrs = ~(np.isreal(rs)) - badio = np.logical_or(np.logical_or(~(np.isreal(rs)), io <= 0), - np.isnan(io)) + badio = np.logical_or( + np.logical_or(~(np.isreal(rs)), io <= 0), np.isnan(io) + ) goodr = np.logical_and(~badrsh, ~imagrs) goodr = np.logical_and(goodr, ~negrs) goodr = np.logical_and(goodr, ~badrs) goodr = np.logical_and(goodr, ~badio) - matrix = np.vstack((ee / 1000., np.zeros(len(ee)))).T + matrix = np.vstack((ee / 1000.0, np.zeros(len(ee)))).T eff = np.linalg.lstsq(matrix, isc, rcond=None)[0][0] pisc = eff * ee / 1000 pisc_error = np.abs(pisc - isc) / isc # check for departure from linear relation between Isc and Ee - badiph = pisc_error > .05 + badiph = pisc_error > 0.05 u = np.logical_and(goodr, ~badiph) return u @@ -1066,68 +1159,77 @@ def _check_converge(prevparams, result, vmp, imp, i): convergeparam = {} - imperror = (result['i_mp'] - imp) / imp * 100. - vmperror = (result['v_mp'] - vmp) / vmp * 100. - pmperror = (result['p_mp'] - (imp * vmp)) / (imp * vmp) * 100. + imperror = (result["i_mp"] - imp) / imp * 100.0 + vmperror = (result["v_mp"] - vmp) / vmp * 100.0 + pmperror = (result["p_mp"] - (imp * vmp)) / (imp * vmp) * 100.0 - convergeparam['imperrmax'] = max(imperror) # max of the error in Imp - convergeparam['imperrmin'] = min(imperror) # min of the error in Imp + convergeparam["imperrmax"] = max(imperror) # max of the error in Imp + convergeparam["imperrmin"] = min(imperror) # min of the error in Imp # max of the absolute error in Imp - convergeparam['imperrabsmax'] = max(abs(imperror)) + convergeparam["imperrabsmax"] = max(abs(imperror)) # mean of the error in Imp - convergeparam['imperrmean'] = np.mean(imperror, axis=0) + convergeparam["imperrmean"] = np.mean(imperror, axis=0) # std of the error in Imp - convergeparam['imperrstd'] = np.std(imperror, axis=0, ddof=1) + convergeparam["imperrstd"] = np.std(imperror, axis=0, ddof=1) - convergeparam['vmperrmax'] = max(vmperror) # max of the error in Vmp - convergeparam['vmperrmin'] = min(vmperror) # min of the error in Vmp + convergeparam["vmperrmax"] = max(vmperror) # max of the error in Vmp + convergeparam["vmperrmin"] = min(vmperror) # min of the error in Vmp # max of the absolute error in Vmp - convergeparam['vmperrabsmax'] = max(abs(vmperror)) + convergeparam["vmperrabsmax"] = max(abs(vmperror)) # mean of the error in Vmp - convergeparam['vmperrmean'] = np.mean(vmperror, axis=0) + convergeparam["vmperrmean"] = np.mean(vmperror, axis=0) # std of the error in Vmp - convergeparam['vmperrstd'] = np.std(vmperror, axis=0, ddof=1) + convergeparam["vmperrstd"] = np.std(vmperror, axis=0, ddof=1) - convergeparam['pmperrmax'] = max(pmperror) # max of the error in Pmp - convergeparam['pmperrmin'] = min(pmperror) # min of the error in Pmp + convergeparam["pmperrmax"] = max(pmperror) # max of the error in Pmp + convergeparam["pmperrmin"] = min(pmperror) # min of the error in Pmp # max of the abs err. in Pmp - convergeparam['pmperrabsmax'] = max(abs(pmperror)) + convergeparam["pmperrabsmax"] = max(abs(pmperror)) # mean error in Pmp - convergeparam['pmperrmean'] = np.mean(pmperror, axis=0) + convergeparam["pmperrmean"] = np.mean(pmperror, axis=0) # std error Pmp - convergeparam['pmperrstd'] = np.std(pmperror, axis=0, ddof=1) - - if prevparams['state'] != 0.0: - convergeparam['imperrstdchange'] = np.abs( - convergeparam['imperrstd'] / prevparams['imperrstd'] - 1.) - convergeparam['vmperrstdchange'] = np.abs( - convergeparam['vmperrstd'] / prevparams['vmperrstd'] - 1.) - convergeparam['pmperrstdchange'] = np.abs( - convergeparam['pmperrstd'] / prevparams['pmperrstd'] - 1.) - convergeparam['imperrmeanchange'] = np.abs( - convergeparam['imperrmean'] / prevparams['imperrmean'] - 1.) - convergeparam['vmperrmeanchange'] = np.abs( - convergeparam['vmperrmean'] / prevparams['vmperrmean'] - 1.) - convergeparam['pmperrmeanchange'] = np.abs( - convergeparam['pmperrmean'] / prevparams['pmperrmean'] - 1.) - convergeparam['imperrabsmaxchange'] = np.abs( - convergeparam['imperrabsmax'] / prevparams['imperrabsmax'] - 1.) - convergeparam['vmperrabsmaxchange'] = np.abs( - convergeparam['vmperrabsmax'] / prevparams['vmperrabsmax'] - 1.) - convergeparam['pmperrabsmaxchange'] = np.abs( - convergeparam['pmperrabsmax'] / prevparams['pmperrabsmax'] - 1.) - convergeparam['state'] = 1.0 + convergeparam["pmperrstd"] = np.std(pmperror, axis=0, ddof=1) + + if prevparams["state"] != 0.0: + convergeparam["imperrstdchange"] = np.abs( + convergeparam["imperrstd"] / prevparams["imperrstd"] - 1.0 + ) + convergeparam["vmperrstdchange"] = np.abs( + convergeparam["vmperrstd"] / prevparams["vmperrstd"] - 1.0 + ) + convergeparam["pmperrstdchange"] = np.abs( + convergeparam["pmperrstd"] / prevparams["pmperrstd"] - 1.0 + ) + convergeparam["imperrmeanchange"] = np.abs( + convergeparam["imperrmean"] / prevparams["imperrmean"] - 1.0 + ) + convergeparam["vmperrmeanchange"] = np.abs( + convergeparam["vmperrmean"] / prevparams["vmperrmean"] - 1.0 + ) + convergeparam["pmperrmeanchange"] = np.abs( + convergeparam["pmperrmean"] / prevparams["pmperrmean"] - 1.0 + ) + convergeparam["imperrabsmaxchange"] = np.abs( + convergeparam["imperrabsmax"] / prevparams["imperrabsmax"] - 1.0 + ) + convergeparam["vmperrabsmaxchange"] = np.abs( + convergeparam["vmperrabsmax"] / prevparams["vmperrabsmax"] - 1.0 + ) + convergeparam["pmperrabsmaxchange"] = np.abs( + convergeparam["pmperrabsmax"] / prevparams["pmperrabsmax"] - 1.0 + ) + convergeparam["state"] = 1.0 else: - convergeparam['imperrstdchange'] = float("Inf") - convergeparam['vmperrstdchange'] = float("Inf") - convergeparam['pmperrstdchange'] = float("Inf") - convergeparam['imperrmeanchange'] = float("Inf") - convergeparam['vmperrmeanchange'] = float("Inf") - convergeparam['pmperrmeanchange'] = float("Inf") - convergeparam['imperrabsmaxchange'] = float("Inf") - convergeparam['vmperrabsmaxchange'] = float("Inf") - convergeparam['pmperrabsmaxchange'] = float("Inf") - convergeparam['state'] = 1. + convergeparam["imperrstdchange"] = float("Inf") + convergeparam["vmperrstdchange"] = float("Inf") + convergeparam["pmperrstdchange"] = float("Inf") + convergeparam["imperrmeanchange"] = float("Inf") + convergeparam["vmperrmeanchange"] = float("Inf") + convergeparam["pmperrmeanchange"] = float("Inf") + convergeparam["imperrabsmaxchange"] = float("Inf") + convergeparam["vmperrabsmaxchange"] = float("Inf") + convergeparam["pmperrabsmaxchange"] = float("Inf") + convergeparam["state"] = 1.0 return convergeparam @@ -1172,8 +1274,11 @@ def _update_rsh_fixed_pt(vmp, imp, iph, io, rs, rsh, nnsvth): for i in range(niter): _, z = _calc_theta_phi_exact(vmp, imp, iph, io, rs, x1, nnsvth) with np.errstate(divide="ignore"): - next_x1 = (1 + z) / z * ((iph + io) * x1 / imp - nnsvth * z / imp - - 2 * vmp / imp) + next_x1 = ( + (1 + z) + / z + * ((iph + io) * x1 / imp - nnsvth * z / imp - 2 * vmp / imp) + ) x1 = next_x1 return x1 @@ -1239,22 +1344,26 @@ def _calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth): argw = np.where( nnsvth == 0, np.nan, - rsh * io / nnsvth * np.exp(rsh * (iph + io - imp) / nnsvth)) + rsh * io / nnsvth * np.exp(rsh * (iph + io - imp) / nnsvth), + ) phi = np.where(argw > 0, lambertw(argw).real, np.nan) # NaN where argw overflows. Switch to log space to evaluate u = np.isinf(argw) if np.any(u): logargw = ( - np.log(rsh[u]) + np.log(io[u]) - np.log(nnsvth[u]) - + rsh[u] * (iph[u] + io[u] - imp[u]) / nnsvth[u]) + np.log(rsh[u]) + + np.log(io[u]) + - np.log(nnsvth[u]) + + rsh[u] * (iph[u] + io[u] - imp[u]) / nnsvth[u] + ) # Three iterations of Newton-Raphson method to solve w+log(w)=logargW. # The initial guess is w=logargW. Where direct evaluation (above) # results in NaN from overflow, 3 iterations of Newton's method gives # approximately 8 digits of precision. x = logargw for i in range(3): - x *= ((1. - np.log(x) + logargw) / (1. + x)) + x *= (1.0 - np.log(x) + logargw) / (1.0 + x) phi[u] = x phi = np.transpose(phi) @@ -1264,8 +1373,13 @@ def _calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth): argw = np.where( nnsvth == 0, np.nan, - rsh / (rsh + rs) * rs * io / nnsvth * np.exp( - rsh / (rsh + rs) * (rs * (iph + io) + vmp) / nnsvth)) + rsh + / (rsh + rs) + * rs + * io + / nnsvth + * np.exp(rsh / (rsh + rs) * (rs * (iph + io) + vmp) / nnsvth), + ) theta = np.where(argw > 0, lambertw(argw).real, np.nan) # NaN where argw overflows. Switch to log space to evaluate @@ -1273,27 +1387,43 @@ def _calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth): if np.any(u): with np.errstate(divide="ignore"): logargw = ( - np.log(rsh[u]) - np.log(rsh[u] + rs[u]) + np.log(rs[u]) - + np.log(io[u]) - np.log(nnsvth[u]) + np.log(rsh[u]) + - np.log(rsh[u] + rs[u]) + + np.log(rs[u]) + + np.log(io[u]) + - np.log(nnsvth[u]) + (rsh[u] / (rsh[u] + rs[u])) - * (rs[u] * (iph[u] + io[u]) + vmp[u]) / nnsvth[u]) + * (rs[u] * (iph[u] + io[u]) + vmp[u]) + / nnsvth[u] + ) # Three iterations of Newton-Raphson method to solve w+log(w)=logargW. # The initial guess is w=logargW. Where direct evaluation (above) # results in NaN from overflow, 3 iterations of Newton's method gives # approximately 8 digits of precision. x = logargw for i in range(3): - x *= ((1. - np.log(x) + logargw) / (1. + x)) + x *= (1.0 - np.log(x) + logargw) / (1.0 + x) theta[u] = x theta = np.transpose(theta) return theta, phi -def pvsyst_temperature_coeff(alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, - R_sh_ref, R_sh_0, R_s, cells_in_series, - R_sh_exp=5.5, EgRef=1.121, irrad_ref=1000, - temp_ref=25): +def pvsyst_temperature_coeff( + alpha_sc, + gamma_ref, + mu_gamma, + I_L_ref, + I_o_ref, + R_sh_ref, + R_sh_0, + R_s, + cells_in_series, + R_sh_exp=5.5, + EgRef=1.121, + irrad_ref=1000, + temp_ref=25, +): r""" Calculates the temperature coefficient of power for a pvsyst single diode model. @@ -1360,19 +1490,57 @@ def pvsyst_temperature_coeff(alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, of Photovoltaics v5(1), January 2015. """ - def maxp(temp_cell, irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref, - I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef, - temp_ref): + def maxp( + temp_cell, + irrad_ref, + alpha_sc, + gamma_ref, + mu_gamma, + I_L_ref, + I_o_ref, + R_sh_ref, + R_sh_0, + R_s, + cells_in_series, + R_sh_exp, + EgRef, + temp_ref, + ): params = calcparams_pvsyst( - irrad_ref, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_L_ref, - I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef, - irrad_ref, temp_ref) + irrad_ref, + temp_cell, + alpha_sc, + gamma_ref, + mu_gamma, + I_L_ref, + I_o_ref, + R_sh_ref, + R_sh_0, + R_s, + cells_in_series, + R_sh_exp, + EgRef, + irrad_ref, + temp_ref, + ) res = bishop88_mpp(*params) return res[2] - args = (irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref, - I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef, - temp_ref) + args = ( + irrad_ref, + alpha_sc, + gamma_ref, + mu_gamma, + I_L_ref, + I_o_ref, + R_sh_ref, + R_sh_0, + R_s, + cells_in_series, + R_sh_exp, + EgRef, + temp_ref, + ) pmp = maxp(temp_ref, *args) gamma_pdc = _first_order_centered_difference(maxp, x0=temp_ref, args=args) diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py index cde50655dc..f0276460e4 100644 --- a/pvlib/ivtools/utils.py +++ b/pvlib/ivtools/utils.py @@ -10,9 +10,9 @@ # A small number used to decide when a slope is equivalent to zero -EPS_slope = np.finfo('float').eps**(1/3) +EPS_slope = np.finfo("float").eps ** (1 / 3) # A small number used to decide when a slope is equivalent to zero -EPS_val = np.finfo('float').eps +EPS_val = np.finfo("float").eps def _numdiff(x, f): @@ -70,60 +70,122 @@ def _numdiff(x, f): # points. Calculate displacements ff = np.vstack((f[:-4], f[1:-3], f[2:-2], f[3:-1], f[4:])).T - a0 = (np.vstack((x[:-4], x[1:-3], x[2:-2], x[3:-1], x[4:])).T - - np.tile(x[2:-2], [5, 1]).T) + a0 = ( + np.vstack((x[:-4], x[1:-3], x[2:-2], x[3:-1], x[4:])).T + - np.tile(x[2:-2], [5, 1]).T + ) u1 = np.zeros(a0.shape) left = np.zeros(a0.shape) u2 = np.zeros(a0.shape) u1[:, 0] = ( - a0[:, 1] * a0[:, 2] * a0[:, 3] + a0[:, 1] * a0[:, 2] * a0[:, 4] - + a0[:, 1] * a0[:, 3] * a0[:, 4] + a0[:, 2] * a0[:, 3] * a0[:, 4]) + a0[:, 1] * a0[:, 2] * a0[:, 3] + + a0[:, 1] * a0[:, 2] * a0[:, 4] + + a0[:, 1] * a0[:, 3] * a0[:, 4] + + a0[:, 2] * a0[:, 3] * a0[:, 4] + ) u1[:, 1] = ( - a0[:, 0] * a0[:, 2] * a0[:, 3] + a0[:, 0] * a0[:, 2] * a0[:, 4] - + a0[:, 0] * a0[:, 3] * a0[:, 4] + a0[:, 2] * a0[:, 3] * a0[:, 4]) + a0[:, 0] * a0[:, 2] * a0[:, 3] + + a0[:, 0] * a0[:, 2] * a0[:, 4] + + a0[:, 0] * a0[:, 3] * a0[:, 4] + + a0[:, 2] * a0[:, 3] * a0[:, 4] + ) u1[:, 2] = ( - a0[:, 0] * a0[:, 1] * a0[:, 3] + a0[:, 0] * a0[:, 1] * a0[:, 4] - + a0[:, 0] * a0[:, 3] * a0[:, 4] + a0[:, 1] * a0[:, 3] * a0[:, 4]) + a0[:, 0] * a0[:, 1] * a0[:, 3] + + a0[:, 0] * a0[:, 1] * a0[:, 4] + + a0[:, 0] * a0[:, 3] * a0[:, 4] + + a0[:, 1] * a0[:, 3] * a0[:, 4] + ) u1[:, 3] = ( - a0[:, 0] * a0[:, 1] * a0[:, 2] + a0[:, 0] * a0[:, 1] * a0[:, 4] - + a0[:, 0] * a0[:, 2] * a0[:, 4] + a0[:, 1] * a0[:, 2] * a0[:, 4]) + a0[:, 0] * a0[:, 1] * a0[:, 2] + + a0[:, 0] * a0[:, 1] * a0[:, 4] + + a0[:, 0] * a0[:, 2] * a0[:, 4] + + a0[:, 1] * a0[:, 2] * a0[:, 4] + ) u1[:, 4] = ( - a0[:, 0] * a0[:, 1] * a0[:, 2] + a0[:, 0] * a0[:, 1] * a0[:, 3] - + a0[:, 0] * a0[:, 2] * a0[:, 3] + a0[:, 1] * a0[:, 2] * a0[:, 3]) - - left[:, 0] = (a0[:, 0] - a0[:, 1]) * (a0[:, 0] - a0[:, 2]) * \ - (a0[:, 0] - a0[:, 3]) * (a0[:, 0] - a0[:, 4]) - left[:, 1] = (a0[:, 1] - a0[:, 0]) * (a0[:, 1] - a0[:, 2]) * \ - (a0[:, 1] - a0[:, 3]) * (a0[:, 1] - a0[:, 4]) - left[:, 2] = (a0[:, 2] - a0[:, 0]) * (a0[:, 2] - a0[:, 1]) * \ - (a0[:, 2] - a0[:, 3]) * (a0[:, 2] - a0[:, 4]) - left[:, 3] = (a0[:, 3] - a0[:, 0]) * (a0[:, 3] - a0[:, 1]) * \ - (a0[:, 3] - a0[:, 2]) * (a0[:, 3] - a0[:, 4]) - left[:, 4] = (a0[:, 4] - a0[:, 0]) * (a0[:, 4] - a0[:, 1]) * \ - (a0[:, 4] - a0[:, 2]) * (a0[:, 4] - a0[:, 3]) + a0[:, 0] * a0[:, 1] * a0[:, 2] + + a0[:, 0] * a0[:, 1] * a0[:, 3] + + a0[:, 0] * a0[:, 2] * a0[:, 3] + + a0[:, 1] * a0[:, 2] * a0[:, 3] + ) + + left[:, 0] = ( + (a0[:, 0] - a0[:, 1]) + * (a0[:, 0] - a0[:, 2]) + * (a0[:, 0] - a0[:, 3]) + * (a0[:, 0] - a0[:, 4]) + ) + left[:, 1] = ( + (a0[:, 1] - a0[:, 0]) + * (a0[:, 1] - a0[:, 2]) + * (a0[:, 1] - a0[:, 3]) + * (a0[:, 1] - a0[:, 4]) + ) + left[:, 2] = ( + (a0[:, 2] - a0[:, 0]) + * (a0[:, 2] - a0[:, 1]) + * (a0[:, 2] - a0[:, 3]) + * (a0[:, 2] - a0[:, 4]) + ) + left[:, 3] = ( + (a0[:, 3] - a0[:, 0]) + * (a0[:, 3] - a0[:, 1]) + * (a0[:, 3] - a0[:, 2]) + * (a0[:, 3] - a0[:, 4]) + ) + left[:, 4] = ( + (a0[:, 4] - a0[:, 0]) + * (a0[:, 4] - a0[:, 1]) + * (a0[:, 4] - a0[:, 2]) + * (a0[:, 4] - a0[:, 3]) + ) df[2:-2] = np.sum(-(u1 / left) * ff, axis=1) # second derivative u2[:, 0] = ( - a0[:, 1] * a0[:, 2] + a0[:, 1] * a0[:, 3] + a0[:, 1] * a0[:, 4] - + a0[:, 2] * a0[:, 3] + a0[:, 2] * a0[:, 4] + a0[:, 3] * a0[:, 4]) + a0[:, 1] * a0[:, 2] + + a0[:, 1] * a0[:, 3] + + a0[:, 1] * a0[:, 4] + + a0[:, 2] * a0[:, 3] + + a0[:, 2] * a0[:, 4] + + a0[:, 3] * a0[:, 4] + ) u2[:, 1] = ( - a0[:, 0] * a0[:, 2] + a0[:, 0] * a0[:, 3] + a0[:, 0] * a0[:, 4] - + a0[:, 2] * a0[:, 3] + a0[:, 2] * a0[:, 4] + a0[:, 3] * a0[:, 4]) + a0[:, 0] * a0[:, 2] + + a0[:, 0] * a0[:, 3] + + a0[:, 0] * a0[:, 4] + + a0[:, 2] * a0[:, 3] + + a0[:, 2] * a0[:, 4] + + a0[:, 3] * a0[:, 4] + ) u2[:, 2] = ( - a0[:, 0] * a0[:, 1] + a0[:, 0] * a0[:, 3] + a0[:, 0] * a0[:, 4] - + a0[:, 1] * a0[:, 3] + a0[:, 1] * a0[:, 3] + a0[:, 3] * a0[:, 4]) + a0[:, 0] * a0[:, 1] + + a0[:, 0] * a0[:, 3] + + a0[:, 0] * a0[:, 4] + + a0[:, 1] * a0[:, 3] + + a0[:, 1] * a0[:, 3] + + a0[:, 3] * a0[:, 4] + ) u2[:, 3] = ( - a0[:, 0] * a0[:, 1] + a0[:, 0] * a0[:, 2] + a0[:, 0] * a0[:, 4] - + a0[:, 1] * a0[:, 2] + a0[:, 1] * a0[:, 4] + a0[:, 2] * a0[:, 4]) + a0[:, 0] * a0[:, 1] + + a0[:, 0] * a0[:, 2] + + a0[:, 0] * a0[:, 4] + + a0[:, 1] * a0[:, 2] + + a0[:, 1] * a0[:, 4] + + a0[:, 2] * a0[:, 4] + ) u2[:, 4] = ( - a0[:, 0] * a0[:, 1] + a0[:, 0] * a0[:, 2] + a0[:, 0] * a0[:, 3] - + a0[:, 1] * a0[:, 2] + a0[:, 1] * a0[:, 4] + a0[:, 2] * a0[:, 3]) + a0[:, 0] * a0[:, 1] + + a0[:, 0] * a0[:, 2] + + a0[:, 0] * a0[:, 3] + + a0[:, 1] * a0[:, 2] + + a0[:, 1] * a0[:, 4] + + a0[:, 2] * a0[:, 3] + ) - df2[2:-2] = 2. * np.sum(u2 * ff, axis=1) + df2[2:-2] = 2.0 * np.sum(u2 * ff, axis=1) return df, df2 @@ -158,24 +220,24 @@ def rectify_iv_curve(voltage, current, decimals=None): equal to the average of current at duplicated voltages. """ - df = pd.DataFrame(data=np.vstack((voltage, current)).T, columns=['v', 'i']) + df = pd.DataFrame(data=np.vstack((voltage, current)).T, columns=["v", "i"]) # restrict to first quadrant df.dropna(inplace=True) - df = df[(df['v'] >= 0) & (df['i'] >= 0)] + df = df[(df["v"] >= 0) & (df["i"] >= 0)] # sort pairs on voltage, then current - df = df.sort_values(by=['v', 'i'], ascending=[True, False]) + df = df.sort_values(by=["v", "i"], ascending=[True, False]) # eliminate duplicate voltage points if decimals is not None: - df['v'] = np.round(df['v'], decimals=decimals) + df["v"] = np.round(df["v"], decimals=decimals) - _, inv = np.unique(df['v'], return_inverse=True) + _, inv = np.unique(df["v"], return_inverse=True) df.index = inv # average current at each common voltage df = df.groupby(by=inv).mean() tmp = np.array(df).T - return tmp[0, ], tmp[1, ] + return tmp[0,], tmp[1,] def _schumaker_qspline(x, y): @@ -250,7 +312,7 @@ def _schumaker_qspline(x, y): # [3], Eq. 9 for interior points # fix tuning parameters in [2], Eq 9 at chi = .5 and eta = .5 - s[u] = pdelta[u] / (0.5*left[u] + 0.5*right[u]) + s[u] = pdelta[u] / (0.5 * left[u] + 0.5 * right[u]) # [3], Eq. 7 for left endpoint left_end = 2.0 * delta[0] - s[1] @@ -293,7 +355,7 @@ def _schumaker_qspline(x, y): # calculate coefficients uu = np.zeros((k, 6)) - uu[:(n - 1), :] = np.array([tmpx, tmpx2, tmpy, tmps, tmps2, delta]).T + uu[: (n - 1), :] = np.array([tmpx, tmpx2, tmpy, tmps, tmps2, delta]).T # [2], Algorithm 4.1 subpart 1 of Step 5 # original x values that are left points of intervals without internal @@ -301,25 +363,25 @@ def _schumaker_qspline(x, y): # MATLAB differs from NumPy, boolean indices must be same size as # array - xk[:(n - 1)][u] = tmpx[u] - yk[:(n - 1)][u] = tmpy[u] + xk[: (n - 1)][u] = tmpx[u] + yk[: (n - 1)][u] = tmpy[u] # constant term for each polynomial for intervals without knots - a[:(n - 1), 2][u] = tmpy[u] - a[:(n - 1), 1][u] = s[:-1][u] - a[:(n - 1), 0][u] = 0.5 * diffs[u] / delx[u] # leading coefficients + a[: (n - 1), 2][u] = tmpy[u] + a[: (n - 1), 1][u] = s[:-1][u] + a[: (n - 1), 0][u] = 0.5 * diffs[u] / delx[u] # leading coefficients # [2], Algorithm 4.1 subpart 2 of Step 5 # original x values that are left points of intervals with internal knots - xk[:(n-1)][~u] = tmpx[~u] - yk[:(n-1)][~u] = tmpy[~u] + xk[: (n - 1)][~u] = tmpx[~u] + yk[: (n - 1)][~u] = tmpy[~u] aa = s[:-1] - delta b = s[1:] - delta # Since the above two lines can lead to numerical errors, aa and b # are rounded to 0.0 is their absolute value is small enough. - aa[np.isclose(aa, 0., atol=EPS_val)] = 0. - b[np.isclose(b, 0., atol=EPS_val)] = 0. + aa[np.isclose(aa, 0.0, atol=EPS_val)] = 0.0 + b[np.isclose(b, 0.0, atol=EPS_val)] = 0.0 sbar = np.zeros(k) eta = np.zeros(k) @@ -332,103 +394,133 @@ def _schumaker_qspline(x, y): v = np.logical_and(~u, t0) # len(u) == (n - 1) always q = np.sum(v) # number of this type of knot to add - if q > 0.: - xk[(n - 1):(n + q - 1)] = .5 * (tmpx[v] + tmpx2[v]) # knot location - uu[(n - 1):(n + q - 1), :] = np.array([tmpx[v], tmpx2[v], tmpy[v], - tmps[v], tmps2[v], delta[v]]).T - xi[:(n-1)][v] = xk[(n - 1):(n + q - 1)] + if q > 0.0: + xk[(n - 1) : (n + q - 1)] = 0.5 * (tmpx[v] + tmpx2[v]) # knot location + uu[(n - 1) : (n + q - 1), :] = np.array( + [tmpx[v], tmpx2[v], tmpy[v], tmps[v], tmps2[v], delta[v]] + ).T + xi[: (n - 1)][v] = xk[(n - 1) : (n + q - 1)] t1 = np.abs(aa) > np.abs(b) w = np.logical_and(~u, ~v) # second 'else' in Algorithm 4.1 Step 5 w = np.logical_and(w, t1) r = np.sum(w) - if r > 0.: - xk[(n + q - 1):(n + q + r - 1)] = tmpx2[w] + aa[w] * delx[w] / diffs[w] - uu[(n + q - 1):(n + q + r - 1), :] = np.array([tmpx[w], tmpx2[w], - tmpy[w], tmps[w], - tmps2[w], delta[w]]).T - xi[:(n - 1)][w] = xk[(n + q - 1):(n + q + r - 1)] + if r > 0.0: + xk[(n + q - 1) : (n + q + r - 1)] = ( + tmpx2[w] + aa[w] * delx[w] / diffs[w] + ) + uu[(n + q - 1) : (n + q + r - 1), :] = np.array( + [tmpx[w], tmpx2[w], tmpy[w], tmps[w], tmps2[w], delta[w]] + ).T + xi[: (n - 1)][w] = xk[(n + q - 1) : (n + q + r - 1)] z = np.logical_and(~u, ~v) # last 'else' in Algorithm 4.1 Step 5 z = np.logical_and(z, ~w) ss = np.sum(z) - if ss > 0.: - xk[(n + q + r - 1):(n + q + r + ss - 1)] = \ + if ss > 0.0: + xk[(n + q + r - 1) : (n + q + r + ss - 1)] = ( tmpx[z] + b[z] * delx[z] / diffs[z] - uu[(n + q + r - 1):(n + q + r + ss - 1), :] = \ - np.array([tmpx[z], tmpx2[z], tmpy[z], tmps[z], tmps2[z], - delta[z]]).T - xi[:(n-1)][z] = xk[(n + q + r - 1):(n + q + r + ss - 1)] + ) + uu[(n + q + r - 1) : (n + q + r + ss - 1), :] = np.array( + [tmpx[z], tmpx2[z], tmpy[z], tmps[z], tmps2[z], delta[z]] + ).T + xi[: (n - 1)][z] = xk[(n + q + r - 1) : (n + q + r + ss - 1)] # define polynomial coefficients for intervals with added knots ff = ~u - sbar[:(n-1)][ff] = ( - (2 * uu[:(n - 1), 5][ff] - uu[:(n-1), 4][ff]) - + (uu[:(n - 1), 4][ff] - uu[:(n-1), 3][ff]) - * (xi[:(n - 1)][ff] - uu[:(n-1), 0][ff]) - / (uu[:(n - 1), 1][ff] - uu[:(n-1), 0][ff])) - eta[:(n-1)][ff] = ( - (sbar[:(n - 1)][ff] - uu[:(n-1), 3][ff]) - / (xi[:(n - 1)][ff] - uu[:(n-1), 0][ff])) - - sbar[(n - 1):(n + q + r + ss - 1)] = \ - (2 * uu[(n - 1):(n + q + r + ss - 1), 5] - - uu[(n - 1):(n + q + r + ss - 1), 4]) + \ - (uu[(n - 1):(n + q + r + ss - 1), 4] - - uu[(n - 1):(n + q + r + ss - 1), 3]) * \ - (xk[(n - 1):(n + q + r + ss - 1)] - - uu[(n - 1):(n + q + r + ss - 1), 0]) / \ - (uu[(n - 1):(n + q + r + ss - 1), 1] - - uu[(n - 1):(n + q + r + ss - 1), 0]) - eta[(n - 1):(n + q + r + ss - 1)] = \ - (sbar[(n - 1):(n + q + r + ss - 1)] - - uu[(n - 1):(n + q + r + ss - 1), 3]) / \ - (xk[(n - 1):(n + q + r + ss - 1)] - - uu[(n - 1):(n + q + r + ss - 1), 0]) + sbar[: (n - 1)][ff] = (2 * uu[: (n - 1), 5][ff] - uu[: (n - 1), 4][ff]) + ( + uu[: (n - 1), 4][ff] - uu[: (n - 1), 3][ff] + ) * (xi[: (n - 1)][ff] - uu[: (n - 1), 0][ff]) / ( + uu[: (n - 1), 1][ff] - uu[: (n - 1), 0][ff] + ) + eta[: (n - 1)][ff] = (sbar[: (n - 1)][ff] - uu[: (n - 1), 3][ff]) / ( + xi[: (n - 1)][ff] - uu[: (n - 1), 0][ff] + ) + + sbar[(n - 1) : (n + q + r + ss - 1)] = ( + 2 * uu[(n - 1) : (n + q + r + ss - 1), 5] + - uu[(n - 1) : (n + q + r + ss - 1), 4] + ) + ( + uu[(n - 1) : (n + q + r + ss - 1), 4] + - uu[(n - 1) : (n + q + r + ss - 1), 3] + ) * ( + xk[(n - 1) : (n + q + r + ss - 1)] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) / ( + uu[(n - 1) : (n + q + r + ss - 1), 1] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) + eta[(n - 1) : (n + q + r + ss - 1)] = ( + sbar[(n - 1) : (n + q + r + ss - 1)] + - uu[(n - 1) : (n + q + r + ss - 1), 3] + ) / ( + xk[(n - 1) : (n + q + r + ss - 1)] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) # constant term for polynomial for intervals with internal knots - a[:(n - 1), 2][~u] = uu[:(n - 1), 2][~u] - a[:(n - 1), 1][~u] = uu[:(n - 1), 3][~u] - a[:(n - 1), 0][~u] = 0.5 * eta[:(n - 1)][~u] # leading coefficient - - a[(n - 1):(n + q + r + ss - 1), 2] = \ - uu[(n - 1):(n + q + r + ss - 1), 2] + \ - uu[(n - 1):(n + q + r + ss - 1), 3] * \ - (xk[(n - 1):(n + q + r + ss - 1)] - - uu[(n - 1):(n + q + r + ss - 1), 0]) + \ - .5 * eta[(n - 1):(n + q + r + ss - 1)] * \ - (xk[(n - 1):(n + q + r + ss - 1)] - - uu[(n - 1):(n + q + r + ss - 1), 0]) ** 2. - a[(n - 1):(n + q + r + ss - 1), 1] = sbar[(n - 1):(n + q + r + ss - 1)] - a[(n - 1):(n + q + r + ss - 1), 0] = \ - .5 * (uu[(n - 1):(n + q + r + ss - 1), 4] - - sbar[(n - 1):(n + q + r + ss - 1)]) / \ - (uu[(n - 1):(n + q + r + ss - 1), 1] - - uu[(n - 1):(n + q + r + ss - 1), 0]) - - yk[(n - 1):(n + q + r + ss - 1)] = a[(n - 1):(n + q + r + ss - 1), 2] + a[: (n - 1), 2][~u] = uu[: (n - 1), 2][~u] + a[: (n - 1), 1][~u] = uu[: (n - 1), 3][~u] + a[: (n - 1), 0][~u] = 0.5 * eta[: (n - 1)][~u] # leading coefficient + + a[(n - 1) : (n + q + r + ss - 1), 2] = ( + uu[(n - 1) : (n + q + r + ss - 1), 2] + + uu[(n - 1) : (n + q + r + ss - 1), 3] + * ( + xk[(n - 1) : (n + q + r + ss - 1)] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) + + 0.5 + * eta[(n - 1) : (n + q + r + ss - 1)] + * ( + xk[(n - 1) : (n + q + r + ss - 1)] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) + ** 2.0 + ) + a[(n - 1) : (n + q + r + ss - 1), 1] = sbar[(n - 1) : (n + q + r + ss - 1)] + a[(n - 1) : (n + q + r + ss - 1), 0] = ( + 0.5 + * ( + uu[(n - 1) : (n + q + r + ss - 1), 4] + - sbar[(n - 1) : (n + q + r + ss - 1)] + ) + / ( + uu[(n - 1) : (n + q + r + ss - 1), 1] + - uu[(n - 1) : (n + q + r + ss - 1), 0] + ) + ) + + yk[(n - 1) : (n + q + r + ss - 1)] = a[(n - 1) : (n + q + r + ss - 1), 2] xk[n + q + r + ss - 1] = x[n - 1] yk[n + q + r + ss - 1] = y[n - 1] - flag[(n - 1):(n + q + r + ss - 1)] = True # these are all inserted knots + flag[(n - 1) : (n + q + r + ss - 1)] = True # these are all inserted knots tmp = np.vstack((xk, a.T, yk, flag)).T # sort output in terms of increasing x (original plus added knots) - tmp2 = tmp[tmp[:, 0].argsort(kind='mergesort')] + tmp2 = tmp[tmp[:, 0].argsort(kind="mergesort")] t = tmp2[:, 0] outn = len(t) - c = tmp2[0:(outn - 1), 1:4] + c = tmp2[0 : (outn - 1), 1:4] yhat = tmp2[:, 4] kflag = tmp2[:, 5] return t, c, yhat, kflag -def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), - voc_points=3, isc_points=3, mp_fit_order=4): - ''' +def astm_e1036( + v, + i, + imax_limits=(0.75, 1.15), + vmax_limits=(0.75, 1.15), + voc_points=3, + isc_points=3, + mp_fit_order=4, +): + """ Extract photovoltaic IV parameters according to ASTM E1036. Assumes that the power producing portion of the curve is in the first quadrant. @@ -468,64 +560,65 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), .. [1] Standard Test Methods for Electrical Performance of Nonconcentrator Terrestrial Photovoltaic Modules and Arrays Using Reference Cells, ASTM E1036-15(2019), :doi:`10.1520/E1036-15R19` - ''' + """ # Adapted from https://github.com/NREL/iv_params # Copyright (c) 2022, Alliance for Sustainable Energy, LLC # All rights reserved. df = pd.DataFrame() - df['v'] = v - df['i'] = i - df['p'] = df['v'] * df['i'] + df["v"] = v + df["i"] = i + df["p"] = df["v"] * df["i"] # determine if we can use voc and isc estimates - i_min_ind = df['i'].abs().idxmin() - v_min_ind = df['v'].abs().idxmin() - voc_est = df['v'][i_min_ind] - isc_est = df['i'][v_min_ind] + i_min_ind = df["i"].abs().idxmin() + v_min_ind = df["v"].abs().idxmin() + voc_est = df["v"][i_min_ind] + isc_est = df["i"][v_min_ind] # accept the estimates if they are close enough # if not, perform a linear fit - if abs(df['i'][i_min_ind]) <= isc_est * 0.001: + if abs(df["i"][i_min_ind]) <= isc_est * 0.001: voc = voc_est else: - df['i_abs'] = df['i'].abs() - voc_df = df.nsmallest(voc_points, 'i_abs') - voc_fit = Poly.fit(voc_df['i'], voc_df['v'], 1) + df["i_abs"] = df["i"].abs() + voc_df = df.nsmallest(voc_points, "i_abs") + voc_fit = Poly.fit(voc_df["i"], voc_df["v"], 1) voc = voc_fit(0) - if abs(df['v'][v_min_ind]) <= voc_est * 0.005: + if abs(df["v"][v_min_ind]) <= voc_est * 0.005: isc = isc_est else: - df['v_abs'] = df['v'].abs() - isc_df = df.nsmallest(isc_points, 'v_abs') - isc_fit = Poly.fit(isc_df['v'], isc_df['i'], 1) + df["v_abs"] = df["v"].abs() + isc_df = df.nsmallest(isc_points, "v_abs") + isc_fit = Poly.fit(isc_df["v"], isc_df["i"], 1) isc = isc_fit(0) # estimate max power point - max_index = df['p'].idxmax() + max_index = df["p"].idxmax() mp_est = df.loc[max_index] # filter around max power mask = ( - (df['i'] >= imax_limits[0] * mp_est['i']) & - (df['i'] <= imax_limits[1] * mp_est['i']) & - (df['v'] >= vmax_limits[0] * mp_est['v']) & - (df['v'] <= vmax_limits[1] * mp_est['v']) + (df["i"] >= imax_limits[0] * mp_est["i"]) + & (df["i"] <= imax_limits[1] * mp_est["i"]) + & (df["v"] >= vmax_limits[0] * mp_est["v"]) + & (df["v"] <= vmax_limits[1] * mp_est["v"]) ) filtered = df[mask] # fit polynomial and find max - mp_fit = Poly.fit(filtered['v'], filtered['p'], mp_fit_order) + mp_fit = Poly.fit(filtered["v"], filtered["p"], mp_fit_order) # Note that this root finding procedure differs from # the suggestion in the standard roots = mp_fit.deriv().roots() # only consider real roots roots = roots.real[abs(roots.imag) < 1e-5] # only consider roots in the relevant part of the domain - roots = roots[(roots < filtered['v'].max()) & - (roots > filtered['v'].min())] + roots = roots[ + (roots < filtered["v"].max()) & (roots > filtered["v"].min()) + ] vmp = roots[np.argmax(mp_fit(roots))] pmp = mp_fit(vmp) # Imp isn't mentioned for update in the @@ -535,12 +628,12 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15), ff = pmp / (voc * isc) result = {} - result['voc'] = voc - result['isc'] = isc - result['vmp'] = vmp - result['imp'] = imp - result['pmp'] = pmp - result['ff'] = ff - result['mp_fit'] = mp_fit + result["voc"] = voc + result["isc"] = isc + result["vmp"] = vmp + result["imp"] = imp + result["pmp"] = pmp + result["ff"] = ff + result["mp_fit"] = mp_fit return result diff --git a/pvlib/location.py b/pvlib/location.py index 9259f410fa..2c580123e7 100644 --- a/pvlib/location.py +++ b/pvlib/location.py @@ -59,9 +59,9 @@ class Location: pvlib.pvsystem.PVSystem """ - def __init__(self, latitude, longitude, tz='UTC', altitude=None, - name=None): - + def __init__( + self, latitude, longitude, tz="UTC", altitude=None, name=None + ): self.latitude = latitude self.longitude = longitude @@ -69,16 +69,16 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=None, self.tz = tz self.pytz = pytz.timezone(tz) elif isinstance(tz, datetime.timezone): - self.tz = 'UTC' + self.tz = "UTC" self.pytz = pytz.UTC elif isinstance(tz, datetime.tzinfo): self.tz = tz.zone self.pytz = tz elif isinstance(tz, (int, float)): self.tz = tz - self.pytz = pytz.FixedOffset(tz*60) + self.pytz = pytz.FixedOffset(tz * 60) else: - raise TypeError('Invalid tz specification') + raise TypeError("Invalid tz specification") if altitude is None: altitude = lookup_altitude(latitude, longitude) @@ -88,9 +88,10 @@ def __init__(self, latitude, longitude, tz='UTC', altitude=None, self.name = name def __repr__(self): - attrs = ['name', 'latitude', 'longitude', 'altitude', 'tz'] - return ('Location: \n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs)) + attrs = ["name", "latitude", "longitude", "altitude", "tz"] + return "Location: \n " + "\n ".join( + f"{attr}: {getattr(self, attr)}" for attr in attrs + ) @classmethod def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): @@ -114,21 +115,22 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs): # might need code to handle the difference between tmy2 and tmy3 # determine if we're dealing with TMY2 or TMY3 data - tmy2 = tmy_metadata.get('City', False) + tmy2 = tmy_metadata.get("City", False) - latitude = tmy_metadata['latitude'] - longitude = tmy_metadata['longitude'] + latitude = tmy_metadata["latitude"] + longitude = tmy_metadata["longitude"] if tmy2: - name = tmy_metadata['City'] + name = tmy_metadata["City"] else: - name = tmy_metadata['Name'] + name = tmy_metadata["Name"] - tz = tmy_metadata['TZ'] - altitude = tmy_metadata['altitude'] + tz = tmy_metadata["TZ"] + altitude = tmy_metadata["altitude"] - new_object = cls(latitude, longitude, tz=tz, altitude=altitude, - name=name, **kwargs) + new_object = cls( + latitude, longitude, tz=tz, altitude=altitude, name=name, **kwargs + ) # not sure if this should be assigned regardless of input. if tmy_data is not None: @@ -155,24 +157,26 @@ def from_epw(cls, metadata, data=None, **kwargs): Location """ - latitude = metadata['latitude'] - longitude = metadata['longitude'] + latitude = metadata["latitude"] + longitude = metadata["longitude"] - name = metadata['city'] + name = metadata["city"] - tz = metadata['TZ'] - altitude = metadata['altitude'] + tz = metadata["TZ"] + altitude = metadata["altitude"] - new_object = cls(latitude, longitude, tz=tz, altitude=altitude, - name=name, **kwargs) + new_object = cls( + latitude, longitude, tz=tz, altitude=altitude, name=name, **kwargs + ) if data is not None: new_object.weather = data return new_object - def get_solarposition(self, times, pressure=None, temperature=12, - **kwargs): + def get_solarposition( + self, times, pressure=None, temperature=12, **kwargs + ): """ Uses the :py:func:`pvlib.solarposition.get_solarposition` function to calculate the solar zenith, azimuth, etc. at this location. @@ -198,15 +202,24 @@ def get_solarposition(self, times, pressure=None, temperature=12, if pressure is None: pressure = atmosphere.alt2pres(self.altitude) - return solarposition.get_solarposition(times, latitude=self.latitude, - longitude=self.longitude, - altitude=self.altitude, - pressure=pressure, - temperature=temperature, - **kwargs) - - def get_clearsky(self, times, model='ineichen', solar_position=None, - dni_extra=None, **kwargs): + return solarposition.get_solarposition( + times, + latitude=self.latitude, + longitude=self.longitude, + altitude=self.altitude, + pressure=pressure, + temperature=temperature, + **kwargs, + ) + + def get_clearsky( + self, + times, + model="ineichen", + solar_position=None, + dni_extra=None, + **kwargs, + ): """ Calculate the clear sky estimates of GHI, DNI, and/or DHI at this location. @@ -236,49 +249,63 @@ def get_clearsky(self, times, model='ineichen', solar_position=None, dni_extra = irradiance.get_extra_radiation(times) try: - pressure = kwargs.pop('pressure') + pressure = kwargs.pop("pressure") except KeyError: pressure = atmosphere.alt2pres(self.altitude) if solar_position is None: solar_position = self.get_solarposition(times, pressure=pressure) - apparent_zenith = solar_position['apparent_zenith'] - apparent_elevation = solar_position['apparent_elevation'] + apparent_zenith = solar_position["apparent_zenith"] + apparent_elevation = solar_position["apparent_elevation"] - if model == 'ineichen': + if model == "ineichen": try: - linke_turbidity = kwargs.pop('linke_turbidity') + linke_turbidity = kwargs.pop("linke_turbidity") except KeyError: - interp_turbidity = kwargs.pop('interp_turbidity', True) + interp_turbidity = kwargs.pop("interp_turbidity", True) linke_turbidity = clearsky.lookup_linke_turbidity( - times, self.latitude, self.longitude, - interp_turbidity=interp_turbidity) + times, + self.latitude, + self.longitude, + interp_turbidity=interp_turbidity, + ) try: - airmass_absolute = kwargs.pop('airmass_absolute') + airmass_absolute = kwargs.pop("airmass_absolute") except KeyError: airmass_absolute = self.get_airmass( - times, solar_position=solar_position)['airmass_absolute'] - - cs = clearsky.ineichen(apparent_zenith, airmass_absolute, - linke_turbidity, altitude=self.altitude, - dni_extra=dni_extra, **kwargs) - elif model == 'haurwitz': + times, solar_position=solar_position + )["airmass_absolute"] + + cs = clearsky.ineichen( + apparent_zenith, + airmass_absolute, + linke_turbidity, + altitude=self.altitude, + dni_extra=dni_extra, + **kwargs, + ) + elif model == "haurwitz": cs = clearsky.haurwitz(apparent_zenith) - elif model == 'simplified_solis': + elif model == "simplified_solis": cs = clearsky.simplified_solis( - apparent_elevation, pressure=pressure, dni_extra=dni_extra, - **kwargs) + apparent_elevation, + pressure=pressure, + dni_extra=dni_extra, + **kwargs, + ) else: - raise ValueError('{} is not a valid clear sky model. Must be ' - 'one of ineichen, simplified_solis, haurwitz' - .format(model)) + raise ValueError( + "{} is not a valid clear sky model. Must be " + "one of ineichen, simplified_solis, haurwitz".format(model) + ) return cs - def get_airmass(self, times=None, solar_position=None, - model='kastenyoung1989'): + def get_airmass( + self, times=None, solar_position=None, model="kastenyoung1989" + ): """ Calculate the relative and absolute airmass. @@ -310,25 +337,26 @@ def get_airmass(self, times=None, solar_position=None, solar_position = self.get_solarposition(times) if model in atmosphere.APPARENT_ZENITH_MODELS: - zenith = solar_position['apparent_zenith'] + zenith = solar_position["apparent_zenith"] elif model in atmosphere.TRUE_ZENITH_MODELS: - zenith = solar_position['zenith'] + zenith = solar_position["zenith"] else: - raise ValueError(f'{model} is not a valid airmass model') + raise ValueError(f"{model} is not a valid airmass model") airmass_relative = atmosphere.get_relative_airmass(zenith, model) pressure = atmosphere.alt2pres(self.altitude) - airmass_absolute = atmosphere.get_absolute_airmass(airmass_relative, - pressure) + airmass_absolute = atmosphere.get_absolute_airmass( + airmass_relative, pressure + ) airmass = pd.DataFrame(index=solar_position.index) - airmass['airmass_relative'] = airmass_relative - airmass['airmass_absolute'] = airmass_absolute + airmass["airmass_relative"] = airmass_relative + airmass["airmass_absolute"] = airmass_absolute return airmass - def get_sun_rise_set_transit(self, times, method='pyephem', **kwargs): + def get_sun_rise_set_transit(self, times, method="pyephem", **kwargs): """ Calculate sunrise, sunset and transit times. @@ -349,23 +377,26 @@ def get_sun_rise_set_transit(self, times, method='pyephem', **kwargs): Column names are: ``sunrise, sunset, transit``. """ - if method == 'pyephem': + if method == "pyephem": result = solarposition.sun_rise_set_transit_ephem( - times, self.latitude, self.longitude, **kwargs) - elif method == 'spa': + times, self.latitude, self.longitude, **kwargs + ) + elif method == "spa": result = solarposition.sun_rise_set_transit_spa( - times, self.latitude, self.longitude, **kwargs) - elif method == 'geometric': + times, self.latitude, self.longitude, **kwargs + ) + elif method == "geometric": sr, ss, tr = solarposition.sun_rise_set_transit_geometric( - times, self.latitude, self.longitude, **kwargs) - result = pd.DataFrame(index=times, - data={'sunrise': sr, - 'sunset': ss, - 'transit': tr}) + times, self.latitude, self.longitude, **kwargs + ) + result = pd.DataFrame( + index=times, data={"sunrise": sr, "sunset": ss, "transit": tr} + ) else: - raise ValueError('{} is not a valid method. Must be ' - 'one of pyephem, spa, geometric' - .format(method)) + raise ValueError( + "{} is not a valid method. Must be " + "one of pyephem, spa, geometric".format(method) + ) return result @@ -436,13 +467,13 @@ def lookup_altitude(latitude, longitude): """ pvlib_path = pathlib.Path(__file__).parent - filepath = pvlib_path / 'data' / 'Altitude.h5' + filepath = pvlib_path / "data" / "Altitude.h5" - latitude_index = _degrees_to_index(latitude, coordinate='latitude') - longitude_index = _degrees_to_index(longitude, coordinate='longitude') + latitude_index = _degrees_to_index(latitude, coordinate="latitude") + longitude_index = _degrees_to_index(longitude, coordinate="longitude") - with h5py.File(filepath, 'r') as alt_h5_file: - alt = alt_h5_file['Altitude'][latitude_index, longitude_index] + with h5py.File(filepath, "r") as alt_h5_file: + alt = alt_h5_file["Altitude"][latitude_index, longitude_index] # 255 is a special value that means nodata. Fallback to 0 if nodata. if alt == 255: diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8456aac114..8271e42da3 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -21,18 +21,24 @@ # keys that are used to detect input data and assign data to appropriate # ModelChain attribute # for ModelChain.weather -WEATHER_KEYS = ('ghi', 'dhi', 'dni', 'wind_speed', 'temp_air', - 'precipitable_water') +WEATHER_KEYS = ( + "ghi", + "dhi", + "dni", + "wind_speed", + "temp_air", + "precipitable_water", +) # for ModelChain.total_irrad -POA_KEYS = ('poa_global', 'poa_direct', 'poa_diffuse') +POA_KEYS = ("poa_global", "poa_direct", "poa_diffuse") # Optional keys to communicate temperature data. If provided, # 'cell_temperature' overrides ModelChain.temperature_model and sets # ModelChain.cell_temperature to the data. If 'module_temperature' is provdied, # overrides ModelChain.temperature_model with # pvlib.temperature.sapm_celL_from_module -TEMPERATURE_KEYS = ('module_temperature', 'cell_temperature') +TEMPERATURE_KEYS = ("module_temperature", "cell_temperature") DATA_KEYS = WEATHER_KEYS + POA_KEYS + TEMPERATURE_KEYS @@ -48,14 +54,22 @@ # http://prod.sandia.gov/techlib/access-control.cgi/1985/850330.pdf # pvlib python does not implement that model, so use the SAPM instead. PVWATTS_CONFIG = dict( - dc_model='pvwatts', ac_model='pvwatts', losses_model='pvwatts', - transposition_model='perez', aoi_model='physical', - spectral_model='no_loss', temperature_model='sapm' + dc_model="pvwatts", + ac_model="pvwatts", + losses_model="pvwatts", + transposition_model="perez", + aoi_model="physical", + spectral_model="no_loss", + temperature_model="sapm", ) SAPM_CONFIG = dict( - dc_model='sapm', ac_model='sandia', losses_model='no_loss', - aoi_model='sapm', spectral_model='sapm', temperature_model='sapm' + dc_model="sapm", + ac_model="sandia", + losses_model="no_loss", + aoi_model="sapm", + spectral_model="sapm", + temperature_model="sapm", ) @@ -76,15 +90,17 @@ def get_orientation(strategy, **kwargs): ------- surface_tilt, surface_azimuth """ - if strategy == 'south_at_latitude_tilt': + if strategy == "south_at_latitude_tilt": surface_azimuth = 180 - surface_tilt = kwargs['latitude'] - elif strategy == 'flat': + surface_tilt = kwargs["latitude"] + elif strategy == "flat": surface_azimuth = 180 surface_tilt = 0 else: - raise ValueError('invalid orientation strategy. strategy must ' - 'be one of south_at_latitude, flat,') + raise ValueError( + "invalid orientation strategy. strategy must " + "be one of south_at_latitude, flat," + ) return surface_tilt, surface_azimuth @@ -103,9 +119,9 @@ def _getmcattr(self, attr): def _mcr_repr(obj): - ''' + """ Helper for ModelChainResult.__repr__ - ''' + """ if isinstance(obj, tuple): return "Tuple (" + ", ".join([_mcr_repr(o) for o in obj]) + ")" if isinstance(obj, pd.DataFrame): @@ -117,7 +133,7 @@ def _mcr_repr(obj): # Type for fields that vary between arrays -T = TypeVar('T') +T = TypeVar("T") PerArray = Union[T, Tuple[T, ...]] @@ -127,10 +143,19 @@ def _mcr_repr(obj): class ModelChainResult: # these attributes are used in __setattr__ to determine the correct type. _singleton_tuples: bool = field(default=False) - _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier', - 'spectral_modifier', 'cell_temperature', - 'effective_irradiance', 'dc', 'diode_params', - 'dc_ohmic_losses', 'weather', 'albedo'} + _per_array_fields = { + "total_irrad", + "aoi", + "aoi_modifier", + "spectral_modifier", + "cell_temperature", + "effective_irradiance", + "dc", + "diode_params", + "dc_ohmic_losses", + "weather", + "albedo", + } # system-level information solar_position: Optional[pd.DataFrame] = field(default=None) @@ -173,16 +198,18 @@ class ModelChainResult: incidence (degrees); see :py:func:`~pvlib.irradiance.aoi` for details. """ - aoi_modifier: Optional[PerArray[Union[pd.Series, float]]] = \ - field(default=None) + aoi_modifier: Optional[PerArray[Union[pd.Series, float]]] = field( + default=None + ) """Series (or tuple of Series, one for each array) containing angle of incidence modifier (unitless) calculated by ``ModelChain.aoi_model``, which reduces direct irradiance for reflections; see :py:meth:`~pvlib.pvsystem.PVSystem.get_iam` for details. """ - spectral_modifier: Optional[PerArray[Union[pd.Series, float]]] = \ - field(default=None) + spectral_modifier: Optional[PerArray[Union[pd.Series, float]]] = field( + default=None + ) """Series (or tuple of Series, one for each array) containing spectral modifier (unitless) calculated by ``ModelChain.spectral_model``, which adjusts broadband plane-of-array irradiance for spectral content. @@ -199,8 +226,9 @@ class ModelChainResult: reflections and spectral content. """ - dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = \ - field(default=None) + dc: Optional[PerArray[Union[pd.Series, pd.DataFrame]]] = field( + default=None + ) """Series or DataFrame (or tuple of Series or DataFrame, one for each array) containing DC power (W) for each array, calculated by ``ModelChain.dc_model``. @@ -236,9 +264,11 @@ def _result_type(self, value): """Coerce `value` to the correct type according to ``self._singleton_tuples``.""" # Allow None to pass through without being wrapped in a tuple - if (self._singleton_tuples - and not isinstance(value, tuple) - and value is not None): + if ( + self._singleton_tuples + and not isinstance(value, tuple) + and value is not None + ): return (value,) return value @@ -261,18 +291,16 @@ def _head(obj): else: num_arrays = 1 - desc1 = ('=== ModelChainResult === \n') - desc2 = (f'Number of Arrays: {num_arrays} \n') - attr = 'times' - desc3 = ('times (first 3)\n' + - f'{_head(_getmcattr(self, attr))}' + - '\n') + desc1 = "=== ModelChainResult === \n" + desc2 = f"Number of Arrays: {num_arrays} \n" + attr = "times" + desc3 = "times (first 3)\n" + f"{_head(_getmcattr(self, attr))}" + "\n" lines = [] for attr in mc_attrs: - if not (attr.startswith('_') or attr=='times'): - lines.append(f' {attr}: ' + _mcr_repr(getattr(self, attr))) - desc4 = '\n'.join(lines) - return (desc1 + desc2 + desc3 + desc4) + if not (attr.startswith("_") or attr == "times"): + lines.append(f" {attr}: " + _mcr_repr(getattr(self, attr))) + desc4 = "\n".join(lines) + return desc1 + desc2 + desc3 + desc4 class ModelChain: @@ -355,16 +383,23 @@ class ModelChain: Name of ModelChain instance. """ - def __init__(self, system, location, - clearsky_model='ineichen', - transposition_model='haydavies', - solar_position_method='nrel_numpy', - airmass_model='kastenyoung1989', - dc_model=None, ac_model=None, aoi_model=None, - spectral_model=None, temperature_model=None, - dc_ohmic_model='no_loss', - losses_model='no_loss', name=None): - + def __init__( + self, + system, + location, + clearsky_model="ineichen", + transposition_model="haydavies", + solar_position_method="nrel_numpy", + airmass_model="kastenyoung1989", + dc_model=None, + ac_model=None, + aoi_model=None, + spectral_model=None, + temperature_model=None, + dc_ohmic_model="no_loss", + losses_model="no_loss", + name=None, + ): self.name = name self.system = system @@ -386,13 +421,16 @@ def __init__(self, system, location, self.results = ModelChainResult() - @classmethod - def with_pvwatts(cls, system, location, - clearsky_model='ineichen', - airmass_model='kastenyoung1989', - name=None, - **kwargs): + def with_pvwatts( + cls, + system, + location, + clearsky_model="ineichen", + airmass_model="kastenyoung1989", + name=None, + **kwargs, + ): """ ModelChain that follows the PVWatts methods. @@ -469,21 +507,26 @@ def with_pvwatts(cls, system, location, config = PVWATTS_CONFIG.copy() config.update(kwargs) return ModelChain( - system, location, + system, + location, clearsky_model=clearsky_model, airmass_model=airmass_model, name=name, - **config + **config, ) @classmethod - def with_sapm(cls, system, location, - clearsky_model='ineichen', - transposition_model='haydavies', - solar_position_method='nrel_numpy', - airmass_model='kastenyoung1989', - name=None, - **kwargs): + def with_sapm( + cls, + system, + location, + clearsky_model="ineichen", + transposition_model="haydavies", + solar_position_method="nrel_numpy", + airmass_model="kastenyoung1989", + name=None, + **kwargs, + ): """ ModelChain that follows the Sandia Array Performance Model (SAPM) methods. @@ -547,24 +590,33 @@ def with_sapm(cls, system, location, config = SAPM_CONFIG.copy() config.update(kwargs) return ModelChain( - system, location, + system, + location, clearsky_model=clearsky_model, transposition_model=transposition_model, solar_position_method=solar_position_method, airmass_model=airmass_model, name=name, - **config + **config, ) def __repr__(self): attrs = [ - 'name', 'clearsky_model', - 'transposition_model', 'solar_position_method', - 'airmass_model', 'dc_model', 'ac_model', 'aoi_model', - 'spectral_model', 'temperature_model', 'losses_model' + "name", + "clearsky_model", + "transposition_model", + "solar_position_method", + "airmass_model", + "dc_model", + "ac_model", + "aoi_model", + "spectral_model", + "temperature_model", + "losses_model", ] - return ('ModelChain: \n ' + '\n '.join( - f'{attr}: {_getmcattr(self, attr)}' for attr in attrs)) + return "ModelChain: \n " + "\n ".join( + f"{attr}: {_getmcattr(self, attr)}" for attr in attrs + ) @property def dc_model(self): @@ -582,77 +634,110 @@ def dc_model(self, model): if model in _DC_MODEL_PARAMS.keys(): # validate module parameters module_parameters = tuple( - array.module_parameters for array in self.system.arrays) - missing_params = ( - _DC_MODEL_PARAMS[model] - _common_keys(module_parameters)) + array.module_parameters for array in self.system.arrays + ) + missing_params = _DC_MODEL_PARAMS[model] - _common_keys( + module_parameters + ) if missing_params: # some parameters are not in module.keys() - raise ValueError(model + ' selected for the DC model but ' - 'one or more Arrays are missing ' - 'one or more required parameters ' - ' : ' + str(missing_params)) - if model == 'sapm': + raise ValueError( + model + " selected for the DC model but " + "one or more Arrays are missing " + "one or more required parameters " + " : " + str(missing_params) + ) + if model == "sapm": self._dc_model = self.sapm - elif model == 'desoto': + elif model == "desoto": self._dc_model = self.desoto - elif model == 'cec': + elif model == "cec": self._dc_model = self.cec - elif model == 'pvsyst': + elif model == "pvsyst": self._dc_model = self.pvsyst - elif model == 'pvwatts': + elif model == "pvwatts": self._dc_model = self.pvwatts_dc else: - raise ValueError(model + ' is not a valid DC power model') + raise ValueError(model + " is not a valid DC power model") else: self._dc_model = partial(model, self) def infer_dc_model(self): """Infer DC power model from Array module parameters.""" params = _common_keys( - tuple(array.module_parameters for array in self.system.arrays)) - if {'A0', 'A1', 'C7'} <= params: - return self.sapm, 'sapm' - elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', - 'Adjust'} <= params: - return self.cec, 'cec' - elif {'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s'} <= params: - return self.desoto, 'desoto' - elif {'gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', 'R_sh_ref', - 'R_sh_0', 'R_sh_exp', 'R_s'} <= params: - return self.pvsyst, 'pvsyst' - elif {'pdc0', 'gamma_pdc'} <= params: - return self.pvwatts_dc, 'pvwatts' + tuple(array.module_parameters for array in self.system.arrays) + ) + if {"A0", "A1", "C7"} <= params: + return self.sapm, "sapm" + elif { + "a_ref", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_s", + "Adjust", + } <= params: + return self.cec, "cec" + elif {"a_ref", "I_L_ref", "I_o_ref", "R_sh_ref", "R_s"} <= params: + return self.desoto, "desoto" + elif { + "gamma_ref", + "mu_gamma", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_sh_0", + "R_sh_exp", + "R_s", + } <= params: + return self.pvsyst, "pvsyst" + elif {"pdc0", "gamma_pdc"} <= params: + return self.pvwatts_dc, "pvwatts" else: raise ValueError( - 'Could not infer DC model from the module_parameters ' - 'attributes of system.arrays. Check the module_parameters ' - 'attributes or explicitly set the model with the dc_model ' - 'keyword argument.') + "Could not infer DC model from the module_parameters " + "attributes of system.arrays. Check the module_parameters " + "attributes or explicitly set the model with the dc_model " + "keyword argument." + ) def sapm(self): - dc = self.system.sapm(self.results.effective_irradiance, - self.results.cell_temperature) + dc = self.system.sapm( + self.results.effective_irradiance, self.results.cell_temperature + ) self.results.dc = self.system.scale_voltage_current_power(dc) return self def _singlediode(self, calcparams_model_function): - def _make_diode_params(photocurrent, saturation_current, - resistance_series, resistance_shunt, - nNsVth): + def _make_diode_params( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ): return pd.DataFrame( - {'I_L': photocurrent, 'I_o': saturation_current, - 'R_s': resistance_series, 'R_sh': resistance_shunt, - 'nNsVth': nNsVth} + { + "I_L": photocurrent, + "I_o": saturation_current, + "R_s": resistance_series, + "R_sh": resistance_shunt, + "nNsVth": nNsVth, + } ) - params = calcparams_model_function(self.results.effective_irradiance, - self.results.cell_temperature, - unwrap=False) - self.results.diode_params = tuple(itertools.starmap( - _make_diode_params, params)) - self.results.dc = tuple(itertools.starmap( - self.system.singlediode, params)) + + params = calcparams_model_function( + self.results.effective_irradiance, + self.results.cell_temperature, + unwrap=False, + ) + self.results.diode_params = tuple( + itertools.starmap(_make_diode_params, params) + ) + self.results.dc = tuple( + itertools.starmap(self.system.singlediode, params) + ) self.results.dc = self.system.scale_voltage_current_power( - self.results.dc, - unwrap=False + self.results.dc, unwrap=False ) self.results.dc = tuple(dc.fillna(0) for dc in self.results.dc) # If the system has one Array, unwrap the single return value @@ -690,9 +775,9 @@ def pvwatts_dc(self): dc = self.system.pvwatts_dc( self.results.effective_irradiance, self.results.cell_temperature, - unwrap=False + unwrap=False, ) - p_mp = tuple(pd.DataFrame(s, columns=['p_mp']) for s in dc) + p_mp = tuple(pd.DataFrame(s, columns=["p_mp"]) for s in dc) scaled = self.system.scale_voltage_current_power(p_mp) self.results.dc = _tuple_from_dfs(scaled, "p_mp") return self @@ -707,14 +792,14 @@ def ac_model(self, model): self._ac_model = self.infer_ac_model() elif isinstance(model, str): model = model.lower() - if model == 'sandia': + if model == "sandia": self._ac_model = self.sandia_inverter - elif model in 'adr': + elif model in "adr": self._ac_model = self.adr_inverter - elif model == 'pvwatts': + elif model == "pvwatts": self._ac_model = self.pvwatts_inverter else: - raise ValueError(model + ' is not a valid AC power model') + raise ValueError(model + " is not a valid AC power model") else: self._ac_model = partial(model, self) @@ -726,35 +811,36 @@ def infer_ac_model(self): if _adr_params(inverter_params): if self.system.num_arrays > 1: raise ValueError( - 'The adr inverter function cannot be used for an inverter', - ' with multiple MPPT inputs') + "The adr inverter function cannot be used for an inverter", + " with multiple MPPT inputs", + ) else: return self.adr_inverter if _pvwatts_params(inverter_params): return self.pvwatts_inverter - raise ValueError('could not infer AC model from ' - 'system.inverter_parameters. Check ' - 'system.inverter_parameters or explicitly ' - 'set the model with the ac_model kwarg.') + raise ValueError( + "could not infer AC model from " + "system.inverter_parameters. Check " + "system.inverter_parameters or explicitly " + "set the model with the ac_model kwarg." + ) def sandia_inverter(self): self.results.ac = self.system.get_ac( - 'sandia', - _tuple_from_dfs(self.results.dc, 'p_mp'), - v_dc=_tuple_from_dfs(self.results.dc, 'v_mp') + "sandia", + _tuple_from_dfs(self.results.dc, "p_mp"), + v_dc=_tuple_from_dfs(self.results.dc, "v_mp"), ) return self def adr_inverter(self): self.results.ac = self.system.get_ac( - 'adr', - self.results.dc['p_mp'], - v_dc=self.results.dc['v_mp'] + "adr", self.results.dc["p_mp"], v_dc=self.results.dc["v_mp"] ) return self def pvwatts_inverter(self): - ac = self.system.get_ac('pvwatts', self.results.dc) + ac = self.system.get_ac("pvwatts", self.results.dc) self.results.ac = ac.fillna(0) return self @@ -768,77 +854,76 @@ def aoi_model(self, model): self._aoi_model = self.infer_aoi_model() elif isinstance(model, str): model = model.lower() - if model == 'ashrae': + if model == "ashrae": self._aoi_model = self.ashrae_aoi_loss - elif model == 'physical': + elif model == "physical": self._aoi_model = self.physical_aoi_loss - elif model == 'sapm': + elif model == "sapm": self._aoi_model = self.sapm_aoi_loss - elif model == 'martin_ruiz': + elif model == "martin_ruiz": self._aoi_model = self.martin_ruiz_aoi_loss - elif model == 'interp': + elif model == "interp": self._aoi_model = self.interp_aoi_loss - elif model == 'no_loss': + elif model == "no_loss": self._aoi_model = self.no_aoi_loss else: - raise ValueError(model + ' is not a valid aoi loss model') + raise ValueError(model + " is not a valid aoi loss model") else: self._aoi_model = partial(model, self) def infer_aoi_model(self): module_parameters = tuple( - array.module_parameters for array in self.system.arrays) + array.module_parameters for array in self.system.arrays + ) params = _common_keys(module_parameters) - if iam._IAM_MODEL_PARAMS['physical'] <= params: + if iam._IAM_MODEL_PARAMS["physical"] <= params: return self.physical_aoi_loss - elif iam._IAM_MODEL_PARAMS['sapm'] <= params: + elif iam._IAM_MODEL_PARAMS["sapm"] <= params: return self.sapm_aoi_loss - elif iam._IAM_MODEL_PARAMS['ashrae'] <= params: + elif iam._IAM_MODEL_PARAMS["ashrae"] <= params: return self.ashrae_aoi_loss - elif iam._IAM_MODEL_PARAMS['martin_ruiz'] <= params: + elif iam._IAM_MODEL_PARAMS["martin_ruiz"] <= params: return self.martin_ruiz_aoi_loss - elif iam._IAM_MODEL_PARAMS['interp'] <= params: + elif iam._IAM_MODEL_PARAMS["interp"] <= params: return self.interp_aoi_loss else: - raise ValueError('could not infer AOI model from ' - 'system.arrays[i].module_parameters. Check that ' - 'the module_parameters for all Arrays in ' - 'system.arrays contain parameters for the ' - 'physical, aoi, ashrae, martin_ruiz or interp ' - 'model; explicitly set the model with the ' - 'aoi_model kwarg; or set aoi_model="no_loss".') + raise ValueError( + "could not infer AOI model from " + "system.arrays[i].module_parameters. Check that " + "the module_parameters for all Arrays in " + "system.arrays contain parameters for the " + "physical, aoi, ashrae, martin_ruiz or interp " + "model; explicitly set the model with the " + 'aoi_model kwarg; or set aoi_model="no_loss".' + ) def ashrae_aoi_loss(self): self.results.aoi_modifier = self.system.get_iam( - self.results.aoi, - iam_model='ashrae' + self.results.aoi, iam_model="ashrae" ) return self def physical_aoi_loss(self): self.results.aoi_modifier = self.system.get_iam( - self.results.aoi, - iam_model='physical' + self.results.aoi, iam_model="physical" ) return self def sapm_aoi_loss(self): self.results.aoi_modifier = self.system.get_iam( - self.results.aoi, - iam_model='sapm' + self.results.aoi, iam_model="sapm" ) return self def martin_ruiz_aoi_loss(self): self.results.aoi_modifier = self.system.get_iam( - self.results.aoi, iam_model='martin_ruiz' + self.results.aoi, iam_model="martin_ruiz" ) return self def interp_aoi_loss(self): self.results.aoi_modifier = self.system.get_iam( - self.results.aoi, - iam_model='interp' + self.results.aoi, iam_model="interp" ) return self @@ -859,48 +944,51 @@ def spectral_model(self, model): self._spectral_model = self.infer_spectral_model() elif isinstance(model, str): model = model.lower() - if model == 'first_solar': + if model == "first_solar": self._spectral_model = self.first_solar_spectral_loss - elif model == 'sapm': + elif model == "sapm": self._spectral_model = self.sapm_spectral_loss - elif model == 'no_loss': + elif model == "no_loss": self._spectral_model = self.no_spectral_loss else: - raise ValueError(model + ' is not a valid spectral loss model') + raise ValueError(model + " is not a valid spectral loss model") else: self._spectral_model = partial(model, self) def infer_spectral_model(self): """Infer spectral model from system attributes.""" module_parameters = tuple( - array.module_parameters for array in self.system.arrays) + array.module_parameters for array in self.system.arrays + ) params = _common_keys(module_parameters) - if {'A4', 'A3', 'A2', 'A1', 'A0'} <= params: + if {"A4", "A3", "A2", "A1", "A0"} <= params: return self.sapm_spectral_loss - elif ((('Technology' in params or - 'Material' in params) and - (self.system._infer_cell_type() is not None)) or - 'first_solar_spectral_coefficients' in params): + elif ( + ("Technology" in params or "Material" in params) + and (self.system._infer_cell_type() is not None) + ) or "first_solar_spectral_coefficients" in params: return self.first_solar_spectral_loss else: - raise ValueError('could not infer spectral model from ' - 'system.arrays[i].module_parameters. Check that ' - 'the module_parameters for all Arrays in ' - 'system.arrays contain valid ' - 'first_solar_spectral_coefficients, a valid ' - 'Material or Technology value, or set ' - 'spectral_model="no_loss".') + raise ValueError( + "could not infer spectral model from " + "system.arrays[i].module_parameters. Check that " + "the module_parameters for all Arrays in " + "system.arrays contain valid " + "first_solar_spectral_coefficients, a valid " + "Material or Technology value, or set " + 'spectral_model="no_loss".' + ) def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( - _tuple_from_dfs(self.results.weather, 'precipitable_water'), - self.results.airmass['airmass_absolute'] + _tuple_from_dfs(self.results.weather, "precipitable_water"), + self.results.airmass["airmass_absolute"], ) return self def sapm_spectral_loss(self): self.results.spectral_modifier = self.system.sapm_spectral_loss( - self.results.airmass['airmass_absolute'] + self.results.airmass["airmass_absolute"] ) return self @@ -921,30 +1009,33 @@ def temperature_model(self, model): self._temperature_model = self.infer_temperature_model() elif isinstance(model, str): model = model.lower() - if model == 'sapm': + if model == "sapm": self._temperature_model = self.sapm_temp - elif model == 'pvsyst': + elif model == "pvsyst": self._temperature_model = self.pvsyst_temp - elif model == 'faiman': + elif model == "faiman": self._temperature_model = self.faiman_temp - elif model == 'fuentes': + elif model == "fuentes": self._temperature_model = self.fuentes_temp - elif model == 'noct_sam': + elif model == "noct_sam": self._temperature_model = self.noct_sam_temp else: - raise ValueError(model + ' is not a valid temperature model') + raise ValueError(model + " is not a valid temperature model") # check system.temperature_model_parameters for consistency name_from_params = self.infer_temperature_model().__name__ if self._temperature_model.__name__ != name_from_params: - common_params = _common_keys(tuple( - array.temperature_model_parameters - for array in self.system.arrays)) + common_params = _common_keys( + tuple( + array.temperature_model_parameters + for array in self.system.arrays + ) + ) raise ValueError( - f'Temperature model {self._temperature_model.__name__} is ' - f'inconsistent with PVSystem temperature model ' - f'parameters. All Arrays in system.arrays must have ' - f'consistent parameters. Common temperature model ' - f'parameters: {common_params}' + f"Temperature model {self._temperature_model.__name__} is " + f"inconsistent with PVSystem temperature model " + f"parameters. All Arrays in system.arrays must have " + f"consistent parameters. Common temperature model " + f"parameters: {common_params}" ) else: self._temperature_model = partial(model, self) @@ -952,28 +1043,31 @@ def temperature_model(self, model): def infer_temperature_model(self): """Infer temperature model from system attributes.""" temperature_model_parameters = tuple( - array.temperature_model_parameters for array in self.system.arrays) + array.temperature_model_parameters for array in self.system.arrays + ) params = _common_keys(temperature_model_parameters) - if {'a', 'b', 'deltaT'} <= params: + if {"a", "b", "deltaT"} <= params: return self.sapm_temp - elif {'u_c', 'u_v'} <= params: + elif {"u_c", "u_v"} <= params: return self.pvsyst_temp - elif {'u0', 'u1'} <= params: + elif {"u0", "u1"} <= params: return self.faiman_temp - elif {'noct_installed'} <= params: + elif {"noct_installed"} <= params: return self.fuentes_temp - elif {'noct', 'module_efficiency'} <= params: + elif {"noct", "module_efficiency"} <= params: return self.noct_sam_temp else: - raise ValueError('Could not infer temperature model from ' - 'ModelChain.system. ' - 'If Arrays are used to construct the PVSystem, ' - 'check that all Arrays in ' - 'ModelChain.system.arrays ' - 'have parameters for the same temperature model. ' - 'If Arrays are not used, check that the PVSystem ' - 'attributes `racking_model` and `module_type` ' - 'are valid.') + raise ValueError( + "Could not infer temperature model from " + "ModelChain.system. " + "If Arrays are used to construct the PVSystem, " + "check that all Arrays in " + "ModelChain.system.arrays " + "have parameters for the same temperature model. " + "If Arrays are not used, check that the PVSystem " + "attributes `racking_model` and `module_type` " + "are valid." + ) def _set_celltemp(self, model): """Set self.results.cell_temperature using the given cell @@ -991,31 +1085,33 @@ def _set_celltemp(self, model): self """ - poa = _irrad_for_celltemp(self.results.total_irrad, - self.results.effective_irradiance) - temp_air = _tuple_from_dfs(self.results.weather, 'temp_air') - wind_speed = _tuple_from_dfs(self.results.weather, 'wind_speed') + poa = _irrad_for_celltemp( + self.results.total_irrad, self.results.effective_irradiance + ) + temp_air = _tuple_from_dfs(self.results.weather, "temp_air") + wind_speed = _tuple_from_dfs(self.results.weather, "wind_speed") kwargs = {} - if model == 'noct_sam': - kwargs['effective_irradiance'] = self.results.effective_irradiance + if model == "noct_sam": + kwargs["effective_irradiance"] = self.results.effective_irradiance self.results.cell_temperature = self.system.get_cell_temperature( - poa, temp_air, wind_speed, model=model, **kwargs) + poa, temp_air, wind_speed, model=model, **kwargs + ) return self def sapm_temp(self): - return self._set_celltemp('sapm') + return self._set_celltemp("sapm") def pvsyst_temp(self): - return self._set_celltemp('pvsyst') + return self._set_celltemp("pvsyst") def faiman_temp(self): - return self._set_celltemp('faiman') + return self._set_celltemp("faiman") def fuentes_temp(self): - return self._set_celltemp('fuentes') + return self._set_celltemp("fuentes") def noct_sam_temp(self): - return self._set_celltemp('noct_sam') + return self._set_celltemp("noct_sam") @property def dc_ohmic_model(self): @@ -1025,12 +1121,12 @@ def dc_ohmic_model(self): def dc_ohmic_model(self, model): if isinstance(model, str): model = model.lower() - if model == 'dc_ohms_from_percent': + if model == "dc_ohms_from_percent": self._dc_ohmic_model = self.dc_ohms_from_percent - elif model == 'no_loss': + elif model == "no_loss": self._dc_ohmic_model = self.no_dc_ohmic_loss else: - raise ValueError(model + ' is not a valid losses model') + raise ValueError(model + " is not a valid losses model") else: self._dc_ohmic_model = partial(model, self) @@ -1044,17 +1140,18 @@ def dc_ohms_from_percent(self): Rw = self.system.dc_ohms_from_percent() if isinstance(self.results.dc, tuple): self.results.dc_ohmic_losses = tuple( - pvsystem.dc_ohmic_losses(Rw, df['i_mp']) + pvsystem.dc_ohmic_losses(Rw, df["i_mp"]) for Rw, df in zip(Rw, self.results.dc) ) for df, loss in zip(self.results.dc, self.results.dc_ohmic_losses): - df['p_mp'] = df['p_mp'] - loss + df["p_mp"] = df["p_mp"] - loss else: self.results.dc_ohmic_losses = pvsystem.dc_ohmic_losses( - Rw, self.results.dc['i_mp'] + Rw, self.results.dc["i_mp"] + ) + self.results.dc["p_mp"] = ( + self.results.dc["p_mp"] - self.results.dc_ohmic_losses ) - self.results.dc['p_mp'] = (self.results.dc['p_mp'] - - self.results.dc_ohmic_losses) return self def no_dc_ohmic_loss(self): @@ -1070,12 +1167,12 @@ def losses_model(self, model): self._losses_model = self.infer_losses_model() elif isinstance(model, str): model = model.lower() - if model == 'pvwatts': + if model == "pvwatts": self._losses_model = self.pvwatts_losses - elif model == 'no_loss': + elif model == "no_loss": self._losses_model = self.no_extra_losses else: - raise ValueError(model + ' is not a valid losses model') + raise ValueError(model + " is not a valid losses model") else: self._losses_model = partial(model, self) @@ -1083,7 +1180,7 @@ def infer_losses_model(self): raise NotImplementedError def pvwatts_losses(self): - self.results.losses = (100 - self.system.pvwatts_losses()) / 100. + self.results.losses = (100 - self.system.pvwatts_losses()) / 100.0 if isinstance(self.results.dc, tuple): for dc in self.results.dc: dc *= self.results.losses @@ -1097,21 +1194,28 @@ def no_extra_losses(self): def effective_irradiance_model(self): def _eff_irrad(module_parameters, total_irrad, spect_mod, aoi_mod): - fd = module_parameters.get('FD', 1.) - return spect_mod * (total_irrad['poa_direct'] * aoi_mod + - fd * total_irrad['poa_diffuse']) + fd = module_parameters.get("FD", 1.0) + return spect_mod * ( + total_irrad["poa_direct"] * aoi_mod + + fd * total_irrad["poa_diffuse"] + ) + if isinstance(self.results.total_irrad, tuple): self.results.effective_irradiance = tuple( - _eff_irrad(array.module_parameters, ti, sm, am) for - array, ti, sm, am in zip( - self.system.arrays, self.results.total_irrad, - self.results.spectral_modifier, self.results.aoi_modifier)) + _eff_irrad(array.module_parameters, ti, sm, am) + for array, ti, sm, am in zip( + self.system.arrays, + self.results.total_irrad, + self.results.spectral_modifier, + self.results.aoi_modifier, + ) + ) else: self.results.effective_irradiance = _eff_irrad( self.system.arrays[0].module_parameters, self.results.total_irrad, self.results.spectral_modifier, - self.results.aoi_modifier + self.results.aoi_modifier, ) return self @@ -1175,7 +1279,8 @@ def complete_irradiance(self, weather): self.results.weather = _copy(weather) self._assign_times() self.results.solar_position = self.location.get_solarposition( - self.results.times, method=self.solar_position_method) + self.results.times, method=self.solar_position_method + ) # Calculate the irradiance using the component sum equations, # if needed if isinstance(weather, tuple): @@ -1187,54 +1292,62 @@ def complete_irradiance(self, weather): def _complete_irradiance(self, weather): icolumns = set(weather.columns) - wrn_txt = ("This function is not safe at the moment.\n" + - "Results can be too high or negative.\n" + - "Help to improve this function on github:\n" + - "https://github.com/pvlib/pvlib-python \n") - if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: + wrn_txt = ( + "This function is not safe at the moment.\n" + + "Results can be too high or negative.\n" + + "Help to improve this function on github:\n" + + "https://github.com/pvlib/pvlib-python \n" + ) + if {"ghi", "dhi"} <= icolumns and "dni" not in icolumns: clearsky = self.location.get_clearsky( - weather.index, model=self.clearsky_model, - solar_position=self.results.solar_position) + weather.index, + model=self.clearsky_model, + solar_position=self.results.solar_position, + ) complete_irrad_df = pvlib.irradiance.complete_irradiance( solar_zenith=self.results.solar_position.zenith, ghi=weather.ghi, dhi=weather.dhi, dni=None, - dni_clear=clearsky.dni) - weather.loc[:, 'dni'] = complete_irrad_df.dni - elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: + dni_clear=clearsky.dni, + ) + weather.loc[:, "dni"] = complete_irrad_df.dni + elif {"dni", "dhi"} <= icolumns and "ghi" not in icolumns: warnings.warn(wrn_txt, UserWarning) complete_irrad_df = pvlib.irradiance.complete_irradiance( solar_zenith=self.results.solar_position.zenith, ghi=None, dhi=weather.dhi, - dni=weather.dni) - weather.loc[:, 'ghi'] = complete_irrad_df.ghi - elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: + dni=weather.dni, + ) + weather.loc[:, "ghi"] = complete_irrad_df.ghi + elif {"dni", "ghi"} <= icolumns and "dhi" not in icolumns: warnings.warn(wrn_txt, UserWarning) complete_irrad_df = pvlib.irradiance.complete_irradiance( solar_zenith=self.results.solar_position.zenith, ghi=weather.ghi, dhi=None, - dni=weather.dni) - weather.loc[:, 'dhi'] = complete_irrad_df.dhi + dni=weather.dni, + ) + weather.loc[:, "dhi"] = complete_irrad_df.dhi def _prep_inputs_solar_pos(self, weather): """ Assign solar position """ # build weather kwargs for solar position calculation - kwargs = _build_kwargs(['pressure', 'temp_air'], - weather[0] if isinstance(weather, tuple) - else weather) + kwargs = _build_kwargs( + ["pressure", "temp_air"], + weather[0] if isinstance(weather, tuple) else weather, + ) try: - kwargs['temperature'] = kwargs.pop('temp_air') + kwargs["temperature"] = kwargs.pop("temp_air") except KeyError: pass self.results.solar_position = self.location.get_solarposition( - self.results.times, method=self.solar_position_method, - **kwargs) + self.results.times, method=self.solar_position_method, **kwargs + ) return self def _prep_inputs_albedo(self, weather): @@ -1242,10 +1355,9 @@ def _prep_inputs_albedo(self, weather): Get albedo from weather """ try: - self.results.albedo = _tuple_from_dfs(weather, 'albedo') + self.results.albedo = _tuple_from_dfs(weather, "albedo") except KeyError: - self.results.albedo = tuple([ - a.albedo for a in self.system.arrays]) + self.results.albedo = tuple([a.albedo for a in self.system.arrays]) return self def _prep_inputs_airmass(self): @@ -1254,7 +1366,8 @@ def _prep_inputs_airmass(self): """ self.results.airmass = self.location.get_airmass( solar_position=self.results.solar_position, - model=self.airmass_model) + model=self.airmass_model, + ) return self def _prep_inputs_tracking(self): @@ -1262,15 +1375,16 @@ def _prep_inputs_tracking(self): Calculate tracker position and AOI """ self.results.tracking = self.system.singleaxis( - self.results.solar_position['apparent_zenith'], - self.results.solar_position['azimuth']) - self.results.tracking['surface_tilt'] = ( - self.results.tracking['surface_tilt'] - .fillna(self.system.axis_tilt)) - self.results.tracking['surface_azimuth'] = ( - self.results.tracking['surface_azimuth'] - .fillna(self.system.axis_azimuth)) - self.results.aoi = self.results.tracking['aoi'] + self.results.solar_position["apparent_zenith"], + self.results.solar_position["azimuth"], + ) + self.results.tracking["surface_tilt"] = self.results.tracking[ + "surface_tilt" + ].fillna(self.system.axis_tilt) + self.results.tracking["surface_azimuth"] = self.results.tracking[ + "surface_azimuth" + ].fillna(self.system.axis_azimuth) + self.results.aoi = self.results.tracking["aoi"] return self def _prep_inputs_fixed(self): @@ -1278,12 +1392,13 @@ def _prep_inputs_fixed(self): Calculate AOI for fixed tilt system """ self.results.aoi = self.system.get_aoi( - self.results.solar_position['apparent_zenith'], - self.results.solar_position['azimuth']) + self.results.solar_position["apparent_zenith"], + self.results.solar_position["azimuth"], + ) return self def _verify_df(self, data, required): - """ Checks data for column names in required + """Checks data for column names in required Parameters ---------- @@ -1294,17 +1409,20 @@ def _verify_df(self, data, required): ------ ValueError if any of required are not in data.columns. """ + def _verify(data, index=None): if not set(required) <= set(data.columns): tuple_txt = "" if index is None else f"in element {index} " raise ValueError( "Incomplete input data. Data needs to contain " f"{required}. Detected data {tuple_txt}contains: " - f"{list(data.columns)}") + f"{list(data.columns)}" + ) + if not isinstance(data, tuple): _verify(data) else: - for (i, array_data) in enumerate(data): + for i, array_data in enumerate(data): _verify(array_data, i) def _configure_results(self, per_array_data): @@ -1330,11 +1448,12 @@ def _assign_weather(self, data): def _build_weather(data): key_list = [k for k in WEATHER_KEYS if k in data] weather = data[key_list].copy() - if weather.get('wind_speed') is None: - weather['wind_speed'] = 0 - if weather.get('temp_air') is None: - weather['temp_air'] = 20 + if weather.get("wind_speed") is None: + weather["wind_speed"] = 0 + if weather.get("temp_air") is None: + weather["temp_air"] = 20 return weather + if isinstance(data, tuple): weather = tuple(_build_weather(wx) for wx in data) self._configure_results(per_array_data=True) @@ -1349,6 +1468,7 @@ def _assign_total_irrad(self, data): def _build_irrad(data): key_list = [k for k in POA_KEYS if k in data] return data[key_list].copy() + if isinstance(data, tuple): self.results.total_irrad = tuple( _build_irrad(irrad_data) for irrad_data in data @@ -1417,7 +1537,7 @@ def prepare_inputs(self, weather): """ weather = _to_tuple(weather) self._check_multiple_input(weather, strict=False) - self._verify_df(weather, required=['ghi', 'dni', 'dhi']) + self._verify_df(weather, required=["ghi", "dni", "dhi"]) self._assign_weather(weather) self._prep_inputs_solar_pos(weather) @@ -1426,14 +1546,14 @@ def prepare_inputs(self, weather): self._prep_inputs_fixed() self.results.total_irrad = self.system.get_irradiance( - self.results.solar_position['apparent_zenith'], - self.results.solar_position['azimuth'], - _tuple_from_dfs(self.results.weather, 'dni'), - _tuple_from_dfs(self.results.weather, 'ghi'), - _tuple_from_dfs(self.results.weather, 'dhi'), + self.results.solar_position["apparent_zenith"], + self.results.solar_position["azimuth"], + _tuple_from_dfs(self.results.weather, "dni"), + _tuple_from_dfs(self.results.weather, "ghi"), + _tuple_from_dfs(self.results.weather, "dhi"), albedo=self.results.albedo, - airmass=self.results.airmass['airmass_relative'], - model=self.transposition_model + airmass=self.results.airmass["airmass_relative"], + model=self.transposition_model, ) return self @@ -1451,17 +1571,22 @@ def _check_multiple_input(self, data, strict=True): it has the same length as the number of Arrays, but we do not want to fail if the input is not a tuple. """ - if (not strict or self.system.num_arrays == 1) \ - and not isinstance(data, tuple): + if (not strict or self.system.num_arrays == 1) and not isinstance( + data, tuple + ): return if strict and not isinstance(data, tuple): - raise TypeError("Input must be a tuple of length " - f"{self.system.num_arrays}, " - f"got {type(data).__name__}.") + raise TypeError( + "Input must be a tuple of length " + f"{self.system.num_arrays}, " + f"got {type(data).__name__}." + ) if len(data) != self.system.num_arrays: - raise ValueError("Input must be same length as number of Arrays " - f"in system. Expected {self.system.num_arrays}, " - f"got {len(data)}.") + raise ValueError( + "Input must be same length as number of Arrays " + f"in system. Expected {self.system.num_arrays}, " + f"got {len(data)}." + ) _all_same_index(data) def prepare_inputs_from_poa(self, data): @@ -1501,8 +1626,9 @@ def prepare_inputs_from_poa(self, data): self._check_multiple_input(data) self._assign_weather(data) - self._verify_df(data, required=['poa_global', 'poa_direct', - 'poa_diffuse']) + self._verify_df( + data, required=["poa_global", "poa_direct", "poa_diffuse"] + ) self._assign_total_irrad(data) self._prep_inputs_solar_pos(data) @@ -1512,8 +1638,7 @@ def prepare_inputs_from_poa(self, data): return self - def _get_cell_temperature(self, data, - poa, temperature_model_parameters): + def _get_cell_temperature(self, data, poa, temperature_model_parameters): """Extract the cell temperature data from a DataFrame. If 'cell_temperature' column exists in data then it is returned. If @@ -1530,26 +1655,26 @@ def _get_cell_temperature(self, data, ------- Series """ - if 'cell_temperature' in data: - return data['cell_temperature'] + if "cell_temperature" in data: + return data["cell_temperature"] # cell_temperature is not in input. Calculate cell_temperature using # a temperature_model. # If module_temperature is in input data we can use the SAPM cell # temperature model. - if (('module_temperature' in data) and - (self.temperature_model == self.sapm_temp)): + if ("module_temperature" in data) and ( + self.temperature_model == self.sapm_temp + ): # use SAPM cell temperature model only return pvlib.temperature.sapm_cell_from_module( - module_temperature=data['module_temperature'], + module_temperature=data["module_temperature"], poa_global=poa, - deltaT=temperature_model_parameters['deltaT']) + deltaT=temperature_model_parameters["deltaT"], + ) def _prepare_temperature_single_array(self, data, poa): """Set cell_temperature using a single data frame.""" self.results.cell_temperature = self._get_cell_temperature( - data, - poa, - self.system.arrays[0].temperature_model_parameters + data, poa, self.system.arrays[0].temperature_model_parameters ) if self.results.cell_temperature is None: self.temperature_model() @@ -1579,8 +1704,9 @@ def _prepare_temperature(self, data): Assigns attribute ``results.cell_temperature``. """ - poa = _irrad_for_celltemp(self.results.total_irrad, - self.results.effective_irradiance) + poa = _irrad_for_celltemp( + self.results.total_irrad, self.results.effective_irradiance + ) # handle simple case first, single array, data not iterable if not isinstance(data, tuple) and self.system.num_arrays == 1: return self._prepare_temperature_single_array(data, poa) @@ -1590,12 +1716,15 @@ def _prepare_temperature(self, data): # data is tuple, so temperature_model_parameters must also be # tuple. system.temperature_model_parameters is reduced to a dict # if system.num_arrays == 1, so manually access parameters. GH 1192 - t_mod_params = tuple(array.temperature_model_parameters - for array in self.system.arrays) + t_mod_params = tuple( + array.temperature_model_parameters for array in self.system.arrays + ) # find where cell or module temperature is specified in input data - given_cell_temperature = tuple(itertools.starmap( - self._get_cell_temperature, zip(data, poa, t_mod_params) - )) + given_cell_temperature = tuple( + itertools.starmap( + self._get_cell_temperature, zip(data, poa, t_mod_params) + ) + ) # If cell temperature has been specified for all arrays return # immediately and do not try to compute it. if all(cell_temp is not None for cell_temp in given_cell_temperature): @@ -1609,7 +1738,7 @@ def _prepare_temperature(self, data): self.results.cell_temperature = tuple( itertools.starmap( lambda given, modeled: modeled if given is None else given, - zip(given_cell_temperature, self.results.cell_temperature) + zip(given_cell_temperature, self.results.cell_temperature), ) ) return self @@ -1834,11 +1963,12 @@ def run_model_from_effective_irradiance(self, data): """ data = _to_tuple(data) self._check_multiple_input(data) - self._verify_df(data, required=['effective_irradiance']) + self._verify_df(data, required=["effective_irradiance"]) self._assign_weather(data) self._assign_total_irrad(data) self.results.effective_irradiance = _tuple_from_dfs( - data, 'effective_irradiance') + data, "effective_irradiance" + ) self._run_from_effective_irrad(data) return self @@ -1856,13 +1986,13 @@ def _irrad_for_celltemp(total_irrad, effective_irradiance): """ if isinstance(total_irrad, tuple): - if all('poa_global' in df for df in total_irrad): - return _tuple_from_dfs(total_irrad, 'poa_global') + if all("poa_global" in df for df in total_irrad): + return _tuple_from_dfs(total_irrad, "poa_global") else: return effective_irradiance else: - if 'poa_global' in total_irrad: - return total_irrad['poa_global'] + if "poa_global" in total_irrad: + return total_irrad["poa_global"] else: return effective_irradiance @@ -1870,19 +2000,19 @@ def _irrad_for_celltemp(total_irrad, effective_irradiance): def _snl_params(inverter_params): """Return True if `inverter_params` includes parameters for the Sandia inverter model.""" - return {'C0', 'C1', 'C2'} <= inverter_params + return {"C0", "C1", "C2"} <= inverter_params def _adr_params(inverter_params): """Return True if `inverter_params` includes parameters for the ADR inverter model.""" - return {'ADRCoefficients'} <= inverter_params + return {"ADRCoefficients"} <= inverter_params def _pvwatts_params(inverter_params): """Return True if `inverter_params` includes parameters for the PVWatts inverter model.""" - return {'pdc0'} <= inverter_params + return {"pdc0"} <= inverter_params def _copy(data): @@ -1906,8 +2036,10 @@ def _all_same_index(data): def _common_keys(dicts): """Return the intersection of the set of keys for each dictionary in `dicts`""" + def _keys(x): return set(x.keys()) + if isinstance(dicts, tuple): return set.intersection(*map(_keys, dicts)) return _keys(dicts) diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index ab7530f3e5..b4f6b2976f 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -13,9 +13,10 @@ from scipy.special import exp10 -def pvefficiency_adr(effective_irradiance, temp_cell, - k_a, k_d, tc_d, k_rs, k_rsh): - ''' +def pvefficiency_adr( + effective_irradiance, temp_cell, k_a, k_d, tc_d, k_rs, k_rsh +): + """ Calculate PV module efficiency using the ADR model. The efficiency varies with irradiance and operating temperature @@ -103,7 +104,7 @@ def pvefficiency_adr(effective_irradiance, temp_cell, k_a=1.0, k_d=-6.0, tc_d=0.02, k_rs=0.05, k_rsh=0.10) array([1. , 0.92797293]) - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs, Dec. 2022 # Adapted from https://github.com/adriesse/pvpltools-python @@ -114,20 +115,20 @@ def pvefficiency_adr(effective_irradiance, temp_cell, k_rsh = np.array(k_rsh) # normalize the irradiance - G_REF = np.array(1000.) + G_REF = np.array(1000.0) s = effective_irradiance / G_REF # obtain the difference from reference temperature - T_REF = np.array(25.) + T_REF = np.array(25.0) dt = temp_cell - T_REF # equation 29 in JPV - s_o = exp10(k_d + (dt * tc_d)) # noQA: E221 + s_o = exp10(k_d + (dt * tc_d)) # noQA: E221 s_o_ref = exp10(k_d) # equation 28 and 30 in JPV # the constant k_v does not appear here because it cancels out - v = np.log(s / s_o + 1) # noQA: E221 + v = np.log(s / s_o + 1) # noQA: E221 v /= np.log(1 / s_o_ref + 1) # equation 25 in JPV @@ -136,8 +137,9 @@ def pvefficiency_adr(effective_irradiance, temp_cell, return eta -def fit_pvefficiency_adr(effective_irradiance, temp_cell, eta, - dict_output=True, **kwargs): +def fit_pvefficiency_adr( + effective_irradiance, temp_cell, eta, dict_output=True, **kwargs +): """ Determine the parameters of the ADR module efficiency model by non-linear least-squares fit to lab or field measurements. @@ -190,33 +192,35 @@ def fit_pvefficiency_adr(effective_irradiance, temp_cell, eta, eta_max = np.max(eta) - P_NAMES = ['k_a', 'k_d', 'tc_d', 'k_rs', 'k_rsh'] - P_MAX = [+np.inf, 0, +0.1, 1, 1] # noQA: E221 - P_MIN = [0, -12, -0.1, 0.0, 0.0] # noQA: E221 - P0 = [eta_max, -6, 0.0, 1e-3, 1e-3] # noQA: E221 - P_SCALE = [eta_max, 10, 0.1, 1.0, 1.0] + P_NAMES = ["k_a", "k_d", "tc_d", "k_rs", "k_rsh"] + P_MAX = [+np.inf, 0, +0.1, 1, 1] # noQA: E221 + P_MIN = [0, -12, -0.1, 0.0, 0.0] # noQA: E221 + P0 = [eta_max, -6, 0.0, 1e-3, 1e-3] # noQA: E221 + P_SCALE = [eta_max, 10, 0.1, 1.0, 1.0] SIGMA = 1 / np.sqrt(irradiance / 1000) - fit_options = dict(p0=P0, - bounds=[P_MIN, P_MAX], - method='trf', - x_scale=P_SCALE, - loss='soft_l1', - f_scale=eta_max * 0.05, - sigma=SIGMA, - ) + fit_options = dict( + p0=P0, + bounds=[P_MIN, P_MAX], + method="trf", + x_scale=P_SCALE, + loss="soft_l1", + f_scale=eta_max * 0.05, + sigma=SIGMA, + ) fit_options.update(kwargs) def adr_wrapper(xdata, *params): return pvefficiency_adr(*xdata, *params) - result = curve_fit(adr_wrapper, - xdata=[irradiance, temperature], - ydata=eta, - **fit_options, - ) + result = curve_fit( + adr_wrapper, + xdata=[irradiance, temperature], + ydata=eta, + **fit_options, + ) popt = result[0] if dict_output: @@ -232,13 +236,26 @@ def _infer_k_huld(cell_type, pdc0): # equation that has factored Pdc0 out of the polynomial: # P = G/1000 * Pdc0 * (1 + k1 log(Geff) + ...) so these parameters are # multiplied by pdc0 - huld_params = {'csi': (-0.017237, -0.040465, -0.004702, 0.000149, - 0.000170, 0.000005), - 'cis': (-0.005554, -0.038724, -0.003723, -0.000905, - -0.001256, 0.000001), - 'cdte': (-0.046689, -0.072844, -0.002262, 0.000276, - 0.000159, -0.000006)} - k = tuple([x*pdc0 for x in huld_params[cell_type.lower()]]) + huld_params = { + "csi": (-0.017237, -0.040465, -0.004702, 0.000149, 0.000170, 0.000005), + "cis": ( + -0.005554, + -0.038724, + -0.003723, + -0.000905, + -0.001256, + 0.000001, + ), + "cdte": ( + -0.046689, + -0.072844, + -0.002262, + 0.000276, + 0.000159, + -0.000006, + ), + } + k = tuple([x * pdc0 for x in huld_params[cell_type.lower()]]) return k @@ -337,16 +354,23 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): if cell_type is not None: k = _infer_k_huld(cell_type, pdc0) else: - raise ValueError('Either k or cell_type must be specified') + raise ValueError("Either k or cell_type must be specified") gprime = effective_irradiance / 1000 tprime = temp_mod - 25 # accomodate gprime<=0 - with np.errstate(divide='ignore'): - logGprime = np.log(gprime, out=np.zeros_like(gprime), - where=np.array(gprime > 0)) + with np.errstate(divide="ignore"): + logGprime = np.log( + gprime, out=np.zeros_like(gprime), where=np.array(gprime > 0) + ) # Eq. 1 in [1] - pdc = gprime * (pdc0 + k[0] * logGprime + k[1] * logGprime**2 + - k[2] * tprime + k[3] * tprime * logGprime + - k[4] * tprime * logGprime**2 + k[5] * tprime**2) + pdc = gprime * ( + pdc0 + + k[0] * logGprime + + k[1] * logGprime**2 + + k[2] * tprime + + k[3] * tprime * logGprime + + k[4] * tprime * logGprime**2 + + k[5] * tprime**2 + ) return pdc diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f99f0275af..d066410b5f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -20,34 +20,88 @@ from pvlib._deprecation import deprecated import pvlib # used to avoid albedo name collision in the Array class -from pvlib import (atmosphere, iam, inverter, irradiance, - singlediode as _singlediode, spectrum, temperature) +from pvlib import ( + atmosphere, + iam, + inverter, + irradiance, + singlediode as _singlediode, + spectrum, + temperature, +) from pvlib.tools import _build_kwargs, _build_args import pvlib.tools as tools # a dict of required parameter names for each DC power model _DC_MODEL_PARAMS = { - 'sapm': { - 'A0', 'A1', 'A2', 'A3', 'A4', 'B0', 'B1', 'B2', 'B3', - 'B4', 'B5', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', - 'C7', 'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco', - 'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series', - 'IXO', 'IXXO', 'FD'}, - 'desoto': { - 'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_s'}, - 'cec': { - 'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_s', 'Adjust'}, - 'pvsyst': { - 'gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_sh_0', 'R_s', 'alpha_sc', 'EgRef', - 'cells_in_series'}, - 'singlediode': { - 'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_s'}, - 'pvwatts': {'pdc0', 'gamma_pdc'} + "sapm": { + "A0", + "A1", + "A2", + "A3", + "A4", + "B0", + "B1", + "B2", + "B3", + "B4", + "B5", + "C0", + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "Isco", + "Impo", + "Voco", + "Vmpo", + "Aisc", + "Aimp", + "Bvoco", + "Mbvoc", + "Bvmpo", + "Mbvmp", + "N", + "Cells_in_Series", + "IXO", + "IXXO", + "FD", + }, + "desoto": {"alpha_sc", "a_ref", "I_L_ref", "I_o_ref", "R_sh_ref", "R_s"}, + "cec": { + "alpha_sc", + "a_ref", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_s", + "Adjust", + }, + "pvsyst": { + "gamma_ref", + "mu_gamma", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_sh_0", + "R_s", + "alpha_sc", + "EgRef", + "cells_in_series", + }, + "singlediode": { + "alpha_sc", + "a_ref", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_s", + }, + "pvwatts": {"pdc0", "gamma_pdc"}, } @@ -61,13 +115,15 @@ def _unwrap_single_value(func): Adds 'unwrap' as a keyword argument that can be set to False to force the return value to be a tuple, regardless of its length. """ + @functools.wraps(func) def f(*args, **kwargs): - unwrap = kwargs.pop('unwrap', True) + unwrap = kwargs.pop("unwrap", True) x = func(*args, **kwargs) if unwrap and len(x) == 1: return x[0] return x + return f @@ -194,43 +250,56 @@ class PVSystem: pvlib.location.Location """ - def __init__(self, - arrays=None, - surface_tilt=0, surface_azimuth=180, - albedo=None, surface_type=None, - module=None, module_type=None, - module_parameters=None, - temperature_model_parameters=None, - modules_per_string=1, strings_per_inverter=1, - inverter=None, inverter_parameters=None, - racking_model=None, losses_parameters=None, name=None): - + def __init__( + self, + arrays=None, + surface_tilt=0, + surface_azimuth=180, + albedo=None, + surface_type=None, + module=None, + module_type=None, + module_parameters=None, + temperature_model_parameters=None, + modules_per_string=1, + strings_per_inverter=1, + inverter=None, + inverter_parameters=None, + racking_model=None, + losses_parameters=None, + name=None, + ): if arrays is None: if losses_parameters is None: array_losses_parameters = {} else: - array_losses_parameters = _build_kwargs(['dc_ohmic_percent'], - losses_parameters) - self.arrays = (Array( - FixedMount(surface_tilt, surface_azimuth, racking_model), - albedo, - surface_type, - module, - module_type, - module_parameters, - temperature_model_parameters, - modules_per_string, - strings_per_inverter, - array_losses_parameters, - ),) + array_losses_parameters = _build_kwargs( + ["dc_ohmic_percent"], losses_parameters + ) + self.arrays = ( + Array( + FixedMount(surface_tilt, surface_azimuth, racking_model), + albedo, + surface_type, + module, + module_type, + module_parameters, + temperature_model_parameters, + modules_per_string, + strings_per_inverter, + array_losses_parameters, + ), + ) elif isinstance(arrays, Array): self.arrays = (arrays,) elif len(arrays) == 0: - raise ValueError("PVSystem must have at least one Array. " - "If you want to create a PVSystem instance " - "with a single Array pass `arrays=None` and pass " - "values directly to PVSystem attributes, e.g., " - "`surface_tilt=30`") + raise ValueError( + "PVSystem must have at least one Array. " + "If you want to create a PVSystem instance " + "with a single Array pass `arrays=None` and pass " + "values directly to PVSystem attributes, e.g., " + "`surface_tilt=30`" + ) else: self.arrays = tuple(arrays) @@ -248,11 +317,11 @@ def __init__(self, self.name = name def __repr__(self): - repr = f'PVSystem:\n name: {self.name}\n ' + repr = f"PVSystem:\n name: {self.name}\n " for array in self.arrays: - repr += '\n '.join(array.__repr__().split('\n')) - repr += '\n ' - repr += f'inverter: {self.inverter}' + repr += "\n ".join(array.__repr__().split("\n")) + repr += "\n " + repr += f"inverter: {self.inverter}" return repr def _validate_per_array(self, values, system_wide=False): @@ -305,13 +374,24 @@ def get_aoi(self, solar_zenith, solar_azimuth): The angle of incidence """ - return tuple(array.get_aoi(solar_zenith, solar_azimuth) - for array in self.arrays) + return tuple( + array.get_aoi(solar_zenith, solar_azimuth) for array in self.arrays + ) @_unwrap_single_value - def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, albedo=None, - model='haydavies', **kwargs): + def get_irradiance( + self, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=None, + airmass=None, + albedo=None, + model="haydavies", + **kwargs, + ): """ Uses :py:func:`pvlib.irradiance.get_total_irradiance` to calculate the plane of array irradiance components on the tilted @@ -374,17 +454,25 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, albedo = self._validate_per_array(albedo, system_wide=True) return tuple( - array.get_irradiance(solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra=dni_extra, airmass=airmass, - albedo=albedo, model=model, **kwargs) + array.get_irradiance( + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=dni_extra, + airmass=airmass, + albedo=albedo, + model=model, + **kwargs, + ) for array, dni, ghi, dhi, albedo in zip( self.arrays, dni, ghi, dhi, albedo ) ) @_unwrap_single_value - def get_iam(self, aoi, iam_model='physical'): + def get_iam(self, aoi, iam_model="physical"): """ Determine the incidence angle modifier using the method specified by ``iam_model``. @@ -412,12 +500,20 @@ def get_iam(self, aoi, iam_model='physical'): if `iam_model` is not a valid model name. """ aoi = self._validate_per_array(aoi) - return tuple(array.get_iam(aoi, iam_model) - for array, aoi in zip(self.arrays, aoi)) + return tuple( + array.get_iam(aoi, iam_model) + for array, aoi in zip(self.arrays, aoi) + ) @_unwrap_single_value - def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, - effective_irradiance=None): + def get_cell_temperature( + self, + poa_global, + temp_air, + wind_speed, + model, + effective_irradiance=None, + ): """ Determine cell temperature using the method specified by ``model``. @@ -460,16 +556,20 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, temp_air = self._validate_per_array(temp_air, system_wide=True) wind_speed = self._validate_per_array(wind_speed, system_wide=True) # Not used for all models, but Array.get_cell_temperature handles it - effective_irradiance = self._validate_per_array(effective_irradiance, - system_wide=True) + effective_irradiance = self._validate_per_array( + effective_irradiance, system_wide=True + ) return tuple( - array.get_cell_temperature(poa_global, temp_air, wind_speed, - model, effective_irradiance) - for array, poa_global, temp_air, wind_speed, effective_irradiance - in zip( - self.arrays, poa_global, temp_air, wind_speed, - effective_irradiance + array.get_cell_temperature( + poa_global, temp_air, wind_speed, model, effective_irradiance + ) + for array, poa_global, temp_air, wind_speed, effective_irradiance in zip( + self.arrays, + poa_global, + temp_air, + wind_speed, + effective_irradiance, ) ) @@ -497,18 +597,29 @@ def calcparams_desoto(self, effective_irradiance, temp_cell): build_kwargs = functools.partial( _build_kwargs, - ['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', - 'R_s', 'alpha_sc', 'EgRef', 'dEgdT', - 'irrad_ref', 'temp_ref'] + [ + "a_ref", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_s", + "alpha_sc", + "EgRef", + "dEgdT", + "irrad_ref", + "temp_ref", + ], ) return tuple( calcparams_desoto( - effective_irradiance, temp_cell, - **build_kwargs(array.module_parameters) + effective_irradiance, + temp_cell, + **build_kwargs(array.module_parameters), + ) + for array, effective_irradiance, temp_cell in zip( + self.arrays, effective_irradiance, temp_cell ) - for array, effective_irradiance, temp_cell - in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -535,18 +646,30 @@ def calcparams_cec(self, effective_irradiance, temp_cell): build_kwargs = functools.partial( _build_kwargs, - ['a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', - 'R_s', 'alpha_sc', 'Adjust', 'EgRef', 'dEgdT', - 'irrad_ref', 'temp_ref'] + [ + "a_ref", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_s", + "alpha_sc", + "Adjust", + "EgRef", + "dEgdT", + "irrad_ref", + "temp_ref", + ], ) return tuple( calcparams_cec( - effective_irradiance, temp_cell, - **build_kwargs(array.module_parameters) + effective_irradiance, + temp_cell, + **build_kwargs(array.module_parameters), + ) + for array, effective_irradiance, temp_cell in zip( + self.arrays, effective_irradiance, temp_cell ) - for array, effective_irradiance, temp_cell - in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -573,20 +696,32 @@ def calcparams_pvsyst(self, effective_irradiance, temp_cell): build_kwargs = functools.partial( _build_kwargs, - ['gamma_ref', 'mu_gamma', 'I_L_ref', 'I_o_ref', - 'R_sh_ref', 'R_sh_0', 'R_sh_exp', - 'R_s', 'alpha_sc', 'EgRef', - 'irrad_ref', 'temp_ref', - 'cells_in_series'] + [ + "gamma_ref", + "mu_gamma", + "I_L_ref", + "I_o_ref", + "R_sh_ref", + "R_sh_0", + "R_sh_exp", + "R_s", + "alpha_sc", + "EgRef", + "irrad_ref", + "temp_ref", + "cells_in_series", + ], ) return tuple( calcparams_pvsyst( - effective_irradiance, temp_cell, - **build_kwargs(array.module_parameters) + effective_irradiance, + temp_cell, + **build_kwargs(array.module_parameters), + ) + for array, effective_irradiance, temp_cell in zip( + self.arrays, effective_irradiance, temp_cell ) - for array, effective_irradiance, temp_cell - in zip(self.arrays, effective_irradiance, temp_cell) ) @_unwrap_single_value @@ -613,8 +748,9 @@ def sapm(self, effective_irradiance, temp_cell): return tuple( sapm(effective_irradiance, temp_cell, array.module_parameters) - for array, effective_irradiance, temp_cell - in zip(self.arrays, effective_irradiance, temp_cell) + for array, effective_irradiance, temp_cell in zip( + self.arrays, effective_irradiance, temp_cell + ) ) @_unwrap_single_value @@ -634,15 +770,21 @@ def sapm_spectral_loss(self, airmass_absolute): The SAPM spectral loss coefficient. """ return tuple( - spectrum.spectral_factor_sapm(airmass_absolute, - array.module_parameters) + spectrum.spectral_factor_sapm( + airmass_absolute, array.module_parameters + ) for array in self.arrays ) @_unwrap_single_value - def sapm_effective_irradiance(self, poa_direct, poa_diffuse, - airmass_absolute, aoi, - reference_irradiance=1000): + def sapm_effective_irradiance( + self, + poa_direct, + poa_diffuse, + airmass_absolute, + aoi, + reference_irradiance=1000, + ): """ Use the :py:func:`sapm_effective_irradiance` function, the input parameters, and ``self.module_parameters`` to calculate @@ -672,10 +814,15 @@ def sapm_effective_irradiance(self, poa_direct, poa_diffuse, aoi = self._validate_per_array(aoi) return tuple( sapm_effective_irradiance( - poa_direct, poa_diffuse, airmass_absolute, aoi, - array.module_parameters) - for array, poa_direct, poa_diffuse, aoi - in zip(self.arrays, poa_direct, poa_diffuse, aoi) + poa_direct, + poa_diffuse, + airmass_absolute, + aoi, + array.module_parameters, + ) + for array, poa_direct, poa_diffuse, aoi in zip( + self.arrays, poa_direct, poa_diffuse, aoi + ) ) @_unwrap_single_value @@ -711,12 +858,13 @@ def first_solar_spectral_loss(self, pw, airmass_absolute): pw = self._validate_per_array(pw, system_wide=True) def _spectral_correction(array, pw): - if 'first_solar_spectral_coefficients' in \ - array.module_parameters.keys(): - coefficients = \ - array.module_parameters[ - 'first_solar_spectral_coefficients' - ] + if ( + "first_solar_spectral_coefficients" + in array.module_parameters.keys() + ): + coefficients = array.module_parameters[ + "first_solar_spectral_coefficients" + ] module_type = None else: module_type = array._infer_cell_type() @@ -725,21 +873,40 @@ def _spectral_correction(array, pw): return spectrum.spectral_factor_firstsolar( pw, airmass_absolute, module_type, coefficients ) + return tuple( itertools.starmap(_spectral_correction, zip(self.arrays, pw)) ) - def singlediode(self, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth): + def singlediode( + self, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ): """Wrapper around the :py:func:`pvlib.pvsystem.singlediode` function. See :py:func:`pvsystem.singlediode` for details """ - return singlediode(photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + return singlediode( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) - def i_from_v(self, voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth): + def i_from_v( + self, + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ): """Wrapper around the :py:func:`pvlib.pvsystem.i_from_v` function. See :py:func:`pvlib.pvsystem.i_from_v` for details. @@ -747,8 +914,14 @@ def i_from_v(self, voltage, photocurrent, saturation_current, .. versionchanged:: 0.10.0 The function's arguments have been reordered. """ - return i_from_v(voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) + return i_from_v( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) def get_ac(self, model, p_dc, v_dc=None): r"""Calculates AC power from p_dc using the inverter model indicated @@ -790,27 +963,32 @@ def get_ac(self, model, p_dc, v_dc=None): """ model = model.lower() multiple_arrays = self.num_arrays > 1 - if model == 'sandia': + if model == "sandia": p_dc = self._validate_per_array(p_dc) v_dc = self._validate_per_array(v_dc) if multiple_arrays: return inverter.sandia_multi( - v_dc, p_dc, self.inverter_parameters) + v_dc, p_dc, self.inverter_parameters + ) return inverter.sandia(v_dc[0], p_dc[0], self.inverter_parameters) - elif model == 'pvwatts': - kwargs = _build_kwargs(['eta_inv_nom', 'eta_inv_ref'], - self.inverter_parameters) + elif model == "pvwatts": + kwargs = _build_kwargs( + ["eta_inv_nom", "eta_inv_ref"], self.inverter_parameters + ) p_dc = self._validate_per_array(p_dc) if multiple_arrays: return inverter.pvwatts_multi( - p_dc, self.inverter_parameters['pdc0'], **kwargs) + p_dc, self.inverter_parameters["pdc0"], **kwargs + ) return inverter.pvwatts( - p_dc[0], self.inverter_parameters['pdc0'], **kwargs) - elif model == 'adr': + p_dc[0], self.inverter_parameters["pdc0"], **kwargs + ) + elif model == "adr": if multiple_arrays: raise ValueError( - 'The adr inverter function cannot be used for an inverter', - ' with multiple MPPT inputs') + "The adr inverter function cannot be used for an inverter", + " with multiple MPPT inputs", + ) # While this is only used for single-array systems, calling # _validate_per_arry lets us pass in singleton tuples. p_dc = self._validate_per_array(p_dc) @@ -818,8 +996,9 @@ def get_ac(self, model, p_dc, v_dc=None): return inverter.adr(v_dc[0], p_dc[0], self.inverter_parameters) else: raise ValueError( - model + ' is not a valid AC power model.', - ' model must be one of "sandia", "adr" or "pvwatts"') + model + " is not a valid AC power model.", + ' model must be one of "sandia", "adr" or "pvwatts"', + ) @_unwrap_single_value def scale_voltage_current_power(self, data): @@ -840,9 +1019,9 @@ def scale_voltage_current_power(self, data): """ data = self._validate_per_array(data) return tuple( - scale_voltage_current_power(data, - voltage=array.modules_per_string, - current=array.strings) + scale_voltage_current_power( + data, voltage=array.modules_per_string, current=array.strings + ) for array, data in zip(self.arrays, data) ) @@ -858,12 +1037,16 @@ def pvwatts_dc(self, g_poa_effective, temp_cell): g_poa_effective = self._validate_per_array(g_poa_effective) temp_cell = self._validate_per_array(temp_cell) return tuple( - pvwatts_dc(g_poa_effective, temp_cell, - array.module_parameters['pdc0'], - array.module_parameters['gamma_pdc'], - **_build_kwargs(['temp_ref'], array.module_parameters)) - for array, g_poa_effective, temp_cell - in zip(self.arrays, g_poa_effective, temp_cell) + pvwatts_dc( + g_poa_effective, + temp_cell, + array.module_parameters["pdc0"], + array.module_parameters["gamma_pdc"], + **_build_kwargs(["temp_ref"], array.module_parameters), + ) + for array, g_poa_effective, temp_cell in zip( + self.arrays, g_poa_effective, temp_cell + ) ) def pvwatts_losses(self): @@ -874,10 +1057,21 @@ def pvwatts_losses(self): See :py:func:`pvlib.pvsystem.pvwatts_losses` for details. """ - kwargs = _build_kwargs(['soiling', 'shading', 'snow', 'mismatch', - 'wiring', 'connections', 'lid', - 'nameplate_rating', 'age', 'availability'], - self.losses_parameters) + kwargs = _build_kwargs( + [ + "soiling", + "shading", + "snow", + "mismatch", + "wiring", + "connections", + "lid", + "nameplate_rating", + "age", + "availability", + ], + self.losses_parameters, + ) return pvwatts_losses(**kwargs) @_unwrap_single_value @@ -951,14 +1145,20 @@ class Array: Name of Array instance. """ - def __init__(self, mount, - albedo=None, surface_type=None, - module=None, module_type=None, - module_parameters=None, - temperature_model_parameters=None, - modules_per_string=1, strings=1, - array_losses_parameters=None, - name=None): + def __init__( + self, + mount, + albedo=None, + surface_type=None, + module=None, + module_type=None, + module_parameters=None, + temperature_model_parameters=None, + modules_per_string=1, + strings=1, + array_losses_parameters=None, + name=None, + ): self.mount = mount self.surface_type = surface_type @@ -979,8 +1179,9 @@ def __init__(self, mount, self.modules_per_string = modules_per_string if temperature_model_parameters is None: - self.temperature_model_parameters = \ + self.temperature_model_parameters = ( self._infer_temperature_model_params() + ) else: self.temperature_model_parameters = temperature_model_parameters @@ -992,27 +1193,33 @@ def __init__(self, mount, self.name = name def __repr__(self): - attrs = ['name', 'mount', 'module', - 'albedo', 'module_type', - 'temperature_model_parameters', - 'strings', 'modules_per_string'] - - return 'Array:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs + attrs = [ + "name", + "mount", + "module", + "albedo", + "module_type", + "temperature_model_parameters", + "strings", + "modules_per_string", + ] + + return "Array:\n " + "\n ".join( + f"{attr}: {getattr(self, attr)}" for attr in attrs ) def _infer_temperature_model_params(self): # try to infer temperature model parameters from racking_model # and module_type - param_set = f'{self.mount.racking_model}_{self.module_type}' - if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']: - return temperature._temperature_model_params('sapm', param_set) - elif 'freestanding' in param_set: - return temperature._temperature_model_params('pvsyst', - 'freestanding') - elif 'insulated' in param_set: # after SAPM to avoid confusing keys - return temperature._temperature_model_params('pvsyst', - 'insulated') + param_set = f"{self.mount.racking_model}_{self.module_type}" + if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"]: + return temperature._temperature_model_params("sapm", param_set) + elif "freestanding" in param_set: + return temperature._temperature_model_params( + "pvsyst", "freestanding" + ) + elif "insulated" in param_set: # after SAPM to avoid confusing keys + return temperature._temperature_model_params("pvsyst", "insulated") else: return {} @@ -1028,31 +1235,33 @@ def _infer_cell_type(self): """ - _cell_type_dict = {'Multi-c-Si': 'multisi', - 'Mono-c-Si': 'monosi', - 'Thin Film': 'cigs', - 'a-Si/nc': 'asi', - 'CIS': 'cigs', - 'CIGS': 'cigs', - '1-a-Si': 'asi', - 'CdTe': 'cdte', - 'a-Si': 'asi', - '2-a-Si': None, - '3-a-Si': None, - 'HIT-Si': 'monosi', - 'mc-Si': 'multisi', - 'c-Si': 'multisi', - 'Si-Film': 'asi', - 'EFG mc-Si': 'multisi', - 'GaAs': None, - 'a-Si / mono-Si': 'monosi'} - - if 'Technology' in self.module_parameters.keys(): + _cell_type_dict = { + "Multi-c-Si": "multisi", + "Mono-c-Si": "monosi", + "Thin Film": "cigs", + "a-Si/nc": "asi", + "CIS": "cigs", + "CIGS": "cigs", + "1-a-Si": "asi", + "CdTe": "cdte", + "a-Si": "asi", + "2-a-Si": None, + "3-a-Si": None, + "HIT-Si": "monosi", + "mc-Si": "multisi", + "c-Si": "multisi", + "Si-Film": "asi", + "EFG mc-Si": "multisi", + "GaAs": None, + "a-Si / mono-Si": "monosi", + } + + if "Technology" in self.module_parameters.keys(): # CEC module parameter set - cell_type = _cell_type_dict[self.module_parameters['Technology']] - elif 'Material' in self.module_parameters.keys(): + cell_type = _cell_type_dict[self.module_parameters["Technology"]] + elif "Material" in self.module_parameters.keys(): # Sandia module parameter set - cell_type = _cell_type_dict[self.module_parameters['Material']] + cell_type = _cell_type_dict[self.module_parameters["Material"]] else: cell_type = None @@ -1075,13 +1284,26 @@ def get_aoi(self, solar_zenith, solar_azimuth): Then angle of incidence. """ orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) - return irradiance.aoi(orientation['surface_tilt'], - orientation['surface_azimuth'], - solar_zenith, solar_azimuth) + return irradiance.aoi( + orientation["surface_tilt"], + orientation["surface_azimuth"], + solar_zenith, + solar_azimuth, + ) - def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, - dni_extra=None, airmass=None, albedo=None, - model='haydavies', **kwargs): + def get_irradiance( + self, + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=None, + airmass=None, + albedo=None, + model="haydavies", + **kwargs, + ): """ Get plane of array irradiance components. @@ -1136,11 +1358,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, # dni_extra is not needed for all models, but this is easier if dni_extra is None: - if (hasattr(solar_zenith, 'index') and - isinstance(solar_zenith.index, pd.DatetimeIndex)): + if hasattr(solar_zenith, "index") and isinstance( + solar_zenith.index, pd.DatetimeIndex + ): # calculate extraterrestrial irradiance - dni_extra = irradiance.get_extra_radiation( - solar_zenith.index) + dni_extra = irradiance.get_extra_radiation(solar_zenith.index) else: # use the solar constant dni_extra = 1367.0 @@ -1149,17 +1371,22 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, airmass = atmosphere.get_relative_airmass(solar_zenith) orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) - return irradiance.get_total_irradiance(orientation['surface_tilt'], - orientation['surface_azimuth'], - solar_zenith, solar_azimuth, - dni, ghi, dhi, - dni_extra=dni_extra, - airmass=airmass, - albedo=albedo, - model=model, - **kwargs) - - def get_iam(self, aoi, iam_model='physical'): + return irradiance.get_total_irradiance( + orientation["surface_tilt"], + orientation["surface_azimuth"], + solar_zenith, + solar_azimuth, + dni, + ghi, + dhi, + dni_extra=dni_extra, + airmass=airmass, + albedo=albedo, + model=model, + **kwargs, + ) + + def get_iam(self, aoi, iam_model="physical"): """ Determine the incidence angle modifier using the method specified by ``iam_model``. @@ -1188,21 +1415,27 @@ def get_iam(self, aoi, iam_model='physical'): if `iam_model` is not a valid model name. """ model = iam_model.lower() - if model in ['ashrae', 'physical', 'martin_ruiz', 'interp']: + if model in ["ashrae", "physical", "martin_ruiz", "interp"]: func = getattr(iam, model) # get function at pvlib.iam # get all parameters from function signature to retrieve them from # module_parameters if present params = set(inspect.signature(func).parameters.keys()) - params.discard('aoi') # exclude aoi so it can't be repeated + params.discard("aoi") # exclude aoi so it can't be repeated kwargs = _build_kwargs(params, self.module_parameters) return func(aoi, **kwargs) - elif model == 'sapm': + elif model == "sapm": return iam.sapm(aoi, self.module_parameters) else: - raise ValueError(model + ' is not a valid IAM model') - - def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, - effective_irradiance=None): + raise ValueError(model + " is not a valid IAM model") + + def get_cell_temperature( + self, + poa_global, + temp_air, + wind_speed, + model, + effective_irradiance=None, + ): """ Determine cell temperature using the method specified by ``model``. @@ -1243,49 +1476,70 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, """ # convenience wrapper to avoid passing args 2 and 3 every call _build_tcell_args = functools.partial( - _build_args, input_dict=self.temperature_model_parameters, - dict_name='temperature_model_parameters') + _build_args, + input_dict=self.temperature_model_parameters, + dict_name="temperature_model_parameters", + ) - if model == 'sapm': + if model == "sapm": func = temperature.sapm_cell - required = _build_tcell_args(['a', 'b', 'deltaT']) - optional = _build_kwargs(['irrad_ref'], - self.temperature_model_parameters) - elif model == 'pvsyst': + required = _build_tcell_args(["a", "b", "deltaT"]) + optional = _build_kwargs( + ["irrad_ref"], self.temperature_model_parameters + ) + elif model == "pvsyst": func = temperature.pvsyst_cell required = tuple() optional = { - **_build_kwargs(['module_efficiency', 'alpha_absorption'], - self.module_parameters), - **_build_kwargs(['u_c', 'u_v'], - self.temperature_model_parameters) + **_build_kwargs( + ["module_efficiency", "alpha_absorption"], + self.module_parameters, + ), + **_build_kwargs( + ["u_c", "u_v"], self.temperature_model_parameters + ), } - elif model == 'faiman': + elif model == "faiman": func = temperature.faiman required = tuple() - optional = _build_kwargs(['u0', 'u1'], - self.temperature_model_parameters) - elif model == 'fuentes': + optional = _build_kwargs( + ["u0", "u1"], self.temperature_model_parameters + ) + elif model == "fuentes": func = temperature.fuentes - required = _build_tcell_args(['noct_installed']) - optional = _build_kwargs([ - 'wind_height', 'emissivity', 'absorption', - 'surface_tilt', 'module_width', 'module_length'], - self.temperature_model_parameters) + required = _build_tcell_args(["noct_installed"]) + optional = _build_kwargs( + [ + "wind_height", + "emissivity", + "absorption", + "surface_tilt", + "module_width", + "module_length", + ], + self.temperature_model_parameters, + ) if self.mount.module_height is not None: - optional['module_height'] = self.mount.module_height - elif model == 'noct_sam': - func = functools.partial(temperature.noct_sam, - effective_irradiance=effective_irradiance) - required = _build_tcell_args(['noct', 'module_efficiency']) - optional = _build_kwargs(['transmittance_absorptance', - 'array_height', 'mount_standoff'], - self.temperature_model_parameters) + optional["module_height"] = self.mount.module_height + elif model == "noct_sam": + func = functools.partial( + temperature.noct_sam, effective_irradiance=effective_irradiance + ) + required = _build_tcell_args(["noct", "module_efficiency"]) + optional = _build_kwargs( + [ + "transmittance_absorptance", + "array_height", + "mount_standoff", + ], + self.temperature_model_parameters, + ) else: - raise ValueError(f'{model} is not a valid cell temperature model') + raise ValueError(f"{model} is not a valid cell temperature model") - temperature_cell = func(poa_global, temp_air, wind_speed, - *required, **optional) + temperature_cell = func( + poa_global, temp_air, wind_speed, *required, **optional + ) return temperature_cell def dc_ohms_from_percent(self): @@ -1320,37 +1574,40 @@ def dc_ohms_from_percent(self): """ # get relevent Vmp and Imp parameters from CEC parameters - if all(elem in self.module_parameters - for elem in ['V_mp_ref', 'I_mp_ref']): - vmp_ref = self.module_parameters['V_mp_ref'] - imp_ref = self.module_parameters['I_mp_ref'] + if all( + elem in self.module_parameters for elem in ["V_mp_ref", "I_mp_ref"] + ): + vmp_ref = self.module_parameters["V_mp_ref"] + imp_ref = self.module_parameters["I_mp_ref"] # get relevant Vmp and Imp parameters from SAPM parameters - elif all(elem in self.module_parameters for elem in ['Vmpo', 'Impo']): - vmp_ref = self.module_parameters['Vmpo'] - imp_ref = self.module_parameters['Impo'] + elif all(elem in self.module_parameters for elem in ["Vmpo", "Impo"]): + vmp_ref = self.module_parameters["Vmpo"] + imp_ref = self.module_parameters["Impo"] # get relevant Vmp and Imp parameters if they are PVsyst-like - elif all(elem in self.module_parameters for elem in ['Vmpp', 'Impp']): - vmp_ref = self.module_parameters['Vmpp'] - imp_ref = self.module_parameters['Impp'] + elif all(elem in self.module_parameters for elem in ["Vmpp", "Impp"]): + vmp_ref = self.module_parameters["Vmpp"] + imp_ref = self.module_parameters["Impp"] # raise error if relevant Vmp and Imp parameters are not found else: - raise ValueError('Parameters for Vmp and Imp could not be found ' - 'in the array module parameters. Module ' - 'parameters must include one set of ' - '{"V_mp_ref", "I_mp_Ref"}, ' - '{"Vmpo", "Impo"}, or ' - '{"Vmpp", "Impp"}.' - ) + raise ValueError( + "Parameters for Vmp and Imp could not be found " + "in the array module parameters. Module " + "parameters must include one set of " + '{"V_mp_ref", "I_mp_Ref"}, ' + '{"Vmpo", "Impo"}, or ' + '{"Vmpp", "Impp"}.' + ) return dc_ohms_from_percent( vmp_ref, imp_ref, - self.array_losses_parameters['dc_ohmic_percent'], + self.array_losses_parameters["dc_ohmic_percent"], self.modules_per_string, - self.strings) + self.strings, + ) @dataclass @@ -1416,8 +1673,8 @@ class FixedMount(AbstractMount): def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount return { - 'surface_tilt': self.surface_tilt, - 'surface_azimuth': self.surface_azimuth, + "surface_tilt": self.surface_tilt, + "surface_azimuth": self.surface_azimuth, } @@ -1489,11 +1746,12 @@ class SingleAxisTrackerMount(AbstractMount): The height above ground of the center of the module [m]. Used for the Fuentes cell temperature model. """ + axis_tilt: float = 0.0 axis_azimuth: float = 0.0 max_angle: Union[float, tuple] = 90.0 backtrack: bool = True - gcr: float = 2.0/7.0 + gcr: float = 2.0 / 7.0 cross_axis_tilt: float = 0.0 racking_model: Optional[str] = None module_height: Optional[float] = None @@ -1501,20 +1759,35 @@ class SingleAxisTrackerMount(AbstractMount): def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount from pvlib import tracking # avoid circular import issue + tracking_data = tracking.singleaxis( - solar_zenith, solar_azimuth, - self.axis_tilt, self.axis_azimuth, - self.max_angle, self.backtrack, - self.gcr, self.cross_axis_tilt + solar_zenith, + solar_azimuth, + self.axis_tilt, + self.axis_azimuth, + self.max_angle, + self.backtrack, + self.gcr, + self.cross_axis_tilt, ) return tracking_data -def calcparams_desoto(effective_irradiance, temp_cell, - alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, - EgRef=1.121, dEgdT=-0.0002677, - irrad_ref=1000, temp_ref=25): - ''' +def calcparams_desoto( + effective_irradiance, + temp_cell, + alpha_sc, + a_ref, + I_L_ref, + I_o_ref, + R_sh_ref, + R_s, + EgRef=1.121, + dEgdT=-0.0002677, + irrad_ref=1000, + temp_ref=25, +): + """ Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the De Soto et al. model. The five values returned by ``calcparams_desoto`` can be used by @@ -1676,16 +1949,16 @@ def calcparams_desoto(effective_irradiance, temp_cell, * M = unknown Source: [4] - ''' + """ # Boltzmann constant in eV/K, 8.617332478e-05 - k = constants.value('Boltzmann constant in eV/K') + k = constants.value("Boltzmann constant in eV/K") # reference temperature Tref_K = temp_ref + 273.15 Tcell_K = temp_cell + 273.15 - E_g = EgRef * (1 + dEgdT*(Tcell_K - Tref_K)) + E_g = EgRef * (1 + dEgdT * (Tcell_K - Tref_K)) nNsVth = a_ref * (Tcell_K / Tref_K) @@ -1693,10 +1966,16 @@ def calcparams_desoto(effective_irradiance, temp_cell, # used, in place of the product S*M in [1]. effective_irradiance is # equivalent to the product of S (irradiance reaching a module's cells) * # M (spectral adjustment factor) as described in [1]. - IL = effective_irradiance / irrad_ref * \ - (I_L_ref + alpha_sc * (Tcell_K - Tref_K)) - I0 = (I_o_ref * ((Tcell_K / Tref_K) ** 3) * - (np.exp(EgRef / (k*(Tref_K)) - (E_g / (k*(Tcell_K)))))) + IL = ( + effective_irradiance + / irrad_ref + * (I_L_ref + alpha_sc * (Tcell_K - Tref_K)) + ) + I0 = ( + I_o_ref + * ((Tcell_K / Tref_K) ** 3) + * (np.exp(EgRef / (k * (Tref_K)) - (E_g / (k * (Tcell_K))))) + ) # Note that the equation for Rsh differs from [1]. In [1] Rsh is given as # Rsh = Rsh_ref * (S_ref / S) where S is broadband irradiance reaching # the module's cells. If desired this model behavior can be duplicated @@ -1704,7 +1983,7 @@ def calcparams_desoto(effective_irradiance, temp_cell, # irradiance and not applying a spectral loss modifier, i.e., # spectral_modifier = 1.0. # use errstate to silence divide by warning - with np.errstate(divide='ignore'): + with np.errstate(divide="ignore"): Rsh = R_sh_ref * (irrad_ref / effective_irradiance) Rs = R_s @@ -1723,11 +2002,22 @@ def calcparams_desoto(effective_irradiance, temp_cell, return tuple(pd.Series(a, index=index).rename(None) for a in out) -def calcparams_cec(effective_irradiance, temp_cell, - alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, - Adjust, EgRef=1.121, dEgdT=-0.0002677, - irrad_ref=1000, temp_ref=25): - ''' +def calcparams_cec( + effective_irradiance, + temp_cell, + alpha_sc, + a_ref, + I_L_ref, + I_o_ref, + R_sh_ref, + R_s, + Adjust, + EgRef=1.121, + dEgdT=-0.0002677, + irrad_ref=1000, + temp_ref=25, +): + """ Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the CEC model. The CEC model [1]_ differs from the De soto et al. @@ -1828,26 +2118,43 @@ def calcparams_cec(effective_irradiance, temp_cell, singlediode retrieve_sam - ''' + """ # pass adjusted temperature coefficient to desoto - return calcparams_desoto(effective_irradiance, temp_cell, - alpha_sc*(1.0 - Adjust/100), - a_ref, I_L_ref, I_o_ref, - R_sh_ref, R_s, - EgRef=EgRef, dEgdT=dEgdT, - irrad_ref=irrad_ref, temp_ref=temp_ref) - - -def calcparams_pvsyst(effective_irradiance, temp_cell, - alpha_sc, gamma_ref, mu_gamma, - I_L_ref, I_o_ref, - R_sh_ref, R_sh_0, R_s, - cells_in_series, - R_sh_exp=5.5, - EgRef=1.121, - irrad_ref=1000, temp_ref=25): - ''' + return calcparams_desoto( + effective_irradiance, + temp_cell, + alpha_sc * (1.0 - Adjust / 100), + a_ref, + I_L_ref, + I_o_ref, + R_sh_ref, + R_s, + EgRef=EgRef, + dEgdT=dEgdT, + irrad_ref=irrad_ref, + temp_ref=temp_ref, + ) + + +def calcparams_pvsyst( + effective_irradiance, + temp_cell, + alpha_sc, + gamma_ref, + mu_gamma, + I_L_ref, + I_o_ref, + R_sh_ref, + R_sh_0, + R_s, + cells_in_series, + R_sh_exp=5.5, + EgRef=1.121, + irrad_ref=1000, + temp_ref=25, +): + """ Calculates five parameter values for the single diode equation at effective irradiance and cell temperature using the PVsyst v6 model. The PVsyst v6 model is described in [1]_, [2]_, [3]_. @@ -1945,7 +2252,7 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, calcparams_desoto singlediode - ''' + """ # Boltzmann constant in J/K k = constants.k @@ -1960,18 +2267,26 @@ def calcparams_pvsyst(effective_irradiance, temp_cell, gamma = gamma_ref + mu_gamma * (Tcell_K - Tref_K) nNsVth = gamma * k / q * cells_in_series * Tcell_K - IL = effective_irradiance / irrad_ref * \ - (I_L_ref + alpha_sc * (Tcell_K - Tref_K)) + IL = ( + effective_irradiance + / irrad_ref + * (I_L_ref + alpha_sc * (Tcell_K - Tref_K)) + ) - I0 = I_o_ref * ((Tcell_K / Tref_K) ** 3) * \ - (np.exp((q * EgRef) / (k * gamma) * (1 / Tref_K - 1 / Tcell_K))) + I0 = ( + I_o_ref + * ((Tcell_K / Tref_K) ** 3) + * (np.exp((q * EgRef) / (k * gamma) * (1 / Tref_K - 1 / Tcell_K))) + ) - Rsh_tmp = \ - (R_sh_ref - R_sh_0 * np.exp(-R_sh_exp)) / (1.0 - np.exp(-R_sh_exp)) + Rsh_tmp = (R_sh_ref - R_sh_0 * np.exp(-R_sh_exp)) / ( + 1.0 - np.exp(-R_sh_exp) + ) Rsh_base = np.maximum(0.0, Rsh_tmp) - Rsh = Rsh_base + (R_sh_0 - Rsh_base) * \ - np.exp(-R_sh_exp * effective_irradiance / irrad_ref) + Rsh = Rsh_base + (R_sh_0 - Rsh_base) * np.exp( + -R_sh_exp * effective_irradiance / irrad_ref + ) Rs = R_s @@ -2112,16 +2427,16 @@ def retrieve_sam(name=None, path=None): def _normalize_sam_product_names(names): - ''' + """ Replace special characters within the product names to make them more suitable for use as Dataframe column names. - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019 import warnings BAD_CHARS = ' -.()[]:+/",' - GOOD_CHARS = '____________' + GOOD_CHARS = "____________" mapping = str.maketrans(BAD_CHARS, GOOD_CHARS) names = pd.Series(data=names) @@ -2129,37 +2444,38 @@ def _normalize_sam_product_names(names): n_duplicates = names.duplicated().sum() if n_duplicates > 0: - warnings.warn('Original names contain %d duplicate(s).' % n_duplicates) + warnings.warn("Original names contain %d duplicate(s)." % n_duplicates) n_duplicates = norm_names.duplicated().sum() if n_duplicates > 0: warnings.warn( - 'Normalized names contain %d duplicate(s).' % n_duplicates) + "Normalized names contain %d duplicate(s)." % n_duplicates + ) return norm_names.values def _parse_raw_sam_df(csvdata): - df = pd.read_csv(csvdata, index_col=0, skiprows=[1, 2]) - df.columns = df.columns.str.replace(' ', '_') + df.columns = df.columns.str.replace(" ", "_") df.index = _normalize_sam_product_names(df.index) df = df.transpose() - if 'ADRCoefficients' in df.index: - ad_ce = 'ADRCoefficients' + if "ADRCoefficients" in df.index: + ad_ce = "ADRCoefficients" # for each inverter, parses a string of coefficients like # ' 1.33, 2.11, 3.12' into a list containing floats: # [1.33, 2.11, 3.12] - df.loc[ad_ce] = df.loc[ad_ce].map(lambda x: list( - map(float, x.strip(' []').split()))) + df.loc[ad_ce] = df.loc[ad_ce].map( + lambda x: list(map(float, x.strip(" []").split())) + ) return df def sapm(effective_irradiance, temp_cell, module): - ''' + """ The Sandia PV Array Performance Model (SAPM) generates 5 points on a PV module's I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to SAND2004-3535. Assumes a reference cell temperature of 25 C. @@ -2245,7 +2561,7 @@ def sapm(effective_irradiance, temp_cell, module): retrieve_sam pvlib.temperature.sapm_cell pvlib.temperature.sapm_module - ''' + """ # TODO: someday, change temp_ref and irrad_ref to reference_temperature and # reference_irradiance and expose @@ -2256,69 +2572,85 @@ def sapm(effective_irradiance, temp_cell, module): kb = constants.k # Boltzmann's constant in units of J/K # avoid problem with integer input - Ee = np.array(effective_irradiance, dtype='float64') / irrad_ref + Ee = np.array(effective_irradiance, dtype="float64") / irrad_ref # set up masking for 0, positive, and nan inputs - Ee_gt_0 = np.full_like(Ee, False, dtype='bool') - Ee_eq_0 = np.full_like(Ee, False, dtype='bool') + Ee_gt_0 = np.full_like(Ee, False, dtype="bool") + Ee_eq_0 = np.full_like(Ee, False, dtype="bool") notnan = ~np.isnan(Ee) np.greater(Ee, 0, where=notnan, out=Ee_gt_0) np.equal(Ee, 0, where=notnan, out=Ee_eq_0) - Bvmpo = module['Bvmpo'] + module['Mbvmp']*(1 - Ee) - Bvoco = module['Bvoco'] + module['Mbvoc']*(1 - Ee) - delta = module['N'] * kb * (temp_cell + 273.15) / q + Bvmpo = module["Bvmpo"] + module["Mbvmp"] * (1 - Ee) + Bvoco = module["Bvoco"] + module["Mbvoc"] * (1 - Ee) + delta = module["N"] * kb * (temp_cell + 273.15) / q # avoid repeated computation logEe = np.full_like(Ee, np.nan) np.log(Ee, where=Ee_gt_0, out=logEe) logEe = np.where(Ee_eq_0, -np.inf, logEe) # avoid repeated __getitem__ - cells_in_series = module['Cells_in_Series'] + cells_in_series = module["Cells_in_Series"] out = OrderedDict() - out['i_sc'] = ( - module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - temp_ref))) + out["i_sc"] = ( + module["Isco"] * Ee * (1 + module["Aisc"] * (temp_cell - temp_ref)) + ) - out['i_mp'] = ( - module['Impo'] * (module['C0']*Ee + module['C1']*(Ee**2)) * - (1 + module['Aimp']*(temp_cell - temp_ref))) + out["i_mp"] = ( + module["Impo"] + * (module["C0"] * Ee + module["C1"] * (Ee**2)) + * (1 + module["Aimp"] * (temp_cell - temp_ref)) + ) - out['v_oc'] = np.maximum(0, ( - module['Voco'] + cells_in_series * delta * logEe + - Bvoco*(temp_cell - temp_ref))) + out["v_oc"] = np.maximum( + 0, + ( + module["Voco"] + + cells_in_series * delta * logEe + + Bvoco * (temp_cell - temp_ref) + ), + ) - out['v_mp'] = np.maximum(0, ( - module['Vmpo'] + - module['C2'] * cells_in_series * delta * logEe + - module['C3'] * cells_in_series * ((delta * logEe) ** 2) + - Bvmpo*(temp_cell - temp_ref))) + out["v_mp"] = np.maximum( + 0, + ( + module["Vmpo"] + + module["C2"] * cells_in_series * delta * logEe + + module["C3"] * cells_in_series * ((delta * logEe) ** 2) + + Bvmpo * (temp_cell - temp_ref) + ), + ) - out['p_mp'] = out['i_mp'] * out['v_mp'] + out["p_mp"] = out["i_mp"] * out["v_mp"] - out['i_x'] = ( - module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) * - (1 + module['Aisc']*(temp_cell - temp_ref))) + out["i_x"] = ( + module["IXO"] + * (module["C4"] * Ee + module["C5"] * (Ee**2)) + * (1 + module["Aisc"] * (temp_cell - temp_ref)) + ) - out['i_xx'] = ( - module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) * - (1 + module['Aimp']*(temp_cell - temp_ref))) + out["i_xx"] = ( + module["IXXO"] + * (module["C6"] * Ee + module["C7"] * (Ee**2)) + * (1 + module["Aimp"] * (temp_cell - temp_ref)) + ) - if isinstance(out['i_sc'], pd.Series): + if isinstance(out["i_sc"], pd.Series): out = pd.DataFrame(out) return out sapm_spectral_loss = deprecated( - since='0.10.0', - alternative='pvlib.spectrum.spectral_factor_sapm' + since="0.10.0", alternative="pvlib.spectrum.spectral_factor_sapm" )(spectrum.spectral_factor_sapm) -def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, - module): +def sapm_effective_irradiance( + poa_direct, poa_diffuse, airmass_absolute, aoi, module +): r""" Calculates the SAPM effective irradiance using the SAPM spectral loss and SAPM angle of incidence loss functions. @@ -2382,13 +2714,19 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi, F1 = spectrum.spectral_factor_sapm(airmass_absolute, module) F2 = iam.sapm(aoi, module) - Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse) + Ee = F1 * (poa_direct * F2 + module["FD"] * poa_diffuse) return Ee -def singlediode(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, method='lambertw'): +def singlediode( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + method="lambertw", +): r""" Solve the single diode equation to obtain a photovoltaic IV curve. @@ -2497,11 +2835,16 @@ def singlediode(photocurrent, saturation_current, resistance_series, photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) https://doi.org/10.1016/0379-6787(88)90059-2 """ - args = (photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) # collect args + args = ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) # collect args # Calculate points on the IV curve using the LambertW solution to the # single diode equation - if method.lower() == 'lambertw': + if method.lower() == "lambertw": out = _singlediode._lambertw(*args) points = out[:7] else: @@ -2525,7 +2868,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, ) points = i_sc, v_oc, i_mp, v_mp, p_mp, i_x, i_xx - columns = ('i_sc', 'v_oc', 'i_mp', 'v_mp', 'p_mp', 'i_x', 'i_xx') + columns = ("i_sc", "v_oc", "i_mp", "v_mp", "p_mp", "i_x", "i_xx") if all(map(np.isscalar, args)): out = {c: p for c, p in zip(columns, points)} @@ -2542,9 +2885,16 @@ def singlediode(photocurrent, saturation_current, resistance_series, return out -def max_power_point(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.inf, - method='brentq'): +def max_power_point( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau=0, + NsVbi=np.inf, + method="brentq", +): """ Given the single diode equation coefficients, calculates the maximum power point (MPP). @@ -2589,23 +2939,36 @@ def max_power_point(photocurrent, saturation_current, resistance_series, guaranteed to converge. """ i_mp, v_mp, p_mp = _singlediode.bishop88_mpp( - photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, d2mutau, NsVbi, method=method.lower() + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau, + NsVbi, + method=method.lower(), ) if isinstance(photocurrent, pd.Series): - ivp = {'i_mp': i_mp, 'v_mp': v_mp, 'p_mp': p_mp} + ivp = {"i_mp": i_mp, "v_mp": v_mp, "p_mp": p_mp} out = pd.DataFrame(ivp, index=photocurrent.index) else: out = OrderedDict() - out['i_mp'] = i_mp - out['v_mp'] = v_mp - out['p_mp'] = p_mp + out["i_mp"] = i_mp + out["v_mp"] = v_mp + out["p_mp"] = p_mp return out -def v_from_i(current, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, method='lambertw'): - ''' +def v_from_i( + current, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + method="lambertw", +): + """ Device voltage at the given device current for the single diode model. Uses the single diode model (SDM) as described in, e.g., @@ -2669,10 +3032,16 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, .. [1] A. Jain, A. Kapoor, "Exact analytical solutions of the parameters of real solar cells using Lambert W-function", Solar Energy Materials and Solar Cells, 81 (2004) 269-277. - ''' - args = (current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) - if method.lower() == 'lambertw': + """ + args = ( + current, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + if method.lower() == "lambertw": return _singlediode._lambertw_v_from_i(*args) else: # Calculate points on the IV curve using either 'newton' or 'brentq' @@ -2685,9 +3054,16 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, return np.broadcast_to(V, shape) -def i_from_v(voltage, photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, method='lambertw'): - ''' +def i_from_v( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + method="lambertw", +): + """ Device current at the given device voltage for the single diode model. Uses the single diode model (SDM) as described in, e.g., @@ -2751,10 +3127,16 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series, .. [1] A. Jain, A. Kapoor, "Exact analytical solutions of the parameters of real solar cells using Lambert W-function", Solar Energy Materials and Solar Cells, 81 (2004) 269-277. - ''' - args = (voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) - if method.lower() == 'lambertw': + """ + args = ( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + if method.lower() == "lambertw": return _singlediode._lambertw_i_from_v(*args) else: # Calculate points on the IV curve using either 'newton' or 'brentq' @@ -2791,9 +3173,9 @@ def scale_voltage_current_power(data, voltage=1, current=1): # as written, only works with a DataFrame # could make it work with a dict, but it would be more verbose - voltage_keys = ['v_mp', 'v_oc'] - current_keys = ['i_mp', 'i_x', 'i_xx', 'i_sc'] - power_keys = ['p_mp'] + voltage_keys = ["v_mp", "v_oc"] + current_keys = ["i_mp", "i_x", "i_xx", "i_sc"] + power_keys = ["p_mp"] voltage_df = data.filter(voltage_keys, axis=1) * voltage current_df = data.filter(current_keys, axis=1) * current power_df = data.filter(power_keys, axis=1) * voltage * current @@ -2802,7 +3184,7 @@ def scale_voltage_current_power(data, voltage=1, current=1): return df_sorted -def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.): +def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.0): r""" Implements NREL's PVWatts DC power model. The PVWatts DC model [1]_ is: @@ -2846,15 +3228,28 @@ def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.): (2014). """ # noqa: E501 - pdc = (g_poa_effective * 0.001 * pdc0 * - (1 + gamma_pdc * (temp_cell - temp_ref))) + pdc = ( + g_poa_effective + * 0.001 + * pdc0 + * (1 + gamma_pdc * (temp_cell - temp_ref)) + ) return pdc -def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2, - connections=0.5, lid=1.5, nameplate_rating=1, age=0, - availability=3): +def pvwatts_losses( + soiling=2, + shading=3, + snow=0, + mismatch=2, + wiring=2, + connections=0.5, + lid=1.5, + nameplate_rating=1, + age=0, + availability=3, +): r""" Implements NREL's PVWatts system loss model. The PVWatts loss model [1]_ is: @@ -2892,23 +3287,33 @@ def pvwatts_losses(soiling=2, shading=3, snow=0, mismatch=2, wiring=2, (2014). """ - params = [soiling, shading, snow, mismatch, wiring, connections, lid, - nameplate_rating, age, availability] + params = [ + soiling, + shading, + snow, + mismatch, + wiring, + connections, + lid, + nameplate_rating, + age, + availability, + ] # manually looping over params allows for numpy/pandas to handle any # array-like broadcasting that might be necessary. perf = 1 for param in params: - perf *= 1 - param/100 + perf *= 1 - param / 100 - losses = (1 - perf) * 100. + losses = (1 - perf) * 100.0 return losses -def dc_ohms_from_percent(vmp_ref, imp_ref, dc_ohmic_percent, - modules_per_string=1, - strings=1): +def dc_ohms_from_percent( + vmp_ref, imp_ref, dc_ohmic_percent, modules_per_string=1, strings=1 +): r""" Calculate the equivalent resistance of the conductors from the percent ohmic loss of an array at reference conditions. @@ -2998,7 +3403,7 @@ def dc_ohmic_losses(resistance, current): return resistance * current * current -def combine_loss_factors(index, *losses, fill_method='ffill'): +def combine_loss_factors(index, *losses, fill_method="ffill"): r""" Combines Series loss fractions while setting a common index. @@ -3035,6 +3440,6 @@ def combine_loss_factors(index, *losses, fill_method='ffill'): for loss in losses: loss = loss.reindex(index, method=fill_method) - combined_factor *= (1 - loss) + combined_factor *= 1 - loss return 1 - combined_factor diff --git a/pvlib/scaling.py b/pvlib/scaling.py index 2f9e0df594..c1f70f7d4e 100644 --- a/pvlib/scaling.py +++ b/pvlib/scaling.py @@ -127,13 +127,13 @@ def _compute_vr(positions, cloud_speed, tmscales): # Added by Joe Ranalli (@jranalli), Penn State Hazleton, 2021 pos = np.array(positions) - dist = pdist(pos, 'euclidean') + dist = pdist(pos, "euclidean") # Find effective length of position vector, 'dist' is full pairwise n_pairs = len(dist) def fn(x): - return np.abs((x ** 2 - x) / 2 - n_pairs) + return np.abs((x**2 - x) / 2 - n_pairs) n_dist = np.round(scipy.optimize.fmin(fn, np.sqrt(n_pairs), disp=False)) n_dist = n_dist.item() @@ -145,7 +145,7 @@ def fn(x): # 2*rho is because rho_ij = rho_ji. +n_dist accounts for sum(rho_ii=1) denominator = 2 * np.sum(rho) + n_dist - vr[i] = n_dist ** 2 / denominator # Eq 6 of [1] + vr[i] = n_dist**2 / denominator # Eq 6 of [1] return vr @@ -193,7 +193,7 @@ def latlon_to_xy(coordinates): meanlat = np.mean([lat for (lat, lon) in coordinates]) # Mean latitude except TypeError: # Assume it's a single value? meanlat = coordinates[0] - m_per_deg_lon = r_earth * np.cos(np.pi/180 * meanlat) * np.pi/180 + m_per_deg_lon = r_earth * np.cos(np.pi / 180 * meanlat) * np.pi / 180 # Conversion pos = coordinates * np.array(m_per_deg_lat, m_per_deg_lon) @@ -254,16 +254,16 @@ def _compute_wavelet(clearsky_index, dt=None): else: # flatten() succeeded, thus it's a pandas type, so get its dt try: # Assume it's a time series type index dt = clearsky_index.index[1] - clearsky_index.index[0] - dt = dt.seconds + dt.microseconds/1e6 + dt = dt.seconds + dt.microseconds / 1e6 except AttributeError: # It must just be a numeric index - dt = (clearsky_index.index[1] - clearsky_index.index[0]) + dt = clearsky_index.index[1] - clearsky_index.index[0] # Pad the series on both ends in time and place in a dataframe - cs_long = np.pad(vals, (len(vals), len(vals)), 'symmetric') + cs_long = np.pad(vals, (len(vals), len(vals)), "symmetric") cs_long = pd.DataFrame(cs_long) # Compute wavelet time scales - min_tmscale = np.ceil(np.log(dt)/np.log(2)) # Minimum wavelet timescale + min_tmscale = np.ceil(np.log(dt) / np.log(2)) # Minimum wavelet timescale max_tmscale = int(13 - min_tmscale) # maximum wavelet timescale tmscales = np.zeros(max_tmscale) @@ -288,13 +288,13 @@ def _compute_wavelet(clearsky_index, dt=None): # Calculate detail coefficients by difference between successive averages wavelet_long = np.zeros(csi_mean.shape) - for i in np.arange(0, max_tmscale-1): - wavelet_long[i, :] = csi_mean[i, :] - csi_mean[i+1, :] + for i in np.arange(0, max_tmscale - 1): + wavelet_long[i, :] = csi_mean[i, :] - csi_mean[i + 1, :] wavelet_long[-1, :] = csi_mean[-1, :] # Lowest freq (CAn) # Clip off the padding and just return the original time window wavelet = np.zeros([max_tmscale, len(vals)]) for i in np.arange(0, max_tmscale): - wavelet[i, :] = wavelet_long[i, len(vals): 2*len(vals)] + wavelet[i, :] = wavelet_long[i, len(vals) : 2 * len(vals)] return wavelet, tmscales diff --git a/pvlib/shading.py b/pvlib/shading.py index 42aef892f7..fa4a13f6c4 100644 --- a/pvlib/shading.py +++ b/pvlib/shading.py @@ -173,9 +173,9 @@ def masking_angle_passias(surface_tilt, gcr): beta = np.radians(np.array(surface_tilt)) sin_b = np.sin(beta) cos_b = np.cos(beta) - X = 1/gcr + X = 1 / gcr - with np.errstate(divide='ignore', invalid='ignore'): # ignore beta=0 + with np.errstate(divide="ignore", invalid="ignore"): # ignore beta=0 term1 = -X * sin_b * np.log(np.abs(2 * X * cos_b - (X**2 + 1))) / 2 term2 = (X * cos_b - 1) * np.arctan((X * cos_b - 1) / (X * sin_b)) term3 = (1 - X * cos_b) * np.arctan(cos_b / sin_b) @@ -231,11 +231,12 @@ def sky_diffuse_passias(masking_angle): Reference Update", NREL Technical Report NREL/TP-6A20-67399. Available at https://www.nrel.gov/docs/fy18osti/67399.pdf """ - return 1 - cosd(masking_angle/2)**2 + return 1 - cosd(masking_angle / 2) ** 2 -def projected_solar_zenith_angle(solar_zenith, solar_azimuth, - axis_tilt, axis_azimuth): +def projected_solar_zenith_angle( + solar_zenith, solar_azimuth, axis_tilt, axis_azimuth +): r""" Calculate projected solar zenith angle in degrees. diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e76ea8f263..705d7a56d2 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -9,10 +9,7 @@ from scipy.special import lambertw # newton method default parameters for this module -NEWTON_DEFAULT_PARAMS = { - 'tol': 1e-6, - 'maxiter': 100 -} +NEWTON_DEFAULT_PARAMS = {"tol": 1e-6, "maxiter": 100} # intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al. VOLTAGE_BUILTIN = 0.9 # [V] @@ -56,10 +53,20 @@ def estimate_voc(photocurrent, saturation_current, nNsVth): return nNsVth * np.log(np.asarray(photocurrent) / saturation_current + 1.0) -def bishop88(diode_voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, d2mutau=0, - NsVbi=np.inf, breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, gradients=False): +def bishop88( + diode_voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau=0, + NsVbi=np.inf, + breakdown_factor=0.0, + breakdown_voltage=-5.5, + breakdown_exp=3.28, + gradients=False, +): r""" Explicit calculation of points on the IV curve described by the single diode equation. Values are calculated as described in [1]_. @@ -167,11 +174,16 @@ def bishop88(diode_voltage, photocurrent, saturation_current, brk_pwr = np.power(brk_term, -breakdown_exp) i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr else: - i_breakdown = 0. - i = (photocurrent - saturation_current * np.expm1(v_star) # noqa: W503 - - diode_voltage * g_sh - i_recomb - i_breakdown) # noqa: W503 + i_breakdown = 0.0 + i = ( + photocurrent + - saturation_current * np.expm1(v_star) # noqa: W503 + - diode_voltage * g_sh + - i_recomb + - i_breakdown + ) # noqa: W503 v = diode_voltage - i * resistance_series - retval = (i, v, i*v) + retval = (i, v, i * v) if gradients: # calculate recombination loss current gradients where d2mutau > 0 grad_i_recomb = np.where(is_recomb, i_recomb / v_recomb, 0) @@ -181,14 +193,22 @@ def bishop88(diode_voltage, photocurrent, saturation_current, brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1) brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2) brk_fctr = breakdown_factor * g_sh - grad_i_brk = brk_fctr * (brk_pwr + diode_voltage * - -breakdown_exp * brk_pwr_1) - grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503 - * (2 * brk_pwr_1 + diode_voltage # noqa: W503 - * (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503 + grad_i_brk = brk_fctr * ( + brk_pwr + diode_voltage * -breakdown_exp * brk_pwr_1 + ) + grad2i_brk = ( + brk_fctr + * -breakdown_exp # noqa: W503 + * ( + 2 * brk_pwr_1 + + diode_voltage # noqa: W503 + * (-breakdown_exp - 1) + * brk_pwr_2 + ) + ) # noqa: W503 else: - grad_i_brk = 0. - grad2i_brk = 0. + grad_i_brk = 0.0 + grad2i_brk = 0.0 grad_i = -g_diode - g_sh - grad_i_recomb - grad_i_brk # di/dvd grad_v = 1.0 - grad_i * resistance_series # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i @@ -197,18 +217,29 @@ def bishop88(diode_voltage, photocurrent, saturation_current, grad2i = -g_diode / nNsVth - grad_2i_recomb - grad2i_brk # d2i/dvd grad2v = -grad2i * resistance_series # d2v/dvd grad2p = ( - grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_v * grad + + v * (grad2i / grad_v - grad_i * grad2v / grad_v**2) + grad_i ) # d2p/dv/dvd retval += (grad_i, grad_v, grad, grad_p, grad2p) return retval -def bishop88_i_from_v(voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, - d2mutau=0, NsVbi=np.inf, breakdown_factor=0., - breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', method_kwargs=None): +def bishop88_i_from_v( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau=0, + NsVbi=np.inf, + breakdown_factor=0.0, + breakdown_voltage=-5.5, + breakdown_exp=3.28, + method="newton", + method_kwargs=None, +): """ Find current given any voltage. @@ -293,9 +324,18 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, :doi:`10.1016/0379-6787(88)90059-2` """ # collect args - args = (photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, breakdown_exp) + args = ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ) method = method.lower() # method_kwargs create dict if not provided @@ -307,49 +347,90 @@ def fv(x, v, *a): # calculate voltage residual given diode voltage "x" return bishop88(x, *a)[1] - v - if method == 'brentq': + if method == "brentq": # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) # start iteration slightly less than NsVbi when voc_est > NsVbi, to # avoid the asymptote at NsVbi - xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) + xp = np.where(voc_est < NsVbi, voc_est, 0.9999 * NsVbi) # brentq only works with scalar inputs, so we need a set up function # and np.vectorize to repeatedly call the optimizer with the right # arguments for possible array input - def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, breakdown_exp): - return brentq(fv, 0.0, voc, - args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, - breakdown_exp), - **method_kwargs) + def vd_from_brent( + voc, + v, + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ): + return brentq( + fv, + 0.0, + voc, + args=( + v, + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ), + **method_kwargs, + ) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(xp, voltage, *args) - elif method == 'newton': - x0, (voltage, *args), method_kwargs = \ - _prepare_newton_inputs(voltage, (voltage, *args), method_kwargs) - vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=x0, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], - args=args, **method_kwargs) + elif method == "newton": + x0, (voltage, *args), method_kwargs = _prepare_newton_inputs( + voltage, (voltage, *args), method_kwargs + ) + vd = newton( + func=lambda x, *a: fv(x, voltage, *a), + x0=x0, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], + args=args, + **method_kwargs, + ) else: raise NotImplementedError("Method '%s' isn't implemented" % method) # When 'full_output' parameter is specified, returned 'vd' is a tuple with # many elements, where the root is the first one. So we use it to output # the bishop88 result and return tuple(scalar, tuple with method results) - if method_kwargs.get('full_output') is True: + if method_kwargs.get("full_output") is True: return (bishop88(vd[0], *args)[0], vd) else: return bishop88(vd, *args)[0] -def bishop88_v_from_i(current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, - d2mutau=0, NsVbi=np.inf, breakdown_factor=0., - breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', method_kwargs=None): +def bishop88_v_from_i( + current, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau=0, + NsVbi=np.inf, + breakdown_factor=0.0, + breakdown_voltage=-5.5, + breakdown_exp=3.28, + method="newton", + method_kwargs=None, +): """ Find voltage given any current. @@ -434,9 +515,18 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, :doi:`10.1016/0379-6787(88)90059-2` """ # collect args - args = (photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, breakdown_exp) + args = ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ) method = method.lower() # method_kwargs create dict if not provided @@ -448,48 +538,89 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) # start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid # the asymptote at NsVbi - xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) + xp = np.where(voc_est < NsVbi, voc_est, 0.9999 * NsVbi) def fi(x, i, *a): # calculate current residual given diode voltage "x" return bishop88(x, *a)[0] - i - if method == 'brentq': + if method == "brentq": # brentq only works with scalar inputs, so we need a set up function # and np.vectorize to repeatedly call the optimizer with the right # arguments for possible array input - def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, breakdown_exp): - return brentq(fi, 0.0, voc, - args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, - breakdown_exp), - **method_kwargs) + def vd_from_brent( + voc, + i, + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ): + return brentq( + fi, + 0.0, + voc, + args=( + i, + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ), + **method_kwargs, + ) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(xp, current, *args) - elif method == 'newton': - x0, (current, *args), method_kwargs = \ - _prepare_newton_inputs(xp, (current, *args), method_kwargs) - vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], - args=args, **method_kwargs) + elif method == "newton": + x0, (current, *args), method_kwargs = _prepare_newton_inputs( + xp, (current, *args), method_kwargs + ) + vd = newton( + func=lambda x, *a: fi(x, current, *a), + x0=x0, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], + args=args, + **method_kwargs, + ) else: raise NotImplementedError("Method '%s' isn't implemented" % method) # When 'full_output' parameter is specified, returned 'vd' is a tuple with # many elements, where the root is the first one. So we use it to output # the bishop88 result and return tuple(scalar, tuple with method results) - if method_kwargs.get('full_output') is True: + if method_kwargs.get("full_output") is True: return (bishop88(vd[0], *args)[1], vd) else: return bishop88(vd, *args)[1] -def bishop88_mpp(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.inf, - breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, method='newton', method_kwargs=None): +def bishop88_mpp( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau=0, + NsVbi=np.inf, + breakdown_factor=0.0, + breakdown_voltage=-5.5, + breakdown_exp=3.28, + method="newton", + method_kwargs=None, +): """ Find max power point. @@ -573,9 +704,18 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, :doi:`10.1016/0379-6787(88)90059-2` """ # collect args - args = (photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, - breakdown_factor, breakdown_voltage, breakdown_exp) + args = ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + d2mutau, + NsVbi, + breakdown_factor, + breakdown_voltage, + breakdown_exp, + ) method = method.lower() # method_kwargs create dict if not provided @@ -587,30 +727,58 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) # start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid # the asymptote at NsVbi - xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) + xp = np.where(voc_est < NsVbi, voc_est, 0.9999 * NsVbi) def fmpp(x, *a): return bishop88(x, *a, gradients=True)[6] - if method == 'brentq': + if method == "brentq": # break out arguments for numpy.vectorize to handle broadcasting vec_fun = np.vectorize( - lambda voc, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr, - vbr_exp: brentq(fmpp, 0.0, voc, - args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - vbr_a, vbr, vbr_exp), - **method_kwargs) + lambda voc, + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + vbr_a, + vbr, + vbr_exp: brentq( + fmpp, + 0.0, + voc, + args=( + iph, + isat, + rs, + rsh, + gamma, + d2mutau, + NsVbi, + vbr_a, + vbr, + vbr_exp, + ), + **method_kwargs, + ) ) vd = vec_fun(xp, *args) - elif method == 'newton': + elif method == "newton": # make sure all args are numpy arrays if max size > 1 # if voc_est is an array, then make a copy to use for initial guess, v0 - x0, args, method_kwargs = \ - _prepare_newton_inputs(xp, args, method_kwargs) - vd = newton(func=fmpp, x0=x0, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], - args=args, **method_kwargs) + x0, args, method_kwargs = _prepare_newton_inputs( + xp, args, method_kwargs + ) + vd = newton( + func=fmpp, + x0=x0, + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], + args=args, + **method_kwargs, + ) else: raise NotImplementedError("Method '%s' isn't implemented" % method) @@ -618,15 +786,16 @@ def fmpp(x, *a): # many elements, where the root is the first one. So we use it to output # the bishop88 result and return # tuple(tuple with bishop88 solution, tuple with method results) - if method_kwargs.get('full_output') is True: + if method_kwargs.get("full_output") is True: return (bishop88(vd[0], *args), vd) else: return bishop88(vd, *args) def _shape_of_max_size(*args): - return max(((np.size(a), np.shape(a)) for a in args), - key=lambda t: t[0])[1] + return max(((np.size(a), np.shape(a)) for a in args), key=lambda t: t[0])[ + 1 + ] def _prepare_newton_inputs(x0, args, method_kwargs): @@ -662,46 +831,74 @@ def _prepare_newton_inputs(x0, args, method_kwargs): return x0, args, method_kwargs -def _lambertw_v_from_i(current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth): +def _lambertw_v_from_i( + current, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, +): # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - (current, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth))) + output_is_scalar = all( + map( + np.isscalar, + ( + current, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ), + ) + ) # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which # is generally more numerically stable - conductance_shunt = 1. / resistance_shunt + conductance_shunt = 1.0 / resistance_shunt # Ensure that we are working with read-only views of numpy arrays # Turns Series into arrays so that we don't have to worry about # multidimensional broadcasting failing - I, IL, I0, Rs, Gsh, a = \ - np.broadcast_arrays(current, photocurrent, saturation_current, - resistance_series, conductance_shunt, nNsVth) + I, IL, I0, Rs, Gsh, a = np.broadcast_arrays( + current, + photocurrent, + saturation_current, + resistance_series, + conductance_shunt, + nNsVth, + ) # Intitalize output V (I might not be float64) V = np.full_like(I, np.nan, dtype=np.float64) # Determine indices where 0 < Gsh requires implicit model solution - idx_p = 0. < Gsh + idx_p = 0.0 < Gsh # Determine indices where 0 = Gsh allows explicit model solution - idx_z = 0. == Gsh + idx_z = 0.0 == Gsh # Explicit solutions where Gsh=0 if np.any(idx_z): - V[idx_z] = a[idx_z] * np.log1p((IL[idx_z] - I[idx_z]) / I0[idx_z]) - \ - I[idx_z] * Rs[idx_z] + V[idx_z] = ( + a[idx_z] * np.log1p((IL[idx_z] - I[idx_z]) / I0[idx_z]) + - I[idx_z] * Rs[idx_z] + ) # Only compute using LambertW if there are cases with Gsh>0 if np.any(idx_p): # LambertW argument, cannot be float128, may overflow to np.inf # overflow is explicitly handled below, so ignore warnings here - with np.errstate(over='ignore'): - argW = (I0[idx_p] / (Gsh[idx_p] * a[idx_p]) * - np.exp((-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p] * a[idx_p]))) + with np.errstate(over="ignore"): + argW = ( + I0[idx_p] + / (Gsh[idx_p] * a[idx_p]) + * np.exp( + (-I[idx_p] + IL[idx_p] + I0[idx_p]) + / (Gsh[idx_p] * a[idx_p]) + ) + ) # lambertw typically returns complex value with zero imaginary part # may overflow to np.inf @@ -713,10 +910,12 @@ def _lambertw_v_from_i(current, photocurrent, saturation_current, # Only re-compute LambertW if it overflowed if np.any(idx_inf): # Calculate using log(argW) in case argW is really big - logargW = (np.log(I0[idx_p]) - np.log(Gsh[idx_p]) - - np.log(a[idx_p]) + - (-I[idx_p] + IL[idx_p] + I0[idx_p]) / - (Gsh[idx_p] * a[idx_p]))[idx_inf] + logargW = ( + np.log(I0[idx_p]) + - np.log(Gsh[idx_p]) + - np.log(a[idx_p]) + + (-I[idx_p] + IL[idx_p] + I0[idx_p]) / (Gsh[idx_p] * a[idx_p]) + )[idx_inf] # Three iterations of Newton-Raphson method to solve # w+log(w)=logargW. The initial guess is w=logargW. Where direct @@ -724,14 +923,17 @@ def _lambertw_v_from_i(current, photocurrent, saturation_current, # of Newton's method gives approximately 8 digits of precision. w = logargW for _ in range(0, 3): - w = w * (1. - np.log(w) + logargW) / (1. + w) + w = w * (1.0 - np.log(w) + logargW) / (1.0 + w) lambertwterm[idx_inf] = w # Eqn. 3 in Jain and Kapoor, 2004 # V = -I*(Rs + Rsh) + IL*Rsh - a*lambertwterm + I0*Rsh # Recast in terms of Gsh=1/Rsh for better numerical stability. - V[idx_p] = (IL[idx_p] + I0[idx_p] - I[idx_p]) / Gsh[idx_p] - \ - I[idx_p] * Rs[idx_p] - a[idx_p] * lambertwterm + V[idx_p] = ( + (IL[idx_p] + I0[idx_p] - I[idx_p]) / Gsh[idx_p] + - I[idx_p] * Rs[idx_p] + - a[idx_p] * lambertwterm + ) if output_is_scalar: return V.item() @@ -739,46 +941,75 @@ def _lambertw_v_from_i(current, photocurrent, saturation_current, return V -def _lambertw_i_from_v(voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth): +def _lambertw_i_from_v( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, +): # Record if inputs were all scalar - output_is_scalar = all(map(np.isscalar, - (voltage, photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth))) + output_is_scalar = all( + map( + np.isscalar, + ( + voltage, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ), + ) + ) # This transforms Gsh=1/Rsh, including ideal Rsh=np.inf into Gsh=0., which # is generally more numerically stable - conductance_shunt = 1. / resistance_shunt + conductance_shunt = 1.0 / resistance_shunt # Ensure that we are working with read-only views of numpy arrays # Turns Series into arrays so that we don't have to worry about # multidimensional broadcasting failing - V, IL, I0, Rs, Gsh, a = \ - np.broadcast_arrays(voltage, photocurrent, saturation_current, - resistance_series, conductance_shunt, nNsVth) + V, IL, I0, Rs, Gsh, a = np.broadcast_arrays( + voltage, + photocurrent, + saturation_current, + resistance_series, + conductance_shunt, + nNsVth, + ) # Intitalize output I (V might not be float64) - I = np.full_like(V, np.nan, dtype=np.float64) # noqa: E741, N806 + I = np.full_like(V, np.nan, dtype=np.float64) # noqa: E741, N806 # Determine indices where 0 < Rs requires implicit model solution - idx_p = 0. < Rs + idx_p = 0.0 < Rs # Determine indices where 0 = Rs allows explicit model solution - idx_z = 0. == Rs + idx_z = 0.0 == Rs # Explicit solutions where Rs=0 if np.any(idx_z): - I[idx_z] = IL[idx_z] - I0[idx_z] * np.expm1(V[idx_z] / a[idx_z]) - \ - Gsh[idx_z] * V[idx_z] + I[idx_z] = ( + IL[idx_z] + - I0[idx_z] * np.expm1(V[idx_z] / a[idx_z]) + - Gsh[idx_z] * V[idx_z] + ) # Only compute using LambertW if there are cases with Rs>0 # Does NOT handle possibility of overflow, github issue 298 if np.any(idx_p): # LambertW argument, cannot be float128, may overflow to np.inf - argW = Rs[idx_p] * I0[idx_p] / ( - a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.)) * \ - np.exp((Rs[idx_p] * (IL[idx_p] + I0[idx_p]) + V[idx_p]) / - (a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.))) + argW = ( + Rs[idx_p] + * I0[idx_p] + / (a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.0)) + * np.exp( + (Rs[idx_p] * (IL[idx_p] + I0[idx_p]) + V[idx_p]) + / (a[idx_p] * (Rs[idx_p] * Gsh[idx_p] + 1.0)) + ) + ) # lambertw typically returns complex value with zero imaginary part # may overflow to np.inf @@ -787,9 +1018,9 @@ def _lambertw_i_from_v(voltage, photocurrent, saturation_current, # Eqn. 2 in Jain and Kapoor, 2004 # I = -V/(Rs + Rsh) - (a/Rs)*lambertwterm + Rsh*(IL + I0)/(Rs + Rsh) # Recast in terms of Gsh=1/Rsh for better numerical stability. - I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p] * Gsh[idx_p]) / \ - (Rs[idx_p] * Gsh[idx_p] + 1.) - ( - a[idx_p] / Rs[idx_p]) * lambertwterm + I[idx_p] = (IL[idx_p] + I0[idx_p] - V[idx_p] * Gsh[idx_p]) / ( + Rs[idx_p] * Gsh[idx_p] + 1.0 + ) - (a[idx_p] / Rs[idx_p]) * lambertwterm if output_is_scalar: return I.item() @@ -797,30 +1028,39 @@ def _lambertw_i_from_v(voltage, photocurrent, saturation_current, return I -def _lambertw(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, ivcurve_pnts=None): +def _lambertw( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ivcurve_pnts=None, +): # collect args - params = {'photocurrent': photocurrent, - 'saturation_current': saturation_current, - 'resistance_series': resistance_series, - 'resistance_shunt': resistance_shunt, 'nNsVth': nNsVth} + params = { + "photocurrent": photocurrent, + "saturation_current": saturation_current, + "resistance_series": resistance_series, + "resistance_shunt": resistance_shunt, + "nNsVth": nNsVth, + } # Compute short circuit current - i_sc = _lambertw_i_from_v(0., **params) + i_sc = _lambertw_i_from_v(0.0, **params) # Compute open circuit voltage - v_oc = _lambertw_v_from_i(0., **params) + v_oc = _lambertw_v_from_i(0.0, **params) # Set small elements <0 in v_oc to 0 if isinstance(v_oc, np.ndarray): - v_oc[(v_oc < 0) & (v_oc > -1e-12)] = 0. + v_oc[(v_oc < 0) & (v_oc > -1e-12)] = 0.0 elif isinstance(v_oc, (float, int)): if v_oc < 0 and v_oc > -1e-12: - v_oc = 0. + v_oc = 0.0 # Find the voltage, v_mp, where the power is maximized. # Start the golden section search at v_oc * 1.14 - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) + p_mp, v_mp = _golden_sect_DataFrame(params, 0.0, v_oc * 1.14, _pwr_optfcn) # Find Imp using Lambert W i_mp = _lambertw_i_from_v(v_mp, **params) @@ -834,8 +1074,9 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # create ivcurve if ivcurve_pnts: - ivcurve_v = (np.asarray(v_oc)[..., np.newaxis] * - np.linspace(0, 1, ivcurve_pnts)) + ivcurve_v = np.asarray(v_oc)[..., np.newaxis] * np.linspace( + 0, 1, ivcurve_pnts + ) ivcurve_i = _lambertw_i_from_v(ivcurve_v.T, **params).T @@ -845,13 +1086,17 @@ def _lambertw(photocurrent, saturation_current, resistance_series, def _pwr_optfcn(df, loc): - ''' + """ Function to find power from ``i_from_v``. - ''' + """ - current = _lambertw_i_from_v(df[loc], df['photocurrent'], - df['saturation_current'], - df['resistance_series'], - df['resistance_shunt'], df['nNsVth']) + current = _lambertw_i_from_v( + df[loc], + df["photocurrent"], + df["saturation_current"], + df["resistance_series"], + df["resistance_shunt"], + df["nNsVth"], + ) return current * df[loc] diff --git a/pvlib/snow.py b/pvlib/snow.py index 190fe7baae..9126967cf0 100644 --- a/pvlib/snow.py +++ b/pvlib/snow.py @@ -13,8 +13,8 @@ def _time_delta_in_hours(times): return delta.dt.total_seconds().div(3600) -def fully_covered_nrel(snowfall, threshold_snowfall=1.): - ''' +def fully_covered_nrel(snowfall, threshold_snowfall=1.0): + """ Calculates the timesteps when the row's slant height is fully covered by snow. @@ -45,24 +45,31 @@ def fully_covered_nrel(snowfall, threshold_snowfall=1.): .. [2] Ryberg, D; Freeman, J. "Integration, Validation, and Application of a PV Snow Coverage Model in SAM" (2017) NREL Technical Report NREL/TP-6A20-68705 - ''' + """ timestep = _time_delta_in_hours(snowfall.index) hourly_snow_rate = snowfall / timestep # if we can infer a time frequency, use first snowfall value # otherwise the first snowfall value is ignored freq = pd.infer_freq(snowfall.index) if freq is not None: - timedelta = pd.tseries.frequencies.to_offset(freq) / pd.Timedelta('1h') + timedelta = pd.tseries.frequencies.to_offset(freq) / pd.Timedelta("1h") hourly_snow_rate.iloc[0] = snowfall.iloc[0] / timedelta else: # can't infer frequency from index hourly_snow_rate.iloc[0] = 0 # replaces NaN return hourly_snow_rate > threshold_snowfall -def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, - initial_coverage=0, threshold_snowfall=1., - can_slide_coefficient=-80., slide_amount_coefficient=0.197): - ''' +def coverage_nrel( + snowfall, + poa_irradiance, + temp_air, + surface_tilt, + initial_coverage=0, + threshold_snowfall=1.0, + can_slide_coefficient=-80.0, + slide_amount_coefficient=0.197, +): + """ Calculates the fraction of the slant height of a row of modules covered by snow at every time step. @@ -114,7 +121,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, .. [2] Ryberg, D; Freeman, J. (2017). "Integration, Validation, and Application of a PV Snow Coverage Model in SAM" NREL Technical Report NREL/TP-6A20-68705 - ''' + """ # find times with new snowfall new_snowfall = fully_covered_nrel(snowfall, threshold_snowfall) @@ -124,11 +131,14 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, # determine amount that snow can slide in each timestep can_slide = temp_air > poa_irradiance / can_slide_coefficient - slide_amt = slide_amount_coefficient * sind(surface_tilt) * \ - _time_delta_in_hours(poa_irradiance.index) - slide_amt[~can_slide] = 0. + slide_amt = ( + slide_amount_coefficient + * sind(surface_tilt) + * _time_delta_in_hours(poa_irradiance.index) + ) + slide_amt[~can_slide] = 0.0 # don't slide during snow events - slide_amt[new_snowfall] = 0. + slide_amt[new_snowfall] = 0.0 # don't slide in the interval preceding the snowfall data slide_amt.iloc[0] = 0 @@ -148,7 +158,7 @@ def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, def dc_loss_nrel(snow_coverage, num_strings): - ''' + """ Calculates the fraction of DC capacity lost due to snow coverage. DC capacity loss assumes that if a string is partially covered by snow, @@ -183,12 +193,12 @@ def dc_loss_nrel(snow_coverage, num_strings): .. [1] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical Reference Update", NREL Technical Report NREL/TP-6A20-67399. Available at https://www.nrel.gov/docs/fy18osti/67399.pdf - ''' + """ return np.ceil(snow_coverage * num_strings) / num_strings def _townsend_effective_snow(snow_total, snow_events): - ''' + """ Calculates effective snow using the total snowfall received each month and the number of snowfall events each month. @@ -211,16 +221,25 @@ def _townsend_effective_snow(snow_total, snow_events): update from two winters of measurements in the SIERRA. 37th IEEE Photovoltaic Specialists Conference, Seattle, WA, USA. :doi:`10.1109/PVSC.2011.6186627` - ''' + """ snow_events_no_zeros = np.maximum(snow_events, 1) effective_snow = 0.5 * snow_total * (1 + 1 / snow_events_no_zeros) return np.where(snow_events > 0, effective_snow, 0) -def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, - temp_air, poa_global, slant_height, lower_edge_height, - string_factor=1.0, angle_of_repose=40): - ''' +def loss_townsend( + snow_total, + snow_events, + surface_tilt, + relative_humidity, + temp_air, + poa_global, + slant_height, + lower_edge_height, + string_factor=1.0, + angle_of_repose=40, +): + """ Calculates monthly snow loss based on the Townsend monthly snow loss model. @@ -288,13 +307,13 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, update from two winters of measurements in the SIERRA. 37th IEEE Photovoltaic Specialists Conference, Seattle, WA, USA. :doi:`10.1109/PVSC.2011.6186627` - ''' + """ # unit conversions from cm and m to in, from C to K, and from % to fraction # doing this early to facilitate comparison of this code with [1] snow_total_inches = snow_total / 2.54 # to inches - relative_humidity_fraction = relative_humidity / 100. - poa_global_kWh = poa_global / 1000. + relative_humidity_fraction = relative_humidity / 100.0 + poa_global_kWh = poa_global / 1000.0 slant_height_inches = slant_height * 39.37 lower_edge_height_inches = lower_edge_height * 39.37 temp_air_kelvin = temp_air + 273.15 @@ -307,19 +326,19 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, effective_snow = _townsend_effective_snow(snow_total_inches, snow_events) effective_snow_prev = _townsend_effective_snow( - snow_total_prev, - snow_events_prev + snow_total_prev, snow_events_prev ) effective_snow_weighted = ( - 1 / 3 * effective_snow_prev - + 2 / 3 * effective_snow + 1 / 3 * effective_snow_prev + 2 / 3 * effective_snow ) # the lower limit of 0.1 in^2 is per private communication with the model's # author. CWH 1/30/2023 lower_edge_distance = np.clip( - lower_edge_height_inches**2 - effective_snow_weighted**2, a_min=0.1, - a_max=None) + lower_edge_height_inches**2 - effective_snow_weighted**2, + a_min=0.1, + a_max=None, + ) gamma = ( slant_height_inches * effective_snow_weighted @@ -340,7 +359,7 @@ def loss_townsend(snow_total, snow_events, surface_tilt, relative_humidity, loss_fraction = ( C1 * effective_snow_weighted - * cosd(surface_tilt)**2 + * cosd(surface_tilt) ** 2 * ground_interference_term * relative_humidity_fraction / temp_air_kelvin**2 diff --git a/pvlib/soiling.py b/pvlib/soiling.py index 6f81d76f0e..a072be46a4 100644 --- a/pvlib/soiling.py +++ b/pvlib/soiling.py @@ -10,8 +10,15 @@ from pvlib.tools import cosd -def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10, - depo_veloc=None, rain_accum_period=pd.Timedelta('1h')): +def hsu( + rainfall, + cleaning_threshold, + surface_tilt, + pm2_5, + pm10, + depo_veloc=None, + rain_accum_period=pd.Timedelta("1h"), +): """ Calculates soiling ratio given particulate and rain data using the Fixed Velocity model from Humboldt State University (HSU). @@ -65,10 +72,10 @@ def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10, """ # never use mutable input arguments if depo_veloc is None: - depo_veloc = {'2_5': 0.0009, '10': 0.004} + depo_veloc = {"2_5": 0.0009, "10": 0.004} # accumulate rainfall into periods for comparison with threshold - accum_rain = rainfall.rolling(rain_accum_period, closed='right').sum() + accum_rain = rainfall.rolling(rain_accum_period, closed="right").sum() # cleaning is True for intervals with rainfall greater than threshold cleaning_times = accum_rain.index[accum_rain >= cleaning_threshold] @@ -78,11 +85,12 @@ def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10, dt_diff = (dt[1:] - dt[:-1]).total_seconds() # ensure same number of elements in the array, assuming that the interval # prior to the first value is equal in length to the first interval - dt_sec = np.append(dt_diff[0], dt_diff).astype('float64') + dt_sec = np.append(dt_diff[0], dt_diff).astype("float64") horiz_mass_rate = ( - pm2_5 * depo_veloc['2_5'] + np.maximum(pm10 - pm2_5, 0.) - * depo_veloc['10']) * dt_sec + pm2_5 * depo_veloc["2_5"] + + np.maximum(pm10 - pm2_5, 0.0) * depo_veloc["10"] + ) * dt_sec tilted_mass_rate = horiz_mass_rate * cosd(surface_tilt) # assuming no rain # tms -> tilt_mass_rate @@ -90,8 +98,8 @@ def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10, mass_no_cleaning = pd.Series(index=rainfall.index, data=tms_cumsum) # specify dtype so pandas doesn't assume object - mass_removed = pd.Series(index=rainfall.index, dtype='float64') - mass_removed.iloc[0] = 0. + mass_removed = pd.Series(index=rainfall.index, dtype="float64") + mass_removed.iloc[0] = 0.0 mass_removed[cleaning_times] = mass_no_cleaning[cleaning_times] accum_mass = mass_no_cleaning - mass_removed.ffill() @@ -100,9 +108,16 @@ def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10, return soiling_ratio -def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015, - grace_period=14, max_soiling=0.3, manual_wash_dates=None, - initial_soiling=0, rain_accum_period=24): +def kimber( + rainfall, + cleaning_threshold=6, + soiling_loss_rate=0.0015, + grace_period=14, + max_soiling=0.3, + manual_wash_dates=None, + initial_soiling=0, + rain_accum_period=24, +): """ Calculates fraction of energy lost due to soiling given rainfall data and daily loss rate using the Kimber model. @@ -178,27 +193,28 @@ def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015, # get indices as numpy datetime64, calculate timestep as numpy timedelta64, # and convert timestep to fraction of days rain_index_vals = rainfall.index.values - timestep_interval = (rain_index_vals[1] - rain_index_vals[0]) - day_fraction = timestep_interval / np.timedelta64(24, 'h') + timestep_interval = rain_index_vals[1] - rain_index_vals[0] + day_fraction = timestep_interval / np.timedelta64(24, "h") # accumulate rainfall accumulated_rainfall = rainfall.rolling( - rain_accum_period, closed='right').sum() + rain_accum_period, closed="right" + ).sum() # soiling rate soiling = np.ones_like(rainfall.values) * soiling_loss_rate * day_fraction soiling[0] = initial_soiling soiling = np.cumsum(soiling) - soiling = pd.Series(soiling, index=rainfall.index, name='soiling') + soiling = pd.Series(soiling, index=rainfall.index, name="soiling") # rainfall events that clean the panels rain_events = accumulated_rainfall > cleaning_threshold # grace periods windows during which ground is assumed damp, so no soiling - grace_windows = rain_events.rolling(grace_period, closed='right').sum() > 0 + grace_windows = rain_events.rolling(grace_period, closed="right").sum() > 0 # clean panels by subtracting soiling for indices in grace period windows - cleaning = pd.Series(float('NaN'), index=rainfall.index) + cleaning = pd.Series(float("NaN"), index=rainfall.index) cleaning.iloc[0] = 0.0 cleaning[grace_windows] = soiling[grace_windows] diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py index b667ac04e0..e19b423b54 100644 --- a/pvlib/solarposition.py +++ b/pvlib/solarposition.py @@ -8,29 +8,30 @@ # Tony Lorenzo (@alorenzo175), University of Arizona, 2015 # Cliff hansen (@cwhanse), Sandia National Laboratories, 2018 -import os import datetime as dt -try: - from importlib import reload -except ImportError: - try: - from imp import reload - except ImportError: - pass +import os +import warnings +from importlib import reload +import ephem import numpy as np import pandas as pd import scipy.optimize as so -import warnings from pvlib import atmosphere, tools from pvlib.tools import datetime_to_djd, djd_to_datetime -def get_solarposition(time, latitude, longitude, - altitude=None, pressure=None, - method='nrel_numpy', - temperature=12.0, **kwargs): +def get_solarposition( + time, + latitude, + longitude, + altitude=None, + pressure=None, + method="nrel_numpy", + temperature=12.0, + **kwargs, +): """ A convenience wrapper for the solar position calculators. @@ -89,8 +90,8 @@ def get_solarposition(time, latitude, longitude, """ if altitude is None and pressure is None: - altitude = 0. - pressure = 101325. + altitude = 0.0 + pressure = 101325.0 elif altitude is None: altitude = atmosphere.pres2alt(pressure) elif pressure is None: @@ -98,36 +99,68 @@ def get_solarposition(time, latitude, longitude, method = method.lower() if isinstance(time, dt.datetime): - time = pd.DatetimeIndex([time, ]) - - if method == 'nrel_c': - ephem_df = spa_c(time, latitude, longitude, pressure, temperature, - **kwargs) - elif method == 'nrel_numba': - ephem_df = spa_python(time, latitude, longitude, altitude, - pressure, temperature, - how='numba', **kwargs) - elif method == 'nrel_numpy': - ephem_df = spa_python(time, latitude, longitude, altitude, - pressure, temperature, - how='numpy', **kwargs) - elif method == 'pyephem': - ephem_df = pyephem(time, latitude, longitude, - altitude=altitude, - pressure=pressure, - temperature=temperature, **kwargs) - elif method == 'ephemeris': - ephem_df = ephemeris(time, latitude, longitude, pressure, temperature, - **kwargs) + time = pd.DatetimeIndex( + [ + time, + ] + ) + + if method == "nrel_c": + ephem_df = spa_c( + time, latitude, longitude, pressure, temperature, **kwargs + ) + elif method == "nrel_numba": + ephem_df = spa_python( + time, + latitude, + longitude, + altitude, + pressure, + temperature, + how="numba", + **kwargs, + ) + elif method == "nrel_numpy": + ephem_df = spa_python( + time, + latitude, + longitude, + altitude, + pressure, + temperature, + how="numpy", + **kwargs, + ) + elif method == "pyephem": + ephem_df = pyephem( + time, + latitude, + longitude, + altitude=altitude, + pressure=pressure, + temperature=temperature, + **kwargs, + ) + elif method == "ephemeris": + ephem_df = ephemeris( + time, latitude, longitude, pressure, temperature, **kwargs + ) else: - raise ValueError('Invalid solar position method') + raise ValueError("Invalid solar position method") return ephem_df -def spa_c(time, latitude, longitude, pressure=101325., altitude=0., - temperature=12., delta_t=67.0, - raw_spa_output=False): +def spa_c( + time, + latitude, + longitude, + pressure=101325.0, + altitude=0.0, + temperature=12.0, + delta_t=67.0, + raw_spa_output=False, +): r""" Calculate the solar position using the C implementation of the NREL SPA code. @@ -195,42 +228,51 @@ def spa_c(time, latitude, longitude, pressure=101325., altitude=0., try: from pvlib.spa_c_files.spa_py import spa_calc - except ImportError: - raise ImportError('Could not import built-in SPA calculator. ' + - 'You may need to recompile the SPA code.') + except ModuleNotFoundError as e: + raise ModuleNotFoundError( + "Could not import built-in SPA calculator. " + "You may need to recompile the SPA code." + ) from e time_utc = tools._pandas_to_utc(time) spa_out = [] for date in time_utc: - spa_out.append(spa_calc(year=date.year, - month=date.month, - day=date.day, - hour=date.hour, - minute=date.minute, - second=date.second, - time_zone=0, # date uses utc time - latitude=latitude, - longitude=longitude, - elevation=altitude, - pressure=pressure / 100, - temperature=temperature, - delta_t=delta_t - )) + spa_out.append( + spa_calc( + year=date.year, + month=date.month, + day=date.day, + hour=date.hour, + minute=date.minute, + second=date.second, + time_zone=0, # date uses utc time + latitude=latitude, + longitude=longitude, + elevation=altitude, + pressure=pressure / 100, + temperature=temperature, + delta_t=delta_t, + ) + ) spa_df = pd.DataFrame(spa_out, index=time) if raw_spa_output: # rename "time_zone" from raw output from spa_c_files.spa_py.spa_calc() # to "timezone" to match the API of pvlib.solarposition.spa_c() - return spa_df.rename(columns={'time_zone': 'timezone'}) + return spa_df.rename(columns={"time_zone": "timezone"}) else: - dfout = pd.DataFrame({'azimuth': spa_df['azimuth'], - 'apparent_zenith': spa_df['zenith'], - 'apparent_elevation': spa_df['e'], - 'elevation': spa_df['e0'], - 'zenith': 90 - spa_df['e0']}) + dfout = pd.DataFrame( + { + "azimuth": spa_df["azimuth"], + "apparent_zenith": spa_df["zenith"], + "apparent_elevation": spa_df["e"], + "elevation": spa_df["e0"], + "zenith": 90 - spa_df["e0"], + } + ) return dfout @@ -243,23 +285,23 @@ def _spa_python_import(how): # check to see if the spa module was compiled with numba using_numba = spa.USE_NUMBA - if how == 'numpy' and using_numba: + if how == "numpy" and using_numba: # the spa module was compiled to numba code, so we need to # reload the module without compiling # the PVLIB_USE_NUMBA env variable is used to tell the module # to not compile with numba - warnings.warn('Reloading spa to use numpy') - os.environ['PVLIB_USE_NUMBA'] = '0' + warnings.warn("Reloading spa to use numpy") + os.environ["PVLIB_USE_NUMBA"] = "0" spa = reload(spa) - del os.environ['PVLIB_USE_NUMBA'] - elif how == 'numba' and not using_numba: + del os.environ["PVLIB_USE_NUMBA"] + elif how == "numba" and not using_numba: # The spa module was not compiled to numba code, so set # PVLIB_USE_NUMBA so it does compile to numba on reload. - warnings.warn('Reloading spa to use numba') - os.environ['PVLIB_USE_NUMBA'] = '1' + warnings.warn("Reloading spa to use numba") + os.environ["PVLIB_USE_NUMBA"] = "1" spa = reload(spa) - del os.environ['PVLIB_USE_NUMBA'] - elif how != 'numba' and how != 'numpy': + del os.environ["PVLIB_USE_NUMBA"] + elif how != "numba" and how != "numpy": raise ValueError("how must be either 'numba' or 'numpy'") return spa @@ -278,9 +320,18 @@ def _datetime_to_unixtime(dtindex): return np.array((dtindex - epoch) / pd.Timedelta("1s")) -def spa_python(time, latitude, longitude, - altitude=0., pressure=101325., temperature=12., delta_t=67.0, - atmos_refract=None, how='numpy', numthreads=4): +def spa_python( + time, + latitude, + longitude, + altitude=0.0, + pressure=101325.0, + temperature=12.0, + delta_t=67.0, + atmos_refract=None, + how="numpy", + numthreads=4, +): """ Calculate the solar position using a python implementation of the NREL SPA algorithm. @@ -366,7 +417,11 @@ def spa_python(time, latitude, longitude, try: time = pd.DatetimeIndex(time) except (TypeError, ValueError): - time = pd.DatetimeIndex([time, ]) + time = pd.DatetimeIndex( + [ + time, + ] + ) unixtime = _datetime_to_unixtime(time) @@ -376,21 +431,38 @@ def spa_python(time, latitude, longitude, time_utc = tools._pandas_to_utc(time) delta_t = spa.calculate_deltat(time_utc.year, time_utc.month) - app_zenith, zenith, app_elevation, elevation, azimuth, eot = \ - spa.solar_position(unixtime, lat, lon, elev, pressure, temperature, - delta_t, atmos_refract, numthreads) + app_zenith, zenith, app_elevation, elevation, azimuth, eot = ( + spa.solar_position( + unixtime, + lat, + lon, + elev, + pressure, + temperature, + delta_t, + atmos_refract, + numthreads, + ) + ) - result = pd.DataFrame({'apparent_zenith': app_zenith, 'zenith': zenith, - 'apparent_elevation': app_elevation, - 'elevation': elevation, 'azimuth': azimuth, - 'equation_of_time': eot}, - index=time) + result = pd.DataFrame( + { + "apparent_zenith": app_zenith, + "zenith": zenith, + "apparent_elevation": app_elevation, + "elevation": elevation, + "azimuth": azimuth, + "equation_of_time": eot, + }, + index=time, + ) return result -def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy', - delta_t=67.0, numthreads=4): +def sun_rise_set_transit_spa( + times, latitude, longitude, how="numpy", delta_t=67.0, numthreads=4 +): """ Calculate the sunrise, sunset, and sun transit times using the NREL SPA algorithm. @@ -443,10 +515,10 @@ def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy', if times.tz: tzinfo = times.tz else: - raise ValueError('times must be localized') + raise ValueError("times must be localized") # must convert to midnight UTC on day of interest - times_utc = times.tz_convert('UTC') + times_utc = times.tz_convert("UTC") unixtime = _datetime_to_unixtime(times_utc.normalize()) spa = _spa_python_import(how) @@ -455,19 +527,30 @@ def sun_rise_set_transit_spa(times, latitude, longitude, how='numpy', delta_t = spa.calculate_deltat(times_utc.year, times_utc.month) transit, sunrise, sunset = spa.transit_sunrise_sunset( - unixtime, lat, lon, delta_t, numthreads) + unixtime, lat, lon, delta_t, numthreads + ) # arrays are in seconds since epoch format, need to conver to timestamps - transit = pd.to_datetime(transit*1e9, unit='ns', utc=True).tz_convert( - tzinfo).tolist() - sunrise = pd.to_datetime(sunrise*1e9, unit='ns', utc=True).tz_convert( - tzinfo).tolist() - sunset = pd.to_datetime(sunset*1e9, unit='ns', utc=True).tz_convert( - tzinfo).tolist() + transit = ( + pd.to_datetime(transit * 1e9, unit="ns", utc=True) + .tz_convert(tzinfo) + .tolist() + ) + sunrise = ( + pd.to_datetime(sunrise * 1e9, unit="ns", utc=True) + .tz_convert(tzinfo) + .tolist() + ) + sunset = ( + pd.to_datetime(sunset * 1e9, unit="ns", utc=True) + .tz_convert(tzinfo) + .tolist() + ) - return pd.DataFrame(index=times, data={'sunrise': sunrise, - 'sunset': sunset, - 'transit': transit}) + return pd.DataFrame( + index=times, + data={"sunrise": sunrise, "sunset": sunset, "transit": transit}, + ) def _ephem_convert_to_seconds_and_microseconds(date): @@ -481,22 +564,22 @@ def _ephem_convert_to_seconds_and_microseconds(date): def _ephem_to_timezone(date, tzinfo): # utility from unreleased PyEphem 3.6.7.1 - """"Convert a PyEphem Date into a timezone aware python datetime""" + """ "Convert a PyEphem Date into a timezone aware python datetime""" seconds, microseconds = _ephem_convert_to_seconds_and_microseconds(date) date = dt.datetime.fromtimestamp(seconds, tzinfo) date = date.replace(microsecond=microseconds) return date -def _ephem_setup(latitude, longitude, altitude, pressure, temperature, - horizon): - import ephem +def _ephem_setup( + latitude, longitude, altitude, pressure, temperature, horizon +): # initialize a PyEphem observer obs = ephem.Observer() obs.lat = str(latitude) obs.lon = str(longitude) obs.elevation = altitude - obs.pressure = pressure / 100. # convert to mBar + obs.pressure = pressure / 100.0 # convert to mBar obs.temp = temperature obs.horizon = horizon @@ -505,11 +588,16 @@ def _ephem_setup(latitude, longitude, altitude, pressure, temperature, return obs, sun -def sun_rise_set_transit_ephem(times, latitude, longitude, - next_or_previous='next', - altitude=0., - pressure=101325., - temperature=12., horizon='0:00'): +def sun_rise_set_transit_ephem( + times, + latitude, + longitude, + next_or_previous="next", + altitude=0.0, + pressure=101325.0, + temperature=12.0, + horizon="0:00", +): """ Calculate the next sunrise and sunset times using the PyEphem package. @@ -547,31 +635,28 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, pyephem """ - try: - import ephem - except ImportError: - raise ImportError('PyEphem must be installed') - # times must be localized if times.tz: tzinfo = times.tz else: - raise ValueError('times must be localized') + raise ValueError("times must be localized") - obs, sun = _ephem_setup(latitude, longitude, altitude, - pressure, temperature, horizon) + obs, sun = _ephem_setup( + latitude, longitude, altitude, pressure, temperature, horizon + ) # create lists of sunrise and sunset time localized to time.tz - if next_or_previous.lower() == 'next': + if next_or_previous.lower() == "next": rising = obs.next_rising setting = obs.next_setting transit = obs.next_transit - elif next_or_previous.lower() == 'previous': + elif next_or_previous.lower() == "previous": rising = obs.previous_rising setting = obs.previous_setting transit = obs.previous_transit else: - raise ValueError("next_or_previous must be either 'next' or" + - " 'previous'") + raise ValueError( + "next_or_previous must be either 'next' or" + " 'previous'" + ) sunrise = [] sunset = [] @@ -585,13 +670,21 @@ def sun_rise_set_transit_ephem(times, latitude, longitude, sunset.append(_ephem_to_timezone(setting(sun), tzinfo)) trans.append(_ephem_to_timezone(transit(sun), tzinfo)) - return pd.DataFrame(index=times, data={'sunrise': sunrise, - 'sunset': sunset, - 'transit': trans}) + return pd.DataFrame( + index=times, + data={"sunrise": sunrise, "sunset": sunset, "transit": trans}, + ) -def pyephem(time, latitude, longitude, altitude=0., pressure=101325., - temperature=12., horizon='+0:00'): +def pyephem( + time, + latitude, + longitude, + altitude=0.0, + pressure=101325.0, + temperature=12.0, + horizon="+0:00", +): """ Calculate the solar position using the PyEphem package. @@ -632,18 +725,13 @@ def pyephem(time, latitude, longitude, altitude=0., pressure=101325., spa_python, spa_c, ephemeris """ - # Written by Will Holmgren (@wholmgren), University of Arizona, 2014 - try: - import ephem - except ImportError: - raise ImportError('PyEphem must be installed') - time_utc = tools._pandas_to_utc(time) sun_coords = pd.DataFrame(index=time) - obs, sun = _ephem_setup(latitude, longitude, altitude, - pressure, temperature, horizon) + obs, sun = _ephem_setup( + latitude, longitude, altitude, pressure, temperature, horizon + ) # make and fill lists of the sun's altitude and azimuth # this is the pressure and temperature corrected apparent alt/az. @@ -655,8 +743,8 @@ def pyephem(time, latitude, longitude, altitude=0., pressure=101325., alts.append(sun.alt) azis.append(sun.az) - sun_coords['apparent_elevation'] = alts - sun_coords['apparent_azimuth'] = azis + sun_coords["apparent_elevation"] = alts + sun_coords["apparent_azimuth"] = azis # redo it for p=0 to get no atmosphere alt/az obs.pressure = 0 @@ -668,13 +756,13 @@ def pyephem(time, latitude, longitude, altitude=0., pressure=101325., alts.append(sun.alt) azis.append(sun.az) - sun_coords['elevation'] = alts - sun_coords['azimuth'] = azis + sun_coords["elevation"] = alts + sun_coords["azimuth"] = azis # convert to degrees. add zenith sun_coords = np.rad2deg(sun_coords) - sun_coords['apparent_zenith'] = 90 - sun_coords['apparent_elevation'] - sun_coords['zenith'] = 90 - sun_coords['elevation'] + sun_coords["apparent_zenith"] = 90 - sun_coords["apparent_elevation"] + sun_coords["zenith"] = 90 - sun_coords["elevation"] return sun_coords @@ -749,7 +837,7 @@ def ephemeris(time, latitude, longitude, pressure=101325.0, temperature=12.0): Latitude = latitude Longitude = -1 * longitude - Abber = 20 / 3600. + Abber = 20 / 3600.0 LatR = np.radians(Latitude) # the SPA algorithm needs time to be expressed in terms of @@ -759,105 +847,153 @@ def ephemeris(time, latitude, longitude, pressure=101325.0, temperature=12.0): # strip out the day of the year and calculate the decimal hour DayOfYear = time_utc.dayofyear - DecHours = (time_utc.hour + time_utc.minute/60. + time_utc.second/3600. + - time_utc.microsecond/3600.e6) + DecHours = ( + time_utc.hour + + time_utc.minute / 60.0 + + time_utc.second / 3600.0 + + time_utc.microsecond / 3600.0e6 + ) # np.array needed for pandas > 0.20 UnivDate = np.array(DayOfYear) UnivHr = np.array(DecHours) Yr = np.array(time_utc.year) - 1900 - YrBegin = 365 * Yr + np.floor((Yr - 1) / 4.) - 0.5 + YrBegin = 365 * Yr + np.floor((Yr - 1) / 4.0) - 0.5 Ezero = YrBegin + UnivDate - T = Ezero / 36525. + T = Ezero / 36525.0 # Calculate Greenwich Mean Sidereal Time (GMST) - GMST0 = 6 / 24. + 38 / 1440. + ( - 45.836 + 8640184.542 * T + 0.0929 * T ** 2) / 86400. + GMST0 = ( + 6 / 24.0 + + 38 / 1440.0 + + (45.836 + 8640184.542 * T + 0.0929 * T**2) / 86400.0 + ) GMST0 = 360 * (GMST0 - np.floor(GMST0)) - GMSTi = np.mod(GMST0 + 360 * (1.0027379093 * UnivHr / 24.), 360) + GMSTi = np.mod(GMST0 + 360 * (1.0027379093 * UnivHr / 24.0), 360) # Local apparent sidereal time LocAST = np.mod((360 + GMSTi - Longitude), 360) - EpochDate = Ezero + UnivHr / 24. - T1 = EpochDate / 36525. + EpochDate = Ezero + UnivHr / 24.0 + T1 = EpochDate / 36525.0 ObliquityR = np.radians( - 23.452294 - 0.0130125 * T1 - 1.64e-06 * T1 ** 2 + 5.03e-07 * T1 ** 3) - MlPerigee = 281.22083 + 4.70684e-05 * EpochDate + 0.000453 * T1 ** 2 + ( - 3e-06 * T1 ** 3) - MeanAnom = np.mod((358.47583 + 0.985600267 * EpochDate - 0.00015 * - T1 ** 2 - 3e-06 * T1 ** 3), 360) - Eccen = 0.01675104 - 4.18e-05 * T1 - 1.26e-07 * T1 ** 2 + 23.452294 - 0.0130125 * T1 - 1.64e-06 * T1**2 + 5.03e-07 * T1**3 + ) + MlPerigee = ( + 281.22083 + + 4.70684e-05 * EpochDate + + 0.000453 * T1**2 + + (3e-06 * T1**3) + ) + MeanAnom = np.mod( + ( + 358.47583 + + 0.985600267 * EpochDate + - 0.00015 * T1**2 + - 3e-06 * T1**3 + ), + 360, + ) + Eccen = 0.01675104 - 4.18e-05 * T1 - 1.26e-07 * T1**2 EccenAnom = MeanAnom E = 0 while np.max(abs(EccenAnom - E)) > 0.0001: E = EccenAnom - EccenAnom = MeanAnom + np.degrees(Eccen)*np.sin(np.radians(E)) - - TrueAnom = ( - 2 * np.mod(np.degrees(np.arctan2(((1 + Eccen) / (1 - Eccen)) ** 0.5 * - np.tan(np.radians(EccenAnom) / 2.), 1)), 360)) + EccenAnom = MeanAnom + np.degrees(Eccen) * np.sin(np.radians(E)) + + TrueAnom = 2 * np.mod( + np.degrees( + np.arctan2( + ((1 + Eccen) / (1 - Eccen)) ** 0.5 + * np.tan(np.radians(EccenAnom) / 2.0), + 1, + ) + ), + 360, + ) EcLon = np.mod(MlPerigee + TrueAnom, 360) - Abber EcLonR = np.radians(EcLon) - DecR = np.arcsin(np.sin(ObliquityR)*np.sin(EcLonR)) + DecR = np.arcsin(np.sin(ObliquityR) * np.sin(EcLonR)) - RtAscen = np.degrees(np.arctan2(np.cos(ObliquityR)*np.sin(EcLonR), - np.cos(EcLonR))) + RtAscen = np.degrees( + np.arctan2(np.cos(ObliquityR) * np.sin(EcLonR), np.cos(EcLonR)) + ) HrAngle = LocAST - RtAscen HrAngleR = np.radians(HrAngle) HrAngle = HrAngle - (360 * (abs(HrAngle) > 180)) - SunAz = np.degrees(np.arctan2(-np.sin(HrAngleR), - np.cos(LatR)*np.tan(DecR) - - np.sin(LatR)*np.cos(HrAngleR))) + SunAz = np.degrees( + np.arctan2( + -np.sin(HrAngleR), + np.cos(LatR) * np.tan(DecR) - np.sin(LatR) * np.cos(HrAngleR), + ) + ) SunAz[SunAz < 0] += 360 - SunEl = np.degrees(np.arcsin( - np.cos(LatR) * np.cos(DecR) * np.cos(HrAngleR) + - np.sin(LatR) * np.sin(DecR))) + SunEl = np.degrees( + np.arcsin( + np.cos(LatR) * np.cos(DecR) * np.cos(HrAngleR) + + np.sin(LatR) * np.sin(DecR) + ) + ) - SolarTime = (180 + HrAngle) / 15. + SolarTime = (180 + HrAngle) / 15.0 # Calculate refraction correction Elevation = SunEl TanEl = pd.Series(np.tan(np.radians(Elevation)), index=time_utc) - Refract = pd.Series(0., index=time_utc) + Refract = pd.Series(0.0, index=time_utc) Refract[(Elevation > 5) & (Elevation <= 85)] = ( - 58.1/TanEl - 0.07/(TanEl**3) + 8.6e-05/(TanEl**5)) + 58.1 / TanEl - 0.07 / (TanEl**3) + 8.6e-05 / (TanEl**5) + ) Refract[(Elevation > -0.575) & (Elevation <= 5)] = ( - Elevation * - (-518.2 + Elevation*(103.4 + Elevation*(-12.79 + Elevation*0.711))) + - 1735) + Elevation + * ( + -518.2 + + Elevation * (103.4 + Elevation * (-12.79 + Elevation * 0.711)) + ) + + 1735 + ) Refract[(Elevation > -1) & (Elevation <= -0.575)] = -20.774 / TanEl - Refract *= (283/(273. + temperature)) * (pressure/101325.) / 3600. + Refract *= (283 / (273.0 + temperature)) * (pressure / 101325.0) / 3600.0 ApparentSunEl = SunEl + Refract # make output DataFrame DFOut = pd.DataFrame(index=time_utc) - DFOut['apparent_elevation'] = ApparentSunEl - DFOut['elevation'] = SunEl - DFOut['azimuth'] = SunAz - DFOut['apparent_zenith'] = 90 - ApparentSunEl - DFOut['zenith'] = 90 - SunEl - DFOut['solar_time'] = SolarTime + DFOut["apparent_elevation"] = ApparentSunEl + DFOut["elevation"] = SunEl + DFOut["azimuth"] = SunAz + DFOut["apparent_zenith"] = 90 - ApparentSunEl + DFOut["zenith"] = 90 - SunEl + DFOut["solar_time"] = SolarTime DFOut.index = time return DFOut -def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, - altitude=0.0, pressure=101325.0, temperature=12.0, - horizon='+0:00', xtol=1.0e-12): +def calc_time( + lower_bound, + upper_bound, + latitude, + longitude, + attribute, + value, + altitude=0.0, + pressure=101325.0, + temperature=12.0, + horizon="+0:00", + xtol=1.0e-12, +): """ Calculate the time between lower_bound and upper_bound where the attribute is equal to value. Uses PyEphem for @@ -907,8 +1043,9 @@ def calc_time(lower_bound, upper_bound, latitude, longitude, attribute, value, If the given attribute is not an attribute of a PyEphem.Sun object. """ - obs, sun = _ephem_setup(latitude, longitude, altitude, - pressure, temperature, horizon) + obs, sun = _ephem_setup( + latitude, longitude, altitude, pressure, temperature, horizon + ) def compute_attr(thetime, target, attr): obs.date = thetime @@ -918,8 +1055,7 @@ def compute_attr(thetime, target, attr): lb = datetime_to_djd(lower_bound) ub = datetime_to_djd(upper_bound) - djd_root = so.brentq(compute_attr, lb, ub, - (value, attribute), xtol=xtol) + djd_root = so.brentq(compute_attr, lb, ub, (value, attribute), xtol=xtol) return djd_to_datetime(djd_root) @@ -938,8 +1074,6 @@ def pyephem_earthsun_distance(time): pd.Series. Earth-sun distance in AU. """ - import ephem - sun = ephem.Sun() earthsun = [] for thetime in tools._pandas_to_utc(time): @@ -952,7 +1086,7 @@ def pyephem_earthsun_distance(time): return pd.Series(earthsun, index=time) -def nrel_earthsun_distance(time, how='numpy', delta_t=67.0, numthreads=4): +def nrel_earthsun_distance(time, how="numpy", delta_t=67.0, numthreads=4): """ Calculates the distance from the earth to the sun using the NREL SPA algorithm. @@ -994,7 +1128,11 @@ def nrel_earthsun_distance(time, how='numpy', delta_t=67.0, numthreads=4): try: time = pd.DatetimeIndex(time) except (TypeError, ValueError): - time = pd.DatetimeIndex([time, ]) + time = pd.DatetimeIndex( + [ + time, + ] + ) unixtime = _datetime_to_unixtime(time) @@ -1025,7 +1163,7 @@ def _calculate_simple_day_angle(dayofyear, offset=1): ------- day_angle : numeric """ - return (2. * np.pi / 365.) * (dayofyear - offset) + return (2.0 * np.pi / 365.0) * (dayofyear - offset) def equation_of_time_spencer71(dayofyear): @@ -1086,9 +1224,11 @@ def equation_of_time_spencer71(dayofyear): day_angle = _calculate_simple_day_angle(dayofyear) # convert from radians to minutes per day = 24[h/day] * 60[min/h] / 2 / pi eot = (1440.0 / 2 / np.pi) * ( - 0.0000075 + - 0.001868 * np.cos(day_angle) - 0.032077 * np.sin(day_angle) - - 0.014615 * np.cos(2.0 * day_angle) - 0.040849 * np.sin(2.0 * day_angle) + 0.0000075 + + 0.001868 * np.cos(day_angle) + - 0.032077 * np.sin(day_angle) + - 0.014615 * np.cos(2.0 * day_angle) + - 0.040849 * np.sin(2.0 * day_angle) ) return eot @@ -1121,8 +1261,9 @@ def equation_of_time_pvcdrom(dayofyear): equation_of_time_spencer71 """ # day angle relative to Vernal Equinox, typically March 22 (day number 81) - bday = \ + bday = ( _calculate_simple_day_angle(dayofyear) - (2.0 * np.pi / 365.0) * 80.0 + ) # same value but about 2x faster than Spencer (1971) return 9.87 * np.sin(2.0 * bday) - 7.53 * np.cos(bday) - 1.5 * np.sin(bday) @@ -1164,10 +1305,13 @@ def declination_spencer71(dayofyear): """ day_angle = _calculate_simple_day_angle(dayofyear) return ( - 0.006918 - - 0.399912 * np.cos(day_angle) + 0.070257 * np.sin(day_angle) - - 0.006758 * np.cos(2. * day_angle) + 0.000907 * np.sin(2. * day_angle) - - 0.002697 * np.cos(3. * day_angle) + 0.00148 * np.sin(3. * day_angle) + 0.006918 + - 0.399912 * np.cos(day_angle) + + 0.070257 * np.sin(day_angle) + - 0.006758 * np.cos(2.0 * day_angle) + + 0.000907 * np.sin(2.0 * day_angle) + - 0.002697 * np.cos(3.0 * day_angle) + + 0.00148 * np.sin(3.0 * day_angle) ) @@ -1261,28 +1405,31 @@ def solar_azimuth_analytical(latitude, hourangle, declination, zenith): solar_zenith_analytical """ - numer = (np.cos(zenith) * np.sin(latitude) - np.sin(declination)) - denom = (np.sin(zenith) * np.cos(latitude)) + numer = np.cos(zenith) * np.sin(latitude) - np.sin(declination) + denom = np.sin(zenith) * np.cos(latitude) # cases that would generate new NaN values are safely ignored here # since they are dealt with further below - with np.errstate(invalid='ignore', divide='ignore'): + with np.errstate(invalid="ignore", divide="ignore"): cos_azi = numer / denom # when zero division occurs, use the limit value of the analytical # expression - cos_azi = \ - np.where(np.isclose(denom, 0.0, rtol=0.0, atol=1e-8), 1.0, cos_azi) + cos_azi = np.where( + np.isclose(denom, 0.0, rtol=0.0, atol=1e-8), 1.0, cos_azi + ) # when too many round-ups in floating point math take cos_azi beyond # 1.0, use 1.0 - cos_azi = \ - np.where(np.isclose(cos_azi, 1.0, rtol=0.0, atol=1e-8), 1.0, cos_azi) - cos_azi = \ - np.where(np.isclose(cos_azi, -1.0, rtol=0.0, atol=1e-8), -1.0, cos_azi) + cos_azi = np.where( + np.isclose(cos_azi, 1.0, rtol=0.0, atol=1e-8), 1.0, cos_azi + ) + cos_azi = np.where( + np.isclose(cos_azi, -1.0, rtol=0.0, atol=1e-8), -1.0, cos_azi + ) # when NaN values occur in input, ignore and pass to output - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): sign_ha = np.sign(hourangle) return sign_ha * np.arccos(cos_azi) + np.pi @@ -1335,8 +1482,8 @@ def solar_zenith_analytical(latitude, hourangle, declination): hour_angle """ return np.arccos( - np.cos(declination) * np.cos(latitude) * np.cos(hourangle) + - np.sin(declination) * np.sin(latitude) + np.cos(declination) * np.cos(latitude) * np.cos(hourangle) + + np.sin(declination) * np.sin(latitude) ) @@ -1384,14 +1531,14 @@ def hour_angle(times, longitude, equation_of_time): # times must be localized if not times.tz: - raise ValueError('times must be localized') + raise ValueError("times must be localized") # hours - timezone = (times - normalized_times) - (naive_times - times) tzs = np.array([ts.utcoffset().total_seconds() for ts in times]) / 3600 hrs_minus_tzs = _times_to_hours_after_local_midnight(times) - tzs - return 15. * (hrs_minus_tzs - 12.) + longitude + equation_of_time / 4. + return 15.0 * (hrs_minus_tzs - 12.0) + longitude + equation_of_time / 4.0 def _hour_angle_to_hours(times, hourangle, longitude, equation_of_time): @@ -1399,10 +1546,12 @@ def _hour_angle_to_hours(times, hourangle, longitude, equation_of_time): # times must be localized if not times.tz: - raise ValueError('times must be localized') + raise ValueError("times must be localized") tzs = np.array([ts.utcoffset().total_seconds() for ts in times]) / 3600 - hours = (hourangle - longitude - equation_of_time / 4.) / 15. + 12. + tzs + hours = ( + (hourangle - longitude - equation_of_time / 4.0) / 15.0 + 12.0 + tzs + ) return np.asarray(hours) @@ -1413,11 +1562,11 @@ def _local_times_from_hours_since_midnight(times, hours): # times must be localized if not times.tz: - raise ValueError('times must be localized') + raise ValueError("times must be localized") # normalize local times to previous local midnight and add the hours until # sunrise, sunset, and transit - return times.normalize() + pd.to_timedelta(hours, unit='h') + return times.normalize() + pd.to_timedelta(hours, unit="h") def _times_to_hours_after_local_midnight(times): @@ -1425,7 +1574,7 @@ def _times_to_hours_after_local_midnight(times): # times must be localized if not times.tz: - raise ValueError('times must be localized') + raise ValueError("times must be localized") # Some timezones have a DST shift at midnight: # 11:59pm -> 1:00am - results in a nonexistent midnight @@ -1435,16 +1584,18 @@ def _times_to_hours_after_local_midnight(times): # Use Pandas functionality for shifting nonexistent times forward normalized_times = naive_normalized_times.tz_localize( - times.tz, nonexistent='shift_forward', ambiguous='raise') + times.tz, nonexistent="shift_forward", ambiguous="raise" + ) - hrs = (times - normalized_times) / pd.Timedelta('1h') + hrs = (times - normalized_times) / pd.Timedelta("1h") # ensure array return instead of a version-dependent pandas Index return np.array(hrs) -def sun_rise_set_transit_geometric(times, latitude, longitude, declination, - equation_of_time): +def sun_rise_set_transit_geometric( + times, latitude, longitude, declination, equation_of_time +): """ Geometric calculation of solar sunrise, sunset, and transit. @@ -1488,7 +1639,7 @@ def sun_rise_set_transit_geometric(times, latitude, longitude, declination, # times must be localized if not times.tz: - raise ValueError('times must be localized') + raise ValueError("times must be localized") latitude_rad = np.radians(latitude) # radians sunset_angle_rad = np.arccos(-np.tan(declination) * np.tan(latitude_rad)) @@ -1497,9 +1648,11 @@ def sun_rise_set_transit_geometric(times, latitude, longitude, declination, # so sunrise is just negative of sunset sunrise_angle = -sunset_angle sunrise_hour = _hour_angle_to_hours( - times, sunrise_angle, longitude, equation_of_time) + times, sunrise_angle, longitude, equation_of_time + ) sunset_hour = _hour_angle_to_hours( - times, sunset_angle, longitude, equation_of_time) + times, sunset_angle, longitude, equation_of_time + ) transit_hour = _hour_angle_to_hours(times, 0, longitude, equation_of_time) sunrise = _local_times_from_hours_since_midnight(times, sunrise_hour) sunset = _local_times_from_hours_since_midnight(times, sunset_hour) diff --git a/pvlib/spa.py b/pvlib/spa.py index d4181aaa49..5ec0981e9b 100644 --- a/pvlib/spa.py +++ b/pvlib/spa.py @@ -19,12 +19,13 @@ def nocompile(*args, **kwargs): return lambda func: func -if os.getenv('PVLIB_USE_NUMBA', '0') != '0': +if os.getenv("PVLIB_USE_NUMBA", "0") != "0": try: from numba import jit except ImportError: - warnings.warn('Could not import numba, falling back to numpy ' + - 'calculation') + warnings.warn( + "Could not import numba, falling back to numpy " + "calculation" + ) jcompile = nocompile USE_NUMBA = False else: @@ -36,382 +37,394 @@ def nocompile(*args, **kwargs): # heliocentric longitude coefficients -L0 = np.array([ - [175347046.0, 0.0, 0.0], - [3341656.0, 4.6692568, 6283.07585], - [34894.0, 4.6261, 12566.1517], - [3497.0, 2.7441, 5753.3849], - [3418.0, 2.8289, 3.5231], - [3136.0, 3.6277, 77713.7715], - [2676.0, 4.4181, 7860.4194], - [2343.0, 6.1352, 3930.2097], - [1324.0, 0.7425, 11506.7698], - [1273.0, 2.0371, 529.691], - [1199.0, 1.1096, 1577.3435], - [990.0, 5.233, 5884.927], - [902.0, 2.045, 26.298], - [857.0, 3.508, 398.149], - [780.0, 1.179, 5223.694], - [753.0, 2.533, 5507.553], - [505.0, 4.583, 18849.228], - [492.0, 4.205, 775.523], - [357.0, 2.92, 0.067], - [317.0, 5.849, 11790.629], - [284.0, 1.899, 796.298], - [271.0, 0.315, 10977.079], - [243.0, 0.345, 5486.778], - [206.0, 4.806, 2544.314], - [205.0, 1.869, 5573.143], - [202.0, 2.458, 6069.777], - [156.0, 0.833, 213.299], - [132.0, 3.411, 2942.463], - [126.0, 1.083, 20.775], - [115.0, 0.645, 0.98], - [103.0, 0.636, 4694.003], - [102.0, 0.976, 15720.839], - [102.0, 4.267, 7.114], - [99.0, 6.21, 2146.17], - [98.0, 0.68, 155.42], - [86.0, 5.98, 161000.69], - [85.0, 1.3, 6275.96], - [85.0, 3.67, 71430.7], - [80.0, 1.81, 17260.15], - [79.0, 3.04, 12036.46], - [75.0, 1.76, 5088.63], - [74.0, 3.5, 3154.69], - [74.0, 4.68, 801.82], - [70.0, 0.83, 9437.76], - [62.0, 3.98, 8827.39], - [61.0, 1.82, 7084.9], - [57.0, 2.78, 6286.6], - [56.0, 4.39, 14143.5], - [56.0, 3.47, 6279.55], - [52.0, 0.19, 12139.55], - [52.0, 1.33, 1748.02], - [51.0, 0.28, 5856.48], - [49.0, 0.49, 1194.45], - [41.0, 5.37, 8429.24], - [41.0, 2.4, 19651.05], - [39.0, 6.17, 10447.39], - [37.0, 6.04, 10213.29], - [37.0, 2.57, 1059.38], - [36.0, 1.71, 2352.87], - [36.0, 1.78, 6812.77], - [33.0, 0.59, 17789.85], - [30.0, 0.44, 83996.85], - [30.0, 2.74, 1349.87], - [25.0, 3.16, 4690.48] -]) -L1 = np.array([ - [628331966747.0, 0.0, 0.0], - [206059.0, 2.678235, 6283.07585], - [4303.0, 2.6351, 12566.1517], - [425.0, 1.59, 3.523], - [119.0, 5.796, 26.298], - [109.0, 2.966, 1577.344], - [93.0, 2.59, 18849.23], - [72.0, 1.14, 529.69], - [68.0, 1.87, 398.15], - [67.0, 4.41, 5507.55], - [59.0, 2.89, 5223.69], - [56.0, 2.17, 155.42], - [45.0, 0.4, 796.3], - [36.0, 0.47, 775.52], - [29.0, 2.65, 7.11], - [21.0, 5.34, 0.98], - [19.0, 1.85, 5486.78], - [19.0, 4.97, 213.3], - [17.0, 2.99, 6275.96], - [16.0, 0.03, 2544.31], - [16.0, 1.43, 2146.17], - [15.0, 1.21, 10977.08], - [12.0, 2.83, 1748.02], - [12.0, 3.26, 5088.63], - [12.0, 5.27, 1194.45], - [12.0, 2.08, 4694.0], - [11.0, 0.77, 553.57], - [10.0, 1.3, 6286.6], - [10.0, 4.24, 1349.87], - [9.0, 2.7, 242.73], - [9.0, 5.64, 951.72], - [8.0, 5.3, 2352.87], - [6.0, 2.65, 9437.76], - [6.0, 4.67, 4690.48] -]) -L2 = np.array([ - [52919.0, 0.0, 0.0], - [8720.0, 1.0721, 6283.0758], - [309.0, 0.867, 12566.152], - [27.0, 0.05, 3.52], - [16.0, 5.19, 26.3], - [16.0, 3.68, 155.42], - [10.0, 0.76, 18849.23], - [9.0, 2.06, 77713.77], - [7.0, 0.83, 775.52], - [5.0, 4.66, 1577.34], - [4.0, 1.03, 7.11], - [4.0, 3.44, 5573.14], - [3.0, 5.14, 796.3], - [3.0, 6.05, 5507.55], - [3.0, 1.19, 242.73], - [3.0, 6.12, 529.69], - [3.0, 0.31, 398.15], - [3.0, 2.28, 553.57], - [2.0, 4.38, 5223.69], - [2.0, 3.75, 0.98] -]) -L3 = np.array([ - [289.0, 5.844, 6283.076], - [35.0, 0.0, 0.0], - [17.0, 5.49, 12566.15], - [3.0, 5.2, 155.42], - [1.0, 4.72, 3.52], - [1.0, 5.3, 18849.23], - [1.0, 5.97, 242.73] -]) -L4 = np.array([ - [114.0, 3.142, 0.0], - [8.0, 4.13, 6283.08], - [1.0, 3.84, 12566.15] -]) -L5 = np.array([ - [1.0, 3.14, 0.0] -]) +L0 = np.array( + [ + [175347046.0, 0.0, 0.0], + [3341656.0, 4.6692568, 6283.07585], + [34894.0, 4.6261, 12566.1517], + [3497.0, 2.7441, 5753.3849], + [3418.0, 2.8289, 3.5231], + [3136.0, 3.6277, 77713.7715], + [2676.0, 4.4181, 7860.4194], + [2343.0, 6.1352, 3930.2097], + [1324.0, 0.7425, 11506.7698], + [1273.0, 2.0371, 529.691], + [1199.0, 1.1096, 1577.3435], + [990.0, 5.233, 5884.927], + [902.0, 2.045, 26.298], + [857.0, 3.508, 398.149], + [780.0, 1.179, 5223.694], + [753.0, 2.533, 5507.553], + [505.0, 4.583, 18849.228], + [492.0, 4.205, 775.523], + [357.0, 2.92, 0.067], + [317.0, 5.849, 11790.629], + [284.0, 1.899, 796.298], + [271.0, 0.315, 10977.079], + [243.0, 0.345, 5486.778], + [206.0, 4.806, 2544.314], + [205.0, 1.869, 5573.143], + [202.0, 2.458, 6069.777], + [156.0, 0.833, 213.299], + [132.0, 3.411, 2942.463], + [126.0, 1.083, 20.775], + [115.0, 0.645, 0.98], + [103.0, 0.636, 4694.003], + [102.0, 0.976, 15720.839], + [102.0, 4.267, 7.114], + [99.0, 6.21, 2146.17], + [98.0, 0.68, 155.42], + [86.0, 5.98, 161000.69], + [85.0, 1.3, 6275.96], + [85.0, 3.67, 71430.7], + [80.0, 1.81, 17260.15], + [79.0, 3.04, 12036.46], + [75.0, 1.76, 5088.63], + [74.0, 3.5, 3154.69], + [74.0, 4.68, 801.82], + [70.0, 0.83, 9437.76], + [62.0, 3.98, 8827.39], + [61.0, 1.82, 7084.9], + [57.0, 2.78, 6286.6], + [56.0, 4.39, 14143.5], + [56.0, 3.47, 6279.55], + [52.0, 0.19, 12139.55], + [52.0, 1.33, 1748.02], + [51.0, 0.28, 5856.48], + [49.0, 0.49, 1194.45], + [41.0, 5.37, 8429.24], + [41.0, 2.4, 19651.05], + [39.0, 6.17, 10447.39], + [37.0, 6.04, 10213.29], + [37.0, 2.57, 1059.38], + [36.0, 1.71, 2352.87], + [36.0, 1.78, 6812.77], + [33.0, 0.59, 17789.85], + [30.0, 0.44, 83996.85], + [30.0, 2.74, 1349.87], + [25.0, 3.16, 4690.48], + ] +) +L1 = np.array( + [ + [628331966747.0, 0.0, 0.0], + [206059.0, 2.678235, 6283.07585], + [4303.0, 2.6351, 12566.1517], + [425.0, 1.59, 3.523], + [119.0, 5.796, 26.298], + [109.0, 2.966, 1577.344], + [93.0, 2.59, 18849.23], + [72.0, 1.14, 529.69], + [68.0, 1.87, 398.15], + [67.0, 4.41, 5507.55], + [59.0, 2.89, 5223.69], + [56.0, 2.17, 155.42], + [45.0, 0.4, 796.3], + [36.0, 0.47, 775.52], + [29.0, 2.65, 7.11], + [21.0, 5.34, 0.98], + [19.0, 1.85, 5486.78], + [19.0, 4.97, 213.3], + [17.0, 2.99, 6275.96], + [16.0, 0.03, 2544.31], + [16.0, 1.43, 2146.17], + [15.0, 1.21, 10977.08], + [12.0, 2.83, 1748.02], + [12.0, 3.26, 5088.63], + [12.0, 5.27, 1194.45], + [12.0, 2.08, 4694.0], + [11.0, 0.77, 553.57], + [10.0, 1.3, 6286.6], + [10.0, 4.24, 1349.87], + [9.0, 2.7, 242.73], + [9.0, 5.64, 951.72], + [8.0, 5.3, 2352.87], + [6.0, 2.65, 9437.76], + [6.0, 4.67, 4690.48], + ] +) +L2 = np.array( + [ + [52919.0, 0.0, 0.0], + [8720.0, 1.0721, 6283.0758], + [309.0, 0.867, 12566.152], + [27.0, 0.05, 3.52], + [16.0, 5.19, 26.3], + [16.0, 3.68, 155.42], + [10.0, 0.76, 18849.23], + [9.0, 2.06, 77713.77], + [7.0, 0.83, 775.52], + [5.0, 4.66, 1577.34], + [4.0, 1.03, 7.11], + [4.0, 3.44, 5573.14], + [3.0, 5.14, 796.3], + [3.0, 6.05, 5507.55], + [3.0, 1.19, 242.73], + [3.0, 6.12, 529.69], + [3.0, 0.31, 398.15], + [3.0, 2.28, 553.57], + [2.0, 4.38, 5223.69], + [2.0, 3.75, 0.98], + ] +) +L3 = np.array( + [ + [289.0, 5.844, 6283.076], + [35.0, 0.0, 0.0], + [17.0, 5.49, 12566.15], + [3.0, 5.2, 155.42], + [1.0, 4.72, 3.52], + [1.0, 5.3, 18849.23], + [1.0, 5.97, 242.73], + ] +) +L4 = np.array( + [[114.0, 3.142, 0.0], [8.0, 4.13, 6283.08], [1.0, 3.84, 12566.15]] +) +L5 = np.array([[1.0, 3.14, 0.0]]) # heliocentric latitude coefficients -B0 = np.array([ - [280.0, 3.199, 84334.662], - [102.0, 5.422, 5507.553], - [80.0, 3.88, 5223.69], - [44.0, 3.7, 2352.87], - [32.0, 4.0, 1577.34] -]) -B1 = np.array([ - [9.0, 3.9, 5507.55], - [6.0, 1.73, 5223.69] -]) +B0 = np.array( + [ + [280.0, 3.199, 84334.662], + [102.0, 5.422, 5507.553], + [80.0, 3.88, 5223.69], + [44.0, 3.7, 2352.87], + [32.0, 4.0, 1577.34], + ] +) +B1 = np.array([[9.0, 3.9, 5507.55], [6.0, 1.73, 5223.69]]) # heliocentric radius coefficients -R0 = np.array([ - [100013989.0, 0.0, 0.0], - [1670700.0, 3.0984635, 6283.07585], - [13956.0, 3.05525, 12566.1517], - [3084.0, 5.1985, 77713.7715], - [1628.0, 1.1739, 5753.3849], - [1576.0, 2.8469, 7860.4194], - [925.0, 5.453, 11506.77], - [542.0, 4.564, 3930.21], - [472.0, 3.661, 5884.927], - [346.0, 0.964, 5507.553], - [329.0, 5.9, 5223.694], - [307.0, 0.299, 5573.143], - [243.0, 4.273, 11790.629], - [212.0, 5.847, 1577.344], - [186.0, 5.022, 10977.079], - [175.0, 3.012, 18849.228], - [110.0, 5.055, 5486.778], - [98.0, 0.89, 6069.78], - [86.0, 5.69, 15720.84], - [86.0, 1.27, 161000.69], - [65.0, 0.27, 17260.15], - [63.0, 0.92, 529.69], - [57.0, 2.01, 83996.85], - [56.0, 5.24, 71430.7], - [49.0, 3.25, 2544.31], - [47.0, 2.58, 775.52], - [45.0, 5.54, 9437.76], - [43.0, 6.01, 6275.96], - [39.0, 5.36, 4694.0], - [38.0, 2.39, 8827.39], - [37.0, 0.83, 19651.05], - [37.0, 4.9, 12139.55], - [36.0, 1.67, 12036.46], - [35.0, 1.84, 2942.46], - [33.0, 0.24, 7084.9], - [32.0, 0.18, 5088.63], - [32.0, 1.78, 398.15], - [28.0, 1.21, 6286.6], - [28.0, 1.9, 6279.55], - [26.0, 4.59, 10447.39] -]) -R1 = np.array([ - [103019.0, 1.10749, 6283.07585], - [1721.0, 1.0644, 12566.1517], - [702.0, 3.142, 0.0], - [32.0, 1.02, 18849.23], - [31.0, 2.84, 5507.55], - [25.0, 1.32, 5223.69], - [18.0, 1.42, 1577.34], - [10.0, 5.91, 10977.08], - [9.0, 1.42, 6275.96], - [9.0, 0.27, 5486.78] -]) -R2 = np.array([ - [4359.0, 5.7846, 6283.0758], - [124.0, 5.579, 12566.152], - [12.0, 3.14, 0.0], - [9.0, 3.63, 77713.77], - [6.0, 1.87, 5573.14], - [3.0, 5.47, 18849.23] -]) -R3 = np.array([ - [145.0, 4.273, 6283.076], - [7.0, 3.92, 12566.15] -]) -R4 = np.array([ - [4.0, 2.56, 6283.08] -]) +R0 = np.array( + [ + [100013989.0, 0.0, 0.0], + [1670700.0, 3.0984635, 6283.07585], + [13956.0, 3.05525, 12566.1517], + [3084.0, 5.1985, 77713.7715], + [1628.0, 1.1739, 5753.3849], + [1576.0, 2.8469, 7860.4194], + [925.0, 5.453, 11506.77], + [542.0, 4.564, 3930.21], + [472.0, 3.661, 5884.927], + [346.0, 0.964, 5507.553], + [329.0, 5.9, 5223.694], + [307.0, 0.299, 5573.143], + [243.0, 4.273, 11790.629], + [212.0, 5.847, 1577.344], + [186.0, 5.022, 10977.079], + [175.0, 3.012, 18849.228], + [110.0, 5.055, 5486.778], + [98.0, 0.89, 6069.78], + [86.0, 5.69, 15720.84], + [86.0, 1.27, 161000.69], + [65.0, 0.27, 17260.15], + [63.0, 0.92, 529.69], + [57.0, 2.01, 83996.85], + [56.0, 5.24, 71430.7], + [49.0, 3.25, 2544.31], + [47.0, 2.58, 775.52], + [45.0, 5.54, 9437.76], + [43.0, 6.01, 6275.96], + [39.0, 5.36, 4694.0], + [38.0, 2.39, 8827.39], + [37.0, 0.83, 19651.05], + [37.0, 4.9, 12139.55], + [36.0, 1.67, 12036.46], + [35.0, 1.84, 2942.46], + [33.0, 0.24, 7084.9], + [32.0, 0.18, 5088.63], + [32.0, 1.78, 398.15], + [28.0, 1.21, 6286.6], + [28.0, 1.9, 6279.55], + [26.0, 4.59, 10447.39], + ] +) +R1 = np.array( + [ + [103019.0, 1.10749, 6283.07585], + [1721.0, 1.0644, 12566.1517], + [702.0, 3.142, 0.0], + [32.0, 1.02, 18849.23], + [31.0, 2.84, 5507.55], + [25.0, 1.32, 5223.69], + [18.0, 1.42, 1577.34], + [10.0, 5.91, 10977.08], + [9.0, 1.42, 6275.96], + [9.0, 0.27, 5486.78], + ] +) +R2 = np.array( + [ + [4359.0, 5.7846, 6283.0758], + [124.0, 5.579, 12566.152], + [12.0, 3.14, 0.0], + [9.0, 3.63, 77713.77], + [6.0, 1.87, 5573.14], + [3.0, 5.47, 18849.23], + ] +) +R3 = np.array([[145.0, 4.273, 6283.076], [7.0, 3.92, 12566.15]]) +R4 = np.array([[4.0, 2.56, 6283.08]]) # longitude and obliquity nutation coefficients -NUTATION_ABCD_ARRAY = np.array([ - [-171996, -174.2, 92025, 8.9], - [-13187, -1.6, 5736, -3.1], - [-2274, -0.2, 977, -0.5], - [2062, 0.2, -895, 0.5], - [1426, -3.4, 54, -0.1], - [712, 0.1, -7, 0], - [-517, 1.2, 224, -0.6], - [-386, -0.4, 200, 0], - [-301, 0, 129, -0.1], - [217, -0.5, -95, 0.3], - [-158, 0, 0, 0], - [129, 0.1, -70, 0], - [123, 0, -53, 0], - [63, 0, 0, 0], - [63, 0.1, -33, 0], - [-59, 0, 26, 0], - [-58, -0.1, 32, 0], - [-51, 0, 27, 0], - [48, 0, 0, 0], - [46, 0, -24, 0], - [-38, 0, 16, 0], - [-31, 0, 13, 0], - [29, 0, 0, 0], - [29, 0, -12, 0], - [26, 0, 0, 0], - [-22, 0, 0, 0], - [21, 0, -10, 0], - [17, -0.1, 0, 0], - [16, 0, -8, 0], - [-16, 0.1, 7, 0], - [-15, 0, 9, 0], - [-13, 0, 7, 0], - [-12, 0, 6, 0], - [11, 0, 0, 0], - [-10, 0, 5, 0], - [-8, 0, 3, 0], - [7, 0, -3, 0], - [-7, 0, 0, 0], - [-7, 0, 3, 0], - [-7, 0, 3, 0], - [6, 0, 0, 0], - [6, 0, -3, 0], - [6, 0, -3, 0], - [-6, 0, 3, 0], - [-6, 0, 3, 0], - [5, 0, 0, 0], - [-5, 0, 3, 0], - [-5, 0, 3, 0], - [-5, 0, 3, 0], - [4, 0, 0, 0], - [4, 0, 0, 0], - [4, 0, 0, 0], - [-4, 0, 0, 0], - [-4, 0, 0, 0], - [-4, 0, 0, 0], - [3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], - [-3, 0, 0, 0], -]) - -NUTATION_YTERM_ARRAY = np.array([ - [0, 0, 0, 0, 1], - [-2, 0, 0, 2, 2], - [0, 0, 0, 2, 2], - [0, 0, 0, 0, 2], - [0, 1, 0, 0, 0], - [0, 0, 1, 0, 0], - [-2, 1, 0, 2, 2], - [0, 0, 0, 2, 1], - [0, 0, 1, 2, 2], - [-2, -1, 0, 2, 2], - [-2, 0, 1, 0, 0], - [-2, 0, 0, 2, 1], - [0, 0, -1, 2, 2], - [2, 0, 0, 0, 0], - [0, 0, 1, 0, 1], - [2, 0, -1, 2, 2], - [0, 0, -1, 0, 1], - [0, 0, 1, 2, 1], - [-2, 0, 2, 0, 0], - [0, 0, -2, 2, 1], - [2, 0, 0, 2, 2], - [0, 0, 2, 2, 2], - [0, 0, 2, 0, 0], - [-2, 0, 1, 2, 2], - [0, 0, 0, 2, 0], - [-2, 0, 0, 2, 0], - [0, 0, -1, 2, 1], - [0, 2, 0, 0, 0], - [2, 0, -1, 0, 1], - [-2, 2, 0, 2, 2], - [0, 1, 0, 0, 1], - [-2, 0, 1, 0, 1], - [0, -1, 0, 0, 1], - [0, 0, 2, -2, 0], - [2, 0, -1, 2, 1], - [2, 0, 1, 2, 2], - [0, 1, 0, 2, 2], - [-2, 1, 1, 0, 0], - [0, -1, 0, 2, 2], - [2, 0, 0, 2, 1], - [2, 0, 1, 0, 0], - [-2, 0, 2, 2, 2], - [-2, 0, 1, 2, 1], - [2, 0, -2, 0, 1], - [2, 0, 0, 0, 1], - [0, -1, 1, 0, 0], - [-2, -1, 0, 2, 1], - [-2, 0, 0, 0, 1], - [0, 0, 2, 2, 1], - [-2, 0, 2, 0, 1], - [-2, 1, 0, 2, 1], - [0, 0, 1, -2, 0], - [-1, 0, 1, 0, 0], - [-2, 1, 0, 0, 0], - [1, 0, 0, 0, 0], - [0, 0, 1, 2, 0], - [0, 0, -2, 2, 2], - [-1, -1, 1, 0, 0], - [0, 1, 1, 0, 0], - [0, -1, 1, 2, 2], - [2, -1, -1, 2, 2], - [0, 0, 3, 2, 2], - [2, -1, 0, 2, 2], -]) - - -@jcompile('float64(int64, int64, int64, int64, int64, int64, int64)', - nopython=True) +NUTATION_ABCD_ARRAY = np.array( + [ + [-171996, -174.2, 92025, 8.9], + [-13187, -1.6, 5736, -3.1], + [-2274, -0.2, 977, -0.5], + [2062, 0.2, -895, 0.5], + [1426, -3.4, 54, -0.1], + [712, 0.1, -7, 0], + [-517, 1.2, 224, -0.6], + [-386, -0.4, 200, 0], + [-301, 0, 129, -0.1], + [217, -0.5, -95, 0.3], + [-158, 0, 0, 0], + [129, 0.1, -70, 0], + [123, 0, -53, 0], + [63, 0, 0, 0], + [63, 0.1, -33, 0], + [-59, 0, 26, 0], + [-58, -0.1, 32, 0], + [-51, 0, 27, 0], + [48, 0, 0, 0], + [46, 0, -24, 0], + [-38, 0, 16, 0], + [-31, 0, 13, 0], + [29, 0, 0, 0], + [29, 0, -12, 0], + [26, 0, 0, 0], + [-22, 0, 0, 0], + [21, 0, -10, 0], + [17, -0.1, 0, 0], + [16, 0, -8, 0], + [-16, 0.1, 7, 0], + [-15, 0, 9, 0], + [-13, 0, 7, 0], + [-12, 0, 6, 0], + [11, 0, 0, 0], + [-10, 0, 5, 0], + [-8, 0, 3, 0], + [7, 0, -3, 0], + [-7, 0, 0, 0], + [-7, 0, 3, 0], + [-7, 0, 3, 0], + [6, 0, 0, 0], + [6, 0, -3, 0], + [6, 0, -3, 0], + [-6, 0, 3, 0], + [-6, 0, 3, 0], + [5, 0, 0, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [-5, 0, 3, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [-4, 0, 0, 0], + [3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + [-3, 0, 0, 0], + ] +) + +NUTATION_YTERM_ARRAY = np.array( + [ + [0, 0, 0, 0, 1], + [-2, 0, 0, 2, 2], + [0, 0, 0, 2, 2], + [0, 0, 0, 0, 2], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [-2, 1, 0, 2, 2], + [0, 0, 0, 2, 1], + [0, 0, 1, 2, 2], + [-2, -1, 0, 2, 2], + [-2, 0, 1, 0, 0], + [-2, 0, 0, 2, 1], + [0, 0, -1, 2, 2], + [2, 0, 0, 0, 0], + [0, 0, 1, 0, 1], + [2, 0, -1, 2, 2], + [0, 0, -1, 0, 1], + [0, 0, 1, 2, 1], + [-2, 0, 2, 0, 0], + [0, 0, -2, 2, 1], + [2, 0, 0, 2, 2], + [0, 0, 2, 2, 2], + [0, 0, 2, 0, 0], + [-2, 0, 1, 2, 2], + [0, 0, 0, 2, 0], + [-2, 0, 0, 2, 0], + [0, 0, -1, 2, 1], + [0, 2, 0, 0, 0], + [2, 0, -1, 0, 1], + [-2, 2, 0, 2, 2], + [0, 1, 0, 0, 1], + [-2, 0, 1, 0, 1], + [0, -1, 0, 0, 1], + [0, 0, 2, -2, 0], + [2, 0, -1, 2, 1], + [2, 0, 1, 2, 2], + [0, 1, 0, 2, 2], + [-2, 1, 1, 0, 0], + [0, -1, 0, 2, 2], + [2, 0, 0, 2, 1], + [2, 0, 1, 0, 0], + [-2, 0, 2, 2, 2], + [-2, 0, 1, 2, 1], + [2, 0, -2, 0, 1], + [2, 0, 0, 0, 1], + [0, -1, 1, 0, 0], + [-2, -1, 0, 2, 1], + [-2, 0, 0, 0, 1], + [0, 0, 2, 2, 1], + [-2, 0, 2, 0, 1], + [-2, 1, 0, 2, 1], + [0, 0, 1, -2, 0], + [-1, 0, 1, 0, 0], + [-2, 1, 0, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 1, 2, 0], + [0, 0, -2, 2, 2], + [-1, -1, 1, 0, 0], + [0, 1, 1, 0, 0], + [0, -1, 1, 2, 2], + [2, -1, -1, 2, 2], + [0, 0, 3, 2, 2], + [2, -1, 0, 2, 2], + ] +) + + +@jcompile( + "float64(int64, int64, int64, int64, int64, int64, int64)", nopython=True +) def julian_day_dt(year, month, day, hour, minute, second, microsecond): """This is the original way to calculate the julian day from the NREL paper. However, it is much faster to convert to unix/epoch time and then convert to julian day. Note that the date must be UTC.""" if month <= 2: - year = year-1 - month = month+12 - a = int(year/100) + year = year - 1 + month = month + 12 + a = int(year / 100) b = 2 - a + int(a * 0.25) - frac_of_day = (microsecond / 1e6 + (second + minute * 60 + hour * 3600) - ) * 1.0 / (3600*24) + frac_of_day = ( + (microsecond / 1e6 + (second + minute * 60 + hour * 3600)) + * 1.0 + / (3600 * 24) + ) d = day + frac_of_day jd = int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + d - 1524.5 if jd > 2299160.0: @@ -420,31 +433,31 @@ def julian_day_dt(year, month, day, hour, minute, second, microsecond): return jd -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def julian_day(unixtime): jd = unixtime * 1.0 / 86400 + 2440587.5 return jd -@jcompile('float64(float64, float64)', nopython=True) +@jcompile("float64(float64, float64)", nopython=True) def julian_ephemeris_day(julian_day, delta_t): jde = julian_day + delta_t * 1.0 / 86400 return jde -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def julian_century(julian_day): jc = (julian_day - 2451545) * 1.0 / 36525 return jc -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def julian_ephemeris_century(julian_ephemeris_day): jce = (julian_ephemeris_day - 2451545) * 1.0 / 36525 return jce -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def julian_ephemeris_millennium(julian_ephemeris_century): jme = julian_ephemeris_century * 1.0 / 10 return jme @@ -456,12 +469,13 @@ def julian_ephemeris_millennium(julian_ephemeris_century): @jcompile(nopython=True) def sum_mult_cos_add_mult(arr, x): # shared calculation used for heliocentric longitude, latitude, and radius - s = 0. + s = 0.0 for row in range(arr.shape[0]): s += arr[row, 0] * np.cos(arr[row, 1] + arr[row, 2] * x) return s -@jcompile('float64(float64)', nopython=True) + +@jcompile("float64(float64)", nopython=True) def heliocentric_longitude(jme): l0 = sum_mult_cos_add_mult(L0, jme) l1 = sum_mult_cos_add_mult(L1, jme) @@ -470,22 +484,24 @@ def heliocentric_longitude(jme): l4 = sum_mult_cos_add_mult(L4, jme) l5 = sum_mult_cos_add_mult(L5, jme) - l_rad = (l0 + l1 * jme + l2 * jme**2 + l3 * jme**3 + l4 * jme**4 + - l5 * jme**5)/10**8 + l_rad = ( + l0 + l1 * jme + l2 * jme**2 + l3 * jme**3 + l4 * jme**4 + l5 * jme**5 + ) / 10**8 l = np.rad2deg(l_rad) return l % 360 -@jcompile('float64(float64)', nopython=True) + +@jcompile("float64(float64)", nopython=True) def heliocentric_latitude(jme): b0 = sum_mult_cos_add_mult(B0, jme) b1 = sum_mult_cos_add_mult(B1, jme) - b_rad = (b0 + b1 * jme)/10**8 + b_rad = (b0 + b1 * jme) / 10**8 b = np.rad2deg(b_rad) return b -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def heliocentric_radius_vector(jme): r0 = sum_mult_cos_add_mult(R0, jme) r1 = sum_mult_cos_add_mult(R1, jme) @@ -493,72 +509,84 @@ def heliocentric_radius_vector(jme): r3 = sum_mult_cos_add_mult(R3, jme) r4 = sum_mult_cos_add_mult(R4, jme) - r = (r0 + r1 * jme + r2 * jme**2 + r3 * jme**3 + r4 * jme**4)/10**8 + r = (r0 + r1 * jme + r2 * jme**2 + r3 * jme**3 + r4 * jme**4) / 10**8 return r -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def geocentric_longitude(heliocentric_longitude): theta = heliocentric_longitude + 180.0 return theta % 360 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def geocentric_latitude(heliocentric_latitude): - beta = -1.0*heliocentric_latitude + beta = -1.0 * heliocentric_latitude return beta -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def mean_elongation(julian_ephemeris_century): - x0 = (297.85036 - + 445267.111480 * julian_ephemeris_century - - 0.0019142 * julian_ephemeris_century**2 - + julian_ephemeris_century**3 / 189474) + x0 = ( + 297.85036 + + 445267.111480 * julian_ephemeris_century + - 0.0019142 * julian_ephemeris_century**2 + + julian_ephemeris_century**3 / 189474 + ) return x0 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def mean_anomaly_sun(julian_ephemeris_century): - x1 = (357.52772 - + 35999.050340 * julian_ephemeris_century - - 0.0001603 * julian_ephemeris_century**2 - - julian_ephemeris_century**3 / 300000) + x1 = ( + 357.52772 + + 35999.050340 * julian_ephemeris_century + - 0.0001603 * julian_ephemeris_century**2 + - julian_ephemeris_century**3 / 300000 + ) return x1 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def mean_anomaly_moon(julian_ephemeris_century): - x2 = (134.96298 - + 477198.867398 * julian_ephemeris_century - + 0.0086972 * julian_ephemeris_century**2 - + julian_ephemeris_century**3 / 56250) + x2 = ( + 134.96298 + + 477198.867398 * julian_ephemeris_century + + 0.0086972 * julian_ephemeris_century**2 + + julian_ephemeris_century**3 / 56250 + ) return x2 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def moon_argument_latitude(julian_ephemeris_century): - x3 = (93.27191 - + 483202.017538 * julian_ephemeris_century - - 0.0036825 * julian_ephemeris_century**2 - + julian_ephemeris_century**3 / 327270) + x3 = ( + 93.27191 + + 483202.017538 * julian_ephemeris_century + - 0.0036825 * julian_ephemeris_century**2 + + julian_ephemeris_century**3 / 327270 + ) return x3 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def moon_ascending_longitude(julian_ephemeris_century): - x4 = (125.04452 - - 1934.136261 * julian_ephemeris_century - + 0.0020708 * julian_ephemeris_century**2 - + julian_ephemeris_century**3 / 450000) + x4 = ( + 125.04452 + - 1934.136261 * julian_ephemeris_century + + 0.0020708 * julian_ephemeris_century**2 + + julian_ephemeris_century**3 / 450000 + ) return x4 @jcompile( - 'void(float64, float64, float64, float64, float64, float64, float64[:])', - nopython=True) -def longitude_obliquity_nutation(julian_ephemeris_century, x0, x1, x2, x3, x4, - out): + "void(float64, float64, float64, float64, float64, float64, float64[:])", + nopython=True, +) +def longitude_obliquity_nutation( + julian_ephemeris_century, x0, x1, x2, x3, x4, out +): delta_psi_sum = 0.0 delta_eps_sum = 0.0 for row in range(NUTATION_YTERM_ARRAY.shape[0]): @@ -567,16 +595,16 @@ def longitude_obliquity_nutation(julian_ephemeris_century, x0, x1, x2, x3, x4, c = NUTATION_ABCD_ARRAY[row, 2] d = NUTATION_ABCD_ARRAY[row, 3] arg = np.radians( - NUTATION_YTERM_ARRAY[row, 0]*x0 + - NUTATION_YTERM_ARRAY[row, 1]*x1 + - NUTATION_YTERM_ARRAY[row, 2]*x2 + - NUTATION_YTERM_ARRAY[row, 3]*x3 + - NUTATION_YTERM_ARRAY[row, 4]*x4 + NUTATION_YTERM_ARRAY[row, 0] * x0 + + NUTATION_YTERM_ARRAY[row, 1] * x1 + + NUTATION_YTERM_ARRAY[row, 2] * x2 + + NUTATION_YTERM_ARRAY[row, 3] * x3 + + NUTATION_YTERM_ARRAY[row, 4] * x4 ) delta_psi_sum += (a + b * julian_ephemeris_century) * np.sin(arg) delta_eps_sum += (c + d * julian_ephemeris_century) * np.cos(arg) - delta_psi = delta_psi_sum*1.0/36000000 - delta_eps = delta_eps_sum*1.0/36000000 + delta_psi = delta_psi_sum * 1.0 / 36000000 + delta_eps = delta_eps_sum * 1.0 / 36000000 # seems like we ought to be able to return a tuple here instead # of resorting to `out`, but returning a UniTuple from this # function caused calculations elsewhere to give the wrong result. @@ -586,246 +614,324 @@ def longitude_obliquity_nutation(julian_ephemeris_century, x0, x1, x2, x3, x4, out[1] = delta_eps -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def mean_ecliptic_obliquity(julian_ephemeris_millennium): - U = 1.0*julian_ephemeris_millennium/10 - e0 = (84381.448 - 4680.93 * U - 1.55 * U**2 - + 1999.25 * U**3 - 51.38 * U**4 - 249.67 * U**5 - - 39.05 * U**6 + 7.12 * U**7 + 27.87 * U**8 - + 5.79 * U**9 + 2.45 * U**10) + U = 1.0 * julian_ephemeris_millennium / 10 + e0 = ( + 84381.448 + - 4680.93 * U + - 1.55 * U**2 + + 1999.25 * U**3 + - 51.38 * U**4 + - 249.67 * U**5 + - 39.05 * U**6 + + 7.12 * U**7 + + 27.87 * U**8 + + 5.79 * U**9 + + 2.45 * U**10 + ) return e0 -@jcompile('float64(float64, float64)', nopython=True) +@jcompile("float64(float64, float64)", nopython=True) def true_ecliptic_obliquity(mean_ecliptic_obliquity, obliquity_nutation): e0 = mean_ecliptic_obliquity deleps = obliquity_nutation - e = e0*1.0/3600 + deleps + e = e0 * 1.0 / 3600 + deleps return e -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def aberration_correction(earth_radius_vector): deltau = -20.4898 / (3600 * earth_radius_vector) return deltau -@jcompile('float64(float64, float64, float64)', nopython=True) -def apparent_sun_longitude(geocentric_longitude, longitude_nutation, - aberration_correction): +@jcompile("float64(float64, float64, float64)", nopython=True) +def apparent_sun_longitude( + geocentric_longitude, longitude_nutation, aberration_correction +): lamd = geocentric_longitude + longitude_nutation + aberration_correction return lamd -@jcompile('float64(float64, float64)', nopython=True) +@jcompile("float64(float64, float64)", nopython=True) def mean_sidereal_time(julian_day, julian_century): - v0 = (280.46061837 + 360.98564736629 * (julian_day - 2451545) - + 0.000387933 * julian_century**2 - julian_century**3 / 38710000) + v0 = ( + 280.46061837 + + 360.98564736629 * (julian_day - 2451545) + + 0.000387933 * julian_century**2 + - julian_century**3 / 38710000 + ) return v0 % 360.0 -@jcompile('float64(float64, float64, float64)', nopython=True) -def apparent_sidereal_time(mean_sidereal_time, longitude_nutation, - true_ecliptic_obliquity): +@jcompile("float64(float64, float64, float64)", nopython=True) +def apparent_sidereal_time( + mean_sidereal_time, longitude_nutation, true_ecliptic_obliquity +): v = mean_sidereal_time + longitude_nutation * np.cos( - np.radians(true_ecliptic_obliquity)) + np.radians(true_ecliptic_obliquity) + ) return v -@jcompile('float64(float64, float64, float64)', nopython=True) -def geocentric_sun_right_ascension(apparent_sun_longitude, - true_ecliptic_obliquity, - geocentric_latitude): +@jcompile("float64(float64, float64, float64)", nopython=True) +def geocentric_sun_right_ascension( + apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude +): true_ecliptic_obliquity_rad = np.radians(true_ecliptic_obliquity) apparent_sun_longitude_rad = np.radians(apparent_sun_longitude) - num = (np.sin(apparent_sun_longitude_rad) - * np.cos(true_ecliptic_obliquity_rad) - - np.tan(np.radians(geocentric_latitude)) - * np.sin(true_ecliptic_obliquity_rad)) + num = np.sin(apparent_sun_longitude_rad) * np.cos( + true_ecliptic_obliquity_rad + ) - np.tan(np.radians(geocentric_latitude)) * np.sin( + true_ecliptic_obliquity_rad + ) alpha = np.degrees(np.arctan2(num, np.cos(apparent_sun_longitude_rad))) return alpha % 360 -@jcompile('float64(float64, float64, float64)', nopython=True) -def geocentric_sun_declination(apparent_sun_longitude, true_ecliptic_obliquity, - geocentric_latitude): +@jcompile("float64(float64, float64, float64)", nopython=True) +def geocentric_sun_declination( + apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude +): geocentric_latitude_rad = np.radians(geocentric_latitude) true_ecliptic_obliquity_rad = np.radians(true_ecliptic_obliquity) - delta = np.degrees(np.arcsin(np.sin(geocentric_latitude_rad) * - np.cos(true_ecliptic_obliquity_rad) + - np.cos(geocentric_latitude_rad) * - np.sin(true_ecliptic_obliquity_rad) * - np.sin(np.radians(apparent_sun_longitude)))) + delta = np.degrees( + np.arcsin( + np.sin(geocentric_latitude_rad) + * np.cos(true_ecliptic_obliquity_rad) + + np.cos(geocentric_latitude_rad) + * np.sin(true_ecliptic_obliquity_rad) + * np.sin(np.radians(apparent_sun_longitude)) + ) + ) return delta -@jcompile('float64(float64, float64, float64)', nopython=True) -def local_hour_angle(apparent_sidereal_time, observer_longitude, - sun_right_ascension): +@jcompile("float64(float64, float64, float64)", nopython=True) +def local_hour_angle( + apparent_sidereal_time, observer_longitude, sun_right_ascension +): """Measured westward from south""" H = apparent_sidereal_time + observer_longitude - sun_right_ascension return H % 360 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def equatorial_horizontal_parallax(earth_radius_vector): xi = 8.794 / (3600 * earth_radius_vector) return xi -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def uterm(observer_latitude): u = np.arctan(0.99664719 * np.tan(np.radians(observer_latitude))) return u -@jcompile('float64(float64, float64, float64)', nopython=True) +@jcompile("float64(float64, float64, float64)", nopython=True) def xterm(u, observer_latitude, observer_elevation): - x = (np.cos(u) + observer_elevation / 6378140 - * np.cos(np.radians(observer_latitude))) + x = np.cos(u) + observer_elevation / 6378140 * np.cos( + np.radians(observer_latitude) + ) return x -@jcompile('float64(float64, float64, float64)', nopython=True) +@jcompile("float64(float64, float64, float64)", nopython=True) def yterm(u, observer_latitude, observer_elevation): - y = (0.99664719 * np.sin(u) + observer_elevation / 6378140 - * np.sin(np.radians(observer_latitude))) + y = 0.99664719 * np.sin(u) + observer_elevation / 6378140 * np.sin( + np.radians(observer_latitude) + ) return y -@jcompile('float64(float64, float64,float64, float64)', nopython=True) -def parallax_sun_right_ascension(xterm, equatorial_horizontal_parallax, - local_hour_angle, geocentric_sun_declination): - equatorial_horizontal_parallax_rad = \ - np.radians(equatorial_horizontal_parallax) +@jcompile("float64(float64, float64,float64, float64)", nopython=True) +def parallax_sun_right_ascension( + xterm, + equatorial_horizontal_parallax, + local_hour_angle, + geocentric_sun_declination, +): + equatorial_horizontal_parallax_rad = np.radians( + equatorial_horizontal_parallax + ) local_hour_angle_rad = np.radians(local_hour_angle) - num = (-xterm * np.sin(equatorial_horizontal_parallax_rad) - * np.sin(local_hour_angle_rad)) - denom = (np.cos(np.radians(geocentric_sun_declination)) - - xterm * np.sin(equatorial_horizontal_parallax_rad) - * np.cos(local_hour_angle_rad)) + num = ( + -xterm + * np.sin(equatorial_horizontal_parallax_rad) + * np.sin(local_hour_angle_rad) + ) + denom = np.cos(np.radians(geocentric_sun_declination)) - xterm * np.sin( + equatorial_horizontal_parallax_rad + ) * np.cos(local_hour_angle_rad) delta_alpha = np.degrees(np.arctan2(num, denom)) return delta_alpha -@jcompile('float64(float64, float64)', nopython=True) -def topocentric_sun_right_ascension(geocentric_sun_right_ascension, - parallax_sun_right_ascension): +@jcompile("float64(float64, float64)", nopython=True) +def topocentric_sun_right_ascension( + geocentric_sun_right_ascension, parallax_sun_right_ascension +): alpha_prime = geocentric_sun_right_ascension + parallax_sun_right_ascension return alpha_prime -@jcompile('float64(float64, float64, float64, float64, float64, float64)', - nopython=True) -def topocentric_sun_declination(geocentric_sun_declination, xterm, yterm, - equatorial_horizontal_parallax, - parallax_sun_right_ascension, - local_hour_angle): +@jcompile( + "float64(float64, float64, float64, float64, float64, float64)", + nopython=True, +) +def topocentric_sun_declination( + geocentric_sun_declination, + xterm, + yterm, + equatorial_horizontal_parallax, + parallax_sun_right_ascension, + local_hour_angle, +): geocentric_sun_declination_rad = np.radians(geocentric_sun_declination) - equatorial_horizontal_parallax_rad = \ - np.radians(equatorial_horizontal_parallax) - - num = ((np.sin(geocentric_sun_declination_rad) - yterm - * np.sin(equatorial_horizontal_parallax_rad)) - * np.cos(np.radians(parallax_sun_right_ascension))) - denom = (np.cos(geocentric_sun_declination_rad) - xterm - * np.sin(equatorial_horizontal_parallax_rad) - * np.cos(np.radians(local_hour_angle))) + equatorial_horizontal_parallax_rad = np.radians( + equatorial_horizontal_parallax + ) + + num = ( + np.sin(geocentric_sun_declination_rad) + - yterm * np.sin(equatorial_horizontal_parallax_rad) + ) * np.cos(np.radians(parallax_sun_right_ascension)) + denom = np.cos(geocentric_sun_declination_rad) - xterm * np.sin( + equatorial_horizontal_parallax_rad + ) * np.cos(np.radians(local_hour_angle)) delta = np.degrees(np.arctan2(num, denom)) return delta -@jcompile('float64(float64, float64)', nopython=True) -def topocentric_local_hour_angle(local_hour_angle, - parallax_sun_right_ascension): +@jcompile("float64(float64, float64)", nopython=True) +def topocentric_local_hour_angle( + local_hour_angle, parallax_sun_right_ascension +): H_prime = local_hour_angle - parallax_sun_right_ascension return H_prime -@jcompile('float64(float64, float64, float64)', nopython=True) -def topocentric_elevation_angle_without_atmosphere(observer_latitude, - topocentric_sun_declination, - topocentric_local_hour_angle - ): +@jcompile("float64(float64, float64, float64)", nopython=True) +def topocentric_elevation_angle_without_atmosphere( + observer_latitude, + topocentric_sun_declination, + topocentric_local_hour_angle, +): observer_latitude_rad = np.radians(observer_latitude) topocentric_sun_declination_rad = np.radians(topocentric_sun_declination) - e0 = np.degrees(np.arcsin( - np.sin(observer_latitude_rad) - * np.sin(topocentric_sun_declination_rad) - + np.cos(observer_latitude_rad) - * np.cos(topocentric_sun_declination_rad) - * np.cos(np.radians(topocentric_local_hour_angle)))) + e0 = np.degrees( + np.arcsin( + np.sin(observer_latitude_rad) + * np.sin(topocentric_sun_declination_rad) + + np.cos(observer_latitude_rad) + * np.cos(topocentric_sun_declination_rad) + * np.cos(np.radians(topocentric_local_hour_angle)) + ) + ) return e0 -@jcompile('float64(float64, float64, float64, float64)', nopython=True) -def atmospheric_refraction_correction(local_pressure, local_temp, - topocentric_elevation_angle_wo_atmosphere, - atmos_refract): +@jcompile("float64(float64, float64, float64, float64)", nopython=True) +def atmospheric_refraction_correction( + local_pressure, + local_temp, + topocentric_elevation_angle_wo_atmosphere, + atmos_refract, +): # switch sets delta_e when the sun is below the horizon switch = topocentric_elevation_angle_wo_atmosphere >= -1.0 * ( - 0.26667 + atmos_refract) - delta_e = ((local_pressure / 1010.0) * (283.0 / (273 + local_temp)) - * 1.02 / (60 * np.tan(np.radians( - topocentric_elevation_angle_wo_atmosphere - + 10.3 / (topocentric_elevation_angle_wo_atmosphere - + 5.11))))) * switch + 0.26667 + atmos_refract + ) + delta_e = ( + (local_pressure / 1010.0) + * (283.0 / (273 + local_temp)) + * 1.02 + / ( + 60 + * np.tan( + np.radians( + topocentric_elevation_angle_wo_atmosphere + + 10.3 / (topocentric_elevation_angle_wo_atmosphere + 5.11) + ) + ) + ) + ) * switch return delta_e -@jcompile('float64(float64, float64)', nopython=True) -def topocentric_elevation_angle(topocentric_elevation_angle_without_atmosphere, - atmospheric_refraction_correction): - e = (topocentric_elevation_angle_without_atmosphere - + atmospheric_refraction_correction) +@jcompile("float64(float64, float64)", nopython=True) +def topocentric_elevation_angle( + topocentric_elevation_angle_without_atmosphere, + atmospheric_refraction_correction, +): + e = ( + topocentric_elevation_angle_without_atmosphere + + atmospheric_refraction_correction + ) return e -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def topocentric_zenith_angle(topocentric_elevation_angle): theta = 90 - topocentric_elevation_angle return theta -@jcompile('float64(float64, float64, float64)', nopython=True) -def topocentric_astronomers_azimuth(topocentric_local_hour_angle, - topocentric_sun_declination, - observer_latitude): +@jcompile("float64(float64, float64, float64)", nopython=True) +def topocentric_astronomers_azimuth( + topocentric_local_hour_angle, + topocentric_sun_declination, + observer_latitude, +): topocentric_local_hour_angle_rad = np.radians(topocentric_local_hour_angle) observer_latitude_rad = np.radians(observer_latitude) num = np.sin(topocentric_local_hour_angle_rad) - denom = (np.cos(topocentric_local_hour_angle_rad) - * np.sin(observer_latitude_rad) - - np.tan(np.radians(topocentric_sun_declination)) - * np.cos(observer_latitude_rad)) + denom = np.cos(topocentric_local_hour_angle_rad) * np.sin( + observer_latitude_rad + ) - np.tan(np.radians(topocentric_sun_declination)) * np.cos( + observer_latitude_rad + ) gamma = np.degrees(np.arctan2(num, denom)) return gamma % 360 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def topocentric_azimuth_angle(topocentric_astronomers_azimuth): phi = topocentric_astronomers_azimuth + 180 return phi % 360 -@jcompile('float64(float64)', nopython=True) +@jcompile("float64(float64)", nopython=True) def sun_mean_longitude(julian_ephemeris_millennium): - M = (280.4664567 + 360007.6982779 * julian_ephemeris_millennium - + 0.03032028 * julian_ephemeris_millennium**2 - + julian_ephemeris_millennium**3 / 49931 - - julian_ephemeris_millennium**4 / 15300 - - julian_ephemeris_millennium**5 / 2000000) + M = ( + 280.4664567 + + 360007.6982779 * julian_ephemeris_millennium + + 0.03032028 * julian_ephemeris_millennium**2 + + julian_ephemeris_millennium**3 / 49931 + - julian_ephemeris_millennium**4 / 15300 + - julian_ephemeris_millennium**5 / 2000000 + ) return M -@jcompile('float64(float64, float64, float64, float64)', nopython=True) -def equation_of_time(sun_mean_longitude, geocentric_sun_right_ascension, - longitude_nutation, true_ecliptic_obliquity): - E = (sun_mean_longitude - 0.0057183 - geocentric_sun_right_ascension + - longitude_nutation * np.cos(np.radians(true_ecliptic_obliquity))) +@jcompile("float64(float64, float64, float64, float64)", nopython=True) +def equation_of_time( + sun_mean_longitude, + geocentric_sun_right_ascension, + longitude_nutation, + true_ecliptic_obliquity, +): + E = ( + sun_mean_longitude + - 0.0057183 + - geocentric_sun_right_ascension + + longitude_nutation * np.cos(np.radians(true_ecliptic_obliquity)) + ) # limit between 0 and 360 E = E % 360 # convert to minutes @@ -837,8 +943,11 @@ def equation_of_time(sun_mean_longitude, geocentric_sun_right_ascension, return E -@jcompile('void(float64[:], float64[:], float64[:], float64[:,:])', - nopython=True, nogil=True) +@jcompile( + "void(float64[:], float64[:], float64[:], float64[:,:])", + nopython=True, + nogil=True, +) def solar_position_loop(unixtime, delta_t, loc_args, out): """Loop through the time array and calculate the solar position""" lat = loc_args[0] @@ -896,13 +1005,16 @@ def solar_position_loop(unixtime, delta_t, loc_args, out): x = xterm(u, lat, elev) y = yterm(u, lat, elev) delta_alpha = parallax_sun_right_ascension(x, xi, H, delta) - delta_prime = topocentric_sun_declination(delta, x, y, xi, delta_alpha, - H) + delta_prime = topocentric_sun_declination( + delta, x, y, xi, delta_alpha, H + ) H_prime = topocentric_local_hour_angle(H, delta_alpha) - e0 = topocentric_elevation_angle_without_atmosphere(lat, delta_prime, - H_prime) - delta_e = atmospheric_refraction_correction(pressure, temp, e0, - atmos_refract) + e0 = topocentric_elevation_angle_without_atmosphere( + lat, delta_prime, H_prime + ) + delta_e = atmospheric_refraction_correction( + pressure, temp, e0, atmos_refract + ) e = topocentric_elevation_angle(e0, delta_e) theta = topocentric_zenith_angle(e) theta0 = topocentric_zenith_angle(e0) @@ -916,14 +1028,27 @@ def solar_position_loop(unixtime, delta_t, loc_args, out): out[5, i] = eot -def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads, sst=False, esd=False): +def solar_position_numba( + unixtime, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads, + sst=False, + esd=False, +): """Calculate the solar position using the numba compiled functions and multiple threads. Very slow if functions are not numba compiled. """ # these args are the same for each thread - loc_args = np.array([lat, lon, elev, pressure, temp, - atmos_refract, sst, esd], dtype=np.float64) + loc_args = np.array( + [lat, lon, elev, pressure, temp, atmos_refract, sst, esd], + dtype=np.float64, + ) # turn delta_t into an array if it isn't already delta_t = np.full_like(unixtime, delta_t, dtype=np.float64) @@ -953,12 +1078,13 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t, split1 = np.array_split(delta_t, numthreads) split2 = np.array_split(result, numthreads, axis=1) chunks = [ - [a0, a1, loc_args, a2] - for a0, a1, a2 in zip(split0, split1, split2) + [a0, a1, loc_args, a2] for a0, a1, a2 in zip(split0, split1, split2) ] # Spawn one thread per chunk - threads = [threading.Thread(target=solar_position_loop, args=chunk) - for chunk in chunks] + threads = [ + threading.Thread(target=solar_position_loop, args=chunk) + for chunk in chunks + ] for thread in threads: thread.start() for thread in threads: @@ -966,8 +1092,19 @@ def solar_position_numba(unixtime, lat, lon, elev, pressure, temp, delta_t, return result -def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads, sst=False, esd=False): +def solar_position_numpy( + unixtime, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads, + sst=False, + esd=False, +): """Calculate the solar position assuming unixtime is a numpy array. Note this function will not work if the solar position functions were compiled with numba. @@ -980,7 +1117,7 @@ def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t, jme = julian_ephemeris_millennium(jce) R = heliocentric_radius_vector(jme) if esd: - return (R, ) + return (R,) L = heliocentric_longitude(jme) B = heliocentric_latitude(jme) Theta = geocentric_longitude(L) @@ -1014,10 +1151,12 @@ def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t, delta_alpha = parallax_sun_right_ascension(x, xi, H, delta) delta_prime = topocentric_sun_declination(delta, x, y, xi, delta_alpha, H) H_prime = topocentric_local_hour_angle(H, delta_alpha) - e0 = topocentric_elevation_angle_without_atmosphere(lat, delta_prime, - H_prime) - delta_e = atmospheric_refraction_correction(pressure, temp, e0, - atmos_refract) + e0 = topocentric_elevation_angle_without_atmosphere( + lat, delta_prime, H_prime + ) + delta_e = atmospheric_refraction_correction( + pressure, temp, e0, atmos_refract + ) e = topocentric_elevation_angle(e0, delta_e) theta = topocentric_zenith_angle(e) theta0 = topocentric_zenith_angle(e0) @@ -1026,9 +1165,19 @@ def solar_position_numpy(unixtime, lat, lon, elev, pressure, temp, delta_t, return theta, theta0, e, e0, phi, eot -def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads=8, sst=False, esd=False): - +def solar_position( + unixtime, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads=8, + sst=False, + esd=False, +): """ Calculate the solar position using the NREL SPA algorithm described in [1]. @@ -1092,9 +1241,19 @@ def solar_position(unixtime, lat, lon, elev, pressure, temp, delta_t, else: do_calc = solar_position_numpy - result = do_calc(unixtime, lat, lon, elev, pressure, - temp, delta_t, atmos_refract, numthreads, - sst, esd) + result = do_calc( + unixtime, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads, + sst, + esd, + ) if not isinstance(result, np.ndarray): try: @@ -1132,7 +1291,7 @@ def transit_sunrise_sunset(dates, lat, lon, delta_t, numthreads): """ if ((dates % 86400) != 0.0).any(): - raise ValueError('Input dates must be at 00:00 UTC') + raise ValueError("Input dates must be at 00:00 UTC") utday = (dates // 86400) * 86400 ttday0 = utday - delta_t @@ -1140,27 +1299,32 @@ def transit_sunrise_sunset(dates, lat, lon, delta_t, numthreads): ttdayp1 = ttday0 + 86400 # index 0 is v, 1 is alpha, 2 is delta - utday_res = solar_position(utday, 0, 0, 0, 0, 0, delta_t, - 0, numthreads, sst=True) + utday_res = solar_position( + utday, 0, 0, 0, 0, 0, delta_t, 0, numthreads, sst=True + ) v = utday_res[0] - ttday0_res = solar_position(ttday0, 0, 0, 0, 0, 0, delta_t, - 0, numthreads, sst=True) - ttdayn1_res = solar_position(ttdayn1, 0, 0, 0, 0, 0, delta_t, - 0, numthreads, sst=True) - ttdayp1_res = solar_position(ttdayp1, 0, 0, 0, 0, 0, delta_t, - 0, numthreads, sst=True) + ttday0_res = solar_position( + ttday0, 0, 0, 0, 0, 0, delta_t, 0, numthreads, sst=True + ) + ttdayn1_res = solar_position( + ttdayn1, 0, 0, 0, 0, 0, delta_t, 0, numthreads, sst=True + ) + ttdayp1_res = solar_position( + ttdayp1, 0, 0, 0, 0, 0, delta_t, 0, numthreads, sst=True + ) m0 = (ttday0_res[1] - lon - v) / 360 - cos_arg = ((np.sin(np.radians(-0.8333)) - np.sin(np.radians(lat)) - * np.sin(np.radians(ttday0_res[2]))) / - (np.cos(np.radians(lat)) * np.cos(np.radians(ttday0_res[2])))) + cos_arg = ( + np.sin(np.radians(-0.8333)) + - np.sin(np.radians(lat)) * np.sin(np.radians(ttday0_res[2])) + ) / (np.cos(np.radians(lat)) * np.cos(np.radians(ttday0_res[2]))) cos_arg[abs(cos_arg) > 1] = np.nan H0 = np.degrees(np.arccos(cos_arg)) % 180 m = np.empty((3, len(utday))) m[0] = m0 % 1 - m[1] = (m[0] - H0 / 360) - m[2] = (m[0] + H0 / 360) + m[1] = m[0] - H0 / 360 + m[2] = m[0] + H0 / 360 # need to account for fractions of day that may be the next or previous # day in UTC @@ -1187,19 +1351,36 @@ def transit_sunrise_sunset(dates, lat, lon, delta_t, numthreads): Hp = (vs + lon - alpha_prime) % 360 Hp[Hp >= 180] = Hp[Hp >= 180] - 360 - h = np.degrees(np.arcsin(np.sin(np.radians(lat)) * - np.sin(np.radians(delta_prime)) + - np.cos(np.radians(lat)) * - np.cos(np.radians(delta_prime)) - * np.cos(np.radians(Hp)))) + h = np.degrees( + np.arcsin( + np.sin(np.radians(lat)) * np.sin(np.radians(delta_prime)) + + np.cos(np.radians(lat)) + * np.cos(np.radians(delta_prime)) + * np.cos(np.radians(Hp)) + ) + ) T = (m[0] - Hp[0] / 360) * 86400 - R = (m[1] + (h[1] + 0.8333) / (360 * np.cos(np.radians(delta_prime[1])) * - np.cos(np.radians(lat)) * - np.sin(np.radians(Hp[1])))) * 86400 - S = (m[2] + (h[2] + 0.8333) / (360 * np.cos(np.radians(delta_prime[2])) * - np.cos(np.radians(lat)) * - np.sin(np.radians(Hp[2])))) * 86400 + R = ( + m[1] + + (h[1] + 0.8333) + / ( + 360 + * np.cos(np.radians(delta_prime[1])) + * np.cos(np.radians(lat)) + * np.sin(np.radians(Hp[1])) + ) + ) * 86400 + S = ( + m[2] + + (h[2] + 0.8333) + / ( + 360 + * np.cos(np.radians(delta_prime[2])) + * np.cos(np.radians(lat)) + * np.sin(np.radians(Hp[2])) + ) + ) * 86400 S[add_a_day] += 86400 R[sub_a_day] -= 86400 @@ -1239,8 +1420,9 @@ def earthsun_distance(unixtime, delta_t, numthreads): USA, http://www.nrel.gov. """ - R = solar_position(unixtime, 0, 0, 0, 0, 0, delta_t, - 0, numthreads, esd=True)[0] + R = solar_position( + unixtime, 0, 0, 0, 0, 0, delta_t, 0, numthreads, esd=True + )[0] return R @@ -1252,9 +1434,11 @@ def calculate_deltat(year, month): Equations taken from http://eclipse.gsfc.nasa.gov/SEcat5/deltatpoly.html """ - plw = 'Deltat is unknown for years before -1999 and after 3000. ' \ - 'Delta values will be calculated, but the calculations ' \ - 'are not intended to be used for these years.' + plw = ( + "Deltat is unknown for years before -1999 and after 3000. " + "Delta values will be calculated, but the calculations " + "are not intended to be used for these years." + ) try: if np.any((year > 3000) | (year < -1999)): @@ -1265,109 +1449,141 @@ def calculate_deltat(year, month): except TypeError: return 0 - y = year + (month - 0.5)/12 - - deltat = np.where(year < -500, - - -20+32*((y-1820)/100)**2, 0) - - deltat = np.where((-500 <= year) & (year < 500), - - 10583.6-1014.41*(y/100) - + 33.78311*(y/100)**2 - - 5.952053*(y/100)**3 - - 0.1798452*(y/100)**4 - + 0.022174192*(y/100)**5 - + 0.0090316521*(y/100)**6, deltat) - - deltat = np.where((500 <= year) & (year < 1600), - - 1574.2-556.01*((y-1000)/100) - + 71.23472*((y-1000)/100)**2 - + 0.319781*((y-1000)/100)**3 - - 0.8503463*((y-1000)/100)**4 - - 0.005050998*((y-1000)/100)**5 - + 0.0083572073*((y-1000)/100)**6, deltat) - - deltat = np.where((1600 <= year) & (year < 1700), - - 120-0.9808*(y-1600) - - 0.01532*(y-1600)**2 - + (y-1600)**3/7129, deltat) - - deltat = np.where((1700 <= year) & (year < 1800), - - 8.83+0.1603*(y-1700) - - 0.0059285*(y-1700)**2 - + 0.00013336*(y-1700)**3 - - (y-1700)**4/1174000, deltat) - - deltat = np.where((1800 <= year) & (year < 1860), - - 13.72-0.332447*(y-1800) - + 0.0068612*(y-1800)**2 - + 0.0041116*(y-1800)**3 - - 0.00037436*(y-1800)**4 - + 0.0000121272*(y-1800)**5 - - 0.0000001699*(y-1800)**6 - + 0.000000000875*(y-1800)**7, deltat) - - deltat = np.where((1860 <= year) & (year < 1900), - - 7.62+0.5737*(y-1860) - - 0.251754*(y-1860)**2 - + 0.01680668*(y-1860)**3 - - 0.0004473624*(y-1860)**4 - + (y-1860)**5/233174, deltat) - - deltat = np.where((1900 <= year) & (year < 1920), - - -2.79+1.494119*(y-1900) - - 0.0598939*(y-1900)**2 - + 0.0061966*(y-1900)**3 - - 0.000197*(y-1900)**4, deltat) - - deltat = np.where((1920 <= year) & (year < 1941), - - 21.20+0.84493*(y-1920) - - 0.076100*(y-1920)**2 - + 0.0020936*(y-1920)**3, deltat) - - deltat = np.where((1941 <= year) & (year < 1961), - - 29.07+0.407*(y-1950) - - (y-1950)**2/233 - + (y-1950)**3/2547, deltat) - - deltat = np.where((1961 <= year) & (year < 1986), - - 45.45+1.067*(y-1975) - - (y-1975)**2/260 - - (y-1975)**3/718, deltat) - - deltat = np.where((1986 <= year) & (year < 2005), - - 63.86+0.3345*(y-2000) - - 0.060374*(y-2000)**2 - + 0.0017275*(y-2000)**3 - + 0.000651814*(y-2000)**4 - + 0.00002373599*(y-2000)**5, deltat) - - deltat = np.where((2005 <= year) & (year < 2050), - - 62.92+0.32217*(y-2000) - + 0.005589*(y-2000)**2, deltat) - - deltat = np.where((2050 <= year) & (year < 2150), - - -20+32*((y-1820)/100)**2 - - 0.5628*(2150-y), deltat) - - deltat = np.where(year >= 2150, - - -20+32*((y-1820)/100)**2, deltat) - - deltat = deltat.item() if np.isscalar(year) & np.isscalar(month)\ - else deltat + y = year + (month - 0.5) / 12 + + deltat = np.where(year < -500, -20 + 32 * ((y - 1820) / 100) ** 2, 0) + + deltat = np.where( + (-500 <= year) & (year < 500), + 10583.6 + - 1014.41 * (y / 100) + + 33.78311 * (y / 100) ** 2 + - 5.952053 * (y / 100) ** 3 + - 0.1798452 * (y / 100) ** 4 + + 0.022174192 * (y / 100) ** 5 + + 0.0090316521 * (y / 100) ** 6, + deltat, + ) + + deltat = np.where( + (500 <= year) & (year < 1600), + 1574.2 + - 556.01 * ((y - 1000) / 100) + + 71.23472 * ((y - 1000) / 100) ** 2 + + 0.319781 * ((y - 1000) / 100) ** 3 + - 0.8503463 * ((y - 1000) / 100) ** 4 + - 0.005050998 * ((y - 1000) / 100) ** 5 + + 0.0083572073 * ((y - 1000) / 100) ** 6, + deltat, + ) + + deltat = np.where( + (1600 <= year) & (year < 1700), + 120 + - 0.9808 * (y - 1600) + - 0.01532 * (y - 1600) ** 2 + + (y - 1600) ** 3 / 7129, + deltat, + ) + + deltat = np.where( + (1700 <= year) & (year < 1800), + 8.83 + + 0.1603 * (y - 1700) + - 0.0059285 * (y - 1700) ** 2 + + 0.00013336 * (y - 1700) ** 3 + - (y - 1700) ** 4 / 1174000, + deltat, + ) + + deltat = np.where( + (1800 <= year) & (year < 1860), + 13.72 + - 0.332447 * (y - 1800) + + 0.0068612 * (y - 1800) ** 2 + + 0.0041116 * (y - 1800) ** 3 + - 0.00037436 * (y - 1800) ** 4 + + 0.0000121272 * (y - 1800) ** 5 + - 0.0000001699 * (y - 1800) ** 6 + + 0.000000000875 * (y - 1800) ** 7, + deltat, + ) + + deltat = np.where( + (1860 <= year) & (year < 1900), + 7.62 + + 0.5737 * (y - 1860) + - 0.251754 * (y - 1860) ** 2 + + 0.01680668 * (y - 1860) ** 3 + - 0.0004473624 * (y - 1860) ** 4 + + (y - 1860) ** 5 / 233174, + deltat, + ) + + deltat = np.where( + (1900 <= year) & (year < 1920), + -2.79 + + 1.494119 * (y - 1900) + - 0.0598939 * (y - 1900) ** 2 + + 0.0061966 * (y - 1900) ** 3 + - 0.000197 * (y - 1900) ** 4, + deltat, + ) + + deltat = np.where( + (1920 <= year) & (year < 1941), + 21.20 + + 0.84493 * (y - 1920) + - 0.076100 * (y - 1920) ** 2 + + 0.0020936 * (y - 1920) ** 3, + deltat, + ) + + deltat = np.where( + (1941 <= year) & (year < 1961), + 29.07 + + 0.407 * (y - 1950) + - (y - 1950) ** 2 / 233 + + (y - 1950) ** 3 / 2547, + deltat, + ) + + deltat = np.where( + (1961 <= year) & (year < 1986), + 45.45 + + 1.067 * (y - 1975) + - (y - 1975) ** 2 / 260 + - (y - 1975) ** 3 / 718, + deltat, + ) + + deltat = np.where( + (1986 <= year) & (year < 2005), + 63.86 + + 0.3345 * (y - 2000) + - 0.060374 * (y - 2000) ** 2 + + 0.0017275 * (y - 2000) ** 3 + + 0.000651814 * (y - 2000) ** 4 + + 0.00002373599 * (y - 2000) ** 5, + deltat, + ) + + deltat = np.where( + (2005 <= year) & (year < 2050), + 62.92 + 0.32217 * (y - 2000) + 0.005589 * (y - 2000) ** 2, + deltat, + ) + + deltat = np.where( + (2050 <= year) & (year < 2150), + -20 + 32 * ((y - 1820) / 100) ** 2 - 0.5628 * (2150 - y), + deltat, + ) + + deltat = np.where(year >= 2150, -20 + 32 * ((y - 1820) / 100) ** 2, deltat) + + deltat = ( + deltat.item() if np.isscalar(year) & np.isscalar(month) else deltat + ) return deltat diff --git a/pvlib/spa_c_files/setup.py b/pvlib/spa_c_files/setup.py index 65f43597ed..754f3344f9 100644 --- a/pvlib/spa_c_files/setup.py +++ b/pvlib/spa_c_files/setup.py @@ -1,34 +1,32 @@ # setup.py import os -from distutils.core import setup -from distutils.extension import Extension + from Cython.Build import cythonize +from setuptools import Extension, setup DIRNAME = os.path.dirname(__file__) # patch spa.c -with open(os.path.join(DIRNAME, 'spa.c'), 'rb') as f: +with open(os.path.join(DIRNAME, "spa.c"), "rb") as f: SPA_C = f.read() # replace timezone with time_zone to avoid nameclash with the function # __timezone which is defined by a MACRO in pyconfig.h as timezone # see https://bugs.python.org/issue24643 -SPA_C = SPA_C.replace(b'timezone', b'time_zone') -with open(os.path.join(DIRNAME, 'spa.c'), 'wb') as f: +SPA_C = SPA_C.replace(b"timezone", b"time_zone") +with open(os.path.join(DIRNAME, "spa.c"), "wb") as f: f.write(SPA_C) # patch spa.h -with open(os.path.join(DIRNAME, 'spa.h'), 'rb') as f: +with open(os.path.join(DIRNAME, "spa.h"), "rb") as f: SPA_H = f.read() # replace timezone with time_zone to avoid nameclash with the function # __timezone which is defined by a MACRO in pyconfig.h as timezone # see https://bugs.python.org/issue24643 -SPA_H = SPA_H.replace(b'timezone', b'time_zone') -with open(os.path.join(DIRNAME, 'spa.h'), 'wb') as f: +SPA_H = SPA_H.replace(b"timezone", b"time_zone") +with open(os.path.join(DIRNAME, "spa.h"), "wb") as f: f.write(SPA_H) -SPA_SOURCES = [os.path.join(DIRNAME, src) for src in ['spa_py.pyx', 'spa.c']] +SPA_SOURCES = [os.path.join(DIRNAME, src) for src in ["spa_py.pyx", "spa.c"]] -setup( - ext_modules=cythonize([Extension('spa_py', SPA_SOURCES)]) -) +setup(ext_modules=cythonize([Extension("spa_py", SPA_SOURCES)])) diff --git a/pvlib/spa_c_files/spa_py_example.py b/pvlib/spa_c_files/spa_py_example.py index 4b3cf3bc9d..2f561fc4f3 100644 --- a/pvlib/spa_c_files/spa_py_example.py +++ b/pvlib/spa_c_files/spa_py_example.py @@ -2,41 +2,51 @@ import numpy as np EXPECTED = { - 'year': 2004, - 'month': 10, - 'day': 17, - 'hour': 12, - 'minute': 30, - 'second': 30.0, - 'delta_ut1': 0.0, - 'delta_t': 67.0, - 'time_zone': -7.0, - 'longitude': -105.1786, - 'latitude': 39.742476, - 'elevation': 1830.14, - 'pressure': 820.0, - 'temperature': 11.0, - 'slope': 30.0, - 'azm_rotation': -10.0, - 'atmos_refract': 0.5667, - 'function': 3, - 'e0': 39.59209464796398, - 'e': 39.60858878898177, - 'zenith': 50.39141121101823, - 'azimuth_astro': 14.311961805946808, - 'azimuth': 194.3119618059468, - 'incidence': 25.42168493680471, - 'suntransit': 11.765833793714224, - 'sunrise': 6.22578372122376, - 'sunset': 17.320379610556166 + "year": 2004, + "month": 10, + "day": 17, + "hour": 12, + "minute": 30, + "second": 30.0, + "delta_ut1": 0.0, + "delta_t": 67.0, + "time_zone": -7.0, + "longitude": -105.1786, + "latitude": 39.742476, + "elevation": 1830.14, + "pressure": 820.0, + "temperature": 11.0, + "slope": 30.0, + "azm_rotation": -10.0, + "atmos_refract": 0.5667, + "function": 3, + "e0": 39.59209464796398, + "e": 39.60858878898177, + "zenith": 50.39141121101823, + "azimuth_astro": 14.311961805946808, + "azimuth": 194.3119618059468, + "incidence": 25.42168493680471, + "suntransit": 11.765833793714224, + "sunrise": 6.22578372122376, + "sunset": 17.320379610556166, } def spa_calc_example(test=True): result = spa_calc( - year=2004, month=10, day=17, hour=12, minute=30, second=30, - time_zone=-7, longitude=-105.1786, latitude=39.742476, - elevation=1830.14, pressure=820, temperature=11, delta_t=67 + year=2004, + month=10, + day=17, + hour=12, + minute=30, + second=30, + time_zone=-7, + longitude=-105.1786, + latitude=39.742476, + elevation=1830.14, + pressure=820, + temperature=11, + delta_t=67, ) if test: for fieldname, expected_value in EXPECTED.items(): diff --git a/pvlib/spectrum/irradiance.py b/pvlib/spectrum/irradiance.py index 14b2da27ba..a1ed9c8062 100644 --- a/pvlib/spectrum/irradiance.py +++ b/pvlib/spectrum/irradiance.py @@ -248,13 +248,16 @@ def average_photon_energy(spectra): """ if not isinstance(spectra, (pd.Series, pd.DataFrame)): - raise TypeError('`spectra` must be either a' - ' pandas Series or DataFrame') + raise TypeError( + "`spectra` must be either a" " pandas Series or DataFrame" + ) if (spectra < 0).any().any(): - raise ValueError('Spectral irradiance data must be positive') + raise ValueError("Spectral irradiance data must be positive") - hclambda = pd.Series((constants.h*constants.c)/(spectra.T.index*1e-9)) + hclambda = pd.Series( + (constants.h * constants.c) / (spectra.T.index * 1e-9) + ) hclambda.index = spectra.T.index pfd = spectra.div(hclambda) @@ -264,8 +267,8 @@ def integrate(e): int_spectra = integrate(spectra) int_pfd = integrate(pfd) - with np.errstate(invalid='ignore'): - ape = (1/constants.elementary_charge)*int_spectra/int_pfd + with np.errstate(invalid="ignore"): + ape = (1 / constants.elementary_charge) * int_spectra / int_pfd if isinstance(spectra, pd.DataFrame): ape = pd.Series(ape, index=spectra.index) diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py index 3afc210e73..d8f488a75f 100644 --- a/pvlib/spectrum/mismatch.py +++ b/pvlib/spectrum/mismatch.py @@ -4,6 +4,7 @@ a device's photocurrent (or its short-circuit current) of changes in the solar spectrum due to the atmosphere. """ + import pvlib import numpy as np import pandas as pd @@ -88,7 +89,8 @@ def calc_spectral_mismatch_field(sr, e_sun, e_ref=None): # get the reference spectrum at wavelengths matching the measured spectra if e_ref is None: e_ref = pvlib.spectrum.get_reference_spectra( - wavelengths=e_sun.T.index)["global"] + wavelengths=e_sun.T.index + )["global"] # interpolate the sr at the wavelengths of the spectra # reference spectrum wavelengths may differ if e_ref is from caller @@ -112,12 +114,16 @@ def integrate(e): return smm -def spectral_factor_firstsolar(precipitable_water, airmass_absolute, - module_type=None, coefficients=None, - min_precipitable_water=0.1, - max_precipitable_water=8, - min_airmass_absolute=0.58, - max_airmass_absolute=10): +def spectral_factor_firstsolar( + precipitable_water, + airmass_absolute, + module_type=None, + coefficients=None, + min_precipitable_water=0.1, + max_precipitable_water=8, + min_airmass_absolute=0.58, + max_airmass_absolute=10, +): r""" Spectral mismatch modifier based on precipitable water and absolute (pressure-adjusted) air mass. @@ -238,59 +244,105 @@ def spectral_factor_firstsolar(precipitable_water, airmass_absolute, January 2017 """ pw = np.atleast_1d(precipitable_water) - pw = pw.astype('float64') + pw = pw.astype("float64") if np.min(pw) < min_precipitable_water: pw = np.maximum(pw, min_precipitable_water) - warn('Low precipitable water values replaced with ' - f'{min_precipitable_water} cm in the calculation of spectral ' - 'mismatch.') + warn( + "Low precipitable water values replaced with " + f"{min_precipitable_water} cm in the calculation of spectral " + "mismatch." + ) if np.max(pw) > max_precipitable_water: pw[pw > max_precipitable_water] = np.nan - warn('High precipitable water values replaced with np.nan in ' - 'the calculation of spectral mismatch.') + warn( + "High precipitable water values replaced with np.nan in " + "the calculation of spectral mismatch." + ) airmass_absolute = np.minimum(airmass_absolute, max_airmass_absolute) if np.min(airmass_absolute) < min_airmass_absolute: airmass_absolute = np.maximum(airmass_absolute, min_airmass_absolute) - warn('Low airmass values replaced with 'f'{min_airmass_absolute} in ' - 'the calculation of spectral mismatch.') + warn( + "Low airmass values replaced with " + f"{min_airmass_absolute} in " + "the calculation of spectral mismatch." + ) # pvlib.atmosphere.get_absolute_airmass(1, # pvlib.atmosphere.alt2pres(4340)) = 0.58 Elevation of # Mina Pirquita, Argentian = 4340 m. Highest elevation city with # population over 50,000. _coefficients = {} - _coefficients['cdte'] = ( - 0.86273, -0.038948, -0.012506, 0.098871, 0.084658, -0.0042948) - _coefficients['monosi'] = ( - 0.85914, -0.020880, -0.0058853, 0.12029, 0.026814, -0.0017810) - _coefficients['xsi'] = _coefficients['monosi'] - _coefficients['polysi'] = ( - 0.84090, -0.027539, -0.0079224, 0.13570, 0.038024, -0.0021218) - _coefficients['multisi'] = _coefficients['polysi'] - _coefficients['cigs'] = ( - 0.85252, -0.022314, -0.0047216, 0.13666, 0.013342, -0.0008945) - _coefficients['asi'] = ( - 1.12094, -0.047620, -0.0083627, -0.10443, 0.098382, -0.0033818) + _coefficients["cdte"] = ( + 0.86273, + -0.038948, + -0.012506, + 0.098871, + 0.084658, + -0.0042948, + ) + _coefficients["monosi"] = ( + 0.85914, + -0.020880, + -0.0058853, + 0.12029, + 0.026814, + -0.0017810, + ) + _coefficients["xsi"] = _coefficients["monosi"] + _coefficients["polysi"] = ( + 0.84090, + -0.027539, + -0.0079224, + 0.13570, + 0.038024, + -0.0021218, + ) + _coefficients["multisi"] = _coefficients["polysi"] + _coefficients["cigs"] = ( + 0.85252, + -0.022314, + -0.0047216, + 0.13666, + 0.013342, + -0.0008945, + ) + _coefficients["asi"] = ( + 1.12094, + -0.047620, + -0.0083627, + -0.10443, + 0.098382, + -0.0033818, + ) if module_type is not None and coefficients is None: coefficients = _coefficients[module_type.lower()] elif module_type is None and coefficients is not None: pass elif module_type is None and coefficients is None: - raise TypeError('No valid input provided, both module_type and ' + - 'coefficients are None') + raise TypeError( + "No valid input provided, both module_type and " + + "coefficients are None" + ) else: - raise TypeError('Cannot resolve input, must supply only one of ' + - 'module_type and coefficients') + raise TypeError( + "Cannot resolve input, must supply only one of " + + "module_type and coefficients" + ) coeff = coefficients ama = airmass_absolute modifier = ( - coeff[0] + coeff[1]*ama + coeff[2]*pw + coeff[3]*np.sqrt(ama) + - coeff[4]*np.sqrt(pw) + coeff[5]*ama/np.sqrt(pw)) + coeff[0] + + coeff[1] * ama + + coeff[2] * pw + + coeff[3] * np.sqrt(ama) + + coeff[4] * np.sqrt(pw) + + coeff[5] * ama / np.sqrt(pw) + ) return modifier @@ -361,8 +413,13 @@ def spectral_factor_sapm(airmass_absolute, module): """ - am_coeff = [module['A4'], module['A3'], module['A2'], module['A1'], - module['A0']] + am_coeff = [ + module["A4"], + module["A3"], + module["A2"], + module["A1"], + module["A0"], + ] spectral_loss = np.polyval(am_coeff, airmass_absolute) @@ -376,8 +433,13 @@ def spectral_factor_sapm(airmass_absolute, module): return spectral_loss -def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, - module_type=None, coefficients=None): +def spectral_factor_caballero( + precipitable_water, + airmass_absolute, + aod500, + module_type=None, + coefficients=None, +): r""" Estimate a technology-specific spectral mismatch modifier from airmass, aerosol optical depth, and atmospheric precipitable water, @@ -435,33 +497,101 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, """ if module_type is None and coefficients is None: - raise ValueError('Must provide either `module_type` or `coefficients`') + raise ValueError("Must provide either `module_type` or `coefficients`") if module_type is not None and coefficients is not None: - raise ValueError('Only one of `module_type` and `coefficients` should ' - 'be provided') + raise ValueError( + "Only one of `module_type` and `coefficients` should " + "be provided" + ) # Experimental coefficients from [1]_. # The extra 0/1 coefficients at the end are used to enable/disable # terms to match the different equation forms in Table 1. _coefficients = {} - _coefficients['cdte'] = ( - 1.0044, 0.0095, -0.0037, 0.0002, 0.0000, -0.0046, - -0.0182, 0, 0.0095, 0.0068, 0, 1) - _coefficients['monosi'] = ( - 0.9706, 0.0377, -0.0123, 0.0025, -0.0002, 0.0159, - -0.0165, 0, -0.0016, -0.0027, 1, 0) - _coefficients['multisi'] = ( - 0.9836, 0.0254, -0.0085, 0.0016, -0.0001, 0.0094, - -0.0132, 0, -0.0002, -0.0011, 1, 0) - _coefficients['cigs'] = ( - 0.9801, 0.0283, -0.0092, 0.0019, -0.0001, 0.0117, - -0.0126, 0, -0.0011, -0.0019, 1, 0) - _coefficients['asi'] = ( - 1.1060, -0.0848, 0.0302, -0.0076, 0.0006, -0.1283, - 0.0986, -0.0254, 0.0156, 0.0146, 1, 0) - _coefficients['perovskite'] = ( - 1.0637, -0.0491, 0.0180, -0.0047, 0.0004, -0.0773, - 0.0583, -0.0159, 0.01251, 0.0109, 1, 0) + _coefficients["cdte"] = ( + 1.0044, + 0.0095, + -0.0037, + 0.0002, + 0.0000, + -0.0046, + -0.0182, + 0, + 0.0095, + 0.0068, + 0, + 1, + ) + _coefficients["monosi"] = ( + 0.9706, + 0.0377, + -0.0123, + 0.0025, + -0.0002, + 0.0159, + -0.0165, + 0, + -0.0016, + -0.0027, + 1, + 0, + ) + _coefficients["multisi"] = ( + 0.9836, + 0.0254, + -0.0085, + 0.0016, + -0.0001, + 0.0094, + -0.0132, + 0, + -0.0002, + -0.0011, + 1, + 0, + ) + _coefficients["cigs"] = ( + 0.9801, + 0.0283, + -0.0092, + 0.0019, + -0.0001, + 0.0117, + -0.0126, + 0, + -0.0011, + -0.0019, + 1, + 0, + ) + _coefficients["asi"] = ( + 1.1060, + -0.0848, + 0.0302, + -0.0076, + 0.0006, + -0.1283, + 0.0986, + -0.0254, + 0.0156, + 0.0146, + 1, + 0, + ) + _coefficients["perovskite"] = ( + 1.0637, + -0.0491, + 0.0180, + -0.0047, + 0.0004, + -0.0773, + 0.0583, + -0.0159, + 0.01251, + 0.0109, + 1, + 0, + ) if module_type is not None: coeff = _coefficients[module_type] @@ -488,16 +618,14 @@ def spectral_factor_caballero(precipitable_water, airmass_absolute, aod500, + coeff[7] * ama**2 ) # Eq 7, with Table 1 - f_PW = (precipitable_water - pw_ref) * ( - coeff[8] - + coeff[9] * np.log(ama) - ) + f_PW = (precipitable_water - pw_ref) * (coeff[8] + coeff[9] * np.log(ama)) modifier = f_AM + f_AOD + f_PW # Eq 5 return modifier -def spectral_factor_pvspec(airmass_absolute, clearsky_index, - module_type=None, coefficients=None): +def spectral_factor_pvspec( + airmass_absolute, clearsky_index, module_type=None, coefficients=None +): r""" Estimate a technology-specific spectral mismatch modifier from absolute airmass and clear sky index using the PVSPEC model. @@ -574,36 +702,41 @@ def spectral_factor_pvspec(airmass_absolute, clearsky_index, """ _coefficients = {} - _coefficients['multisi'] = (0.9847, -0.05237, 0.03034) - _coefficients['monosi'] = (0.9845, -0.05169, 0.03034) - _coefficients['fs-2'] = (1.002, -0.07108, 0.02465) - _coefficients['fs-4'] = (0.9981, -0.05776, 0.02336) - _coefficients['cigs'] = (0.9791, -0.03904, 0.03096) - _coefficients['asi'] = (1.051, -0.1033, 0.009838) + _coefficients["multisi"] = (0.9847, -0.05237, 0.03034) + _coefficients["monosi"] = (0.9845, -0.05169, 0.03034) + _coefficients["fs-2"] = (1.002, -0.07108, 0.02465) + _coefficients["fs-4"] = (0.9981, -0.05776, 0.02336) + _coefficients["cigs"] = (0.9791, -0.03904, 0.03096) + _coefficients["asi"] = (1.051, -0.1033, 0.009838) if module_type is not None and coefficients is None: coefficients = _coefficients[module_type.lower()] elif module_type is None and coefficients is not None: pass elif module_type is None and coefficients is None: - raise ValueError('No valid input provided, both module_type and ' + - 'coefficients are None. module_type can be one of ' + - ", ".join(_coefficients.keys())) + raise ValueError( + "No valid input provided, both module_type and " + + "coefficients are None. module_type can be one of " + + ", ".join(_coefficients.keys()) + ) else: - raise ValueError('Cannot resolve input, must supply only one of ' + - 'module_type and coefficients. module_type can be ' + - 'one of' ", ".join(_coefficients.keys())) + raise ValueError( + "Cannot resolve input, must supply only one of " + + "module_type and coefficients. module_type can be " + + "one of" ", ".join(_coefficients.keys()) + ) coeff = coefficients ama = airmass_absolute kc = clearsky_index - mismatch = coeff[0]*np.power(kc, coeff[1])*np.power(ama, coeff[2]) + mismatch = coeff[0] * np.power(kc, coeff[1]) * np.power(ama, coeff[2]) return mismatch -def spectral_factor_jrc(airmass, clearsky_index, module_type=None, - coefficients=None): +def spectral_factor_jrc( + airmass, clearsky_index, module_type=None, coefficients=None +): r""" Estimate a technology-specific spectral mismatch modifier from airmass and clear sky index using the JRC model. @@ -682,25 +815,29 @@ def spectral_factor_jrc(airmass, clearsky_index, module_type=None, """ _coefficients = {} - _coefficients['multisi'] = (0.00172, 0.000508, 0.00000357) - _coefficients['cdte'] = (0.000643, 0.000130, 0.0000108) + _coefficients["multisi"] = (0.00172, 0.000508, 0.00000357) + _coefficients["cdte"] = (0.000643, 0.000130, 0.0000108) # normalise coefficients by I*sc0, see [1] _coefficients = { - 'multisi': tuple(x / 0.00348 for x in _coefficients['multisi']), - 'cdte': tuple(x / 0.001150 for x in _coefficients['cdte']) + "multisi": tuple(x / 0.00348 for x in _coefficients["multisi"]), + "cdte": tuple(x / 0.001150 for x in _coefficients["cdte"]), } if module_type is not None and coefficients is None: coefficients = _coefficients[module_type.lower()] elif module_type is None and coefficients is not None: pass elif module_type is None and coefficients is None: - raise ValueError('No valid input provided, both module_type and ' + - 'coefficients are None. module_type can be one of ' + - ", ".join(_coefficients.keys())) + raise ValueError( + "No valid input provided, both module_type and " + + "coefficients are None. module_type can be one of " + + ", ".join(_coefficients.keys()) + ) else: - raise ValueError('Cannot resolve input, must supply only one of ' + - 'module_type and coefficients. module_type can be ' + - 'one of' ", ".join(_coefficients.keys())) + raise ValueError( + "Cannot resolve input, must supply only one of " + + "module_type and coefficients. module_type can be " + + "one of" ", ".join(_coefficients.keys()) + ) coeff = coefficients mismatch = ( diff --git a/pvlib/spectrum/response.py b/pvlib/spectrum/response.py index 4da92bb32a..41d09fa761 100644 --- a/pvlib/spectrum/response.py +++ b/pvlib/spectrum/response.py @@ -2,6 +2,7 @@ The ``response`` module in the ``spectrum`` package provides functions for spectral response and quantum efficiency calculations. """ + from pvlib.tools import normalize_max2one import numpy as np import pandas as pd @@ -18,7 +19,7 @@ def get_example_spectral_response(wavelength=None): - ''' + """ Generate a generic smooth spectral response (SR) for tests and experiments. Parameters @@ -46,38 +47,45 @@ def get_example_spectral_response(wavelength=None): .. [1] Driesse, Anton, and Stein, Joshua. "Global Normal Spectral Irradiance in Albuquerque: a One-Year Open Dataset for PV Research". United States 2020. :doi:`10.2172/1814068`. - ''' + """ # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022 - SR_DATA = np.array([[290, 0.00], - [350, 0.27], - [400, 0.37], - [500, 0.52], - [650, 0.71], - [800, 0.88], - [900, 0.97], - [950, 1.00], - [1000, 0.93], - [1050, 0.58], - [1100, 0.21], - [1150, 0.05], - [1190, 0.00]]).transpose() + SR_DATA = np.array( + [ + [290, 0.00], + [350, 0.27], + [400, 0.37], + [500, 0.52], + [650, 0.71], + [800, 0.88], + [900, 0.97], + [950, 1.00], + [1000, 0.93], + [1050, 0.58], + [1100, 0.21], + [1150, 0.05], + [1190, 0.00], + ] + ).transpose() if wavelength is None: resolution = 5.0 wavelength = np.arange(280, 1200 + resolution, resolution) - interpolator = interp1d(SR_DATA[0], SR_DATA[1], - kind='cubic', - bounds_error=False, - fill_value=0.0, - copy=False, - assume_sorted=True) + interpolator = interp1d( + SR_DATA[0], + SR_DATA[1], + kind="cubic", + bounds_error=False, + fill_value=0.0, + copy=False, + assume_sorted=True, + ) sr = pd.Series(data=interpolator(wavelength), index=wavelength) - sr.index.name = 'wavelength' - sr.name = 'spectral_response' + sr.index.name = "wavelength" + sr.name = "spectral_response" return sr diff --git a/pvlib/spectrum/spectrl2.py b/pvlib/spectrum/spectrl2.py index 38739efff3..52d22dbae3 100644 --- a/pvlib/spectrum/spectrl2.py +++ b/pvlib/spectrum/spectrl2.py @@ -8,84 +8,650 @@ import pandas as pd # SPECTRL2 extraterrestrial spectrum and atmospheric absorption coefficients -_SPECTRL2_COEFFS = np.zeros(122, dtype=np.dtype([ - ('wavelength', 'float64'), - ('spectral_irradiance_et', 'float64'), - ('water_vapor_absorption', 'float64'), - ('ozone_absorption', 'float64'), - ('mixed_absorption', 'float64'), -])) -_SPECTRL2_COEFFS['wavelength'] = [ # nm - 300.0, 305.0, 310.0, 315.0, 320.0, 325.0, 330.0, 335.0, 340.0, 345.0, - 350.0, 360.0, 370.0, 380.0, 390.0, 400.0, 410.0, 420.0, 430.0, 440.0, - 450.0, 460.0, 470.0, 480.0, 490.0, 500.0, 510.0, 520.0, 530.0, 540.0, - 550.0, 570.0, 593.0, 610.0, 630.0, 656.0, 667.6, 690.0, 710.0, 718.0, - 724.4, 740.0, 752.5, 757.5, 762.5, 767.5, 780.0, 800.0, 816.0, 823.7, - 831.5, 840.0, 860.0, 880.0, 905.0, 915.0, 925.0, 930.0, 937.0, 948.0, - 965.0, 980.0, 993.5, 1040.0, 1070.0, 1100.0, 1120.0, 1130.0, 1145.0, - 1161.0, 1170.0, 1200.0, 1240.0, 1270.0, 1290.0, 1320.0, 1350.0, 1395.0, - 1442.5, 1462.5, 1477.0, 1497.0, 1520.0, 1539.0, 1558.0, 1578.0, 1592.0, - 1610.0, 1630.0, 1646.0, 1678.0, 1740.0, 1800.0, 1860.0, 1920.0, 1960.0, - 1985.0, 2005.0, 2035.0, 2065.0, 2100.0, 2148.0, 2198.0, 2270.0, 2360.0, - 2450.0, 2500.0, 2600.0, 2700.0, 2800.0, 2900.0, 3000.0, 3100.0, 3200.0, - 3300.0, 3400.0, 3500.0, 3600.0, 3700.0, 3800.0, 3900.0, 4000.0 +_SPECTRL2_COEFFS = np.zeros( + 122, + dtype=np.dtype( + [ + ("wavelength", "float64"), + ("spectral_irradiance_et", "float64"), + ("water_vapor_absorption", "float64"), + ("ozone_absorption", "float64"), + ("mixed_absorption", "float64"), + ] + ), +) +_SPECTRL2_COEFFS["wavelength"] = [ # nm + 300.0, + 305.0, + 310.0, + 315.0, + 320.0, + 325.0, + 330.0, + 335.0, + 340.0, + 345.0, + 350.0, + 360.0, + 370.0, + 380.0, + 390.0, + 400.0, + 410.0, + 420.0, + 430.0, + 440.0, + 450.0, + 460.0, + 470.0, + 480.0, + 490.0, + 500.0, + 510.0, + 520.0, + 530.0, + 540.0, + 550.0, + 570.0, + 593.0, + 610.0, + 630.0, + 656.0, + 667.6, + 690.0, + 710.0, + 718.0, + 724.4, + 740.0, + 752.5, + 757.5, + 762.5, + 767.5, + 780.0, + 800.0, + 816.0, + 823.7, + 831.5, + 840.0, + 860.0, + 880.0, + 905.0, + 915.0, + 925.0, + 930.0, + 937.0, + 948.0, + 965.0, + 980.0, + 993.5, + 1040.0, + 1070.0, + 1100.0, + 1120.0, + 1130.0, + 1145.0, + 1161.0, + 1170.0, + 1200.0, + 1240.0, + 1270.0, + 1290.0, + 1320.0, + 1350.0, + 1395.0, + 1442.5, + 1462.5, + 1477.0, + 1497.0, + 1520.0, + 1539.0, + 1558.0, + 1578.0, + 1592.0, + 1610.0, + 1630.0, + 1646.0, + 1678.0, + 1740.0, + 1800.0, + 1860.0, + 1920.0, + 1960.0, + 1985.0, + 2005.0, + 2035.0, + 2065.0, + 2100.0, + 2148.0, + 2198.0, + 2270.0, + 2360.0, + 2450.0, + 2500.0, + 2600.0, + 2700.0, + 2800.0, + 2900.0, + 3000.0, + 3100.0, + 3200.0, + 3300.0, + 3400.0, + 3500.0, + 3600.0, + 3700.0, + 3800.0, + 3900.0, + 4000.0, ] -_SPECTRL2_COEFFS['spectral_irradiance_et'] = [ # W/m^2/nm - 0.5359, 0.5583, 0.622, 0.6927, 0.7151, 0.8329, 0.9619, 0.9319, 0.9006, - 0.9113, 0.9755, 0.9759, 1.1199, 1.1038, 1.0338, 1.4791, 1.7013, 1.7404, - 1.5872, 1.837, 2.005, 2.043, 1.987, 2.027, 1.896, 1.909, 1.927, 1.831, - 1.891, 1.898, 1.892, 1.84, 1.768, 1.728, 1.658, 1.524, 1.531, 1.42, - 1.399, 1.374, 1.373, 1.298, 1.269, 1.245, 1.223, 1.205, 1.183, 1.148, - 1.091, 1.062, 1.038, 1.022, 0.9987, 0.9472, 0.8932, 0.8682, 0.8297, - 0.8303, 0.814, 0.7869, 0.7683, 0.767, 0.7576, 0.6881, 0.6407, 0.6062, - 0.5859, 0.5702, 0.5641, 0.5442, 0.5334, 0.5016, 0.4775, 0.4427, 0.44, - 0.4168, 0.3914, 0.3589, 0.3275, 0.3175, 0.3073, 0.3004, 0.2928, 0.2755, - 0.2721, 0.2593, 0.2469, 0.244, 0.2435, 0.2348, 0.2205, 0.1908, 0.1711, - 0.1445, 0.1357, 0.123, 0.1238, 0.113, 0.1085, 0.0975, 0.0924, 0.0824, - 0.0746, 0.0683, 0.0638, 0.0495, 0.0485, 0.0386, 0.0366, 0.032, 0.0281, - 0.0248, 0.0221, 0.0196, 0.0175, 0.0157, 0.0141, 0.0127, 0.0115, 0.0104, - 0.0095, 0.0086 +_SPECTRL2_COEFFS["spectral_irradiance_et"] = [ # W/m^2/nm + 0.5359, + 0.5583, + 0.622, + 0.6927, + 0.7151, + 0.8329, + 0.9619, + 0.9319, + 0.9006, + 0.9113, + 0.9755, + 0.9759, + 1.1199, + 1.1038, + 1.0338, + 1.4791, + 1.7013, + 1.7404, + 1.5872, + 1.837, + 2.005, + 2.043, + 1.987, + 2.027, + 1.896, + 1.909, + 1.927, + 1.831, + 1.891, + 1.898, + 1.892, + 1.84, + 1.768, + 1.728, + 1.658, + 1.524, + 1.531, + 1.42, + 1.399, + 1.374, + 1.373, + 1.298, + 1.269, + 1.245, + 1.223, + 1.205, + 1.183, + 1.148, + 1.091, + 1.062, + 1.038, + 1.022, + 0.9987, + 0.9472, + 0.8932, + 0.8682, + 0.8297, + 0.8303, + 0.814, + 0.7869, + 0.7683, + 0.767, + 0.7576, + 0.6881, + 0.6407, + 0.6062, + 0.5859, + 0.5702, + 0.5641, + 0.5442, + 0.5334, + 0.5016, + 0.4775, + 0.4427, + 0.44, + 0.4168, + 0.3914, + 0.3589, + 0.3275, + 0.3175, + 0.3073, + 0.3004, + 0.2928, + 0.2755, + 0.2721, + 0.2593, + 0.2469, + 0.244, + 0.2435, + 0.2348, + 0.2205, + 0.1908, + 0.1711, + 0.1445, + 0.1357, + 0.123, + 0.1238, + 0.113, + 0.1085, + 0.0975, + 0.0924, + 0.0824, + 0.0746, + 0.0683, + 0.0638, + 0.0495, + 0.0485, + 0.0386, + 0.0366, + 0.032, + 0.0281, + 0.0248, + 0.0221, + 0.0196, + 0.0175, + 0.0157, + 0.0141, + 0.0127, + 0.0115, + 0.0104, + 0.0095, + 0.0086, ] -_SPECTRL2_COEFFS['water_vapor_absorption'] = [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.075, 0.0, 0.0, 0.0, 0.0, 0.016, 0.0125, 1.8, 2.5, 0.061, - 0.0008, 0.0001, 1e-05, 1e-05, 0.0006, 0.036, 1.6, 2.5, 0.5, 0.155, 1e-05, - 0.0026, 7.0, 5.0, 5.0, 27.0, 55.0, 45.0, 4.0, 1.48, 0.1, 1e-05, 0.001, 3.2, - 115.0, 70.0, 75.0, 10.0, 5.0, 2.0, 0.002, 0.002, 0.1, 4.0, 200.0, 1000.0, - 185.0, 80.0, 80.0, 12.0, 0.16, 0.002, 0.0005, 0.0001, 1e-05, 0.0001, 0.001, - 0.01, 0.036, 1.1, 130.0, 1000.0, 500.0, 100.0, 4.0, 2.9, 1.0, 0.4, 0.22, - 0.25, 0.33, 0.5, 4.0, 80.0, 310.0, 15000.0, 22000.0, 8000.0, 650.0, 240.0, - 230.0, 100.0, 120.0, 19.5, 3.6, 3.1, 2.5, 1.4, 0.17, 0.0045 +_SPECTRL2_COEFFS["water_vapor_absorption"] = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.075, + 0.0, + 0.0, + 0.0, + 0.0, + 0.016, + 0.0125, + 1.8, + 2.5, + 0.061, + 0.0008, + 0.0001, + 1e-05, + 1e-05, + 0.0006, + 0.036, + 1.6, + 2.5, + 0.5, + 0.155, + 1e-05, + 0.0026, + 7.0, + 5.0, + 5.0, + 27.0, + 55.0, + 45.0, + 4.0, + 1.48, + 0.1, + 1e-05, + 0.001, + 3.2, + 115.0, + 70.0, + 75.0, + 10.0, + 5.0, + 2.0, + 0.002, + 0.002, + 0.1, + 4.0, + 200.0, + 1000.0, + 185.0, + 80.0, + 80.0, + 12.0, + 0.16, + 0.002, + 0.0005, + 0.0001, + 1e-05, + 0.0001, + 0.001, + 0.01, + 0.036, + 1.1, + 130.0, + 1000.0, + 500.0, + 100.0, + 4.0, + 2.9, + 1.0, + 0.4, + 0.22, + 0.25, + 0.33, + 0.5, + 4.0, + 80.0, + 310.0, + 15000.0, + 22000.0, + 8000.0, + 650.0, + 240.0, + 230.0, + 100.0, + 120.0, + 19.5, + 3.6, + 3.1, + 2.5, + 1.4, + 0.17, + 0.0045, ] -_SPECTRL2_COEFFS['ozone_absorption'] = [ - 10.0, 4.8, 2.7, 1.35, 0.8, 0.38, 0.16, 0.075, 0.04, 0.019, 0.007, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.003, 0.006, 0.009, 0.014, 0.021, 0.03, - 0.04, 0.048, 0.063, 0.075, 0.085, 0.12, 0.119, 0.12, 0.09, 0.065, 0.051, - 0.028, 0.018, 0.015, 0.012, 0.01, 0.008, 0.007, 0.006, 0.005, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 +_SPECTRL2_COEFFS["ozone_absorption"] = [ + 10.0, + 4.8, + 2.7, + 1.35, + 0.8, + 0.38, + 0.16, + 0.075, + 0.04, + 0.019, + 0.007, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.003, + 0.006, + 0.009, + 0.014, + 0.021, + 0.03, + 0.04, + 0.048, + 0.063, + 0.075, + 0.085, + 0.12, + 0.119, + 0.12, + 0.09, + 0.065, + 0.051, + 0.028, + 0.018, + 0.015, + 0.012, + 0.01, + 0.008, + 0.007, + 0.006, + 0.005, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, ] -_SPECTRL2_COEFFS['mixed_absorption'] = [ - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, - 0.35, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05, 0.3, - 0.02, 0.0002, 0.00011, 1e-05, 0.05, 0.011, 0.005, 0.0006, 0.0, 0.005, 0.13, - 0.04, 0.06, 0.13, 0.001, 0.0014, 0.0001, 1e-05, 1e-05, 0.0001, 0.001, 4.3, - 0.2, 21.0, 0.13, 1.0, 0.08, 0.001, 0.00038, 0.001, 0.0005, 0.00015, - 0.00014, 0.00066, 100.0, 150.0, 0.13, 0.0095, 0.001, 0.8, 1.9, 1.3, 0.075, - 0.01, 0.00195, 0.004, 0.29, 0.025 +_SPECTRL2_COEFFS["mixed_absorption"] = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 4.0, + 0.35, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.05, + 0.3, + 0.02, + 0.0002, + 0.00011, + 1e-05, + 0.05, + 0.011, + 0.005, + 0.0006, + 0.0, + 0.005, + 0.13, + 0.04, + 0.06, + 0.13, + 0.001, + 0.0014, + 0.0001, + 1e-05, + 1e-05, + 0.0001, + 0.001, + 4.3, + 0.2, + 21.0, + 0.13, + 1.0, + 0.08, + 0.001, + 0.00038, + 0.001, + 0.0005, + 0.00015, + 0.00014, + 0.00066, + 100.0, + 150.0, + 0.13, + 0.0095, + 0.001, + 0.8, + 1.9, + 1.3, + 0.075, + 0.01, + 0.00195, + 0.004, + 0.29, + 0.025, ] -def _spectrl2_transmittances(apparent_zenith, relative_airmass, - surface_pressure, precipitable_water, ozone, - optical_thickness, scattering_albedo, dayofyear): +def _spectrl2_transmittances( + apparent_zenith, + relative_airmass, + surface_pressure, + precipitable_water, + ozone, + optical_thickness, + scattering_albedo, + dayofyear, +): """ Calculate transmittance factors from Section 2 of Bird and Riordan 1984. @@ -107,17 +673,17 @@ def _spectrl2_transmittances(apparent_zenith, relative_airmass, Array with shape (122, N) where N is len(apparent_zenith) """ # add a dimension so that each ndarray is 2d with shape (122, 1) - wavelength = _SPECTRL2_COEFFS['wavelength'][:, np.newaxis] - vapor_coeff = _SPECTRL2_COEFFS['water_vapor_absorption'][:, np.newaxis] - ozone_coeff = _SPECTRL2_COEFFS['ozone_absorption'][:, np.newaxis] - mixed_coeff = _SPECTRL2_COEFFS['mixed_absorption'][:, np.newaxis] + wavelength = _SPECTRL2_COEFFS["wavelength"][:, np.newaxis] + vapor_coeff = _SPECTRL2_COEFFS["water_vapor_absorption"][:, np.newaxis] + ozone_coeff = _SPECTRL2_COEFFS["ozone_absorption"][:, np.newaxis] + mixed_coeff = _SPECTRL2_COEFFS["mixed_absorption"][:, np.newaxis] # ET spectral irradiance correction for earth-sun distance seasonality. # Note that we only want the distance correction coefficient, so set # solar_constant=1: - earth_sun_distance_correction = \ - pvlib.irradiance.get_extra_radiation(dayofyear, method='spencer', - solar_constant=1) # Eq 2-2, 2-3 + earth_sun_distance_correction = pvlib.irradiance.get_extra_radiation( + dayofyear, method="spencer", solar_constant=1 + ) # Eq 2-2, 2-3 # Rayleigh scattering # note: 101300 is used for consistentcy with reference; can't use # atmosphere.get_absolute_airmass because it uses 101325 @@ -134,13 +700,13 @@ def _spectrl2_transmittances(apparent_zenith, relative_airmass, # Water vapor absorption, Eq 2-8 aWM = vapor_coeff * precipitable_water * relative_airmass - vapor_transmittance = np.exp(-0.2385 * aWM / (1 + 20.07 * aWM)**0.45) + vapor_transmittance = np.exp(-0.2385 * aWM / (1 + 20.07 * aWM) ** 0.45) # Ozone absorption ozone_max_height = 22 h0_norm = ozone_max_height / 6370 - ozone_mass_numerator = (1 + h0_norm) - ozone_mass_denominator = np.sqrt(cosd(apparent_zenith)**2 + 2 * h0_norm) + ozone_mass_numerator = 1 + h0_norm + ozone_mass_denominator = np.sqrt(cosd(apparent_zenith) ** 2 + 2 * h0_norm) ozone_mass = ozone_mass_numerator / ozone_mass_denominator # Eq 2-10 ozone_transmittance = np.exp(-ozone_coeff * ozone * ozone_mass) # Eq 2-9 @@ -148,7 +714,7 @@ def _spectrl2_transmittances(apparent_zenith, relative_airmass, aM = mixed_coeff * airmass # Note: the report uses 118.93, but spectrl2_2.c uses 118.3 # mixed_transmittance = np.exp(-1.41 * aM / (1 + 118.93 * aM)**0.45) - mixed_transmittance = np.exp(-1.41 * aM / (1 + 118.3 * aM)**0.45) + mixed_transmittance = np.exp(-1.41 * aM / (1 + 118.3 * aM) ** 0.45) # split out aerosol components for diffuse irradiance calcs aerosol_scattering = np.exp( @@ -171,11 +737,22 @@ def _spectrl2_transmittances(apparent_zenith, relative_airmass, ) -def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, - surface_pressure, relative_airmass, precipitable_water, ozone, - aerosol_turbidity_500nm, dayofyear=None, - scattering_albedo_400nm=0.945, alpha=1.14, - wavelength_variation_factor=0.095, aerosol_asymmetry_factor=0.65): +def spectrl2( + apparent_zenith, + aoi, + surface_tilt, + ground_albedo, + surface_pressure, + relative_airmass, + precipitable_water, + ozone, + aerosol_turbidity_500nm, + dayofyear=None, + scattering_albedo_400nm=0.945, + alpha=1.14, + wavelength_variation_factor=0.095, + aerosol_asymmetry_factor=0.65, +): """ Estimate spectral irradiance using the Bird Simple Spectral Model (SPECTRL2). @@ -279,39 +856,74 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, is_pandas = isinstance(apparent_zenith, pd.Series) if is_pandas: original_index = apparent_zenith.index - (apparent_zenith, aoi, surface_tilt, ground_albedo, surface_pressure, - relative_airmass, precipitable_water, ozone, aerosol_turbidity_500nm, - scattering_albedo_400nm, alpha, wavelength_variation_factor, - aerosol_asymmetry_factor) = \ - tuple(map(np.asanyarray, [ - apparent_zenith, aoi, surface_tilt, ground_albedo, - surface_pressure, relative_airmass, precipitable_water, ozone, - aerosol_turbidity_500nm, scattering_albedo_400nm, alpha, - wavelength_variation_factor, aerosol_asymmetry_factor])) + ( + apparent_zenith, + aoi, + surface_tilt, + ground_albedo, + surface_pressure, + relative_airmass, + precipitable_water, + ozone, + aerosol_turbidity_500nm, + scattering_albedo_400nm, + alpha, + wavelength_variation_factor, + aerosol_asymmetry_factor, + ) = tuple( + map( + np.asanyarray, + [ + apparent_zenith, + aoi, + surface_tilt, + ground_albedo, + surface_pressure, + relative_airmass, + precipitable_water, + ozone, + aerosol_turbidity_500nm, + scattering_albedo_400nm, + alpha, + wavelength_variation_factor, + aerosol_asymmetry_factor, + ], + ) + ) dayofyear = pvlib.tools._pandas_to_doy(original_index).values if not is_pandas and dayofyear is None: - raise ValueError('dayofyear must be specified if not using pandas ' - 'Series inputs') + raise ValueError( + "dayofyear must be specified if not using pandas " "Series inputs" + ) # add a dimension so that each ndarray is 2d with shape (122, 1) - wavelength = _SPECTRL2_COEFFS['wavelength'][:, np.newaxis] - spectrum_et = _SPECTRL2_COEFFS['spectral_irradiance_et'][:, np.newaxis] + wavelength = _SPECTRL2_COEFFS["wavelength"][:, np.newaxis] + spectrum_et = _SPECTRL2_COEFFS["spectral_irradiance_et"][:, np.newaxis] - optical_thickness = \ - pvlib.atmosphere.angstrom_aod_at_lambda(aod0=aerosol_turbidity_500nm, - lambda0=500, alpha=alpha, - lambda1=wavelength) # Eq 2-7 + optical_thickness = pvlib.atmosphere.angstrom_aod_at_lambda( + aod0=aerosol_turbidity_500nm, + lambda0=500, + alpha=alpha, + lambda1=wavelength, + ) # Eq 2-7 # Eq 3-16 - scattering_albedo = scattering_albedo_400nm * \ - np.exp(-wavelength_variation_factor * np.log(wavelength / 400)**2) + scattering_albedo = scattering_albedo_400nm * np.exp( + -wavelength_variation_factor * np.log(wavelength / 400) ** 2 + ) - spectrl2 = _spectrl2_transmittances(apparent_zenith, relative_airmass, - surface_pressure, precipitable_water, - ozone, optical_thickness, - scattering_albedo, dayofyear) + spectrl2 = _spectrl2_transmittances( + apparent_zenith, + relative_airmass, + surface_pressure, + precipitable_water, + ozone, + optical_thickness, + scattering_albedo, + dayofyear, + ) D, Tr, Ta, Tw, To, Tu, Tas, Taa = spectrl2 spectrum_et_adj = spectrum_et * D @@ -320,7 +932,7 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, cosZ = cosd(apparent_zenith) # Eq 3-17 - Cs = np.where(wavelength <= 450, ((wavelength + 550)/1000)**1.8, 1.0) + Cs = np.where(wavelength <= 450, ((wavelength + 550) / 1000) ** 1.8, 1.0) ALG = np.log(1 - aerosol_asymmetry_factor) # Eq 3-14 BFS = ALG * (0.0783 + ALG * (-0.3824 - ALG * 0.5874)) # Eq 3-13 AFS = ALG * (1.459 + ALG * (0.1595 + ALG * 0.4129)) # Eq 3-12 @@ -328,10 +940,16 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, Fsp = 1 - 0.5 * np.exp((AFS + BFS / 1.8) / 1.8) # Eq 3.15 # evaluate the "primed terms" -- transmittances evaluated at airmass=1.8 - primes = _spectrl2_transmittances(apparent_zenith, 1.8, - surface_pressure, precipitable_water, - ozone, optical_thickness, - scattering_albedo, dayofyear) + primes = _spectrl2_transmittances( + apparent_zenith, + 1.8, + surface_pressure, + precipitable_water, + ozone, + optical_thickness, + scattering_albedo, + dayofyear, + ) _, Trp, Tap, Twp, Top, Tup, Tasp, Taap = primes # Note: not sure what the correct form of this equation is. @@ -339,7 +957,7 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, # spectrl2_2.c uses Tu'. sky_reflectivity = ( # Top * Twp * Taap * (0.5 * (1-Trp) + (1-Fsp) * Trp * (1-Tasp)) - Tup * Twp * Taap * (0.5 * (1-Trp) + (1-Fsp) * Trp * (1-Tasp)) + Tup * Twp * Taap * (0.5 * (1 - Trp) + (1 - Fsp) * Trp * (1 - Tasp)) ) # Eq 3-8 # a common factor for 3-5 and 3-6 @@ -373,12 +991,14 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, # don't need surface_azimuth if we provide projection_ratio. # Also constrain cos zenith to avoid blowup, as in irradiance.haydavies projection_ratio = aoi_projection_nn / np.maximum(cosZ, 0.01745) - Isky = pvlib.irradiance.haydavies(surface_tilt=surface_tilt, - surface_azimuth=None, - dhi=Is, - dni=Id, - dni_extra=spectrum_et_adj, - projection_ratio=projection_ratio) + Isky = pvlib.irradiance.haydavies( + surface_tilt=surface_tilt, + surface_azimuth=None, + dhi=Is, + dni=Id, + dni_extra=spectrum_et_adj, + projection_ratio=projection_ratio, + ) ghi = Id * cosZ + Is Iground = pvlib.irradiance.get_ground_diffuse(surface_tilt, ghi, albedo=rg) @@ -386,12 +1006,12 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo, Itilt = Ibeam + Isky + Iground wavelength_1d = wavelength.reshape(-1) # only needs 1 dimension return { - 'wavelength': wavelength_1d, - 'dni_extra': spectrum_et_adj, - 'dhi': Is, - 'dni': Id, - 'poa_sky_diffuse': Isky, - 'poa_ground_diffuse': Iground, - 'poa_direct': Ibeam, - 'poa_global': Itilt, + "wavelength": wavelength_1d, + "dni_extra": spectrum_et_adj, + "dhi": Is, + "dni": Id, + "poa_sky_diffuse": Isky, + "poa_ground_diffuse": Iground, + "poa_direct": Ibeam, + "poa_global": Itilt, } diff --git a/pvlib/temperature.py b/pvlib/temperature.py index ab31ffaf56..53dd889987 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -6,7 +6,6 @@ import numpy as np import pandas as pd from pvlib.tools import sind -from pvlib._deprecation import warn_deprecated from pvlib.tools import _get_sample_intervals import scipy import scipy.constants @@ -14,14 +13,20 @@ TEMPERATURE_MODEL_PARAMETERS = { - 'sapm': { - 'open_rack_glass_glass': {'a': -3.47, 'b': -.0594, 'deltaT': 3}, - 'close_mount_glass_glass': {'a': -2.98, 'b': -.0471, 'deltaT': 1}, - 'open_rack_glass_polymer': {'a': -3.56, 'b': -.0750, 'deltaT': 3}, - 'insulated_back_glass_polymer': {'a': -2.81, 'b': -.0455, 'deltaT': 0}, + "sapm": { + "open_rack_glass_glass": {"a": -3.47, "b": -0.0594, "deltaT": 3}, + "close_mount_glass_glass": {"a": -2.98, "b": -0.0471, "deltaT": 1}, + "open_rack_glass_polymer": {"a": -3.56, "b": -0.0750, "deltaT": 3}, + "insulated_back_glass_polymer": { + "a": -2.81, + "b": -0.0455, + "deltaT": 0, + }, + }, + "pvsyst": { + "freestanding": {"u_c": 29.0, "u_v": 0}, + "insulated": {"u_c": 15.0, "u_v": 0}, }, - 'pvsyst': {'freestanding': {'u_c': 29.0, 'u_v': 0}, - 'insulated': {'u_c': 15.0, 'u_v': 0}} } """Dictionary of temperature parameters organized by model. @@ -47,16 +52,19 @@ def _temperature_model_params(model, parameter_set): params = TEMPERATURE_MODEL_PARAMETERS[model] return params[parameter_set] except KeyError: - msg = ('{} is not a named set of parameters for the {} cell' - ' temperature model.' - ' See pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS' - ' for names'.format(parameter_set, model)) + msg = ( + "{} is not a named set of parameters for the {} cell" + " temperature model." + " See pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS" + " for names".format(parameter_set, model) + ) raise KeyError(msg) -def sapm_cell(poa_global, temp_air, wind_speed, a, b, deltaT, - irrad_ref=1000.): - r''' +def sapm_cell( + poa_global, temp_air, wind_speed, a, b, deltaT, irrad_ref=1000.0 +): + r""" Calculate cell temperature per the Sandia Array Performance Model. See [1]_ for details on the Sandia Array Performance Model. @@ -154,15 +162,15 @@ def sapm_cell(poa_global, temp_air, wind_speed, a, b, deltaT, >>> params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] >>> sapm_cell(1000, 10, 0, **params) 44.11703066106086 - ''' - module_temperature = sapm_module(poa_global, temp_air, wind_speed, - a, b) - return sapm_cell_from_module(module_temperature, poa_global, deltaT, - irrad_ref) + """ + module_temperature = sapm_module(poa_global, temp_air, wind_speed, a, b) + return sapm_cell_from_module( + module_temperature, poa_global, deltaT, irrad_ref + ) def sapm_module(poa_global, temp_air, wind_speed, a, b): - r''' + r""" Calculate module back surface temperature per the Sandia Array Performance Model. @@ -240,13 +248,14 @@ def sapm_module(poa_global, temp_air, wind_speed, a, b): -------- sapm_cell sapm_cell_from_module - ''' + """ return poa_global * np.exp(a + b * wind_speed) + temp_air -def sapm_cell_from_module(module_temperature, poa_global, deltaT, - irrad_ref=1000.): - r''' +def sapm_cell_from_module( + module_temperature, poa_global, deltaT, irrad_ref=1000.0 +): + r""" Calculate cell temperature from module temperature using the Sandia Array Performance Model. @@ -322,12 +331,19 @@ def sapm_cell_from_module(module_temperature, poa_global, deltaT, -------- sapm_cell sapm_module - ''' + """ return module_temperature + (poa_global / irrad_ref) * deltaT -def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0, - module_efficiency=0.1, alpha_absorption=0.9): +def pvsyst_cell( + poa_global, + temp_air, + wind_speed=1.0, + u_c=29.0, + u_v=0.0, + module_efficiency=0.1, + alpha_absorption=0.9, +): r""" Calculate cell temperature using an empirical heat loss factor model as implemented in PVsyst. @@ -430,7 +446,7 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0, def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84): - r''' + r""" Calculate cell or module temperature using the Faiman model. The Faiman model uses an empirical heat loss factor model [1]_ and is @@ -490,7 +506,7 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84): -------- pvlib.temperature.faiman_rad - ''' # noQA: E501 + """ # noQA: E501 # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Dec., 2019 @@ -506,9 +522,17 @@ def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84): return temp_air + temp_difference -def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None, - u0=25.0, u1=6.84, sky_view=1.0, emissivity=0.88): - r''' +def faiman_rad( + poa_global, + temp_air, + wind_speed=1.0, + ir_down=None, + u0=25.0, + u1=6.84, + sky_view=1.0, + emissivity=0.88, +): + r""" Calculate cell or module temperature using the Faiman model augmented with a radiative loss term. @@ -596,7 +620,7 @@ def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None, -------- pvlib.temperature.faiman - ''' # noQA: E501 + """ # noQA: E501 # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2022 @@ -606,7 +630,7 @@ def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None, if ir_down is None: qrad_sky = 0.0 else: - ir_up = sigma * ((temp_air - abs_zero)**4) + ir_up = sigma * ((temp_air - abs_zero) ** 4) qrad_sky = emissivity * sky_view * (ir_up - ir_down) heat_input = poa_global - qrad_sky @@ -616,7 +640,7 @@ def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None, def ross(poa_global, temp_air, noct): - r''' + r""" Calculate cell temperature using the Ross model. The Ross model [1]_ assumes the difference between cell temperature @@ -657,13 +681,14 @@ def ross(poa_global, temp_air, noct): .. [1] Ross, R. G. Jr., (1981). "Design Techniques for Flat-Plate Photovoltaic Arrays". 15th IEEE Photovoltaic Specialist Conference, Orlando, FL. - ''' + """ # factor of 0.1 converts irradiance from W/m2 to mW/cm2 - return temp_air + (noct - 20.) / 80. * poa_global * 0.1 + return temp_air + (noct - 20.0) / 80.0 * poa_global * 0.1 -def _fuentes_hconv(tave, windmod, tinoct, temp_delta, xlen, tilt, - check_reynold): +def _fuentes_hconv( + tave, windmod, tinoct, temp_delta, xlen, tilt, check_reynold +): # Calculate the convective coefficient as in Fuentes 1987 -- a mixture of # free, laminar, and turbulent convection. densair = 0.003484 * 101325.0 / tave # density @@ -682,9 +707,9 @@ def _fuentes_hconv(tave, windmod, tinoct, temp_delta, xlen, tilt, # NB: Fuentes hardwires sind(tilt) as 0.5 for tilt=30 grashof = 9.8 / tave * temp_delta * xlen**3 / visair**2 * sind(tilt) # product of Nusselt number and (k/l) - hfree = 0.21 * (grashof * 0.71)**0.32 * condair / xlen + hfree = 0.21 * (grashof * 0.71) ** 0.32 * condair / xlen # combine free and forced components - hconv = (hfree**3 + hforce**3)**(1/3) + hconv = (hfree**3 + hforce**3) ** (1 / 3) return hconv @@ -693,9 +718,19 @@ def _hydraulic_diameter(width, height): return 2 * (width * height) / (width + height) -def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, - wind_height=9.144, emissivity=0.84, absorption=0.83, - surface_tilt=30, module_width=0.31579, module_length=1.2): +def fuentes( + poa_global, + temp_air, + wind_speed, + noct_installed, + module_height=5, + wind_height=9.144, + emissivity=0.84, + absorption=0.83, + surface_tilt=30, + module_width=0.31579, + module_length=1.2, +): """ Calculate cell or module temperature using the Fuentes model. @@ -785,8 +820,9 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, # convective coefficient of top surface of module at NOCT windmod = 1.0 tave = (tinoct + 293.15) / 2 - hconv = _fuentes_hconv(tave, windmod, tinoct, tinoct - 293.15, xlen, - surface_tilt, False) + hconv = _fuentes_hconv( + tave, windmod, tinoct, tinoct - 293.15, xlen, surface_tilt, False + ) # determine the ground temperature ratio and the ratio of the total # convection to the top side convection @@ -796,12 +832,13 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, - emiss * boltz * (tinoct**4 - 282.21**4) - hconv * (tinoct - 293.15) ) / ((hground + hconv) * (tinoct - 293.15)) - tground = (tinoct**4 - backrat * (tinoct**4 - 293.15**4))**0.25 + tground = (tinoct**4 - backrat * (tinoct**4 - 293.15**4)) ** 0.25 tground = np.clip(tground, 293.15, tinoct) tgrat = (tground - 293.15) / (tinoct - 293.15) - convrat = (absorp * 800 - emiss * boltz * ( - 2 * tinoct**4 - 282.21**4 - tground**4)) / (hconv * (tinoct - 293.15)) + convrat = ( + absorp * 800 - emiss * boltz * (2 * tinoct**4 - 282.21**4 - tground**4) + ) / (hconv * (tinoct - 293.15)) # adjust the capacitance (thermal mass) of the module based on the INOCT. # It is a function of INOCT because high INOCT implies thermal coupling @@ -830,13 +867,14 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, # wind speed at module height -- Equation 22 # not sure why the 1e-4 factor is included -- maybe the equations don't # behave well if wind == 0? - windmod_array = wind_speed * (module_height/wind_height)**0.2 + 1e-4 + windmod_array = wind_speed * (module_height / wind_height) ** 0.2 + 1e-4 tmod0 = 293.15 tmod_array = np.zeros_like(poa_global) - iterator = zip(tamb_array, sun_array, windmod_array, tsky_array, - timedelta_hours) + iterator = zip( + tamb_array, sun_array, windmod_array, tsky_array, timedelta_hours + ) for i, (tamb, sun, windmod, tsky, dtime) in enumerate(iterator): # solve the heat transfer equation, iterating because the heat loss # terms depend on tmod. NB Fuentes doesn't show that 10 iterations is @@ -845,16 +883,22 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, for j in range(10): # overall convective coefficient tave = (tmod + tamb) / 2 - hconv = convrat * _fuentes_hconv(tave, windmod, tinoct, - abs(tmod-tamb), xlen, - surface_tilt, True) + hconv = convrat * _fuentes_hconv( + tave, + windmod, + tinoct, + abs(tmod - tamb), + xlen, + surface_tilt, + True, + ) # sky radiation coefficient (Equation 3) hsky = emiss * boltz * (tmod**2 + tsky**2) * (tmod + tsky) # ground radiation coeffieicient (Equation 4) tground = tamb + tgrat * (tmod - tamb) hground = emiss * boltz * (tmod**2 + tground**2) * (tmod + tground) # thermal lag -- Equation 8 - eigen = - (hconv + hsky + hground) / cap * dtime * 3600 + eigen = -(hconv + hsky + hground) / cap * dtime * 3600 # not sure why this check is done, maybe as a speed optimization? if eigen > -10: ex = np.exp(eigen) @@ -863,35 +907,54 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5, # Equation 7 -- note that `sun` and `sun0` already account for # absorption (alpha) tmod = tmod0 * ex + ( - (1 - ex) * ( + (1 - ex) + * ( hconv * tamb + hsky * tsky + hground * tground + sun0 + (sun - sun0) / eigen - ) + sun - sun0 + ) + + sun + - sun0 ) / (hconv + hsky + hground) tmod_array[i] = tmod tmod0 = tmod sun0 = sun - return pd.Series(tmod_array - 273.15, index=poa_global.index, name='tmod') + return pd.Series(tmod_array - 273.15, index=poa_global.index, name="tmod") def _adj_for_mounting_standoff(x): # supports noct cell temperature function. Except for x > 3.5, the SAM code # and documentation aren't clear on the precise intervals. The choice of # < or <= here is pvlib's. - return np.piecewise(x, [x <= 0, (x > 0) & (x < 0.5), - (x >= 0.5) & (x < 1.5), (x >= 1.5) & (x < 2.5), - (x >= 2.5) & (x <= 3.5), x > 3.5], - [0., 18., 11., 6., 2., 0.]) - - -def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, - effective_irradiance=None, transmittance_absorptance=0.9, - array_height=1, mount_standoff=4): - r''' + return np.piecewise( + x, + [ + x <= 0, + (x > 0) & (x < 0.5), + (x >= 0.5) & (x < 1.5), + (x >= 1.5) & (x < 2.5), + (x >= 2.5) & (x <= 3.5), + x > 3.5, + ], + [0.0, 18.0, 11.0, 6.0, 2.0, 0.0], + ) + + +def noct_sam( + poa_global, + temp_air, + wind_speed, + noct, + module_efficiency, + effective_irradiance=None, + transmittance_absorptance=0.9, + array_height=1, + mount_standoff=4, +): + r""" Cell temperature model from the System Advisor Model (SAM). The model is described in [1]_, Section 10.6. @@ -952,7 +1015,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, Ryberg, D., 2018, "SAM Photovoltaic Model Technical Reference Update", National Renewable Energy Laboratory Report NREL/TP-6A20-67399. - ''' + """ # in [1] the denominator for irr_ratio isn't precisely clear. From # reproducing output of the SAM function noct_celltemp_t, we determined # that: @@ -961,7 +1024,7 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, # - Geff_total (SAM) is POA irradiance after reflections and # adjustment for spectrum. Equivalent to effective_irradiance if effective_irradiance is None: - irr_ratio = 1. + irr_ratio = 1.0 else: irr_ratio = effective_irradiance / poa_global @@ -971,14 +1034,15 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency, wind_adj = 0.61 * wind_speed else: raise ValueError( - f'array_height must be 1 or 2, {array_height} was given') + f"array_height must be 1 or 2, {array_height} was given" + ) noct_adj = noct + _adj_for_mounting_standoff(mount_standoff) tau_alpha = transmittance_absorptance * irr_ratio # [1] Eq. 10.37 isn't clear on exactly what "G" is. SAM SSC code uses # poa_global where G appears - cell_temp_init = poa_global / 800. * (noct_adj - 20.) + cell_temp_init = poa_global / 800.0 * (noct_adj - 20.0) heat_loss = 1 - module_efficiency / tau_alpha wind_loss = 9.5 / (5.7 + 3.8 * wind_adj) return temp_air + cell_temp_init * heat_loss * wind_loss @@ -1042,14 +1106,17 @@ def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): """ # `sample_interval` in minutes: - sample_interval, samples_per_window = \ - _get_sample_intervals(times=temp_cell.index, win_length=20) + sample_interval, samples_per_window = _get_sample_intervals( + times=temp_cell.index, win_length=20 + ) if sample_interval >= 20: - warnings.warn("temperature.prilliman only applies smoothing when " - "the sampling interval is shorter than 20 minutes " - f"(input sampling interval: {sample_interval} minutes);" - " returning input temperature series unchanged") + warnings.warn( + "temperature.prilliman only applies smoothing when " + "the sampling interval is shorter than 20 minutes " + f"(input sampling interval: {sample_interval} minutes);" + " returning input temperature series unchanged" + ) # too coarsely sampled for smoothing to be relevant return temp_cell @@ -1062,9 +1129,10 @@ def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): temp_cell_prefixed = np.append(prefix, temp_cell.values) # generate matrix of integers for creating windows with indexing - H = scipy.linalg.hankel(np.arange(samples_per_window), - np.arange(samples_per_window - 1, - len(temp_cell_prefixed) - 1)) + H = scipy.linalg.hankel( + np.arange(samples_per_window), + np.arange(samples_per_window - 1, len(temp_cell_prefixed) - 1), + ) # each row of `subsets` is the values in one window subsets = temp_cell_prefixed[H].T @@ -1087,7 +1155,12 @@ def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): a = [0.0046, 0.00046, -0.00023, -1.6e-5] wind_speed = wind_speed.values - p = a[0] + a[1]*wind_speed + a[2]*unit_mass + a[3]*wind_speed*unit_mass + p = ( + a[0] + + a[1] * wind_speed + + a[2] * unit_mass + + a[3] * wind_speed * unit_mass + ) # calculate the time lag for each sample in the window, paying attention # to units (seconds for `timedeltas`, minutes for `sample_interval`) timedeltas = np.arange(samples_per_window, 0, -1) * sample_interval * 60 @@ -1138,8 +1211,15 @@ def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): return smoothed -def generic_linear(poa_global, temp_air, wind_speed, u_const, du_wind, - module_efficiency, absorptance): +def generic_linear( + poa_global, + temp_air, + wind_speed, + u_const, + du_wind, + module_efficiency, + absorptance, +): """ Calculate cell temperature using a generic linear heat loss factor model. @@ -1194,8 +1274,8 @@ def generic_linear(poa_global, temp_air, wind_speed, u_const, du_wind, return temp_air + temp_difference -class GenericLinearModel(): - ''' +class GenericLinearModel: + """ A class that can both use and convert parameters of linear module temperature models: faiman, pvsyst, noct_sam, sapm_module and generic_linear. @@ -1254,11 +1334,11 @@ class GenericLinearModel(): See also -------- pvlib.temperature.generic_linear - ''' + """ + # Contributed by Anton Driesse (@adriesse), PV Performance Labs, Sept. 2022 def __init__(self, module_efficiency, absorptance): - self.u_const = np.nan self.du_wind = np.nan self.eta = module_efficiency @@ -1267,12 +1347,12 @@ def __init__(self, module_efficiency, absorptance): return None def __repr__(self): + return self.__class__.__name__ + ": " + vars(self).__repr__() - return self.__class__.__name__ + ': ' + vars(self).__repr__() - - def __call__(self, poa_global, temp_air, wind_speed, - module_efficiency=None): - ''' + def __call__( + self, poa_global, temp_air, wind_speed, module_efficiency=None + ): + """ Calculate module temperature using the generic_linear model and previously initialized parameters. @@ -1300,16 +1380,22 @@ def __call__(self, poa_global, temp_air, wind_speed, -------- get_generic pvlib.temperature.generic_linear - ''' + """ if module_efficiency is None: module_efficiency = self.eta - return generic_linear(poa_global, temp_air, wind_speed, - self.u_const, self.du_wind, - module_efficiency, self.alpha) + return generic_linear( + poa_global, + temp_air, + wind_speed, + self.u_const, + self.du_wind, + module_efficiency, + self.alpha, + ) def get_generic_linear(self): - ''' + """ Get the generic linear model parameters to use with the separate generic linear module temperature calculation function. @@ -1320,21 +1406,23 @@ def get_generic_linear(self): See also -------- pvlib.temperature.generic_linear - ''' - return dict(u_const=self.u_const, - du_wind=self.du_wind, - module_efficiency=self.eta, - absorptance=self.alpha) + """ + return dict( + u_const=self.u_const, + du_wind=self.du_wind, + module_efficiency=self.eta, + absorptance=self.alpha, + ) def use_faiman(self, u0, u1): - ''' + """ Use the Faiman model parameters to set the generic_model equivalents. Parameters ---------- u0, u1 : float See :py:func:`pvlib.temperature.faiman` for details. - ''' + """ net_absorptance = self.alpha - self.eta self.u_const = u0 * net_absorptance self.du_wind = u1 * net_absorptance @@ -1342,7 +1430,7 @@ def use_faiman(self, u0, u1): return self def to_faiman(self): - ''' + """ Convert the generic model parameters to Faiman equivalents. Returns @@ -1350,16 +1438,17 @@ def to_faiman(self): model_parameters : dict See :py:func:`pvlib.temperature.faiman` for model parameter details. - ''' + """ net_absorptance = self.alpha - self.eta u0 = self.u_const / net_absorptance u1 = self.du_wind / net_absorptance return dict(u0=u0, u1=u1) - def use_pvsyst(self, u_c, u_v, module_efficiency=None, - alpha_absorption=None): - ''' + def use_pvsyst( + self, u_c, u_v, module_efficiency=None, alpha_absorption=None + ): + """ Use the PVsyst model parameters to set the generic_model equivalents. Parameters @@ -1374,7 +1463,7 @@ def use_pvsyst(self, u_c, u_v, module_efficiency=None, ----- The optional parameters are primarily for convenient compatibility with existing function signatures. - ''' + """ if module_efficiency is not None: self.eta = module_efficiency @@ -1391,7 +1480,7 @@ def use_pvsyst(self, u_c, u_v, module_efficiency=None, return self def to_pvsyst(self): - ''' + """ Convert the generic model parameters to PVsyst model equivalents. Returns @@ -1399,7 +1488,7 @@ def to_pvsyst(self): model_parameters : dict See :py:func:`pvlib.temperature.pvsyst_cell` for model parameter details. - ''' + """ net_absorptance_glm = self.alpha - self.eta net_absorptance_pvsyst = self.alpha * (1.0 - self.eta) absorptance_ratio = net_absorptance_glm / net_absorptance_pvsyst @@ -1407,14 +1496,17 @@ def to_pvsyst(self): u_c = self.u_const / absorptance_ratio u_v = self.du_wind / absorptance_ratio - return dict(u_c=u_c, - u_v=u_v, - module_efficiency=self.eta, - alpha_absorption=self.alpha) - - def use_noct_sam(self, noct, module_efficiency=None, - transmittance_absorptance=None): - ''' + return dict( + u_c=u_c, + u_v=u_v, + module_efficiency=self.eta, + alpha_absorption=self.alpha, + ) + + def use_noct_sam( + self, noct, module_efficiency=None, transmittance_absorptance=None + ): + """ Use the NOCT SAM model parameters to set the generic_model equivalents. Parameters @@ -1429,7 +1521,7 @@ def use_noct_sam(self, noct, module_efficiency=None, ----- The optional parameters are primarily for convenient compatibility with existing function signatures. - ''' + """ if module_efficiency is not None: self.eta = module_efficiency @@ -1446,7 +1538,7 @@ def use_noct_sam(self, noct, module_efficiency=None, return self def to_noct_sam(self): - ''' + """ Convert the generic model parameters to NOCT SAM model equivalents. Returns @@ -1454,19 +1546,21 @@ def to_noct_sam(self): model_parameters : dict See :py:func:`pvlib.temperature.noct_sam` for model parameter details. - ''' + """ # NOCT is determined with wind speed near module height # the adjustment reduces the wind coefficient for use with 10m wind wind_adj = 0.51 u_noct = self.u_const + self.du_wind / wind_adj noct = 20.0 + (800.0 * self.alpha) / u_noct - return dict(noct=noct, - module_efficiency=self.eta, - transmittance_absorptance=self.alpha) + return dict( + noct=noct, + module_efficiency=self.eta, + transmittance_absorptance=self.alpha, + ) def use_sapm(self, a, b, wind_fit_low=1.4, wind_fit_high=5.4): - ''' + """ Use the SAPM model parameters to set the generic_model equivalents. In the SAPM the heat transfer coefficient increases exponentially @@ -1494,7 +1588,7 @@ def use_sapm(self, a, b, wind_fit_low=1.4, wind_fit_high=5.4): at 10 m height. Both the SAPM model and the conversion functions can work with wind speed data at different heights as long as the same height is used consistently throughout. - ''' + """ u_low = 1.0 / np.exp(a + b * wind_fit_low) u_high = 1.0 / np.exp(a + b * wind_fit_high) @@ -1508,7 +1602,7 @@ def use_sapm(self, a, b, wind_fit_low=1.4, wind_fit_high=5.4): return self def to_sapm(self, wind_fit_low=1.4, wind_fit_high=5.4): - ''' + """ Convert the generic model parameters to SAPM model equivalents. In the SAPM the heat transfer coefficient increases exponentially @@ -1539,7 +1633,7 @@ def to_sapm(self, wind_fit_low=1.4, wind_fit_high=5.4): at 10 m height. Both the SAPM model and the conversion functions can work with wind speed data at different heights as long as the same height is used consistently throughout. - ''' + """ net_absorptance = self.alpha - self.eta u_const = self.u_const / net_absorptance du_wind = self.du_wind / net_absorptance @@ -1547,8 +1641,9 @@ def to_sapm(self, wind_fit_low=1.4, wind_fit_high=5.4): u_low = u_const + du_wind * wind_fit_low u_high = u_const + du_wind * wind_fit_high - b = - ((np.log(u_high) - np.log(u_low)) / - (wind_fit_high - wind_fit_low)) - a = - (np.log(u_low) + b * wind_fit_low) + b = -( + (np.log(u_high) - np.log(u_low)) / (wind_fit_high - wind_fit_low) + ) + a = -(np.log(u_low) + b * wind_fit_low) return dict(a=a, b=b) diff --git a/pvlib/tests/bifacial/test_infinite_sheds.py b/pvlib/tests/bifacial/test_infinite_sheds.py index 1f6dadfd5f..474d5fecab 100644 --- a/pvlib/tests/bifacial/test_infinite_sheds.py +++ b/pvlib/tests/bifacial/test_infinite_sheds.py @@ -12,80 +12,91 @@ @pytest.fixture def test_system(): - syst = {'height': 1.0, - 'pitch': 2., - 'surface_tilt': 30., - 'surface_azimuth': 180., - 'rotation': -30.} # rotation of right edge relative to horizontal - syst['gcr'] = 1.0 / syst['pitch'] + syst = { + "height": 1.0, + "pitch": 2.0, + "surface_tilt": 30.0, + "surface_azimuth": 180.0, + "rotation": -30.0, + } # rotation of right edge relative to horizontal + syst["gcr"] = 1.0 / syst["pitch"] pts = np.linspace(0, 1, num=3) sqr3 = np.sqrt(3) / 4 # c_i,j = cos(angle from point i to edge of row j), j=0 is row = -1 # c_i,j = cos(angle from point i to edge of row j), j=0 is row = -1 - c00 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3)**2) # right edge row -1 + c00 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3) ** 2) # right edge row -1 c01 = -sqr3 / np.sqrt(1.25**2 + sqr3**2) # right edge row 0 c02 = sqr3 / np.sqrt(0.75**2 + sqr3**2) # left edge of row 0 - c03 = (2 - sqr3) / np.sqrt(1.25**2 + (2 - sqr3)**2) # right edge of row 1 + c03 = (2 - sqr3) / np.sqrt( + 1.25**2 + (2 - sqr3) ** 2 + ) # right edge of row 1 vf_0 = 0.5 * (c03 - c02 + c01 - c00) # vf at point 0 - c10 = (-3 - sqr3) / np.sqrt(1.25**2 + (3 + sqr3)**2) # right edge row -1 - c11 = (-1 - sqr3) / np.sqrt(1.25**2 + (1 + sqr3)**2) # right edge row 0 - c12 = (-1 + sqr3) / np.sqrt(0.75**2 + (-1 + sqr3)**2) # left edge row 0 - c13 = (1 - sqr3) / np.sqrt(1.25**2 + (1 - sqr3)**2) # right edge row + c10 = (-3 - sqr3) / np.sqrt(1.25**2 + (3 + sqr3) ** 2) # right edge row -1 + c11 = (-1 - sqr3) / np.sqrt(1.25**2 + (1 + sqr3) ** 2) # right edge row 0 + c12 = (-1 + sqr3) / np.sqrt(0.75**2 + (-1 + sqr3) ** 2) # left edge row 0 + c13 = (1 - sqr3) / np.sqrt(1.25**2 + (1 - sqr3) ** 2) # right edge row vf_1 = 0.5 * (c13 - c12 + c11 - c10) # vf at point 1 - c20 = -(4 + sqr3) / np.sqrt(1.25**2 + (4 + sqr3)**2) # right edge row -1 - c21 = (-2 + sqr3) / np.sqrt(0.75**2 + (-2 + sqr3)**2) # left edge row 0 - c22 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3)**2) # right edge row 0 - c23 = (0 - sqr3) / np.sqrt(1.25**2 + (0 - sqr3)**2) # right edge row 1 + c20 = -(4 + sqr3) / np.sqrt(1.25**2 + (4 + sqr3) ** 2) # right edge row -1 + c21 = (-2 + sqr3) / np.sqrt(0.75**2 + (-2 + sqr3) ** 2) # left edge row 0 + c22 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3) ** 2) # right edge row 0 + c23 = (0 - sqr3) / np.sqrt(1.25**2 + (0 - sqr3) ** 2) # right edge row 1 vf_2 = 0.5 * (c23 - c22 + c21 - c20) # vf at point 1 vfs_ground_sky = np.array([vf_0, vf_1, vf_2]) return syst, pts, vfs_ground_sky def test__poa_ground_shadows(): - poa_ground, f_gnd_beam, df, vf_gnd_sky = (300., 0.5, 0.5, 0.2) + poa_ground, f_gnd_beam, df, vf_gnd_sky = (300.0, 0.5, 0.5, 0.2) result = infinite_sheds._poa_ground_shadows( - poa_ground, f_gnd_beam, df, vf_gnd_sky) - expected = 300. * (0.5 * 0.5 + 0.5 * 0.2) + poa_ground, f_gnd_beam, df, vf_gnd_sky + ) + expected = 300.0 * (0.5 * 0.5 + 0.5 * 0.2) assert np.isclose(result, expected) # vector inputs - poa_ground = np.array([300., 300.]) + poa_ground = np.array([300.0, 300.0]) f_gnd_beam = np.array([0.5, 0.5]) - df = np.array([0.5, 0.]) + df = np.array([0.5, 0.0]) vf_gnd_sky = np.array([0.2, 0.2]) result = infinite_sheds._poa_ground_shadows( - poa_ground, f_gnd_beam, df, vf_gnd_sky) - expected_vec = np.array([expected, 300. * 0.5]) + poa_ground, f_gnd_beam, df, vf_gnd_sky + ) + expected_vec = np.array([expected, 300.0 * 0.5]) assert np.allclose(result, expected_vec) def test__shaded_fraction_floats(): result = infinite_sheds._shaded_fraction( - solar_zenith=60., solar_azimuth=180., surface_tilt=60., - surface_azimuth=180., gcr=1.0) + solar_zenith=60.0, + solar_azimuth=180.0, + surface_tilt=60.0, + surface_azimuth=180.0, + gcr=1.0, + ) assert np.isclose(result, 0.5) def test__shaded_fraction_array(): - solar_zenith = np.array([0., 60., 90., 60.]) - solar_azimuth = np.array([180., 180., 180., 180.]) - surface_azimuth = np.array([180., 180., 180., 210.]) - surface_tilt = np.array([30., 60., 0., 30.]) + solar_zenith = np.array([0.0, 60.0, 90.0, 60.0]) + solar_azimuth = np.array([180.0, 180.0, 180.0, 180.0]) + surface_azimuth = np.array([180.0, 180.0, 180.0, 210.0]) + surface_tilt = np.array([30.0, 60.0, 0.0, 30.0]) gcr = 1.0 result = infinite_sheds._shaded_fraction( - solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr) + solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr + ) x = 0.75 + np.sqrt(3) / 2 - expected = np.array([0.0, 0.5, 0., (x - 1) / x]) + expected = np.array([0.0, 0.5, 0.0, (x - 1) / x]) assert np.allclose(result, expected) def test_get_irradiance_poa(): # singleton inputs - solar_zenith = 0. - solar_azimuth = 180. - surface_tilt = 0. - surface_azimuth = 180. + solar_zenith = 0.0 + solar_azimuth = 180.0 + surface_tilt = 0.0 + surface_azimuth = 180.0 gcr = 0.5 - height = 1. + height = 1.0 pitch = 1 ghi = 1000 dhi = 300 @@ -94,224 +105,373 @@ def test_get_irradiance_poa(): iam = 1.0 npoints = 100 res = infinite_sheds.get_irradiance_poa( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, - albedo, iam=iam, npoints=npoints) - expected_diffuse = np.array([300.]) - expected_direct = np.array([700.]) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam=iam, + npoints=npoints, + ) + expected_diffuse = np.array([300.0]) + expected_direct = np.array([700.0]) expected_global = expected_diffuse + expected_direct - expected_shaded_fraction = np.array([0.]) - assert np.isclose(res['poa_global'], expected_global) - assert np.isclose(res['poa_diffuse'], expected_diffuse) - assert np.isclose(res['poa_direct'], expected_direct) - assert np.isclose(res['shaded_fraction'], expected_shaded_fraction) + expected_shaded_fraction = np.array([0.0]) + assert np.isclose(res["poa_global"], expected_global) + assert np.isclose(res["poa_diffuse"], expected_diffuse) + assert np.isclose(res["poa_direct"], expected_direct) + assert np.isclose(res["shaded_fraction"], expected_shaded_fraction) # vector inputs - surface_tilt = np.array([0., 0., 0., 0.]) - height = 1. - surface_azimuth = np.array([180., 180., 180., 180.]) + surface_tilt = np.array([0.0, 0.0, 0.0, 0.0]) + height = 1.0 + surface_azimuth = np.array([180.0, 180.0, 180.0, 180.0]) gcr = 0.5 pitch = 1 - solar_zenith = np.array([0., 45., 45., 90.]) - solar_azimuth = np.array([180., 180., 135., 180.]) - expected_diffuse = np.array([300., 300., 300., 300.]) + solar_zenith = np.array([0.0, 45.0, 45.0, 90.0]) + solar_azimuth = np.array([180.0, 180.0, 135.0, 180.0]) + expected_diffuse = np.array([300.0, 300.0, 300.0, 300.0]) expected_direct = np.array( - [700., 350. * np.sqrt(2), 350. * np.sqrt(2), 0.]) + [700.0, 350.0 * np.sqrt(2), 350.0 * np.sqrt(2), 0.0] + ) expected_global = expected_diffuse + expected_direct - expected_shaded_fraction = np.array( - [0., 0., 0., 0.]) + expected_shaded_fraction = np.array([0.0, 0.0, 0.0, 0.0]) res = infinite_sheds.get_irradiance_poa( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, - albedo, iam=iam, npoints=npoints) - assert np.allclose(res['poa_global'], expected_global) - assert np.allclose(res['poa_diffuse'], expected_diffuse) - assert np.allclose(res['poa_direct'], expected_direct) - assert np.allclose(res['shaded_fraction'], expected_shaded_fraction) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam=iam, + npoints=npoints, + ) + assert np.allclose(res["poa_global"], expected_global) + assert np.allclose(res["poa_diffuse"], expected_diffuse) + assert np.allclose(res["poa_direct"], expected_direct) + assert np.allclose(res["shaded_fraction"], expected_shaded_fraction) # series inputs surface_tilt = pd.Series(surface_tilt) surface_azimuth = pd.Series(data=surface_azimuth, index=surface_tilt.index) solar_zenith = pd.Series(solar_zenith, index=surface_tilt.index) solar_azimuth = pd.Series(data=solar_azimuth, index=surface_tilt.index) expected_diffuse = pd.Series( - data=expected_diffuse, index=surface_tilt.index) - expected_direct = pd.Series( - data=expected_direct, index=surface_tilt.index) + data=expected_diffuse, index=surface_tilt.index + ) + expected_direct = pd.Series(data=expected_direct, index=surface_tilt.index) expected_global = expected_diffuse + expected_direct - expected_global.name = 'poa_global' # to match output Series + expected_global.name = "poa_global" # to match output Series expected_shaded_fraction = pd.Series( - data=expected_shaded_fraction, index=surface_tilt.index) - expected_shaded_fraction.name = 'shaded_fraction' # to match output Series + data=expected_shaded_fraction, index=surface_tilt.index + ) + expected_shaded_fraction.name = "shaded_fraction" # to match output Series res = infinite_sheds.get_irradiance_poa( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, - albedo, iam=iam, npoints=npoints) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam=iam, + npoints=npoints, + ) assert isinstance(res, pd.DataFrame) - assert_series_equal(res['poa_global'], expected_global) - assert_series_equal(res['shaded_fraction'], expected_shaded_fraction) - assert all(k in res.columns for k in [ - 'poa_global', 'poa_diffuse', 'poa_direct', 'poa_ground_diffuse', - 'poa_sky_diffuse', 'shaded_fraction']) + assert_series_equal(res["poa_global"], expected_global) + assert_series_equal(res["shaded_fraction"], expected_shaded_fraction) + assert all( + k in res.columns + for k in [ + "poa_global", + "poa_diffuse", + "poa_direct", + "poa_ground_diffuse", + "poa_sky_diffuse", + "shaded_fraction", + ] + ) def test__backside_tilt(): - tilt = np.array([0., 30., 30., 180.]) - system_azimuth = np.array([180., 150., 270., 0.]) + tilt = np.array([0.0, 30.0, 30.0, 180.0]) + system_azimuth = np.array([180.0, 150.0, 270.0, 0.0]) back_tilt, back_az = infinite_sheds._backside(tilt, system_azimuth) - assert np.allclose(back_tilt, np.array([180., 150., 150., 0.])) - assert np.allclose(back_az, np.array([0., 330., 90., 180.])) + assert np.allclose(back_tilt, np.array([180.0, 150.0, 150.0, 0.0])) + assert np.allclose(back_az, np.array([0.0, 330.0, 90.0, 180.0])) @pytest.mark.parametrize("vectorize", [True, False]) def test_get_irradiance(vectorize): # singleton inputs - solar_zenith = 0. - solar_azimuth = 180. - surface_tilt = 0. - surface_azimuth = 180. + solar_zenith = 0.0 + solar_azimuth = 180.0 + surface_tilt = 0.0 + surface_azimuth = 180.0 gcr = 0.5 - height = 1. - pitch = 1. - ghi = 1000. - dhi = 300. - dni = 700. - albedo = 0. + height = 1.0 + pitch = 1.0 + ghi = 1000.0 + dhi = 300.0 + dni = 700.0 + albedo = 0.0 iam_front = 1.0 iam_back = 1.0 npoints = 100 result = infinite_sheds.get_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back, - bifaciality=0.8, shade_factor=-0.02, transmission_factor=0, - npoints=npoints, vectorize=vectorize) - expected_front_diffuse = np.array([300.]) - expected_front_direct = np.array([700.]) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam_front, + iam_back, + bifaciality=0.8, + shade_factor=-0.02, + transmission_factor=0, + npoints=npoints, + vectorize=vectorize, + ) + expected_front_diffuse = np.array([300.0]) + expected_front_direct = np.array([700.0]) expected_front_global = expected_front_diffuse + expected_front_direct - expected_shaded_fraction_front = np.array([0.]) - expected_shaded_fraction_back = np.array([0.]) - assert np.isclose(result['poa_front'], expected_front_global) - assert np.isclose(result['poa_front_diffuse'], expected_front_diffuse) - assert np.isclose(result['poa_front_direct'], expected_front_direct) - assert np.isclose(result['poa_global'], result['poa_front']) - assert np.isclose(result['shaded_fraction_front'], - expected_shaded_fraction_front) - assert np.isclose(result['shaded_fraction_back'], - expected_shaded_fraction_back) + expected_shaded_fraction_front = np.array([0.0]) + expected_shaded_fraction_back = np.array([0.0]) + assert np.isclose(result["poa_front"], expected_front_global) + assert np.isclose(result["poa_front_diffuse"], expected_front_diffuse) + assert np.isclose(result["poa_front_direct"], expected_front_direct) + assert np.isclose(result["poa_global"], result["poa_front"]) + assert np.isclose( + result["shaded_fraction_front"], expected_shaded_fraction_front + ) + assert np.isclose( + result["shaded_fraction_back"], expected_shaded_fraction_back + ) # series inputs - ghi = pd.Series([1000., 500., 500., np.nan]) - dhi = pd.Series([300., 500., 500., 500.], index=ghi.index) - dni = pd.Series([700., 0., 0., 700.], index=ghi.index) - solar_zenith = pd.Series([0., 0., 0., 135.], index=ghi.index) - surface_tilt = pd.Series([0., 0., 90., 0.], index=ghi.index) + ghi = pd.Series([1000.0, 500.0, 500.0, np.nan]) + dhi = pd.Series([300.0, 500.0, 500.0, 500.0], index=ghi.index) + dni = pd.Series([700.0, 0.0, 0.0, 700.0], index=ghi.index) + solar_zenith = pd.Series([0.0, 0.0, 0.0, 135.0], index=ghi.index) + surface_tilt = pd.Series([0.0, 0.0, 90.0, 0.0], index=ghi.index) result = infinite_sheds.get_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back, - bifaciality=0.8, shade_factor=-0.02, transmission_factor=0, - npoints=npoints, vectorize=vectorize) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam_front, + iam_back, + bifaciality=0.8, + shade_factor=-0.02, + transmission_factor=0, + npoints=npoints, + vectorize=vectorize, + ) result_front = infinite_sheds.get_irradiance_poa( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, - albedo, iam=iam_front, vectorize=vectorize) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam=iam_front, + vectorize=vectorize, + ) assert isinstance(result, pd.DataFrame) expected_poa_global = pd.Series( - [1000., 500., result_front['poa_global'][2] * (1 + 0.8 * 0.98), - np.nan], index=ghi.index, name='poa_global') + [ + 1000.0, + 500.0, + result_front["poa_global"][2] * (1 + 0.8 * 0.98), + np.nan, + ], + index=ghi.index, + name="poa_global", + ) expected_shaded_fraction = pd.Series( - result_front['shaded_fraction'], index=ghi.index, - name='shaded_fraction_front') - assert_series_equal(result['poa_global'], expected_poa_global) - assert_series_equal(result['shaded_fraction_front'], - expected_shaded_fraction) + result_front["shaded_fraction"], + index=ghi.index, + name="shaded_fraction_front", + ) + assert_series_equal(result["poa_global"], expected_poa_global) + assert_series_equal( + result["shaded_fraction_front"], expected_shaded_fraction + ) def test_get_irradiance_limiting_gcr(): # test confirms that irradiance on widely spaced rows is approximately # the same as for a single row array - solar_zenith = 0. - solar_azimuth = 180. - surface_tilt = 90. - surface_azimuth = 180. + solar_zenith = 0.0 + solar_azimuth = 180.0 + surface_tilt = 90.0 + surface_azimuth = 180.0 gcr = 0.00001 - height = 1. - pitch = 100. - ghi = 1000. - dhi = 300. - dni = 700. - albedo = 1. + height = 1.0 + pitch = 100.0 + ghi = 1000.0 + dhi = 300.0 + dni = 700.0 + albedo = 1.0 iam_front = 1.0 iam_back = 1.0 npoints = 100 result = infinite_sheds.get_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, albedo, iam_front, iam_back, - bifaciality=1., shade_factor=-0.00, transmission_factor=0., - npoints=npoints) - expected_ground_diffuse = np.array([500.]) - expected_sky_diffuse = np.array([150.]) - expected_direct = np.array([0.]) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + iam_front, + iam_back, + bifaciality=1.0, + shade_factor=-0.00, + transmission_factor=0.0, + npoints=npoints, + ) + expected_ground_diffuse = np.array([500.0]) + expected_sky_diffuse = np.array([150.0]) + expected_direct = np.array([0.0]) expected_diffuse = expected_ground_diffuse + expected_sky_diffuse expected_poa = expected_diffuse + expected_direct - expected_shaded_fraction_front = np.array([0.]) - expected_shaded_fraction_back = np.array([0.]) - assert np.isclose(result['poa_front'], expected_poa, rtol=0.01) - assert np.isclose(result['poa_front_diffuse'], expected_diffuse, rtol=0.01) - assert np.isclose(result['poa_front_direct'], expected_direct) - assert np.isclose(result['poa_front_sky_diffuse'], expected_sky_diffuse, - rtol=0.01) - assert np.isclose(result['poa_front_ground_diffuse'], - expected_ground_diffuse, rtol=0.01) - assert np.isclose(result['poa_front'], result['poa_back']) - assert np.isclose(result['poa_front_diffuse'], result['poa_back_diffuse']) - assert np.isclose(result['poa_front_direct'], result['poa_back_direct']) - assert np.isclose(result['poa_front_sky_diffuse'], - result['poa_back_sky_diffuse']) - assert np.isclose(result['poa_front_ground_diffuse'], - result['poa_back_ground_diffuse']) - assert np.isclose(result['shaded_fraction_front'], - expected_shaded_fraction_front) - assert np.isclose(result['shaded_fraction_back'], - expected_shaded_fraction_back) + expected_shaded_fraction_front = np.array([0.0]) + expected_shaded_fraction_back = np.array([0.0]) + assert np.isclose(result["poa_front"], expected_poa, rtol=0.01) + assert np.isclose(result["poa_front_diffuse"], expected_diffuse, rtol=0.01) + assert np.isclose(result["poa_front_direct"], expected_direct) + assert np.isclose( + result["poa_front_sky_diffuse"], expected_sky_diffuse, rtol=0.01 + ) + assert np.isclose( + result["poa_front_ground_diffuse"], expected_ground_diffuse, rtol=0.01 + ) + assert np.isclose(result["poa_front"], result["poa_back"]) + assert np.isclose(result["poa_front_diffuse"], result["poa_back_diffuse"]) + assert np.isclose(result["poa_front_direct"], result["poa_back_direct"]) + assert np.isclose( + result["poa_front_sky_diffuse"], result["poa_back_sky_diffuse"] + ) + assert np.isclose( + result["poa_front_ground_diffuse"], result["poa_back_ground_diffuse"] + ) + assert np.isclose( + result["shaded_fraction_front"], expected_shaded_fraction_front + ) + assert np.isclose( + result["shaded_fraction_back"], expected_shaded_fraction_back + ) def test_get_irradiance_with_haydavies(): # singleton inputs - solar_zenith = 0. - solar_azimuth = 180. - surface_tilt = 0. - surface_azimuth = 180. + solar_zenith = 0.0 + solar_azimuth = 180.0 + surface_tilt = 0.0 + surface_azimuth = 180.0 gcr = 0.5 - height = 1. - pitch = 1. - ghi = 1000. - dhi = 300. - dni = 700. - albedo = 0. - dni_extra = 1413. - model = 'haydavies' + height = 1.0 + pitch = 1.0 + ghi = 1000.0 + dhi = 300.0 + dni = 700.0 + albedo = 0.0 + dni_extra = 1413.0 + model = "haydavies" iam_front = 1.0 iam_back = 1.0 npoints = 100 result = infinite_sheds.get_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, albedo, model, dni_extra, - iam_front, iam_back, bifaciality=0.8, shade_factor=-0.02, - transmission_factor=0, npoints=npoints) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + model, + dni_extra, + iam_front, + iam_back, + bifaciality=0.8, + shade_factor=-0.02, + transmission_factor=0, + npoints=npoints, + ) expected_front_diffuse = np.array([151.38]) expected_front_direct = np.array([848.62]) expected_front_global = expected_front_diffuse + expected_front_direct - expected_shaded_fraction_front = np.array([0.]) - expected_shaded_fraction_back = np.array([0.]) - assert np.isclose(result['poa_front'], expected_front_global) - assert np.isclose(result['poa_front_diffuse'], expected_front_diffuse) - assert np.isclose(result['poa_front_direct'], expected_front_direct) - assert np.isclose(result['poa_global'], result['poa_front']) - assert np.isclose(result['shaded_fraction_front'], - expected_shaded_fraction_front) - assert np.isclose(result['shaded_fraction_back'], - expected_shaded_fraction_back) + expected_shaded_fraction_front = np.array([0.0]) + expected_shaded_fraction_back = np.array([0.0]) + assert np.isclose(result["poa_front"], expected_front_global) + assert np.isclose(result["poa_front_diffuse"], expected_front_diffuse) + assert np.isclose(result["poa_front_direct"], expected_front_direct) + assert np.isclose(result["poa_global"], result["poa_front"]) + assert np.isclose( + result["shaded_fraction_front"], expected_shaded_fraction_front + ) + assert np.isclose( + result["shaded_fraction_back"], expected_shaded_fraction_back + ) # test for when dni_extra is not supplied - with pytest.raises(ValueError, match='supply dni_extra for haydavies'): + with pytest.raises(ValueError, match="supply dni_extra for haydavies"): result = infinite_sheds.get_irradiance( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, - gcr, height, pitch, ghi, dhi, dni, albedo, model, None, - iam_front, iam_back, bifaciality=0.8, shade_factor=-0.02, - transmission_factor=0, npoints=npoints) + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + gcr, + height, + pitch, + ghi, + dhi, + dni, + albedo, + model, + None, + iam_front, + iam_back, + bifaciality=0.8, + shade_factor=-0.02, + transmission_factor=0, + npoints=npoints, + ) diff --git a/pvlib/tests/bifacial/test_pvfactors.py b/pvlib/tests/bifacial/test_pvfactors.py index a199999364..eddb27b398 100644 --- a/pvlib/tests/bifacial/test_pvfactors.py +++ b/pvlib/tests/bifacial/test_pvfactors.py @@ -12,15 +12,16 @@ def example_values(): https://github.com/SunPower/pvfactors/blob/master/README.rst#quick-start """ inputs = dict( - timestamps=pd.DatetimeIndex([datetime(2017, 8, 31, 11), - datetime(2017, 8, 31, 12)]), - solar_zenith=[20., 10.], - solar_azimuth=[110., 140.], - surface_tilt=[10., 0.], - surface_azimuth=[90., 90.], - axis_azimuth=0., - dni=[1000., 300.], - dhi=[50., 500.], + timestamps=pd.DatetimeIndex( + [datetime(2017, 8, 31, 11), datetime(2017, 8, 31, 12)] + ), + solar_zenith=[20.0, 10.0], + solar_azimuth=[110.0, 140.0], + surface_tilt=[10.0, 0.0], + surface_azimuth=[90.0, 90.0], + axis_azimuth=0.0, + dni=[1000.0, 300.0], + dhi=[50.0, 500.0], gcr=0.4, pvrow_height=1.75, pvrow_width=2.44, @@ -29,15 +30,19 @@ def example_values(): index_observed_pvrow=1, rho_front_pvrow=0.03, rho_back_pvrow=0.05, - horizon_band_angle=15., + horizon_band_angle=15.0, ) outputs = dict( - expected_ipoa_front=pd.Series([1034.95474708997, 795.4423259036623], - index=inputs['timestamps'], - name=('total_inc_front')), - expected_ipoa_back=pd.Series([92.12563846416197, 78.05831585685098], - index=inputs['timestamps'], - name=('total_inc_back')), + expected_ipoa_front=pd.Series( + [1034.95474708997, 795.4423259036623], + index=inputs["timestamps"], + name=("total_inc_front"), + ), + expected_ipoa_back=pd.Series( + [92.12563846416197, 78.05831585685098], + index=inputs["timestamps"], + name=("total_inc_back"), + ), ) return inputs, outputs @@ -47,8 +52,8 @@ def test_pvfactors_timeseries_list(example_values): """Test basic pvfactors functionality with list inputs""" inputs, outputs = example_values ipoa_inc_front, ipoa_inc_back, _, _ = pvfactors_timeseries(**inputs) - assert_series_equal(ipoa_inc_front, outputs['expected_ipoa_front']) - assert_series_equal(ipoa_inc_back, outputs['expected_ipoa_back']) + assert_series_equal(ipoa_inc_front, outputs["expected_ipoa_front"]) + assert_series_equal(ipoa_inc_back, outputs["expected_ipoa_back"]) @requires_pvfactors @@ -56,13 +61,19 @@ def test_pvfactors_timeseries_pandas(example_values): """Test basic pvfactors functionality with Series inputs""" inputs, outputs = example_values - for key in ['solar_zenith', 'solar_azimuth', 'surface_tilt', - 'surface_azimuth', 'dni', 'dhi']: - inputs[key] = pd.Series(inputs[key], index=inputs['timestamps']) + for key in [ + "solar_zenith", + "solar_azimuth", + "surface_tilt", + "surface_azimuth", + "dni", + "dhi", + ]: + inputs[key] = pd.Series(inputs[key], index=inputs["timestamps"]) ipoa_inc_front, ipoa_inc_back, _, _ = pvfactors_timeseries(**inputs) - assert_series_equal(ipoa_inc_front, outputs['expected_ipoa_front']) - assert_series_equal(ipoa_inc_back, outputs['expected_ipoa_back']) + assert_series_equal(ipoa_inc_front, outputs["expected_ipoa_front"]) + assert_series_equal(ipoa_inc_back, outputs["expected_ipoa_back"]) @requires_pvfactors @@ -70,13 +81,13 @@ def test_pvfactors_scalar_orientation(example_values): """test that surface_tilt and surface_azimuth inputs can be scalars""" # GH 1127, GH 1332 inputs, outputs = example_values - inputs['surface_tilt'] = 10. - inputs['surface_azimuth'] = 90. + inputs["surface_tilt"] = 10.0 + inputs["surface_azimuth"] = 90.0 # the second tilt is supposed to be zero, so we need to # update the expected irradiances too: - outputs['expected_ipoa_front'].iloc[1] = 800.6524022701132 - outputs['expected_ipoa_back'].iloc[1] = 81.72135884745822 + outputs["expected_ipoa_front"].iloc[1] = 800.6524022701132 + outputs["expected_ipoa_back"].iloc[1] = 81.72135884745822 ipoa_inc_front, ipoa_inc_back, _, _ = pvfactors_timeseries(**inputs) - assert_series_equal(ipoa_inc_front, outputs['expected_ipoa_front']) - assert_series_equal(ipoa_inc_back, outputs['expected_ipoa_back']) + assert_series_equal(ipoa_inc_front, outputs["expected_ipoa_front"]) + assert_series_equal(ipoa_inc_back, outputs["expected_ipoa_back"]) diff --git a/pvlib/tests/bifacial/test_utils.py b/pvlib/tests/bifacial/test_utils.py index e24af42b3e..c2a424c54c 100644 --- a/pvlib/tests/bifacial/test_utils.py +++ b/pvlib/tests/bifacial/test_utils.py @@ -1,6 +1,7 @@ """ test bifical.utils """ + import numpy as np import pytest from pvlib.bifacial import utils @@ -11,42 +12,44 @@ @pytest.fixture def test_system_fixed_tilt(): - syst = {'height': 1.0, - 'pitch': 2., - 'surface_tilt': 30., - 'surface_azimuth': 180., - 'axis_azimuth': None, - 'rotation': -30.} - syst['gcr'] = 1.0 / syst['pitch'] + syst = { + "height": 1.0, + "pitch": 2.0, + "surface_tilt": 30.0, + "surface_azimuth": 180.0, + "axis_azimuth": None, + "rotation": -30.0, + } + syst["gcr"] = 1.0 / syst["pitch"] # view factors from 3 points on the ground between rows to the sky pts = np.linspace(0, 1, num=3) sqr3 = np.sqrt(3) / 4 # c_i,j = cos(angle from point i to edge of row j), j=0 is row = -1 # c_i,j = cos(angle from point i to edge of row j), j=0 is row = -1 - c00 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3)**2) # right edge row -1 + c00 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3) ** 2) # right edge row -1 c01 = -sqr3 / np.sqrt(1.25**2 + sqr3**2) # right edge row 0 c02 = sqr3 / np.sqrt(0.75**2 + sqr3**2) # left edge of row 0 - c03 = (2 - sqr3) / np.sqrt(1.25**2 + (2 - sqr3)**2) # right edge of row 1 + c03 = (2 - sqr3) / np.sqrt( + 1.25**2 + (2 - sqr3) ** 2 + ) # right edge of row 1 vf_0 = 0.5 * (c03 - c02 + c01 - c00) # vf at point 0 - c10 = (-3 - sqr3) / np.sqrt(1.25**2 + (3 + sqr3)**2) # right edge row -1 - c11 = (-1 - sqr3) / np.sqrt(1.25**2 + (1 + sqr3)**2) # right edge row 0 - c12 = (-1 + sqr3) / np.sqrt(0.75**2 + (-1 + sqr3)**2) # left edge row 0 - c13 = (1 - sqr3) / np.sqrt(1.25**2 + (1 - sqr3)**2) # right edge row + c10 = (-3 - sqr3) / np.sqrt(1.25**2 + (3 + sqr3) ** 2) # right edge row -1 + c11 = (-1 - sqr3) / np.sqrt(1.25**2 + (1 + sqr3) ** 2) # right edge row 0 + c12 = (-1 + sqr3) / np.sqrt(0.75**2 + (-1 + sqr3) ** 2) # left edge row 0 + c13 = (1 - sqr3) / np.sqrt(1.25**2 + (1 - sqr3) ** 2) # right edge row vf_1 = 0.5 * (c13 - c12 + c11 - c10) # vf at point 1 - c20 = -(4 + sqr3) / np.sqrt(1.25**2 + (4 + sqr3)**2) # right edge row -1 - c21 = (-2 + sqr3) / np.sqrt(0.75**2 + (-2 + sqr3)**2) # left edge row 0 - c22 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3)**2) # right edge row 0 - c23 = (0 - sqr3) / np.sqrt(1.25**2 + (0 - sqr3)**2) # right edge row 1 + c20 = -(4 + sqr3) / np.sqrt(1.25**2 + (4 + sqr3) ** 2) # right edge row -1 + c21 = (-2 + sqr3) / np.sqrt(0.75**2 + (-2 + sqr3) ** 2) # left edge row 0 + c22 = (-2 - sqr3) / np.sqrt(1.25**2 + (2 + sqr3) ** 2) # right edge row 0 + c23 = (0 - sqr3) / np.sqrt(1.25**2 + (0 - sqr3) ** 2) # right edge row 1 vf_2 = 0.5 * (c23 - c22 + c21 - c20) # vf at point 1 vfs_ground_sky = np.array([[vf_0], [vf_1], [vf_2]]) return syst, pts, vfs_ground_sky def test__solar_projection_tangent(): - tan_phi_f = utils._solar_projection_tangent( - 30, 150, 180) - tan_phi_b = utils._solar_projection_tangent( - 30, 150, 0) + tan_phi_f = utils._solar_projection_tangent(30, 150, 180) + tan_phi_b = utils._solar_projection_tangent(30, 150, 0) assert np.allclose(tan_phi_f, 0.5) assert np.allclose(tan_phi_b, -0.5) assert np.allclose(tan_phi_f, -tan_phi_b) @@ -54,40 +57,54 @@ def test__solar_projection_tangent(): @pytest.mark.parametrize( "gcr,surface_tilt,surface_azimuth,solar_zenith,solar_azimuth,expected", - [(0.5, 0., 180., 0., 180., 0.5), - (1.0, 0., 180., 0., 180., 0.0), - (1.0, 90., 180., 0., 180., 1.0), - (0.5, 45., 180., 45., 270., 1.0 - np.sqrt(2) / 4), - (0.5, 45., 180., 90., 180., 0.), - (np.sqrt(2) / 2, 45, 180, 0, 180, 0.5), - (np.sqrt(2) / 2, 45, 180, 45, 180, 0.0), - (np.sqrt(2) / 2, 45, 180, 45, 90, 0.5), - (np.sqrt(2) / 2, 45, 180, 45, 0, 1.0), - (np.sqrt(2) / 2, 45, 180, 45, 135, 0.5 * (1 - np.sqrt(2) / 2)), - ]) + [ + (0.5, 0.0, 180.0, 0.0, 180.0, 0.5), + (1.0, 0.0, 180.0, 0.0, 180.0, 0.0), + (1.0, 90.0, 180.0, 0.0, 180.0, 1.0), + (0.5, 45.0, 180.0, 45.0, 270.0, 1.0 - np.sqrt(2) / 4), + (0.5, 45.0, 180.0, 90.0, 180.0, 0.0), + (np.sqrt(2) / 2, 45, 180, 0, 180, 0.5), + (np.sqrt(2) / 2, 45, 180, 45, 180, 0.0), + (np.sqrt(2) / 2, 45, 180, 45, 90, 0.5), + (np.sqrt(2) / 2, 45, 180, 45, 0, 1.0), + (np.sqrt(2) / 2, 45, 180, 45, 135, 0.5 * (1 - np.sqrt(2) / 2)), + ], +) def test__unshaded_ground_fraction( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr, - expected): + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr, expected +): # frontside, same for both sides f_sky_beam_f = utils._unshaded_ground_fraction( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr) + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr + ) assert np.allclose(f_sky_beam_f, expected) # backside, should be the same as frontside f_sky_beam_b = utils._unshaded_ground_fraction( - 180. - surface_tilt, surface_azimuth - 180., solar_zenith, - solar_azimuth, gcr) + 180.0 - surface_tilt, + surface_azimuth - 180.0, + solar_zenith, + solar_azimuth, + gcr, + ) assert np.allclose(f_sky_beam_b, expected) def test__vf_ground_sky_2d(test_system_fixed_tilt): # vector input ts, pts, vfs_gnd_sky = test_system_fixed_tilt - vfs = utils.vf_ground_sky_2d(ts['rotation'], ts['gcr'], pts, - ts['pitch'], ts['height'], max_rows=1) + vfs = utils.vf_ground_sky_2d( + ts["rotation"], ts["gcr"], pts, ts["pitch"], ts["height"], max_rows=1 + ) assert np.allclose(vfs, vfs_gnd_sky, rtol=0.1) # middle point vf is off # test with singleton x - vf = utils.vf_ground_sky_2d(ts['rotation'], ts['gcr'], pts[0], - ts['pitch'], ts['height'], max_rows=1) + vf = utils.vf_ground_sky_2d( + ts["rotation"], + ts["gcr"], + pts[0], + ts["pitch"], + ts["height"], + max_rows=1, + ) assert np.isclose(vf, vfs_gnd_sky[0]) @@ -98,8 +115,14 @@ def test_vf_ground_sky_2d_integ(test_system_fixed_tilt, vectorize): # the fixture test_system, which means the ground-to-sky view factor # isn't summed over enough rows for symmetry to hold. vf_integ = utils.vf_ground_sky_2d_integ( - ts['rotation'], ts['gcr'], ts['height'], ts['pitch'], - max_rows=1, npoints=3, vectorize=vectorize) + ts["rotation"], + ts["gcr"], + ts["height"], + ts["pitch"], + max_rows=1, + npoints=3, + vectorize=vectorize, + ) expected_vf_integ = trapezoid(vfs_gnd_sky, pts, axis=0) assert np.isclose(vf_integ, expected_vf_integ, rtol=0.1) @@ -107,42 +130,42 @@ def test_vf_ground_sky_2d_integ(test_system_fixed_tilt, vectorize): def test_vf_row_sky_2d(test_system_fixed_tilt): ts, _, _ = test_system_fixed_tilt # with float input, fx at top of row - vf = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], 1.) - expected = 0.5 * (1 + cosd(ts['surface_tilt'])) + vf = utils.vf_row_sky_2d(ts["surface_tilt"], ts["gcr"], 1.0) + expected = 0.5 * (1 + cosd(ts["surface_tilt"])) assert np.isclose(vf, expected) # with array input - fx = np.array([0., 0.5, 1.]) - vf = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], fx) - phi = masking_angle(ts['surface_tilt'], ts['gcr'], fx) - expected = 0.5 * (1 + cosd(ts['surface_tilt'] + phi)) + fx = np.array([0.0, 0.5, 1.0]) + vf = utils.vf_row_sky_2d(ts["surface_tilt"], ts["gcr"], fx) + phi = masking_angle(ts["surface_tilt"], ts["gcr"], fx) + expected = 0.5 * (1 + cosd(ts["surface_tilt"] + phi)) assert np.allclose(vf, expected) def test_vf_row_sky_2d_integ(test_system_fixed_tilt): ts, _, _ = test_system_fixed_tilt # with float input, check end position - with np.errstate(invalid='ignore'): - vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr'], 1., 1.) - expected = utils.vf_row_sky_2d(ts['surface_tilt'], ts['gcr'], 1.) + with np.errstate(invalid="ignore"): + vf = utils.vf_row_sky_2d_integ(ts["surface_tilt"], ts["gcr"], 1.0, 1.0) + expected = utils.vf_row_sky_2d(ts["surface_tilt"], ts["gcr"], 1.0) assert np.isclose(vf, expected) # with array input - fx0 = np.array([0., 0.5]) - fx1 = np.array([0., 0.8]) - with np.errstate(invalid='ignore'): - vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr'], fx0, fx1) - phi = masking_angle(ts['surface_tilt'], ts['gcr'], fx0[0]) - y0 = 0.5 * (1 + cosd(ts['surface_tilt'] + phi)) + fx0 = np.array([0.0, 0.5]) + fx1 = np.array([0.0, 0.8]) + with np.errstate(invalid="ignore"): + vf = utils.vf_row_sky_2d_integ(ts["surface_tilt"], ts["gcr"], fx0, fx1) + phi = masking_angle(ts["surface_tilt"], ts["gcr"], fx0[0]) + y0 = 0.5 * (1 + cosd(ts["surface_tilt"] + phi)) x = np.arange(fx0[1], fx1[1], 1e-4) - phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x) - y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y)) + phi_y = masking_angle(ts["surface_tilt"], ts["gcr"], x) + y = 0.5 * (1 + cosd(ts["surface_tilt"] + phi_y)) y1 = trapezoid(y, x) / (fx1[1] - fx0[1]) expected = np.array([y0, y1]) assert np.allclose(vf, expected, rtol=1e-3) # with defaults (0, 1) - vf = utils.vf_row_sky_2d_integ(ts['surface_tilt'], ts['gcr']) + vf = utils.vf_row_sky_2d_integ(ts["surface_tilt"], ts["gcr"]) x = np.arange(0, 1, 1e-4) - phi_y = masking_angle(ts['surface_tilt'], ts['gcr'], x) - y = 0.5 * (1 + cosd(ts['surface_tilt'] + phi_y)) + phi_y = masking_angle(ts["surface_tilt"], ts["gcr"], x) + y = 0.5 * (1 + cosd(ts["surface_tilt"] + phi_y)) y1 = trapezoid(y, x) / (1 - 0) assert np.allclose(vf, y1, rtol=1e-3) @@ -150,43 +173,45 @@ def test_vf_row_sky_2d_integ(test_system_fixed_tilt): def test_vf_row_ground_2d(test_system_fixed_tilt): ts, _, _ = test_system_fixed_tilt # with float input, fx at bottom of row - vf = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], 0.) - expected = 0.5 * (1. - cosd(ts['surface_tilt'])) + vf = utils.vf_row_ground_2d(ts["surface_tilt"], ts["gcr"], 0.0) + expected = 0.5 * (1.0 - cosd(ts["surface_tilt"])) assert np.isclose(vf, expected) # with array input - fx = np.array([0., 0.5, 1.0]) - vf = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], fx) - phi = ground_angle(ts['surface_tilt'], ts['gcr'], fx) - expected = 0.5 * (1 - cosd(phi - ts['surface_tilt'])) + fx = np.array([0.0, 0.5, 1.0]) + vf = utils.vf_row_ground_2d(ts["surface_tilt"], ts["gcr"], fx) + phi = ground_angle(ts["surface_tilt"], ts["gcr"], fx) + expected = 0.5 * (1 - cosd(phi - ts["surface_tilt"])) assert np.allclose(vf, expected) def test_vf_ground_2d_integ(test_system_fixed_tilt): ts, _, _ = test_system_fixed_tilt # with float input, check end position - with np.errstate(invalid='ignore'): - vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], - 0., 0.) - expected = utils.vf_row_ground_2d(ts['surface_tilt'], ts['gcr'], 0.) + with np.errstate(invalid="ignore"): + vf = utils.vf_row_ground_2d_integ( + ts["surface_tilt"], ts["gcr"], 0.0, 0.0 + ) + expected = utils.vf_row_ground_2d(ts["surface_tilt"], ts["gcr"], 0.0) assert np.isclose(vf, expected) # with array input - fx0 = np.array([0., 0.5]) - fx1 = np.array([0., 0.8]) - with np.errstate(invalid='ignore'): - vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], - fx0, fx1) - phi = ground_angle(ts['surface_tilt'], ts['gcr'], fx0[0]) - y0 = 0.5 * (1 - cosd(phi - ts['surface_tilt'])) + fx0 = np.array([0.0, 0.5]) + fx1 = np.array([0.0, 0.8]) + with np.errstate(invalid="ignore"): + vf = utils.vf_row_ground_2d_integ( + ts["surface_tilt"], ts["gcr"], fx0, fx1 + ) + phi = ground_angle(ts["surface_tilt"], ts["gcr"], fx0[0]) + y0 = 0.5 * (1 - cosd(phi - ts["surface_tilt"])) x = np.arange(fx0[1], fx1[1], 1e-4) - phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x) - y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt'])) + phi_y = ground_angle(ts["surface_tilt"], ts["gcr"], x) + y = 0.5 * (1 - cosd(phi_y - ts["surface_tilt"])) y1 = trapezoid(y, x) / (fx1[1] - fx0[1]) expected = np.array([y0, y1]) assert np.allclose(vf, expected, rtol=1e-2) # with defaults (0, 1) - vf = utils.vf_row_ground_2d_integ(ts['surface_tilt'], ts['gcr'], 0, 1) + vf = utils.vf_row_ground_2d_integ(ts["surface_tilt"], ts["gcr"], 0, 1) x = np.arange(0, 1, 1e-4) - phi_y = ground_angle(ts['surface_tilt'], ts['gcr'], x) - y = 0.5 * (1 - cosd(phi_y - ts['surface_tilt'])) + phi_y = ground_angle(ts["surface_tilt"], ts["gcr"], x) + y = 0.5 * (1 - cosd(phi_y - ts["surface_tilt"])) y1 = trapezoid(y, x) / (1 - 0) assert np.allclose(vf, y1, rtol=1e-2) diff --git a/pvlib/tests/conftest.py b/pvlib/tests/conftest.py index 21ee60f17b..7f0bfb108b 100644 --- a/pvlib/tests/conftest.py +++ b/pvlib/tests/conftest.py @@ -28,28 +28,32 @@ def wrapper(func): def inner(*args, **kwargs): # fail if the version is too high if pvlib_base_version >= Version(version): - pytest.fail('the tested function is scheduled to be ' - 'removed in %s' % version) + pytest.fail( + "the tested function is scheduled to be " + "removed in %s" % version + ) # otherwise return the function to be executed else: return func(*args, **kwargs) + return inner + return wrapper def _check_pandas_assert_kwargs(kwargs): # handles the change in API related to default # tolerances in pandas 1.1.0. See pvlib GH #1018 - if Version(pd.__version__) >= Version('1.1.0'): - if kwargs.pop('check_less_precise', False): - kwargs['atol'] = 1e-3 - kwargs['rtol'] = 1e-3 + if Version(pd.__version__) >= Version("1.1.0"): + if kwargs.pop("check_less_precise", False): + kwargs["atol"] = 1e-3 + kwargs["rtol"] = 1e-3 else: - kwargs['atol'] = 1e-5 - kwargs['rtol'] = 1e-5 + kwargs["atol"] = 1e-5 + kwargs["rtol"] = 1e-5 else: - kwargs.pop('rtol', None) - kwargs.pop('atol', None) + kwargs.pop("rtol", None) + kwargs.pop("atol", None) return kwargs @@ -70,7 +74,7 @@ def assert_frame_equal(left, right, **kwargs): # commonly used directories in the tests TEST_DIR = Path(__file__).parent -DATA_DIR = TEST_DIR.parent / 'data' +DATA_DIR = TEST_DIR.parent / "data" # pytest-rerunfailures variables @@ -78,9 +82,10 @@ def assert_frame_equal(left, right, **kwargs): RERUNS_DELAY = 2 -platform_is_windows = platform.system() == 'Windows' -skip_windows = pytest.mark.skipif(platform_is_windows, - reason='does not run on windows') +platform_is_windows = platform.system() == "Windows" +skip_windows = pytest.mark.skipif( + platform_is_windows, reason="does not run on windows" +) try: @@ -92,7 +97,8 @@ def assert_frame_equal(left, right, **kwargs): has_bsrn_credentials = False requires_bsrn_credentials = pytest.mark.skipif( - not has_bsrn_credentials, reason='requires bsrn credentials') + not has_bsrn_credentials, reason="requires bsrn credentials" +) try: @@ -105,26 +111,30 @@ def assert_frame_equal(left, right, **kwargs): requires_solaranywhere_credentials = pytest.mark.skipif( not has_solaranywhere_credentials, - reason='requires solaranywhere credentials') + reason="requires solaranywhere credentials", +) try: import statsmodels # noqa: F401 + has_statsmodels = True except ImportError: has_statsmodels = False requires_statsmodels = pytest.mark.skipif( - not has_statsmodels, reason='requires statsmodels') + not has_statsmodels, reason="requires statsmodels" +) try: import ephem # noqa: F401 + has_ephem = True except ImportError: has_ephem = False -requires_ephem = pytest.mark.skipif(not has_ephem, reason='requires ephem') +requires_ephem = pytest.mark.skipif(not has_ephem, reason="requires ephem") def has_spa_c(): @@ -140,7 +150,8 @@ def has_spa_c(): try: - import numba # noqa: F401 + import numba # noqa: F401 + has_numba = True except ImportError: has_numba = False @@ -151,16 +162,19 @@ def has_spa_c(): try: import pvfactors # noqa: F401 + has_pvfactors = True except ImportError: has_pvfactors = False -requires_pvfactors = pytest.mark.skipif(not has_pvfactors, - reason='requires pvfactors') +requires_pvfactors = pytest.mark.skipif( + not has_pvfactors, reason="requires pvfactors" +) try: import PySAM # noqa: F401 + has_pysam = True except ImportError: has_pysam = False @@ -169,27 +183,32 @@ def has_spa_c(): has_pandas_2_0 = Version(pd.__version__) >= Version("2.0.0") -requires_pandas_2_0 = pytest.mark.skipif(not has_pandas_2_0, - reason="requires pandas>=2.0.0") +requires_pandas_2_0 = pytest.mark.skipif( + not has_pandas_2_0, reason="requires pandas>=2.0.0" +) @pytest.fixture() def golden(): - return Location(39.742476, -105.1786, 'America/Denver', 1830.14) + return Location(39.742476, -105.1786, "America/Denver", 1830.14) @pytest.fixture() def golden_mst(): - return Location(39.742476, -105.1786, 'MST', 1830.14) + return Location(39.742476, -105.1786, "MST", 1830.14) @pytest.fixture() def expected_solpos(): - return pd.DataFrame({'elevation': 39.872046, - 'apparent_zenith': 50.111622, - 'azimuth': 194.340241, - 'apparent_elevation': 39.888378}, - index=['2003-10-17T12:30:30Z']) + return pd.DataFrame( + { + "elevation": 39.872046, + "apparent_zenith": 50.111622, + "azimuth": 194.340241, + "apparent_elevation": 39.888378, + }, + index=["2003-10-17T12:30:30Z"], + ) @pytest.fixture(scope="session") @@ -198,8 +217,8 @@ def sam_data(): with warnings.catch_warnings(): # ignore messages about duplicate entries in the databases. warnings.simplefilter("ignore", UserWarning) - data['sandiamod'] = pvlib.pvsystem.retrieve_sam('sandiamod') - data['adrinverter'] = pvlib.pvsystem.retrieve_sam('adrinverter') + data["sandiamod"] = pvlib.pvsystem.retrieve_sam("sandiamod") + data["adrinverter"] = pvlib.pvsystem.retrieve_sam("adrinverter") return data @@ -212,22 +231,22 @@ def pvsyst_module_params(): parameters if required without affecting other tests. """ parameters = { - 'gamma_ref': 1.05, - 'mu_gamma': 0.001, - 'I_L_ref': 6.0, - 'I_o_ref': 5e-9, - 'EgRef': 1.121, - 'R_sh_ref': 300, - 'R_sh_0': 1000, - 'R_s': 0.5, - 'R_sh_exp': 5.5, - 'cells_in_series': 60, - 'alpha_sc': 0.001, + "gamma_ref": 1.05, + "mu_gamma": 0.001, + "I_L_ref": 6.0, + "I_o_ref": 5e-9, + "EgRef": 1.121, + "R_sh_ref": 300, + "R_sh_0": 1000, + "R_s": 0.5, + "R_sh_exp": 5.5, + "cells_in_series": 60, + "alpha_sc": 0.001, } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def adr_inverter_parameters(): """ Define some ADR inverter parameters for testing. @@ -236,25 +255,34 @@ def adr_inverter_parameters(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'Ablerex Electronics Co., Ltd.: ES 2200-US-240 (240Vac)' - '[CEC 2011]', - 'Vac': 240., - 'Pacmax': 2110., - 'Pnom': 2200., - 'Vnom': 396., - 'Vmin': 155., - 'Vmax': 413., - 'Vdcmax': 500., - 'MPPTHi': 450., - 'MPPTLow': 150., - 'Pnt': 0.25, - 'ADRCoefficients': [0.01385, 0.0152, 0.00794, 0.00286, -0.01872, - -0.01305, 0.0, 0.0, 0.0] + "Name": "Ablerex Electronics Co., Ltd.: ES 2200-US-240 (240Vac)" + "[CEC 2011]", + "Vac": 240.0, + "Pacmax": 2110.0, + "Pnom": 2200.0, + "Vnom": 396.0, + "Vmin": 155.0, + "Vmax": 413.0, + "Vdcmax": 500.0, + "MPPTHi": 450.0, + "MPPTLow": 150.0, + "Pnt": 0.25, + "ADRCoefficients": [ + 0.01385, + 0.0152, + 0.00794, + 0.00286, + -0.01872, + -0.01305, + 0.0, + 0.0, + 0.0, + ], } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def cec_inverter_parameters(): """ Define some CEC inverter parameters for testing. @@ -263,26 +291,26 @@ def cec_inverter_parameters(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'ABB: MICRO-0.25-I-OUTD-US-208 208V [CEC 2014]', - 'Vac': 208.0, - 'Paco': 250.0, - 'Pdco': 259.5220505, - 'Vdco': 40.24260317, - 'Pso': 1.771614224, - 'C0': -2.48e-5, - 'C1': -9.01e-5, - 'C2': 6.69e-4, - 'C3': -0.0189, - 'Pnt': 0.02, - 'Vdcmax': 65.0, - 'Idcmax': 10.0, - 'Mppt_low': 20.0, - 'Mppt_high': 50.0, + "Name": "ABB: MICRO-0.25-I-OUTD-US-208 208V [CEC 2014]", + "Vac": 208.0, + "Paco": 250.0, + "Pdco": 259.5220505, + "Vdco": 40.24260317, + "Pso": 1.771614224, + "C0": -2.48e-5, + "C1": -9.01e-5, + "C2": 6.69e-4, + "C3": -0.0189, + "Pnt": 0.02, + "Vdcmax": 65.0, + "Idcmax": 10.0, + "Mppt_low": 20.0, + "Mppt_high": 50.0, } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def cec_module_params(): """ Define some CEC module parameters for testing. @@ -291,33 +319,33 @@ def cec_module_params(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'Example Module', - 'BIPV': 'Y', - 'Date': '4/28/2008', - 'T_NOCT': 65, - 'A_c': 0.67, - 'N_s': 18, - 'I_sc_ref': 7.5, - 'V_oc_ref': 10.4, - 'I_mp_ref': 6.6, - 'V_mp_ref': 8.4, - 'alpha_sc': 0.003, - 'beta_oc': -0.04, - 'a_ref': 0.473, - 'I_L_ref': 7.545, - 'I_o_ref': 1.94e-09, - 'R_s': 0.094, - 'R_sh_ref': 15.72, - 'Adjust': 10.6, - 'gamma_r': -0.5, - 'Version': 'MM105', - 'PTC': 48.9, - 'Technology': 'Multi-c-Si', + "Name": "Example Module", + "BIPV": "Y", + "Date": "4/28/2008", + "T_NOCT": 65, + "A_c": 0.67, + "N_s": 18, + "I_sc_ref": 7.5, + "V_oc_ref": 10.4, + "I_mp_ref": 6.6, + "V_mp_ref": 8.4, + "alpha_sc": 0.003, + "beta_oc": -0.04, + "a_ref": 0.473, + "I_L_ref": 7.545, + "I_o_ref": 1.94e-09, + "R_s": 0.094, + "R_sh_ref": 15.72, + "Adjust": 10.6, + "gamma_r": -0.5, + "Version": "MM105", + "PTC": 48.9, + "Technology": "Multi-c-Si", } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def cec_module_cs5p_220m(): """ Define Canadian Solar CS5P-220M module parameters for testing. @@ -326,33 +354,33 @@ def cec_module_cs5p_220m(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'Canadian Solar CS5P-220M', - 'BIPV': 'N', - 'Date': '10/5/2009', - 'T_NOCT': 42.4, - 'A_c': 1.7, - 'N_s': 96, - 'I_sc_ref': 5.1, - 'V_oc_ref': 59.4, - 'I_mp_ref': 4.69, - 'V_mp_ref': 46.9, - 'alpha_sc': 0.004539, - 'beta_oc': -0.22216, - 'a_ref': 2.6373, - 'I_L_ref': 5.114, - 'I_o_ref': 8.196e-10, - 'R_s': 1.065, - 'R_sh_ref': 381.68, - 'Adjust': 8.7, - 'gamma_r': -0.476, - 'Version': 'MM106', - 'PTC': 200.1, - 'Technology': 'Mono-c-Si', + "Name": "Canadian Solar CS5P-220M", + "BIPV": "N", + "Date": "10/5/2009", + "T_NOCT": 42.4, + "A_c": 1.7, + "N_s": 96, + "I_sc_ref": 5.1, + "V_oc_ref": 59.4, + "I_mp_ref": 4.69, + "V_mp_ref": 46.9, + "alpha_sc": 0.004539, + "beta_oc": -0.22216, + "a_ref": 2.6373, + "I_L_ref": 5.114, + "I_o_ref": 8.196e-10, + "R_s": 1.065, + "R_sh_ref": 381.68, + "Adjust": 8.7, + "gamma_r": -0.476, + "Version": "MM106", + "PTC": 200.1, + "Technology": "Mono-c-Si", } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def cec_module_spr_e20_327(): """ Define SunPower SPR-E20-327 module parameters for testing. @@ -361,33 +389,33 @@ def cec_module_spr_e20_327(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'SunPower SPR-E20-327', - 'BIPV': 'N', - 'Date': '1/14/2013', - 'T_NOCT': 46, - 'A_c': 1.631, - 'N_s': 96, - 'I_sc_ref': 6.46, - 'V_oc_ref': 65.1, - 'I_mp_ref': 5.98, - 'V_mp_ref': 54.7, - 'alpha_sc': 0.004522, - 'beta_oc': -0.23176, - 'a_ref': 2.6868, - 'I_L_ref': 6.468, - 'I_o_ref': 1.88e-10, - 'R_s': 0.37, - 'R_sh_ref': 298.13, - 'Adjust': -0.1862, - 'gamma_r': -0.386, - 'Version': 'NRELv1', - 'PTC': 301.4, - 'Technology': 'Mono-c-Si', + "Name": "SunPower SPR-E20-327", + "BIPV": "N", + "Date": "1/14/2013", + "T_NOCT": 46, + "A_c": 1.631, + "N_s": 96, + "I_sc_ref": 6.46, + "V_oc_ref": 65.1, + "I_mp_ref": 5.98, + "V_mp_ref": 54.7, + "alpha_sc": 0.004522, + "beta_oc": -0.23176, + "a_ref": 2.6868, + "I_L_ref": 6.468, + "I_o_ref": 1.88e-10, + "R_s": 0.37, + "R_sh_ref": 298.13, + "Adjust": -0.1862, + "gamma_r": -0.386, + "Version": "NRELv1", + "PTC": 301.4, + "Technology": "Mono-c-Si", } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def cec_module_fs_495(): """ Define First Solar FS-495 module parameters for testing. @@ -396,40 +424,40 @@ def cec_module_fs_495(): parameters if required without affecting other tests. """ parameters = { - 'Name': 'First Solar FS-495', - 'BIPV': 'N', - 'Date': '9/18/2014', - 'T_NOCT': 44.6, - 'A_c': 0.72, - 'N_s': 216, - 'I_sc_ref': 1.55, - 'V_oc_ref': 86.5, - 'I_mp_ref': 1.4, - 'V_mp_ref': 67.9, - 'alpha_sc': 0.000924, - 'beta_oc': -0.22741, - 'a_ref': 2.9482, - 'I_L_ref': 1.563, - 'I_o_ref': 2.64e-13, - 'R_s': 6.804, - 'R_sh_ref': 806.27, - 'Adjust': -10.65, - 'gamma_r': -0.264, - 'Version': 'NRELv1', - 'PTC': 89.7, - 'Technology': 'CdTe', + "Name": "First Solar FS-495", + "BIPV": "N", + "Date": "9/18/2014", + "T_NOCT": 44.6, + "A_c": 0.72, + "N_s": 216, + "I_sc_ref": 1.55, + "V_oc_ref": 86.5, + "I_mp_ref": 1.4, + "V_mp_ref": 67.9, + "alpha_sc": 0.000924, + "beta_oc": -0.22741, + "a_ref": 2.9482, + "I_L_ref": 1.563, + "I_o_ref": 2.64e-13, + "R_s": 6.804, + "R_sh_ref": 806.27, + "Adjust": -10.65, + "gamma_r": -0.264, + "Version": "NRELv1", + "PTC": 89.7, + "Technology": "CdTe", } return parameters -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def sapm_temperature_cs5p_220m(): # SAPM temperature model parameters for Canadian_Solar_CS5P_220M # (glass/polymer) in open rack - return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3} + return {"a": -3.40641, "b": -0.0842075, "deltaT": 3} -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def sapm_module_params(): """ Define SAPM model parameters for Canadian Solar CS5P 220M module. @@ -437,40 +465,42 @@ def sapm_module_params(): The scope of the fixture is set to ``'function'`` to allow tests to modify parameters if required without affecting other tests. """ - parameters = {'Material': 'c-Si', - 'Cells_in_Series': 96, - 'Parallel_Strings': 1, - 'A0': 0.928385, - 'A1': 0.068093, - 'A2': -0.0157738, - 'A3': 0.0016606, - 'A4': -6.93E-05, - 'B0': 1, - 'B1': -0.002438, - 'B2': 0.0003103, - 'B3': -0.00001246, - 'B4': 2.11E-07, - 'B5': -1.36E-09, - 'C0': 1.01284, - 'C1': -0.0128398, - 'C2': 0.279317, - 'C3': -7.24463, - 'C4': 0.996446, - 'C5': 0.003554, - 'C6': 1.15535, - 'C7': -0.155353, - 'Isco': 5.09115, - 'Impo': 4.54629, - 'Voco': 59.2608, - 'Vmpo': 48.3156, - 'Aisc': 0.000397, - 'Aimp': 0.000181, - 'Bvoco': -0.21696, - 'Mbvoc': 0.0, - 'Bvmpo': -0.235488, - 'Mbvmp': 0.0, - 'N': 1.4032, - 'IXO': 4.97599, - 'IXXO': 3.18803, - 'FD': 1} + parameters = { + "Material": "c-Si", + "Cells_in_Series": 96, + "Parallel_Strings": 1, + "A0": 0.928385, + "A1": 0.068093, + "A2": -0.0157738, + "A3": 0.0016606, + "A4": -6.93e-05, + "B0": 1, + "B1": -0.002438, + "B2": 0.0003103, + "B3": -0.00001246, + "B4": 2.11e-07, + "B5": -1.36e-09, + "C0": 1.01284, + "C1": -0.0128398, + "C2": 0.279317, + "C3": -7.24463, + "C4": 0.996446, + "C5": 0.003554, + "C6": 1.15535, + "C7": -0.155353, + "Isco": 5.09115, + "Impo": 4.54629, + "Voco": 59.2608, + "Vmpo": 48.3156, + "Aisc": 0.000397, + "Aimp": 0.000181, + "Bvoco": -0.21696, + "Mbvoc": 0.0, + "Bvmpo": -0.235488, + "Mbvmp": 0.0, + "N": 1.4032, + "IXO": 4.97599, + "IXXO": 3.18803, + "FD": 1, + } return parameters diff --git a/pvlib/tests/iotools/test_acis.py b/pvlib/tests/iotools/test_acis.py index 8458e9b930..19df0beea5 100644 --- a/pvlib/tests/iotools/test_acis.py +++ b/pvlib/tests/iotools/test_acis.py @@ -6,8 +6,11 @@ import numpy as np import pytest from pvlib.iotools import ( - get_acis_prism, get_acis_nrcc, get_acis_mpe, - get_acis_station_data, get_acis_available_stations + get_acis_prism, + get_acis_nrcc, + get_acis_mpe, + get_acis_station_data, + get_acis_available_stations, ) from ..conftest import RERUNS, RERUNS_DELAY, assert_frame_equal from requests import HTTPError @@ -17,86 +20,102 @@ @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_acis_prism(): # map_variables=True - df, meta = get_acis_prism(40.001, -80.001, '2020-01-01', '2020-01-02') + df, meta = get_acis_prism(40.001, -80.001, "2020-01-01", "2020-01-02") expected = pd.DataFrame( - [ - [0.5, 5, 0, 2.5, 0, 62, 0], - [0, 5, -3, 1, 0, 64, 0] + [[0.5, 5, 0, 2.5, 0, 62, 0], [0, 5, -3, 1, 0, 64, 0]], + columns=[ + "precipitation", + "temp_air_max", + "temp_air_min", + "temp_air_average", + "cooling_degree_days", + "heating_degree_days", + "growing_degree_days", ], - columns=['precipitation', 'temp_air_max', 'temp_air_min', - 'temp_air_average', 'cooling_degree_days', - 'heating_degree_days', 'growing_degree_days'], - index=pd.to_datetime(['2020-01-01', '2020-01-02']), + index=pd.to_datetime(["2020-01-01", "2020-01-02"]), ) assert_frame_equal(df, expected) - expected_meta = {'latitude': 40, 'longitude': -80, 'altitude': 298.0944} + expected_meta = {"latitude": 40, "longitude": -80, "altitude": 298.0944} assert meta == expected_meta # map_variables=False - df, meta = get_acis_prism(40.001, -80.001, '2020-01-01', '2020-01-02', - map_variables=False) - expected.columns = ['pcpn', 'maxt', 'mint', 'avgt', 'cdd', 'hdd', 'gdd'] + df, meta = get_acis_prism( + 40.001, -80.001, "2020-01-01", "2020-01-02", map_variables=False + ) + expected.columns = ["pcpn", "maxt", "mint", "avgt", "cdd", "hdd", "gdd"] assert_frame_equal(df, expected) - expected_meta = {'lat': 40, 'lon': -80, 'elev': 298.0944} + expected_meta = {"lat": 40, "lon": -80, "elev": 298.0944} assert meta == expected_meta @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) -@pytest.mark.parametrize('grid, expected', [ - (1, [[0.51, 5, 0, 2.5, 0, 62, 0]]), - (3, [[0.51, 5, -1, 2.0, 0, 63, 0]]) -]) +@pytest.mark.parametrize( + "grid, expected", + [(1, [[0.51, 5, 0, 2.5, 0, 62, 0]]), (3, [[0.51, 5, -1, 2.0, 0, 63, 0]])], +) def test_get_acis_nrcc(grid, expected): # map_variables=True - df, meta = get_acis_nrcc(40.001, -80.001, '2020-01-01', '2020-01-01', grid) + df, meta = get_acis_nrcc(40.001, -80.001, "2020-01-01", "2020-01-01", grid) expected = pd.DataFrame( expected, - columns=['precipitation', 'temp_air_max', 'temp_air_min', - 'temp_air_average', 'cooling_degree_days', - 'heating_degree_days', 'growing_degree_days'], - index=pd.to_datetime(['2020-01-01']), + columns=[ + "precipitation", + "temp_air_max", + "temp_air_min", + "temp_air_average", + "cooling_degree_days", + "heating_degree_days", + "growing_degree_days", + ], + index=pd.to_datetime(["2020-01-01"]), ) assert_frame_equal(df, expected) - expected_meta = {'latitude': 40., 'longitude': -80., 'altitude': 356.9208} + expected_meta = { + "latitude": 40.0, + "longitude": -80.0, + "altitude": 356.9208, + } assert meta == pytest.approx(expected_meta) # map_variables=False - df, meta = get_acis_nrcc(40.001, -80.001, '2020-01-01', '2020-01-01', grid, - map_variables=False) - expected.columns = ['pcpn', 'maxt', 'mint', 'avgt', 'cdd', 'hdd', 'gdd'] + df, meta = get_acis_nrcc( + 40.001, -80.001, "2020-01-01", "2020-01-01", grid, map_variables=False + ) + expected.columns = ["pcpn", "maxt", "mint", "avgt", "cdd", "hdd", "gdd"] assert_frame_equal(df, expected) - expected_meta = {'lat': 40., 'lon': -80., 'elev': 356.9208} + expected_meta = {"lat": 40.0, "lon": -80.0, "elev": 356.9208} assert meta == pytest.approx(expected_meta) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_acis_nrcc_error(): - with pytest.raises(HTTPError, match='invalid grid'): + with pytest.raises(HTTPError, match="invalid grid"): # 50 is not a valid dataset (or "grid", in ACIS lingo) - _ = get_acis_nrcc(40, -80, '2012-01-01', '2012-01-01', 50) + _ = get_acis_nrcc(40, -80, "2012-01-01", "2012-01-01", 50) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_acis_mpe(): # map_variables=True - df, meta = get_acis_mpe(40.001, -80.001, '2020-01-01', '2020-01-02') + df, meta = get_acis_mpe(40.001, -80.001, "2020-01-01", "2020-01-02") expected = pd.DataFrame( - {'precipitation': [0.4, 0.0]}, - index=pd.to_datetime(['2020-01-01', '2020-01-02']), + {"precipitation": [0.4, 0.0]}, + index=pd.to_datetime(["2020-01-01", "2020-01-02"]), ) assert_frame_equal(df, expected) - expected_meta = {'latitude': 40.0083, 'longitude': -79.9653} + expected_meta = {"latitude": 40.0083, "longitude": -79.9653} assert meta == expected_meta # map_variables=False - df, meta = get_acis_mpe(40.001, -80.001, '2020-01-01', '2020-01-02', - map_variables=False) - expected.columns = ['pcpn'] + df, meta = get_acis_mpe( + 40.001, -80.001, "2020-01-01", "2020-01-02", map_variables=False + ) + expected.columns = ["pcpn"] assert_frame_equal(df, expected) - expected_meta = {'lat': 40.0083, 'lon': -79.9653} + expected_meta = {"lat": 40.0083, "lon": -79.9653} assert meta == expected_meta @@ -104,68 +123,88 @@ def test_get_acis_mpe(): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_acis_station_data(): # map_variables=True - df, meta = get_acis_station_data('ORD', '2020-01-10', '2020-01-12', - trace_val=-99) + df, meta = get_acis_station_data( + "ORD", "2020-01-10", "2020-01-12", trace_val=-99 + ) expected = pd.DataFrame( - [[10., 2., 6., np.nan, 21.34, 0., 0., 0., 59., 0.], - [3., -4., -0.5, np.nan, 9.4, 5.3, 0., 0., 65., 0.], - [-1., -5., -3., np.nan, -99, -99, 5., 0., 68., 0.]], - columns=['temp_air_max', 'temp_air_min', 'temp_air_average', - 'temp_air_observation', 'precipitation', 'snowfall', - 'snowdepth', 'cooling_degree_days', - 'heating_degree_days', 'growing_degree_days'], - index=pd.to_datetime(['2020-01-10', '2020-01-11', '2020-01-12']), + [ + [10.0, 2.0, 6.0, np.nan, 21.34, 0.0, 0.0, 0.0, 59.0, 0.0], + [3.0, -4.0, -0.5, np.nan, 9.4, 5.3, 0.0, 0.0, 65.0, 0.0], + [-1.0, -5.0, -3.0, np.nan, -99, -99, 5.0, 0.0, 68.0, 0.0], + ], + columns=[ + "temp_air_max", + "temp_air_min", + "temp_air_average", + "temp_air_observation", + "precipitation", + "snowfall", + "snowdepth", + "cooling_degree_days", + "heating_degree_days", + "growing_degree_days", + ], + index=pd.to_datetime(["2020-01-10", "2020-01-11", "2020-01-12"]), ) assert_frame_equal(df, expected) expected_meta = { - 'uid': 48, - 'state': 'IL', - 'name': 'CHICAGO OHARE INTL AP', - 'altitude': 204.8256, - 'latitude': 41.96017, - 'longitude': -87.93164 + "uid": 48, + "state": "IL", + "name": "CHICAGO OHARE INTL AP", + "altitude": 204.8256, + "latitude": 41.96017, + "longitude": -87.93164, } expected_meta = { - 'valid_daterange': [ - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], + "valid_daterange": [ + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], [], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'], - ['1958-11-01', '2023-06-15'] + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ["1958-11-01", "2023-06-15"], + ], + "name": "CHICAGO OHARE INTL AP", + "sids": [ + "94846 1", + "111549 2", + "ORD 3", + "72530 4", + "KORD 5", + "USW00094846 6", + "ORD 7", + "USW00094846 32", ], - 'name': 'CHICAGO OHARE INTL AP', - 'sids': ['94846 1', '111549 2', 'ORD 3', '72530 4', 'KORD 5', - 'USW00094846 6', 'ORD 7', 'USW00094846 32'], - 'county': '17031', - 'state': 'IL', - 'climdiv': 'IL02', - 'uid': 48, - 'tzo': -6.0, - 'sid_dates': [ - ['94846 1', '1989-01-19', '9999-12-31'], - ['94846 1', '1958-10-30', '1989-01-01'], - ['111549 2', '1989-01-19', '9999-12-31'], - ['111549 2', '1958-10-30', '1989-01-01'], - ['ORD 3', '1989-01-19', '9999-12-31'], - ['ORD 3', '1958-10-30', '1989-01-01'], - ['72530 4', '1989-01-19', '9999-12-31'], - ['72530 4', '1958-10-30', '1989-01-01'], - ['KORD 5', '1989-01-19', '9999-12-31'], - ['KORD 5', '1958-10-30', '1989-01-01'], - ['USW00094846 6', '1989-01-19', '9999-12-31'], - ['USW00094846 6', '1958-10-30', '1989-01-01'], - ['ORD 7', '1989-01-19', '9999-12-31'], - ['ORD 7', '1958-10-30', '1989-01-01'], - ['USW00094846 32', '1989-01-19', '9999-12-31'], - ['USW00094846 32', '1958-10-30', '1989-01-01']], - 'altitude': 204.8256, - 'longitude': -87.93164, - 'latitude': 41.96017 + "county": "17031", + "state": "IL", + "climdiv": "IL02", + "uid": 48, + "tzo": -6.0, + "sid_dates": [ + ["94846 1", "1989-01-19", "9999-12-31"], + ["94846 1", "1958-10-30", "1989-01-01"], + ["111549 2", "1989-01-19", "9999-12-31"], + ["111549 2", "1958-10-30", "1989-01-01"], + ["ORD 3", "1989-01-19", "9999-12-31"], + ["ORD 3", "1958-10-30", "1989-01-01"], + ["72530 4", "1989-01-19", "9999-12-31"], + ["72530 4", "1958-10-30", "1989-01-01"], + ["KORD 5", "1989-01-19", "9999-12-31"], + ["KORD 5", "1958-10-30", "1989-01-01"], + ["USW00094846 6", "1989-01-19", "9999-12-31"], + ["USW00094846 6", "1958-10-30", "1989-01-01"], + ["ORD 7", "1989-01-19", "9999-12-31"], + ["ORD 7", "1958-10-30", "1989-01-01"], + ["USW00094846 32", "1989-01-19", "9999-12-31"], + ["USW00094846 32", "1958-10-30", "1989-01-01"], + ], + "altitude": 204.8256, + "longitude": -87.93164, + "latitude": 41.96017, } # don't check valid dates since they get extended every day meta.pop("valid_daterange") @@ -173,14 +212,25 @@ def test_get_acis_station_data(): assert meta == expected_meta # map_variables=False - df, meta = get_acis_station_data('ORD', '2020-01-10', '2020-01-12', - trace_val=-99, map_variables=False) - expected.columns = ['maxt', 'mint', 'avgt', 'obst', 'pcpn', 'snow', - 'snwd', 'cdd', 'hdd', 'gdd'] + df, meta = get_acis_station_data( + "ORD", "2020-01-10", "2020-01-12", trace_val=-99, map_variables=False + ) + expected.columns = [ + "maxt", + "mint", + "avgt", + "obst", + "pcpn", + "snow", + "snwd", + "cdd", + "hdd", + "gdd", + ] assert_frame_equal(df, expected) - expected_meta['lat'] = expected_meta.pop('latitude') - expected_meta['lon'] = expected_meta.pop('longitude') - expected_meta['elev'] = expected_meta.pop('altitude') + expected_meta["lat"] = expected_meta.pop("latitude") + expected_meta["lon"] = expected_meta.pop("longitude") + expected_meta["elev"] = expected_meta.pop("altitude") meta.pop("valid_daterange") assert meta == expected_meta @@ -191,23 +241,26 @@ def test_get_acis_available_stations(): # use a very narrow bounding box to hopefully make this test less likely # to fail due to new stations being added in the future lat, lon = 39.8986, -80.1656 - stations = get_acis_available_stations([lat - 0.0001, lat + 0.0001], - [lon - 0.0001, lon + 0.0001]) + stations = get_acis_available_stations( + [lat - 0.0001, lat + 0.0001], [lon - 0.0001, lon + 0.0001] + ) assert len(stations) == 1 station = stations.iloc[0] # test the more relevant values - assert station['name'] == 'WAYNESBURG 1 E' - assert station['sids'] == ['369367 2', 'USC00369367 6', 'WYNP1 7'] - assert station['state'] == 'PA' - assert station['altitude'] == 940. - assert station['tzo'] == -5.0 - assert station['latitude'] == lat - assert station['longitude'] == lon + assert station["name"] == "WAYNESBURG 1 E" + assert station["sids"] == ["369367 2", "USC00369367 6", "WYNP1 7"] + assert station["state"] == "PA" + assert station["altitude"] == 940.0 + assert station["tzo"] == -5.0 + assert station["latitude"] == lat + assert station["longitude"] == lon # check that start/end work as filters - stations = get_acis_available_stations([lat - 0.0001, lat + 0.0001], - [lon - 0.0001, lon + 0.0001], - start='1900-01-01', - end='1900-01-02') + stations = get_acis_available_stations( + [lat - 0.0001, lat + 0.0001], + [lon - 0.0001, lon + 0.0001], + start="1900-01-01", + end="1900-01-02", + ) assert stations.empty diff --git a/pvlib/tests/iotools/test_bsrn.py b/pvlib/tests/iotools/test_bsrn.py index 815b5d84c9..2dab60fd07 100644 --- a/pvlib/tests/iotools/test_bsrn.py +++ b/pvlib/tests/iotools/test_bsrn.py @@ -7,8 +7,13 @@ import os import tempfile from pvlib.iotools import read_bsrn, get_bsrn -from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, assert_index_equal, - requires_bsrn_credentials) +from ..conftest import ( + DATA_DIR, + RERUNS, + RERUNS_DELAY, + assert_index_equal, + requires_bsrn_credentials, +) @pytest.fixture(scope="module") @@ -24,53 +29,60 @@ def bsrn_credentials(): @pytest.fixture def expected_index(): - return pd.date_range(start='20160601', periods=43200, freq='1min', - tz='UTC') - - -@pytest.mark.parametrize('testfile', [ - ('bsrn-pay0616.dat.gz'), - ('bsrn-lr0100-pay0616.dat'), -]) + return pd.date_range( + start="20160601", periods=43200, freq="1min", tz="UTC" + ) + + +@pytest.mark.parametrize( + "testfile", + [ + ("bsrn-pay0616.dat.gz"), + ("bsrn-lr0100-pay0616.dat"), + ], +) def test_read_bsrn(testfile, expected_index): data, metadata = read_bsrn(DATA_DIR / testfile) assert_index_equal(expected_index, data.index) - assert 'ghi' in data.columns - assert 'dni_std' in data.columns - assert 'dhi_min' in data.columns - assert 'lwd_max' in data.columns - assert 'relative_humidity' in data.columns + assert "ghi" in data.columns + assert "dni_std" in data.columns + assert "dhi_min" in data.columns + assert "lwd_max" in data.columns + assert "relative_humidity" in data.columns def test_read_bsrn_logical_records(expected_index): # Test if logical records 0300 and 0500 are correct parsed # and that 0100 is not passed when not specified - data, metadata = read_bsrn(DATA_DIR / 'bsrn-pay0616.dat.gz', - logical_records=['0300', '0500']) + data, metadata = read_bsrn( + DATA_DIR / "bsrn-pay0616.dat.gz", logical_records=["0300", "0500"] + ) assert_index_equal(expected_index, data.index) - assert 'lwu' in data.columns - assert 'uva_global' in data.columns - assert 'uvb_reflected_std' in data.columns - assert 'ghi' not in data.columns + assert "lwu" in data.columns + assert "uva_global" in data.columns + assert "uvb_reflected_std" in data.columns + assert "ghi" not in data.columns def test_read_bsrn_bad_logical_record(): # Test if ValueError is raised if an unsupported logical record is passed - with pytest.raises(ValueError, match='not in'): - read_bsrn(DATA_DIR / 'bsrn-lr0100-pay0616.dat', - logical_records=['dummy']) + with pytest.raises(ValueError, match="not in"): + read_bsrn( + DATA_DIR / "bsrn-lr0100-pay0616.dat", logical_records=["dummy"] + ) def test_read_bsrn_logical_records_not_found(): # Test if an empty dataframe is returned if specified LRs are not present - data, metadata = read_bsrn(DATA_DIR / 'bsrn-lr0100-pay0616.dat', - logical_records=['0300', '0500']) + data, metadata = read_bsrn( + DATA_DIR / "bsrn-lr0100-pay0616.dat", logical_records=["0300", "0500"] + ) assert data.empty # assert that the dataframe is empty - assert 'uva_global' in data.columns - assert 'uvb_reflected_std' in data.columns - assert 'uva_global_max' in data.columns - assert 'dni' not in data.columns - assert 'day' not in data.columns + assert "uva_global" in data.columns + assert "uvb_reflected_std" in data.columns + assert "uva_global_max" in data.columns + assert "dni" not in data.columns + assert "day" not in data.columns @requires_bsrn_credentials @@ -84,20 +96,21 @@ def test_get_bsrn(expected_index, bsrn_credentials): data, metadata = get_bsrn( start=pd.Timestamp(2016, 6, 1), end=pd.Timestamp(2016, 6, 29), - station='tam', + station="tam", username=username, password=password, - save_path=temp_dir.name) + save_path=temp_dir.name, + ) assert_index_equal(expected_index, data.index) - assert 'ghi' in data.columns - assert 'dni_std' in data.columns - assert 'dhi_min' in data.columns - assert 'lwd_max' in data.columns - assert 'relative_humidity' in data.columns + assert "ghi" in data.columns + assert "dni_std" in data.columns + assert "dhi_min" in data.columns + assert "lwd_max" in data.columns + assert "relative_humidity" in data.columns # test that a local file was saved and is read correctly - data2, metadata2 = read_bsrn(os.path.join(temp_dir.name, 'tam0616.dat.gz')) + data2, metadata2 = read_bsrn(os.path.join(temp_dir.name, "tam0616.dat.gz")) assert_index_equal(expected_index, data2.index) - assert 'ghi' in data2.columns + assert "ghi" in data2.columns temp_dir.cleanup() # explicitly remove temporary directory @@ -107,13 +120,14 @@ def test_get_bsrn(expected_index, bsrn_credentials): def test_get_bsrn_bad_station(bsrn_credentials): # Test if KeyError is raised if a bad station name is passed username, password = bsrn_credentials - with pytest.raises(KeyError, match='sub-directory does not exist'): + with pytest.raises(KeyError, match="sub-directory does not exist"): get_bsrn( start=pd.Timestamp(2016, 6, 1), end=pd.Timestamp(2016, 6, 29), - station='not_a_station_name', + station="not_a_station_name", username=username, - password=password) + password=password, + ) @requires_bsrn_credentials @@ -122,10 +136,11 @@ def test_get_bsrn_bad_station(bsrn_credentials): def test_get_bsrn_no_files(bsrn_credentials): username, password = bsrn_credentials # Test if Warning is given if no files are found for the entire time frame - with pytest.warns(UserWarning, match='No files'): + with pytest.warns(UserWarning, match="No files"): get_bsrn( start=pd.Timestamp(1990, 6, 1), end=pd.Timestamp(1990, 6, 29), - station='tam', + station="tam", username=username, - password=password) + password=password, + ) diff --git a/pvlib/tests/iotools/test_crn.py b/pvlib/tests/iotools/test_crn.py index 8d880e0432..aad2329e21 100644 --- a/pvlib/tests/iotools/test_crn.py +++ b/pvlib/tests/iotools/test_crn.py @@ -9,63 +9,216 @@ @pytest.fixture def columns_mapped(): return [ - 'WBANNO', 'UTC_DATE', 'UTC_TIME', 'LST_DATE', 'LST_TIME', 'CRX_VN', - 'longitude', 'latitude', 'temp_air', 'PRECIPITATION', 'ghi', - 'ghi_flag', - 'SURFACE_TEMPERATURE', 'ST_TYPE', 'ST_FLAG', 'relative_humidity', - 'relative_humidity_flag', 'SOIL_MOISTURE_5', 'SOIL_TEMPERATURE_5', - 'WETNESS', 'WET_FLAG', 'wind_speed', 'wind_speed_flag'] + "WBANNO", + "UTC_DATE", + "UTC_TIME", + "LST_DATE", + "LST_TIME", + "CRX_VN", + "longitude", + "latitude", + "temp_air", + "PRECIPITATION", + "ghi", + "ghi_flag", + "SURFACE_TEMPERATURE", + "ST_TYPE", + "ST_FLAG", + "relative_humidity", + "relative_humidity_flag", + "SOIL_MOISTURE_5", + "SOIL_TEMPERATURE_5", + "WETNESS", + "WET_FLAG", + "wind_speed", + "wind_speed_flag", + ] @pytest.fixture def columns_unmapped(): return [ - 'WBANNO', 'UTC_DATE', 'UTC_TIME', 'LST_DATE', 'LST_TIME', 'CRX_VN', - 'LONGITUDE', 'LATITUDE', 'AIR_TEMPERATURE', 'PRECIPITATION', - 'SOLAR_RADIATION', 'SR_FLAG', 'SURFACE_TEMPERATURE', 'ST_TYPE', - 'ST_FLAG', 'RELATIVE_HUMIDITY', 'RH_FLAG', 'SOIL_MOISTURE_5', - 'SOIL_TEMPERATURE_5', 'WETNESS', 'WET_FLAG', 'WIND_1_5', 'WIND_FLAG'] + "WBANNO", + "UTC_DATE", + "UTC_TIME", + "LST_DATE", + "LST_TIME", + "CRX_VN", + "LONGITUDE", + "LATITUDE", + "AIR_TEMPERATURE", + "PRECIPITATION", + "SOLAR_RADIATION", + "SR_FLAG", + "SURFACE_TEMPERATURE", + "ST_TYPE", + "ST_FLAG", + "RELATIVE_HUMIDITY", + "RH_FLAG", + "SOIL_MOISTURE_5", + "SOIL_TEMPERATURE_5", + "WETNESS", + "WET_FLAG", + "WIND_1_5", + "WIND_FLAG", + ] @pytest.fixture def dtypes(): return [ - dtype('int64'), dtype('int64'), dtype('int64'), dtype('int64'), - dtype('int64'), dtype('O'), dtype('float64'), dtype('float64'), - dtype('float64'), dtype('float64'), dtype('float64'), - dtype('int64'), dtype('float64'), dtype('O'), dtype('int64'), - dtype('float64'), dtype('int64'), dtype('float64'), - dtype('float64'), dtype('int64'), dtype('int64'), dtype('float64'), - dtype('int64')] + dtype("int64"), + dtype("int64"), + dtype("int64"), + dtype("int64"), + dtype("int64"), + dtype("O"), + dtype("float64"), + dtype("float64"), + dtype("float64"), + dtype("float64"), + dtype("float64"), + dtype("int64"), + dtype("float64"), + dtype("O"), + dtype("int64"), + dtype("float64"), + dtype("int64"), + dtype("float64"), + dtype("float64"), + dtype("int64"), + dtype("int64"), + dtype("float64"), + dtype("int64"), + ] @pytest.fixture def testfile(): - return DATA_DIR / 'CRNS0101-05-2019-AZ_Tucson_11_W.txt' + return DATA_DIR / "CRNS0101-05-2019-AZ_Tucson_11_W.txt" @pytest.fixture def testfile_problems(): - return DATA_DIR / 'CRN_with_problems.txt' + return DATA_DIR / "CRN_with_problems.txt" def test_read_crn(testfile, columns_mapped, dtypes): - index = pd.DatetimeIndex(['2019-01-01 16:10:00', - '2019-01-01 16:15:00', - '2019-01-01 16:20:00', - '2019-01-01 16:25:00'], - freq=None).tz_localize('UTC') - values = np.array([ - [53131, 20190101, 1610, 20190101, 910, 3, -111.17, 32.24, nan, - 0.0, 296.0, 0, 4.4, 'C', 0, 90.0, 0, nan, nan, 24, 0, 0.78, 0], - [53131, 20190101, 1615, 20190101, 915, 3, -111.17, 32.24, 3.3, - 0.0, 183.0, 0, 4.0, 'C', 0, 87.0, 0, nan, nan, 1182, 0, 0.36, 0], - [53131, 20190101, 1620, 20190101, 920, 3, -111.17, 32.24, 3.5, - 0.0, 340.0, 0, 4.3, 'C', 0, 83.0, 0, nan, nan, 1183, 0, 0.53, 0], - [53131, 20190101, 1625, 20190101, 925, 3, -111.17, 32.24, 4.0, - 0.0, 393.0, 0, 4.8, 'C', 0, 81.0, 0, nan, nan, 1223, 0, 0.64, 0]]) + index = pd.DatetimeIndex( + [ + "2019-01-01 16:10:00", + "2019-01-01 16:15:00", + "2019-01-01 16:20:00", + "2019-01-01 16:25:00", + ], + freq=None, + ).tz_localize("UTC") + values = np.array( + [ + [ + 53131, + 20190101, + 1610, + 20190101, + 910, + 3, + -111.17, + 32.24, + nan, + 0.0, + 296.0, + 0, + 4.4, + "C", + 0, + 90.0, + 0, + nan, + nan, + 24, + 0, + 0.78, + 0, + ], + [ + 53131, + 20190101, + 1615, + 20190101, + 915, + 3, + -111.17, + 32.24, + 3.3, + 0.0, + 183.0, + 0, + 4.0, + "C", + 0, + 87.0, + 0, + nan, + nan, + 1182, + 0, + 0.36, + 0, + ], + [ + 53131, + 20190101, + 1620, + 20190101, + 920, + 3, + -111.17, + 32.24, + 3.5, + 0.0, + 340.0, + 0, + 4.3, + "C", + 0, + 83.0, + 0, + nan, + nan, + 1183, + 0, + 0.53, + 0, + ], + [ + 53131, + 20190101, + 1625, + 20190101, + 925, + 3, + -111.17, + 32.24, + 4.0, + 0.0, + 393.0, + 0, + 4.8, + "C", + 0, + 81.0, + 0, + nan, + nan, + 1223, + 0, + 0.64, + 0, + ], + ] + ) expected = pd.DataFrame(values, columns=columns_mapped, index=index) - for (col, _dtype) in zip(expected.columns, dtypes): + for col, _dtype in zip(expected.columns, dtypes): expected[col] = expected[col].astype(_dtype) out = crn.read_crn(testfile) assert_frame_equal(out, expected) @@ -79,17 +232,65 @@ def test_read_crn_map_variables(testfile, columns_unmapped, dtypes): def test_read_crn_problems(testfile_problems, columns_mapped, dtypes): # GH1025 - index = pd.DatetimeIndex(['2020-07-06 12:00:00', - '2020-07-06 13:10:00'], - freq=None).tz_localize('UTC') - values = np.array([ - [92821, 20200706, 1200, 20200706, 700, '3', -80.69, 28.62, 24.9, - 0.0, np.nan, 0, 25.5, 'C', 0, 93.0, 0, nan, nan, 990, 0, 1.57, 0], - [92821, 20200706, 1310, 20200706, 810, '2.623', -80.69, 28.62, - 26.9, 0.0, 430.0, 0, 30.2, 'C', 0, 87.0, 0, nan, nan, 989, 0, - 1.64, 0]]) + index = pd.DatetimeIndex( + ["2020-07-06 12:00:00", "2020-07-06 13:10:00"], freq=None + ).tz_localize("UTC") + values = np.array( + [ + [ + 92821, + 20200706, + 1200, + 20200706, + 700, + "3", + -80.69, + 28.62, + 24.9, + 0.0, + np.nan, + 0, + 25.5, + "C", + 0, + 93.0, + 0, + nan, + nan, + 990, + 0, + 1.57, + 0, + ], + [ + 92821, + 20200706, + 1310, + 20200706, + 810, + "2.623", + -80.69, + 28.62, + 26.9, + 0.0, + 430.0, + 0, + 30.2, + "C", + 0, + 87.0, + 0, + nan, + nan, + 989, + 0, + 1.64, + 0, + ], + ] + ) expected = pd.DataFrame(values, columns=columns_mapped, index=index) - for (col, _dtype) in zip(expected.columns, dtypes): + for col, _dtype in zip(expected.columns, dtypes): expected[col] = expected[col].astype(_dtype) out = crn.read_crn(testfile_problems) assert_frame_equal(out, expected) diff --git a/pvlib/tests/iotools/test_epw.py b/pvlib/tests/iotools/test_epw.py index f9e9ccba39..3017a4b5b3 100644 --- a/pvlib/tests/iotools/test_epw.py +++ b/pvlib/tests/iotools/test_epw.py @@ -3,7 +3,7 @@ from pvlib.iotools import epw from ..conftest import DATA_DIR, RERUNS, RERUNS_DELAY -epw_testfile = DATA_DIR / 'NLD_Amsterdam062400_IWEC.epw' +epw_testfile = DATA_DIR / "NLD_Amsterdam062400_IWEC.epw" def test_read_epw(): @@ -13,7 +13,7 @@ def test_read_epw(): @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_epw_remote(): - url = 'https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/NLD/NLD_Amsterdam.062400_IWEC/NLD_Amsterdam.062400_IWEC.epw' + url = "https://energyplus-weather.s3.amazonaws.com/europe_wmo_region_6/NLD/NLD_Amsterdam.062400_IWEC/NLD_Amsterdam.062400_IWEC.epw" epw.read_epw(url) diff --git a/pvlib/tests/iotools/test_midc.py b/pvlib/tests/iotools/test_midc.py index 96992babba..5148c03a71 100644 --- a/pvlib/tests/iotools/test_midc.py +++ b/pvlib/tests/iotools/test_midc.py @@ -9,18 +9,19 @@ @pytest.fixture def test_mapping(): return { - 'Direct Normal [W/m^2]': 'dni', - 'Global PSP [W/m^2]': 'ghi', - 'Rel Humidity [%]': 'relative_humidity', - 'Temperature @ 2m [deg C]': 'temp_air', - 'Non Existant': 'variable', + "Direct Normal [W/m^2]": "dni", + "Global PSP [W/m^2]": "ghi", + "Rel Humidity [%]": "relative_humidity", + "Temperature @ 2m [deg C]": "temp_air", + "Non Existant": "variable", } -MIDC_TESTFILE = DATA_DIR / 'midc_20181014.txt' -MIDC_RAW_TESTFILE = DATA_DIR / 'midc_raw_20181018.txt' +MIDC_TESTFILE = DATA_DIR / "midc_20181014.txt" +MIDC_RAW_TESTFILE = DATA_DIR / "midc_raw_20181018.txt" MIDC_RAW_SHORT_HEADER_TESTFILE = ( - DATA_DIR / 'midc_raw_short_header_20191115.txt') + DATA_DIR / "midc_raw_short_header_20191115.txt" +) # TODO: not used, remove? # midc_network_testfile = ('https://midcdmz.nrel.gov/apps/data_api.pl' @@ -41,35 +42,35 @@ def test_midc__format_index(): def test_midc__format_index_tz_conversion(): data = pd.read_csv(MIDC_TESTFILE) - data = data.rename(columns={'MST': 'PST'}) + data = data.rename(columns={"MST": "PST"}) data = midc._format_index(data) - assert data.index[0].tz == pytz.timezone('Etc/GMT+8') + assert data.index[0].tz == pytz.timezone("Etc/GMT+8") def test_midc__format_index_raw(): data = pd.read_csv(MIDC_RAW_TESTFILE) data = midc._format_index_raw(data) - start = pd.Timestamp('20181018 00:00') - start = start.tz_localize('MST') - end = pd.Timestamp('20181018 23:59') - end = end.tz_localize('MST') + start = pd.Timestamp("20181018 00:00") + start = start.tz_localize("MST") + end = pd.Timestamp("20181018 23:59") + end = end.tz_localize("MST") assert data.index[0] == start assert data.index[-1] == end def test_read_midc_var_mapping_as_arg(test_mapping): data = midc.read_midc(MIDC_TESTFILE, variable_map=test_mapping) - assert 'ghi' in data.columns - assert 'temp_air' in data.columns + assert "ghi" in data.columns + assert "temp_air" in data.columns @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_midc_raw_data_from_nrel(): - start_ts = pd.Timestamp('20181018') - end_ts = pd.Timestamp('20181019') - var_map = midc.MIDC_VARIABLE_MAP['UAT'] - data = midc.read_midc_raw_data_from_nrel('UAT', start_ts, end_ts, var_map) + start_ts = pd.Timestamp("20181018") + end_ts = pd.Timestamp("20181019") + var_map = midc.MIDC_VARIABLE_MAP["UAT"] + data = midc.read_midc_raw_data_from_nrel("UAT", start_ts, end_ts, var_map) for k, v in var_map.items(): assert v in data.columns assert data.index.size == 2880 @@ -79,11 +80,10 @@ def test_read_midc_header_length_mismatch(mocker): mock_data = mocker.MagicMock() with MIDC_RAW_SHORT_HEADER_TESTFILE.open() as f: mock_data.text = f.read() - mocker.patch('pvlib.iotools.midc.requests.get', - return_value=mock_data) - start = pd.Timestamp('2019-11-15T00:00:00-06:00') - end = pd.Timestamp('2019-11-15T23:59:00-06:00') - data = midc.read_midc_raw_data_from_nrel('', start, end) + mocker.patch("pvlib.iotools.midc.requests.get", return_value=mock_data) + start = pd.Timestamp("2019-11-15T00:00:00-06:00") + end = pd.Timestamp("2019-11-15T23:59:00-06:00") + data = midc.read_midc_raw_data_from_nrel("", start, end) assert isinstance(data.index, pd.DatetimeIndex) assert data.index[0] == start assert data.index[-1] == end diff --git a/pvlib/tests/iotools/test_panond.py b/pvlib/tests/iotools/test_panond.py index a692d3e119..75c5d8a95c 100644 --- a/pvlib/tests/iotools/test_panond.py +++ b/pvlib/tests/iotools/test_panond.py @@ -5,28 +5,28 @@ from pvlib.iotools import read_panond from pvlib.tests.conftest import DATA_DIR -PAN_FILE = DATA_DIR / 'ET-M772BH550GL.PAN' -OND_FILE = DATA_DIR / 'CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND' +PAN_FILE = DATA_DIR / "ET-M772BH550GL.PAN" +OND_FILE = DATA_DIR / "CPS SCH275KTL-DO-US-800-250kW_275kVA_1.OND" def test_read_panond(): # test that returned contents have expected keys, types, and structure - pan = read_panond(PAN_FILE, encoding='utf-8-sig') - assert list(pan.keys()) == ['PVObject_'] - pan = pan['PVObject_'] - assert pan['PVObject_Commercial']['Model'] == 'ET-M772BH550GL' - assert pan['Voc'] == 49.9 - assert pan['PVObject_IAM']['IAMProfile']['Point_5'] == [50.0, 0.98] - assert pan['BifacialityFactor'] == 0.7 - assert pan['FrontSurface'] == 'fsARCoating' - assert pan['Technol'] == 'mtSiMono' + pan = read_panond(PAN_FILE, encoding="utf-8-sig") + assert list(pan.keys()) == ["PVObject_"] + pan = pan["PVObject_"] + assert pan["PVObject_Commercial"]["Model"] == "ET-M772BH550GL" + assert pan["Voc"] == 49.9 + assert pan["PVObject_IAM"]["IAMProfile"]["Point_5"] == [50.0, 0.98] + assert pan["BifacialityFactor"] == 0.7 + assert pan["FrontSurface"] == "fsARCoating" + assert pan["Technol"] == "mtSiMono" - ond = read_panond(OND_FILE, encoding='utf-8-sig') - assert list(ond.keys()) == ['PVObject_'] - ond = ond['PVObject_'] - assert ond['PVObject_Commercial']['Model'] == 'CPS SCH275KTL-DO/US-800' - assert ond['TanPhiMin'] == -0.75 - assert ond['NbMPPT'] == 12 - assert ond['Converter']['ModeOper'] == 'MPPT' - assert ond['Converter']['ProfilPIOV2']['Point_5'] == [75795.9, 75000.0] + ond = read_panond(OND_FILE, encoding="utf-8-sig") + assert list(ond.keys()) == ["PVObject_"] + ond = ond["PVObject_"] + assert ond["PVObject_Commercial"]["Model"] == "CPS SCH275KTL-DO/US-800" + assert ond["TanPhiMin"] == -0.75 + assert ond["NbMPPT"] == 12 + assert ond["Converter"]["ModeOper"] == "MPPT" + assert ond["Converter"]["ProfilPIOV2"]["Point_5"] == [75795.9, 75000.0] diff --git a/pvlib/tests/iotools/test_psm3.py b/pvlib/tests/iotools/test_psm3.py index b68671696b..d265af1498 100644 --- a/pvlib/tests/iotools/test_psm3.py +++ b/pvlib/tests/iotools/test_psm3.py @@ -11,20 +11,35 @@ from requests import HTTPError from io import StringIO import warnings -from pvlib._deprecation import pvlibDeprecationWarning -TMY_TEST_DATA = DATA_DIR / 'test_psm3_tmy-2017.csv' -YEAR_TEST_DATA = DATA_DIR / 'test_psm3_2017.csv' -YEAR_TEST_DATA_5MIN = DATA_DIR / 'test_psm3_2019_5min.csv' -MANUAL_TEST_DATA = DATA_DIR / 'test_read_psm3.csv' +TMY_TEST_DATA = DATA_DIR / "test_psm3_tmy-2017.csv" +YEAR_TEST_DATA = DATA_DIR / "test_psm3_2017.csv" +YEAR_TEST_DATA_5MIN = DATA_DIR / "test_psm3_2019_5min.csv" +MANUAL_TEST_DATA = DATA_DIR / "test_read_psm3.csv" LATITUDE, LONGITUDE = 40.5137, -108.5449 METADATA_FIELDS = [ - 'Source', 'Location ID', 'City', 'State', 'Country', 'Latitude', - 'Longitude', 'Time Zone', 'Elevation', 'Local Time Zone', - 'Dew Point Units', 'DHI Units', 'DNI Units', 'GHI Units', - 'Temperature Units', 'Pressure Units', 'Wind Direction Units', - 'Wind Speed Units', 'Surface Albedo Units', 'Version'] -PVLIB_EMAIL = 'pvlib-admin@googlegroups.com' + "Source", + "Location ID", + "City", + "State", + "Country", + "Latitude", + "Longitude", + "Time Zone", + "Elevation", + "Local Time Zone", + "Dew Point Units", + "DHI Units", + "DNI Units", + "GHI Units", + "Temperature Units", + "Pressure Units", + "Wind Direction Units", + "Wind Speed Units", + "Surface Albedo Units", + "Version", +] +PVLIB_EMAIL = "pvlib-admin@googlegroups.com" @pytest.fixture(scope="module") @@ -43,7 +58,7 @@ def nrel_api_key(): "WARNING: NREL API KEY environment variable not set! " "Using DEMO_KEY instead. Unexpected failures may occur." ) - demo_key = 'DEMO_KEY' + demo_key = "DEMO_KEY" return demo_key @@ -61,24 +76,30 @@ def assert_psm3_equal(data, metadata, expected): assert np.allclose(data.DHI, expected.DHI) assert np.allclose(data.Temperature, expected.Temperature) assert np.allclose(data.Pressure, expected.Pressure) - assert np.allclose(data['Dew Point'], expected['Dew Point']) - assert np.allclose(data['Surface Albedo'], expected['Surface Albedo']) - assert np.allclose(data['Wind Speed'], expected['Wind Speed']) - assert np.allclose(data['Wind Direction'], expected['Wind Direction']) + assert np.allclose(data["Dew Point"], expected["Dew Point"]) + assert np.allclose(data["Surface Albedo"], expected["Surface Albedo"]) + assert np.allclose(data["Wind Speed"], expected["Wind Speed"]) + assert np.allclose(data["Wind Direction"], expected["Wind Direction"]) # check header for mf in METADATA_FIELDS: assert mf in metadata # check timezone - assert (data.index.tzinfo.zone == 'Etc/GMT%+d' % -metadata['Time Zone']) + assert data.index.tzinfo.zone == "Etc/GMT%+d" % -metadata["Time Zone"] @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_psm3_tmy(nrel_api_key): """test get_psm3 with a TMY""" - data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, - PVLIB_EMAIL, names='tmy-2017', - leap_day=False, map_variables=False) + data, metadata = psm3.get_psm3( + LATITUDE, + LONGITUDE, + nrel_api_key, + PVLIB_EMAIL, + names="tmy-2017", + leap_day=False, + map_variables=False, + ) expected = pd.read_csv(TMY_TEST_DATA) assert_psm3_equal(data, metadata, expected) @@ -87,10 +108,16 @@ def test_get_psm3_tmy(nrel_api_key): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_psm3_singleyear(nrel_api_key): """test get_psm3 with a single year""" - data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, - PVLIB_EMAIL, names='2017', - leap_day=False, map_variables=False, - interval=30) + data, metadata = psm3.get_psm3( + LATITUDE, + LONGITUDE, + nrel_api_key, + PVLIB_EMAIL, + names="2017", + leap_day=False, + map_variables=False, + interval=30, + ) expected = pd.read_csv(YEAR_TEST_DATA) assert_psm3_equal(data, metadata, expected) @@ -99,11 +126,18 @@ def test_get_psm3_singleyear(nrel_api_key): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_psm3_5min(nrel_api_key): """test get_psm3 for 5-minute data""" - data, metadata = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, - PVLIB_EMAIL, names='2019', interval=5, - leap_day=False, map_variables=False) - assert len(data) == 525600/5 - first_day = data.loc['2019-01-01'] + data, metadata = psm3.get_psm3( + LATITUDE, + LONGITUDE, + nrel_api_key, + PVLIB_EMAIL, + names="2019", + interval=5, + leap_day=False, + map_variables=False, + ) + assert len(data) == 525600 / 5 + first_day = data.loc["2019-01-01"] expected = pd.read_csv(YEAR_TEST_DATA_5MIN) assert_psm3_equal(first_day, metadata, expected) @@ -111,23 +145,31 @@ def test_get_psm3_5min(nrel_api_key): @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_psm3_check_leap_day(nrel_api_key): - data_2012, _ = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, - PVLIB_EMAIL, names="2012", interval=60, - leap_day=True, map_variables=False) + data_2012, _ = psm3.get_psm3( + LATITUDE, + LONGITUDE, + nrel_api_key, + PVLIB_EMAIL, + names="2012", + interval=60, + leap_day=True, + map_variables=False, + ) assert len(data_2012) == (8760 + 24) -@pytest.mark.parametrize('latitude, longitude, api_key, names, interval', - [(LATITUDE, LONGITUDE, 'BAD', 'tmy-2017', 60), - (51, -5, nrel_api_key, 'tmy-2017', 60), - (LATITUDE, LONGITUDE, nrel_api_key, 'bad', 60), - (LATITUDE, LONGITUDE, nrel_api_key, '2017', 15), - ]) +@pytest.mark.parametrize( + "latitude, longitude, api_key, names, interval", + [ + (LATITUDE, LONGITUDE, "BAD", "tmy-2017", 60), + (51, -5, nrel_api_key, "tmy-2017", 60), + (LATITUDE, LONGITUDE, nrel_api_key, "bad", 60), + (LATITUDE, LONGITUDE, nrel_api_key, "2017", 15), + ], +) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) -def test_get_psm3_tmy_errors( - latitude, longitude, api_key, names, interval -): +def test_get_psm3_tmy_errors(latitude, longitude, api_key, names, interval): """Test get_psm3() for multiple erroneous input scenarios. These scenarios include: @@ -137,9 +179,16 @@ def test_get_psm3_tmy_errors( * Bad interval, single year -> Intervals can only be 30 or 60 minutes. """ with pytest.raises(HTTPError) as excinfo: - psm3.get_psm3(latitude, longitude, api_key, PVLIB_EMAIL, - names=names, interval=interval, leap_day=False, - map_variables=False) + psm3.get_psm3( + latitude, + longitude, + api_key, + PVLIB_EMAIL, + names=names, + interval=interval, + leap_day=False, + map_variables=False, + ) # ensure the HTTPError caught isn't due to overuse of the API key assert "OVER_RATE_LIMIT" not in str(excinfo.value) @@ -170,12 +219,30 @@ def test_read_psm3(): def test_read_psm3_map_variables(): """test read_psm3 map_variables=True""" data, metadata = psm3.read_psm3(MANUAL_TEST_DATA, map_variables=True) - columns_mapped = ['Year', 'Month', 'Day', 'Hour', 'Minute', 'dhi', 'ghi', - 'dni', 'ghi_clear', 'dhi_clear', 'dni_clear', - 'Cloud Type', 'temp_dew', 'solar_zenith', - 'Fill Flag', 'albedo', 'wind_speed', - 'wind_direction', 'precipitable_water', - 'relative_humidity', 'temp_air', 'pressure'] + columns_mapped = [ + "Year", + "Month", + "Day", + "Hour", + "Minute", + "dhi", + "ghi", + "dni", + "ghi_clear", + "dhi_clear", + "dni_clear", + "Cloud Type", + "temp_dew", + "solar_zenith", + "Fill Flag", + "albedo", + "wind_speed", + "wind_direction", + "precipitable_water", + "relative_humidity", + "temp_air", + "pressure", + ] data, metadata = psm3.read_psm3(MANUAL_TEST_DATA, map_variables=True) assert_index_equal(data.columns, pd.Index(columns_mapped)) @@ -185,14 +252,28 @@ def test_read_psm3_map_variables(): def test_get_psm3_attribute_mapping(nrel_api_key): """Test that pvlib names can be passed in as attributes and get correctly reverse mapped to PSM3 names""" - data, meta = psm3.get_psm3(LATITUDE, LONGITUDE, nrel_api_key, PVLIB_EMAIL, - names=2019, interval=60, - attributes=['ghi', 'wind_speed'], - leap_day=False, map_variables=True) + data, meta = psm3.get_psm3( + LATITUDE, + LONGITUDE, + nrel_api_key, + PVLIB_EMAIL, + names=2019, + interval=60, + attributes=["ghi", "wind_speed"], + leap_day=False, + map_variables=True, + ) # Check that columns are in the correct order (GH1647) expected_columns = [ - 'Year', 'Month', 'Day', 'Hour', 'Minute', 'ghi', 'wind_speed'] + "Year", + "Month", + "Day", + "Hour", + "Minute", + "ghi", + "wind_speed", + ] pd.testing.assert_index_equal(pd.Index(expected_columns), data.columns) - assert 'latitude' in meta.keys() - assert 'longitude' in meta.keys() - assert 'altitude' in meta.keys() + assert "latitude" in meta.keys() + assert "longitude" in meta.keys() + assert "altitude" in meta.keys() diff --git a/pvlib/tests/iotools/test_pvgis.py b/pvlib/tests/iotools/test_pvgis.py index 9a999592a7..25dd8fe764 100644 --- a/pvlib/tests/iotools/test_pvgis.py +++ b/pvlib/tests/iotools/test_pvgis.py @@ -1,6 +1,7 @@ """ test the pvgis IO tools """ + import json import numpy as np import pandas as pd @@ -10,32 +11,61 @@ from pvlib.iotools import get_pvgis_tmy, read_pvgis_tmy from pvlib.iotools import get_pvgis_hourly, read_pvgis_hourly from pvlib.iotools import get_pvgis_horizon -from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, assert_frame_equal, - assert_series_equal) +from ..conftest import ( + DATA_DIR, + RERUNS, + RERUNS_DELAY, + assert_frame_equal, + assert_series_equal, +) # PVGIS Hourly tests # The test files are actual files from PVGIS where the data section have been # reduced to only a few lines -testfile_radiation_csv = DATA_DIR / \ - 'pvgis_hourly_Timeseries_45.000_8.000_SA_30deg_0deg_2016_2016.csv' -testfile_pv_json = DATA_DIR / \ - 'pvgis_hourly_Timeseries_45.000_8.000_SA2_10kWp_CIS_5_2a_2013_2014.json' - -index_radiation_csv = \ - pd.date_range('20160101 00:10', freq='1h', periods=14, tz='UTC') -index_pv_json = \ - pd.date_range('2013-01-01 00:10', freq='1h', periods=10, tz='UTC') +testfile_radiation_csv = ( + DATA_DIR + / "pvgis_hourly_Timeseries_45.000_8.000_SA_30deg_0deg_2016_2016.csv" +) +testfile_pv_json = ( + DATA_DIR + / "pvgis_hourly_Timeseries_45.000_8.000_SA2_10kWp_CIS_5_2a_2013_2014.json" +) + +index_radiation_csv = pd.date_range( + "20160101 00:10", freq="1h", periods=14, tz="UTC" +) +index_pv_json = pd.date_range( + "2013-01-01 00:10", freq="1h", periods=10, tz="UTC" +) columns_radiation_csv = [ - 'Gb(i)', 'Gd(i)', 'Gr(i)', 'H_sun', 'T2m', 'WS10m', 'Int'] + "Gb(i)", + "Gd(i)", + "Gr(i)", + "H_sun", + "T2m", + "WS10m", + "Int", +] columns_radiation_csv_mapped = [ - 'poa_direct', 'poa_sky_diffuse', 'poa_ground_diffuse', 'solar_elevation', - 'temp_air', 'wind_speed', 'Int'] -columns_pv_json = [ - 'P', 'G(i)', 'H_sun', 'T2m', 'WS10m', 'Int'] + "poa_direct", + "poa_sky_diffuse", + "poa_ground_diffuse", + "solar_elevation", + "temp_air", + "wind_speed", + "Int", +] +columns_pv_json = ["P", "G(i)", "H_sun", "T2m", "WS10m", "Int"] columns_pv_json_mapped = [ - 'P', 'poa_global', 'solar_elevation', 'temp_air', 'wind_speed', 'Int'] + "P", + "poa_global", + "solar_elevation", + "temp_air", + "wind_speed", + "Int", +] data_radiation_csv = [ [0.0, 0.0, 0.0, 0.0, 3.44, 1.43, 0.0], @@ -51,7 +81,8 @@ [2.19, 0.94, 0.03, 19.54, 5.73, 0.77, 1.0], [2.11, 0.94, 0.03, 21.82, 6.79, 0.58, 1.0], [4.25, 1.88, 0.05, 21.41, 7.84, 0.4, 1.0], - [0.0, 0.0, 0.0, 0.0, 7.43, 0.72, 0.0]] + [0.0, 0.0, 0.0, 0.0, 7.43, 0.72, 0.0], +] data_pv_json = [ [0.0, 0.0, 0.0, -0.97, 1.52, 0.0], [0.0, 0.0, 0.0, -1.06, 1.45, 0.0], @@ -62,70 +93,133 @@ [0.0, 0.0, 0.0, 0.29, 1.03, 0.0], [0.0, 0.0, 0.0, 1.0, 0.62, 0.0], [1187.2, 129.59, 8.06, 0.97, 0.97, 0.0], - [3950.1, 423.28, 14.8, 1.89, 0.69, 0.0]] - -inputs_radiation_csv = {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0, - 'radiation_database': 'PVGIS-SARAH', - 'Slope': '30 deg.', 'Azimuth': '0 deg.'} + [3950.1, 423.28, 14.8, 1.89, 0.69, 0.0], +] + +inputs_radiation_csv = { + "latitude": 45.0, + "longitude": 8.0, + "elevation": 250.0, + "radiation_database": "PVGIS-SARAH", + "Slope": "30 deg.", + "Azimuth": "0 deg.", +} metadata_radiation_csv = { - 'Gb(i)': 'Beam (direct) irradiance on the inclined plane (plane of the array) (W/m2)', # noqa: E501 - 'Gd(i)': 'Diffuse irradiance on the inclined plane (plane of the array) (W/m2)', # noqa: E501 - 'Gr(i)': 'Reflected irradiance on the inclined plane (plane of the array) (W/m2)', # noqa: E501 - 'H_sun': 'Sun height (degree)', - 'T2m': '2-m air temperature (degree Celsius)', - 'WS10m': '10-m total wind speed (m/s)', - 'Int': '1 means solar radiation values are reconstructed'} + "Gb(i)": "Beam (direct) irradiance on the inclined plane (plane of the array) (W/m2)", # noqa: E501 + "Gd(i)": "Diffuse irradiance on the inclined plane (plane of the array) (W/m2)", # noqa: E501 + "Gr(i)": "Reflected irradiance on the inclined plane (plane of the array) (W/m2)", # noqa: E501 + "H_sun": "Sun height (degree)", + "T2m": "2-m air temperature (degree Celsius)", + "WS10m": "10-m total wind speed (m/s)", + "Int": "1 means solar radiation values are reconstructed", +} inputs_pv_json = { - 'location': {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0}, - 'meteo_data': {'radiation_db': 'PVGIS-SARAH2', 'meteo_db': 'ERA-Interim', - 'year_min': 2013, 'year_max': 2014, 'use_horizon': True, - 'horizon_db': None, 'horizon_data': 'DEM-calculated'}, - 'mounting_system': {'two_axis': { - 'slope': {'value': '-', 'optimal': '-'}, - 'azimuth': {'value': '-', 'optimal': '-'}}}, - 'pv_module': {'technology': 'CIS', 'peak_power': 10.0, 'system_loss': 5.0}} + "location": {"latitude": 45.0, "longitude": 8.0, "elevation": 250.0}, + "meteo_data": { + "radiation_db": "PVGIS-SARAH2", + "meteo_db": "ERA-Interim", + "year_min": 2013, + "year_max": 2014, + "use_horizon": True, + "horizon_db": None, + "horizon_data": "DEM-calculated", + }, + "mounting_system": { + "two_axis": { + "slope": {"value": "-", "optimal": "-"}, + "azimuth": {"value": "-", "optimal": "-"}, + } + }, + "pv_module": {"technology": "CIS", "peak_power": 10.0, "system_loss": 5.0}, +} metadata_pv_json = { - 'inputs': { - 'location': - {'description': 'Selected location', 'variables': { - 'latitude': {'description': 'Latitude', 'units': 'decimal degree'}, # noqa: E501 - 'longitude': {'description': 'Longitude', 'units': 'decimal degree'}, # noqa: E501 - 'elevation': {'description': 'Elevation', 'units': 'm'}}}, - 'meteo_data': { - 'description': 'Sources of meteorological data', - 'variables': { - 'radiation_db': {'description': 'Solar radiation database'}, # noqa: E501 - 'meteo_db': {'description': 'Database used for meteorological variables other than solar radiation'}, # noqa: E501 - 'year_min': {'description': 'First year of the calculations'}, # noqa: E501 - 'year_max': {'description': 'Last year of the calculations'}, # noqa: E501 - 'use_horizon': {'description': 'Include horizon shadows'}, - 'horizon_db': {'description': 'Source of horizon data'}}}, - 'mounting_system': { - 'description': 'Mounting system', - 'choices': 'fixed, vertical_axis, inclined_axis, two_axis', - 'fields': { - 'slope': {'description': 'Inclination angle from the horizontal plane', 'units': 'degree'}, # noqa: E501 - 'azimuth': {'description': 'Orientation (azimuth) angle of the (fixed) PV system (0 = S, 90 = W, -90 = E)', 'units': 'degree'}}}, # noqa: E501 - 'pv_module': { - 'description': 'PV module parameters', - 'variables': { - 'technology': {'description': 'PV technology'}, - 'peak_power': {'description': 'Nominal (peak) power of the PV module', 'units': 'kW'}, # noqa: E501 - 'system_loss': {'description': 'Sum of system losses', 'units': '%'}}}}, # noqa: E501 - 'outputs': { - 'hourly': { - 'type': 'time series', 'timestamp': 'hourly averages', - 'variables': { - 'P': {'description': 'PV system power', 'units': 'W'}, - 'G(i)': {'description': 'Global irradiance on the inclined plane (plane of the array)', 'units': 'W/m2'}, # noqa: E501 - 'H_sun': {'description': 'Sun height', 'units': 'degree'}, - 'T2m': {'description': '2-m air temperature', 'units': 'degree Celsius'}, # noqa: E501 - 'WS10m': {'description': '10-m total wind speed', 'units': 'm/s'}, # noqa: E501 - 'Int': {'description': '1 means solar radiation values are reconstructed'}}}}} # noqa: E501 + "inputs": { + "location": { + "description": "Selected location", + "variables": { + "latitude": { + "description": "Latitude", + "units": "decimal degree", + }, # noqa: E501 + "longitude": { + "description": "Longitude", + "units": "decimal degree", + }, # noqa: E501 + "elevation": {"description": "Elevation", "units": "m"}, + }, + }, + "meteo_data": { + "description": "Sources of meteorological data", + "variables": { + "radiation_db": {"description": "Solar radiation database"}, # noqa: E501 + "meteo_db": { + "description": "Database used for meteorological variables other than solar radiation" + }, # noqa: E501 + "year_min": {"description": "First year of the calculations"}, # noqa: E501 + "year_max": {"description": "Last year of the calculations"}, # noqa: E501 + "use_horizon": {"description": "Include horizon shadows"}, + "horizon_db": {"description": "Source of horizon data"}, + }, + }, + "mounting_system": { + "description": "Mounting system", + "choices": "fixed, vertical_axis, inclined_axis, two_axis", + "fields": { + "slope": { + "description": "Inclination angle from the horizontal plane", + "units": "degree", + }, # noqa: E501 + "azimuth": { + "description": "Orientation (azimuth) angle of the (fixed) PV system (0 = S, 90 = W, -90 = E)", + "units": "degree", + }, + }, + }, # noqa: E501 + "pv_module": { + "description": "PV module parameters", + "variables": { + "technology": {"description": "PV technology"}, + "peak_power": { + "description": "Nominal (peak) power of the PV module", + "units": "kW", + }, # noqa: E501 + "system_loss": { + "description": "Sum of system losses", + "units": "%", + }, + }, + }, + }, # noqa: E501 + "outputs": { + "hourly": { + "type": "time series", + "timestamp": "hourly averages", + "variables": { + "P": {"description": "PV system power", "units": "W"}, + "G(i)": { + "description": "Global irradiance on the inclined plane (plane of the array)", + "units": "W/m2", + }, # noqa: E501 + "H_sun": {"description": "Sun height", "units": "degree"}, + "T2m": { + "description": "2-m air temperature", + "units": "degree Celsius", + }, # noqa: E501 + "WS10m": { + "description": "10-m total wind speed", + "units": "m/s", + }, # noqa: E501 + "Int": { + "description": "1 means solar radiation values are reconstructed" + }, + }, + } + }, +} # noqa: E501 def generate_expected_dataframe(values, columns, index): @@ -133,8 +227,8 @@ def generate_expected_dataframe(values, columns, index): use this dataframe to compare to. """ expected = pd.DataFrame(index=index, data=values, columns=columns) - expected['Int'] = expected['Int'].astype(int) - expected.index.name = 'time' + expected["Int"] = expected["Int"].astype(int) + expected.index.name = "time" expected.index.freq = None return expected @@ -142,50 +236,90 @@ def generate_expected_dataframe(values, columns, index): @pytest.fixture def expected_radiation_csv(): expected = generate_expected_dataframe( - data_radiation_csv, columns_radiation_csv, index_radiation_csv) + data_radiation_csv, columns_radiation_csv, index_radiation_csv + ) return expected @pytest.fixture def expected_radiation_csv_mapped(): expected = generate_expected_dataframe( - data_radiation_csv, columns_radiation_csv_mapped, index_radiation_csv) + data_radiation_csv, columns_radiation_csv_mapped, index_radiation_csv + ) return expected @pytest.fixture def expected_pv_json(): expected = generate_expected_dataframe( - data_pv_json, columns_pv_json, index_pv_json) + data_pv_json, columns_pv_json, index_pv_json + ) return expected @pytest.fixture def expected_pv_json_mapped(): expected = generate_expected_dataframe( - data_pv_json, columns_pv_json_mapped, index_pv_json) + data_pv_json, columns_pv_json_mapped, index_pv_json + ) return expected # Test read_pvgis_hourly function using two different files with different # input arguments (to test variable mapping and pvgis_format) # pytest request.getfixturevalue is used to simplify the input arguments -@pytest.mark.parametrize('testfile,expected_name,metadata_exp,inputs_exp,map_variables,pvgis_format', [ # noqa: E501 - (testfile_radiation_csv, 'expected_radiation_csv', metadata_radiation_csv, - inputs_radiation_csv, False, None), - (testfile_radiation_csv, 'expected_radiation_csv_mapped', - metadata_radiation_csv, inputs_radiation_csv, True, 'csv'), - (testfile_pv_json, 'expected_pv_json', metadata_pv_json, inputs_pv_json, - False, None), - (testfile_pv_json, 'expected_pv_json_mapped', metadata_pv_json, - inputs_pv_json, True, 'json')]) -def test_read_pvgis_hourly(testfile, expected_name, metadata_exp, - inputs_exp, map_variables, pvgis_format, request): +@pytest.mark.parametrize( + "testfile,expected_name,metadata_exp,inputs_exp,map_variables,pvgis_format", + [ # noqa: E501 + ( + testfile_radiation_csv, + "expected_radiation_csv", + metadata_radiation_csv, + inputs_radiation_csv, + False, + None, + ), + ( + testfile_radiation_csv, + "expected_radiation_csv_mapped", + metadata_radiation_csv, + inputs_radiation_csv, + True, + "csv", + ), + ( + testfile_pv_json, + "expected_pv_json", + metadata_pv_json, + inputs_pv_json, + False, + None, + ), + ( + testfile_pv_json, + "expected_pv_json_mapped", + metadata_pv_json, + inputs_pv_json, + True, + "json", + ), + ], +) +def test_read_pvgis_hourly( + testfile, + expected_name, + metadata_exp, + inputs_exp, + map_variables, + pvgis_format, + request, +): # Get expected dataframe from fixture expected = request.getfixturevalue(expected_name) # Read data from file out, inputs, metadata = read_pvgis_hourly( - testfile, map_variables=map_variables, pvgis_format=pvgis_format) + testfile, map_variables=map_variables, pvgis_format=pvgis_format + ) # Assert whether dataframe, metadata, and inputs are as expected assert_frame_equal(out, expected) assert inputs == inputs_exp @@ -196,10 +330,10 @@ def test_read_pvgis_hourly_bad_extension(): # Test if ValueError is raised if file extension cannot be recognized and # pvgis_format is not specified with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"): - read_pvgis_hourly('filename.txt') + read_pvgis_hourly("filename.txt") # Test if ValueError is raised if an unkonwn pvgis_format is specified with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"): - read_pvgis_hourly(testfile_pv_json, pvgis_format='txt') + read_pvgis_hourly(testfile_pv_json, pvgis_format="txt") # Test if TypeError is raised if input is a buffer and pvgis_format=None. # The error text changed in python 3.12. This regex matches both versions: with pytest.raises(TypeError, match="str.*os.PathLike"): @@ -207,44 +341,96 @@ def test_read_pvgis_hourly_bad_extension(): args_radiation_csv = { - 'surface_tilt': 30, 'surface_azimuth': 180, 'outputformat': 'csv', - 'usehorizon': False, 'userhorizon': None, 'raddatabase': 'PVGIS-SARAH', - 'start': 2016, 'end': 2016, 'pvcalculation': False, 'components': True} - -url_hourly_radiation_csv = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=csv&angle=30&aspect=0&usehorizon=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&raddatabase=PVGIS-SARAH&startyear=2016&endyear=2016' # noqa: E501 + "surface_tilt": 30, + "surface_azimuth": 180, + "outputformat": "csv", + "usehorizon": False, + "userhorizon": None, + "raddatabase": "PVGIS-SARAH", + "start": 2016, + "end": 2016, + "pvcalculation": False, + "components": True, +} + +url_hourly_radiation_csv = "https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=csv&angle=30&aspect=0&usehorizon=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&raddatabase=PVGIS-SARAH&startyear=2016&endyear=2016" # noqa: E501 args_pv_json = { - 'surface_tilt': 30, 'surface_azimuth': 180, 'outputformat': 'json', - 'usehorizon': True, 'userhorizon': None, 'raddatabase': 'PVGIS-SARAH2', - 'start': pd.Timestamp(2013, 1, 1), 'end': pd.Timestamp(2014, 5, 1), - 'pvcalculation': True, 'peakpower': 10, 'pvtechchoice': 'CIS', 'loss': 5, - 'trackingtype': 2, 'optimalangles': True, 'components': False, - 'url': 'https://re.jrc.ec.europa.eu/api/v5_2/'} - -url_pv_json = 'https://re.jrc.ec.europa.eu/api/v5_2/seriescalc?lat=45&lon=8&outputformat=json&angle=30&aspect=0&pvtechchoice=CIS&mountingplace=free&trackingtype=2&components=0&usehorizon=1&raddatabase=PVGIS-SARAH2&startyear=2013&endyear=2014&pvcalculation=1&peakpower=10&loss=5&optimalangles=1' # noqa: E501 - - -@pytest.mark.parametrize('testfile,expected_name,args,map_variables,url_test', [ # noqa: E501 - (testfile_radiation_csv, 'expected_radiation_csv', - args_radiation_csv, False, url_hourly_radiation_csv), - (testfile_radiation_csv, 'expected_radiation_csv_mapped', - args_radiation_csv, True, url_hourly_radiation_csv), - (testfile_pv_json, 'expected_pv_json', args_pv_json, False, url_pv_json), - (testfile_pv_json, 'expected_pv_json_mapped', args_pv_json, True, - url_pv_json)]) -def test_get_pvgis_hourly(requests_mock, testfile, expected_name, args, - map_variables, url_test, request): + "surface_tilt": 30, + "surface_azimuth": 180, + "outputformat": "json", + "usehorizon": True, + "userhorizon": None, + "raddatabase": "PVGIS-SARAH2", + "start": pd.Timestamp(2013, 1, 1), + "end": pd.Timestamp(2014, 5, 1), + "pvcalculation": True, + "peakpower": 10, + "pvtechchoice": "CIS", + "loss": 5, + "trackingtype": 2, + "optimalangles": True, + "components": False, + "url": "https://re.jrc.ec.europa.eu/api/v5_2/", +} + +url_pv_json = "https://re.jrc.ec.europa.eu/api/v5_2/seriescalc?lat=45&lon=8&outputformat=json&angle=30&aspect=0&pvtechchoice=CIS&mountingplace=free&trackingtype=2&components=0&usehorizon=1&raddatabase=PVGIS-SARAH2&startyear=2013&endyear=2014&pvcalculation=1&peakpower=10&loss=5&optimalangles=1" # noqa: E501 + + +@pytest.mark.parametrize( + "testfile,expected_name,args,map_variables,url_test", + [ # noqa: E501 + ( + testfile_radiation_csv, + "expected_radiation_csv", + args_radiation_csv, + False, + url_hourly_radiation_csv, + ), + ( + testfile_radiation_csv, + "expected_radiation_csv_mapped", + args_radiation_csv, + True, + url_hourly_radiation_csv, + ), + ( + testfile_pv_json, + "expected_pv_json", + args_pv_json, + False, + url_pv_json, + ), + ( + testfile_pv_json, + "expected_pv_json_mapped", + args_pv_json, + True, + url_pv_json, + ), + ], +) +def test_get_pvgis_hourly( + requests_mock, + testfile, + expected_name, + args, + map_variables, + url_test, + request, +): """Test that get_pvgis_hourly generates the correct URI request and that _parse_pvgis_hourly_json and _parse_pvgis_hourly_csv is called correctly""" # Open local test file containing McClear monthly data - with open(testfile, 'r') as test_file: + with open(testfile, "r") as test_file: mock_response = test_file.read() # Specify the full URI of a specific example, this ensures that all of the # inputs are passing on correctly requests_mock.get(url_test, text=mock_response) # Make API call - an error is raised if requested URI does not match out, inputs, metadata = get_pvgis_hourly( - latitude=45, longitude=8, map_variables=map_variables, **args) + latitude=45, longitude=8, map_variables=map_variables, **args + ) # Get expected dataframe from fixture expected = request.getfixturevalue(expected_name) # Compare out and expected dataframes @@ -257,13 +443,14 @@ def test_get_pvgis_hourly_bad_status_code(requests_mock): with pytest.raises(requests.HTTPError): get_pvgis_hourly(latitude=45, longitude=8, **args_pv_json) # Test if HTTPError is raised and error message is returned if avaiable - requests_mock.get(url_pv_json, status_code=400, - json={'message': 'peakpower Mandatory'}) + requests_mock.get( + url_pv_json, status_code=400, json={"message": "peakpower Mandatory"} + ) with pytest.raises(requests.HTTPError): get_pvgis_hourly(latitude=45, longitude=8, **args_pv_json) -url_bad_outputformat = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=basic&angle=0&aspect=0&pvcalculation=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=0&optimalinclination=0&loss=0' # noqa: E501 +url_bad_outputformat = "https://re.jrc.ec.europa.eu/api/seriescalc?lat=45&lon=8&outputformat=basic&angle=0&aspect=0&pvcalculation=0&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=0&optimalinclination=0&loss=0" # noqa: E501 def test_get_pvgis_hourly_bad_outputformat(requests_mock): @@ -271,166 +458,207 @@ def test_get_pvgis_hourly_bad_outputformat(requests_mock): # E.g. 'basic' is a valid PVGIS format, but is not supported by pvlib requests_mock.get(url_bad_outputformat) with pytest.raises(ValueError): - get_pvgis_hourly(latitude=45, longitude=8, outputformat='basic') + get_pvgis_hourly(latitude=45, longitude=8, outputformat="basic") -url_additional_inputs = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=55.6814&lon=12.5758&outputformat=csv&angle=0&aspect=0&pvcalculation=1&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=1&optimalinclination=0&loss=2&userhorizon=10%2C15%2C20%2C10&peakpower=5' # noqa: E501 +url_additional_inputs = "https://re.jrc.ec.europa.eu/api/seriescalc?lat=55.6814&lon=12.5758&outputformat=csv&angle=0&aspect=0&pvcalculation=1&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=1&optimalinclination=0&loss=2&userhorizon=10%2C15%2C20%2C10&peakpower=5" # noqa: E501 def test_get_pvgis_hourly_additional_inputs(requests_mock): # Test additional inputs, including userhorizons # Necessary to pass a test file in order for the parser not to fail - with open(testfile_radiation_csv, 'r') as test_file: + with open(testfile_radiation_csv, "r") as test_file: mock_response = test_file.read() requests_mock.get(url_additional_inputs, text=mock_response) # Make request with userhorizon specified # Test passes if the request made by get_pvgis_hourly matches exactly the # url passed to the mock request (url_additional_inputs) get_pvgis_hourly( - latitude=55.6814, longitude=12.5758, outputformat='csv', - usehorizon=True, userhorizon=[10, 15, 20, 10], pvcalculation=True, - peakpower=5, loss=2, trackingtype=0, components=True, - optimalangles=True) + latitude=55.6814, + longitude=12.5758, + outputformat="csv", + usehorizon=True, + userhorizon=[10, 15, 20, 10], + pvcalculation=True, + peakpower=5, + loss=2, + trackingtype=0, + components=True, + optimalangles=True, + ) def test_read_pvgis_hourly_empty_file(): # Check if a IOError is raised if file does not contain a data section - with pytest.raises(ValueError, match='No data section'): + with pytest.raises(ValueError, match="No data section"): read_pvgis_hourly( - io.StringIO('1:1\n2:2\n3:3\n4:4\n5:5\n'), - pvgis_format='csv') + io.StringIO("1:1\n2:2\n3:3\n4:4\n5:5\n"), pvgis_format="csv" + ) # PVGIS TMY tests @pytest.fixture def expected(): - return pd.read_csv(DATA_DIR / 'pvgis_tmy_test.csv', index_col='time(UTC)') + return pd.read_csv(DATA_DIR / "pvgis_tmy_test.csv", index_col="time(UTC)") @pytest.fixture def userhorizon_expected(): - return pd.read_json(DATA_DIR / 'tmy_45.000_8.000_userhorizon.json') + return pd.read_json(DATA_DIR / "tmy_45.000_8.000_userhorizon.json") @pytest.fixture def month_year_expected(): return [ - 2018, 2007, 2009, 2013, 2008, 2006, 2011, 2010, 2020, 2006, 2007, 2016] + 2018, + 2007, + 2009, + 2013, + 2008, + 2006, + 2011, + 2010, + 2020, + 2006, + 2007, + 2016, + ] @pytest.fixture def inputs_expected(): return { - 'location': {'latitude': 45.0, 'longitude': 8.0, 'elevation': 250.0}, - 'meteo_data': { - 'radiation_db': 'PVGIS-SARAH3', - 'meteo_db': 'ERA5', - 'year_min': 2005, - 'year_max': 2023, - 'use_horizon': True, - 'horizon_db': 'DEM-calculated'}} + "location": {"latitude": 45.0, "longitude": 8.0, "elevation": 250.0}, + "meteo_data": { + "radiation_db": "PVGIS-SARAH3", + "meteo_db": "ERA5", + "year_min": 2005, + "year_max": 2023, + "use_horizon": True, + "horizon_db": "DEM-calculated", + }, + } @pytest.fixture def epw_meta(): return { - 'loc': 'LOCATION', - 'city': 'unknown', - 'state-prov': '-', - 'country': 'unknown', - 'data_type': 'ECMWF/ERA', - 'WMO_code': 'unknown', - 'latitude': 45.0, - 'longitude': 8.0, - 'TZ': 1.0, - 'altitude': 250.0} + "loc": "LOCATION", + "city": "unknown", + "state-prov": "-", + "country": "unknown", + "data_type": "ECMWF/ERA", + "WMO_code": "unknown", + "latitude": 45.0, + "longitude": 8.0, + "TZ": 1.0, + "altitude": 250.0, + } @pytest.fixture def meta_expected(): - with (DATA_DIR / 'pvgis_tmy_meta.json').open() as f: + with (DATA_DIR / "pvgis_tmy_meta.json").open() as f: return json.load(f) @pytest.fixture def csv_meta(meta_expected): return [ - f"{k}: {v['description']} ({v['units']})" for k, v - in meta_expected['outputs']['tmy_hourly']['variables'].items()] + f"{k}: {v['description']} ({v['units']})" + for k, v in meta_expected["outputs"]["tmy_hourly"]["variables"].items() + ] @pytest.fixture def pvgis_tmy_mapped_columns(): - return ['temp_air', 'relative_humidity', 'ghi', 'dni', 'dhi', 'IR(h)', - 'wind_speed', 'wind_direction', 'pressure'] + return [ + "temp_air", + "relative_humidity", + "ghi", + "dni", + "dhi", + "IR(h)", + "wind_speed", + "wind_direction", + "pressure", + ] @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) -def test_get_pvgis_tmy(expected, month_year_expected, inputs_expected, - meta_expected): +def test_get_pvgis_tmy( + expected, month_year_expected, inputs_expected, meta_expected +): pvgis_data = get_pvgis_tmy(45, 8, map_variables=False) - _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected, pvgis_data) - - -def _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected, pvgis_data): + _compare_pvgis_tmy_json( + expected, + month_year_expected, + inputs_expected, + meta_expected, + pvgis_data, + ) + + +def _compare_pvgis_tmy_json( + expected, month_year_expected, inputs_expected, meta_expected, pvgis_data +): data, months_selected, inputs, meta = pvgis_data # check each column of output separately - for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys(): + for outvar in meta_expected["outputs"]["tmy_hourly"]["variables"].keys(): assert np.allclose(data[outvar], expected[outvar]) assert np.allclose( - [_['month'] for _ in months_selected], np.arange(1, 13, 1)) + [_["month"] for _ in months_selected], np.arange(1, 13, 1) + ) assert np.allclose( - [_['year'] for _ in months_selected], month_year_expected) - inputs_loc = inputs['location'] - assert inputs_loc['latitude'] == inputs_expected['location']['latitude'] - assert inputs_loc['longitude'] == inputs_expected['location']['longitude'] - assert inputs_loc['elevation'] == inputs_expected['location']['elevation'] - inputs_met_data = inputs['meteo_data'] - expected_met_data = inputs_expected['meteo_data'] - assert ( - inputs_met_data['radiation_db'] == expected_met_data['radiation_db']) - assert inputs_met_data['year_min'] == expected_met_data['year_min'] - assert inputs_met_data['year_max'] == expected_met_data['year_max'] - assert inputs_met_data['use_horizon'] == expected_met_data['use_horizon'] - assert inputs_met_data['horizon_db'] == expected_met_data['horizon_db'] + [_["year"] for _ in months_selected], month_year_expected + ) + inputs_loc = inputs["location"] + assert inputs_loc["latitude"] == inputs_expected["location"]["latitude"] + assert inputs_loc["longitude"] == inputs_expected["location"]["longitude"] + assert inputs_loc["elevation"] == inputs_expected["location"]["elevation"] + inputs_met_data = inputs["meteo_data"] + expected_met_data = inputs_expected["meteo_data"] + assert inputs_met_data["radiation_db"] == expected_met_data["radiation_db"] + assert inputs_met_data["year_min"] == expected_met_data["year_min"] + assert inputs_met_data["year_max"] == expected_met_data["year_max"] + assert inputs_met_data["use_horizon"] == expected_met_data["use_horizon"] + assert inputs_met_data["horizon_db"] == expected_met_data["horizon_db"] assert meta == meta_expected @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_pvgis_tmy_kwargs(userhorizon_expected): - _, _, inputs, _ = get_pvgis_tmy(45, 8, usehorizon=False, - map_variables=False) - assert inputs['meteo_data']['use_horizon'] is False + _, _, inputs, _ = get_pvgis_tmy( + 45, 8, usehorizon=False, map_variables=False + ) + assert inputs["meteo_data"]["use_horizon"] is False data, _, _, _ = get_pvgis_tmy( - 45, 8, userhorizon=[0, 10, 20, 30, 40, 15, 25, 5], map_variables=False) - assert np.allclose( - data['G(h)'], userhorizon_expected['G(h)'].values) - assert np.allclose( - data['Gb(n)'], userhorizon_expected['Gb(n)'].values) - assert np.allclose( - data['Gd(h)'], userhorizon_expected['Gd(h)'].values) + 45, 8, userhorizon=[0, 10, 20, 30, 40, 15, 25, 5], map_variables=False + ) + assert np.allclose(data["G(h)"], userhorizon_expected["G(h)"].values) + assert np.allclose(data["Gb(n)"], userhorizon_expected["Gb(n)"].values) + assert np.allclose(data["Gd(h)"], userhorizon_expected["Gd(h)"].values) _, _, inputs, _ = get_pvgis_tmy(45, 8, startyear=2005, map_variables=False) - assert inputs['meteo_data']['year_min'] == 2005 + assert inputs["meteo_data"]["year_min"] == 2005 _, _, inputs, _ = get_pvgis_tmy(45, 8, endyear=2016, map_variables=False) - assert inputs['meteo_data']['year_max'] == 2016 + assert inputs["meteo_data"]["year_max"] == 2016 @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_pvgis_tmy_basic(expected, meta_expected): - pvgis_data = get_pvgis_tmy(45, 8, outputformat='basic', - map_variables=False) + pvgis_data = get_pvgis_tmy( + 45, 8, outputformat="basic", map_variables=False + ) _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data) def _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data): data, _, _, _ = pvgis_data # check each column of output separately - for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys(): + for outvar in meta_expected["outputs"]["tmy_hourly"]["variables"].keys(): assert np.allclose(data[outvar], expected[outvar]) @@ -439,76 +667,96 @@ def _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data): def test_get_pvgis_tmy_coerce_year(): """test utc_offset and coerce_year work as expected""" base_case, _, _, _ = get_pvgis_tmy(45, 8) # Turin - assert str(base_case.index.tz) == 'UTC' - assert base_case.index.name == 'time(UTC)' + assert str(base_case.index.tz) == "UTC" + assert base_case.index.name == "time(UTC)" noon_test_data = [ - base_case[base_case.index.month == m].iloc[12] - for m in range(1, 13)] + base_case[base_case.index.month == m].iloc[12] for m in range(1, 13) + ] cet_tz = 1 # Turin time is CET - cet_name = 'Etc/GMT-1' + cet_name = "Etc/GMT-1" # check indices of rolled data after converting timezone pvgis_data, _, _, _ = get_pvgis_tmy(45, 8, roll_utc_offset=cet_tz) - jan1_midnight = pd.Timestamp('1990-01-01 00:00:00', tz=cet_name) - dec31_midnight = pd.Timestamp('1990-12-31 23:00:00', tz=cet_name) + jan1_midnight = pd.Timestamp("1990-01-01 00:00:00", tz=cet_name) + dec31_midnight = pd.Timestamp("1990-12-31 23:00:00", tz=cet_name) assert pvgis_data.index[0] == jan1_midnight assert pvgis_data.index[-1] == dec31_midnight - assert pvgis_data.index.name == f'time({cet_name})' + assert pvgis_data.index.name == f"time({cet_name})" # spot check rolled data matches original for m, test_case in enumerate(noon_test_data): - expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz] + expected = pvgis_data[pvgis_data.index.month == m + 1].iloc[ + 12 + cet_tz + ] assert all(test_case == expected) # repeat tests with year coerced test_yr = 2021 pvgis_data, _, _, _ = get_pvgis_tmy( - 45, 8, roll_utc_offset=cet_tz, coerce_year=test_yr) - jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00:00:00', tz=cet_name) - dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23:00:00', tz=cet_name) + 45, 8, roll_utc_offset=cet_tz, coerce_year=test_yr + ) + jan1_midnight = pd.Timestamp(f"{test_yr}-01-01 00:00:00", tz=cet_name) + dec31_midnight = pd.Timestamp(f"{test_yr}-12-31 23:00:00", tz=cet_name) assert pvgis_data.index[0] == jan1_midnight assert pvgis_data.index[-1] == dec31_midnight - assert pvgis_data.index.name == f'time({cet_name})' + assert pvgis_data.index.name == f"time({cet_name})" for m, test_case in enumerate(noon_test_data): - expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12+cet_tz] + expected = pvgis_data[pvgis_data.index.month == m + 1].iloc[ + 12 + cet_tz + ] assert all(test_case == expected) # repeat tests with year coerced but utc offset none or zero pvgis_data, _, _, _ = get_pvgis_tmy(45, 8, coerce_year=test_yr) - jan1_midnight = pd.Timestamp(f'{test_yr}-01-01 00:00:00', tz='UTC') - dec31_midnight = pd.Timestamp(f'{test_yr}-12-31 23:00:00', tz='UTC') + jan1_midnight = pd.Timestamp(f"{test_yr}-01-01 00:00:00", tz="UTC") + dec31_midnight = pd.Timestamp(f"{test_yr}-12-31 23:00:00", tz="UTC") assert pvgis_data.index[0] == jan1_midnight assert pvgis_data.index[-1] == dec31_midnight - assert pvgis_data.index.name == 'time(UTC)' + assert pvgis_data.index.name == "time(UTC)" for m, test_case in enumerate(noon_test_data): - expected = pvgis_data[pvgis_data.index.month == m+1].iloc[12] + expected = pvgis_data[pvgis_data.index.month == m + 1].iloc[12] assert all(test_case == expected) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) -def test_get_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta): - pvgis_data = get_pvgis_tmy(45, 8, outputformat='csv', map_variables=False) - _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta, pvgis_data) - - -def _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta, pvgis_data): +def test_get_pvgis_tmy_csv( + expected, month_year_expected, inputs_expected, meta_expected, csv_meta +): + pvgis_data = get_pvgis_tmy(45, 8, outputformat="csv", map_variables=False) + _compare_pvgis_tmy_csv( + expected, + month_year_expected, + inputs_expected, + meta_expected, + csv_meta, + pvgis_data, + ) + + +def _compare_pvgis_tmy_csv( + expected, + month_year_expected, + inputs_expected, + meta_expected, + csv_meta, + pvgis_data, +): data, months_selected, inputs, meta = pvgis_data # check each column of output separately - for outvar in meta_expected['outputs']['tmy_hourly']['variables'].keys(): + for outvar in meta_expected["outputs"]["tmy_hourly"]["variables"].keys(): assert np.allclose(data[outvar], expected[outvar]) assert np.allclose( - [_['month'] for _ in months_selected], np.arange(1, 13, 1)) + [_["month"] for _ in months_selected], np.arange(1, 13, 1) + ) assert np.allclose( - [_['year'] for _ in months_selected], month_year_expected) - assert inputs['latitude'] == inputs_expected['location']['latitude'] - assert inputs['longitude'] == inputs_expected['location']['longitude'] - assert inputs['elevation'] == inputs_expected['location']['elevation'] + [_["year"] for _ in months_selected], month_year_expected + ) + assert inputs["latitude"] == inputs_expected["location"]["latitude"] + assert inputs["longitude"] == inputs_expected["location"]["longitude"] + assert inputs["elevation"] == inputs_expected["location"]["elevation"] for meta_value in meta: if not meta_value: continue # this copyright text tends to change (copyright year range increments # annually, e.g.), so just check the beginning of it: - if meta_value.startswith('PVGIS (c) European'): + if meta_value.startswith("PVGIS (c) European"): continue assert meta_value in csv_meta @@ -516,27 +764,27 @@ def _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_pvgis_tmy_epw(expected, epw_meta): - pvgis_data = get_pvgis_tmy(45, 8, outputformat='epw', map_variables=False) + pvgis_data = get_pvgis_tmy(45, 8, outputformat="epw", map_variables=False) _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data) def _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data): data, _, _, meta = pvgis_data - assert np.allclose(data.ghi, expected['G(h)']) - assert np.allclose(data.dni, expected['Gb(n)']) - assert np.allclose(data.dhi, expected['Gd(h)']) - assert np.allclose(data.temp_air, expected['T2m']) + assert np.allclose(data.ghi, expected["G(h)"]) + assert np.allclose(data.dni, expected["Gb(n)"]) + assert np.allclose(data.dhi, expected["Gd(h)"]) + assert np.allclose(data.temp_air, expected["T2m"]) assert meta == epw_meta @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_pvgis_tmy_error(): - err_msg = 'outputformat: Incorrect value.' + err_msg = "outputformat: Incorrect value." with pytest.raises(requests.HTTPError, match=err_msg): - get_pvgis_tmy(45, 8, outputformat='bad') - with pytest.raises(requests.HTTPError, match='404 Client Error'): - get_pvgis_tmy(45, 8, url='https://re.jrc.ec.europa.eu/') + get_pvgis_tmy(45, 8, outputformat="bad") + with pytest.raises(requests.HTTPError, match="404 Client Error"): + get_pvgis_tmy(45, 8, url="https://re.jrc.ec.europa.eu/") @pytest.mark.remote_data @@ -550,86 +798,126 @@ def test_get_pvgis_map_variables(pvgis_tmy_mapped_columns): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_pvgis_horizon(): pvgis_data, _ = get_pvgis_horizon(35.171051, -106.465158) - horizon_data = pd.read_csv(DATA_DIR / 'test_read_pvgis_horizon.csv', - index_col=0) - horizon_data = horizon_data['horizon_elevation'] + horizon_data = pd.read_csv( + DATA_DIR / "test_read_pvgis_horizon.csv", index_col=0 + ) + horizon_data = horizon_data["horizon_elevation"] assert_series_equal(pvgis_data, horizon_data) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_pvgis_horizon_invalid_coords(): - with pytest.raises(requests.HTTPError, match='lat: Incorrect value'): + with pytest.raises(requests.HTTPError, match="lat: Incorrect value"): _, _ = get_pvgis_horizon(100, 50) # unfeasible latitude def test_read_pvgis_tmy_map_variables(pvgis_tmy_mapped_columns): - fn = DATA_DIR / 'tmy_45.000_8.000_2005_2023.json' + fn = DATA_DIR / "tmy_45.000_8.000_2005_2023.json" actual, _, _, _ = read_pvgis_tmy(fn, map_variables=True) assert all(c in pvgis_tmy_mapped_columns for c in actual.columns) -def test_read_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected): - fn = DATA_DIR / 'tmy_45.000_8.000_2005_2023.json' +def test_read_pvgis_tmy_json( + expected, month_year_expected, inputs_expected, meta_expected +): + fn = DATA_DIR / "tmy_45.000_8.000_2005_2023.json" # infer outputformat from file extensions pvgis_data = read_pvgis_tmy(fn, map_variables=False) - _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected, pvgis_data) + _compare_pvgis_tmy_json( + expected, + month_year_expected, + inputs_expected, + meta_expected, + pvgis_data, + ) # explicit pvgis outputformat - pvgis_data = read_pvgis_tmy(fn, pvgis_format='json', map_variables=False) - _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected, pvgis_data) - with fn.open('r') as fbuf: - pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='json', - map_variables=False) - _compare_pvgis_tmy_json(expected, month_year_expected, inputs_expected, - meta_expected, pvgis_data) + pvgis_data = read_pvgis_tmy(fn, pvgis_format="json", map_variables=False) + _compare_pvgis_tmy_json( + expected, + month_year_expected, + inputs_expected, + meta_expected, + pvgis_data, + ) + with fn.open("r") as fbuf: + pvgis_data = read_pvgis_tmy( + fbuf, pvgis_format="json", map_variables=False + ) + _compare_pvgis_tmy_json( + expected, + month_year_expected, + inputs_expected, + meta_expected, + pvgis_data, + ) def test_read_pvgis_tmy_epw(expected, epw_meta): - fn = DATA_DIR / 'tmy_45.000_8.000_2005_2023.epw' + fn = DATA_DIR / "tmy_45.000_8.000_2005_2023.epw" # infer outputformat from file extensions pvgis_data = read_pvgis_tmy(fn, map_variables=False) _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data) # explicit pvgis outputformat - pvgis_data = read_pvgis_tmy(fn, pvgis_format='epw', map_variables=False) + pvgis_data = read_pvgis_tmy(fn, pvgis_format="epw", map_variables=False) _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data) - with fn.open('r') as fbuf: - pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='epw', - map_variables=False) + with fn.open("r") as fbuf: + pvgis_data = read_pvgis_tmy( + fbuf, pvgis_format="epw", map_variables=False + ) _compare_pvgis_tmy_epw(expected, epw_meta, pvgis_data) -def test_read_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta): - fn = DATA_DIR / 'tmy_45.000_8.000_2005_2023.csv' +def test_read_pvgis_tmy_csv( + expected, month_year_expected, inputs_expected, meta_expected, csv_meta +): + fn = DATA_DIR / "tmy_45.000_8.000_2005_2023.csv" # infer outputformat from file extensions pvgis_data = read_pvgis_tmy(fn, map_variables=False) - _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta, pvgis_data) + _compare_pvgis_tmy_csv( + expected, + month_year_expected, + inputs_expected, + meta_expected, + csv_meta, + pvgis_data, + ) # explicit pvgis outputformat - pvgis_data = read_pvgis_tmy(fn, pvgis_format='csv', map_variables=False) - _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta, pvgis_data) - with fn.open('rb') as fbuf: - pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='csv', - map_variables=False) - _compare_pvgis_tmy_csv(expected, month_year_expected, inputs_expected, - meta_expected, csv_meta, pvgis_data) + pvgis_data = read_pvgis_tmy(fn, pvgis_format="csv", map_variables=False) + _compare_pvgis_tmy_csv( + expected, + month_year_expected, + inputs_expected, + meta_expected, + csv_meta, + pvgis_data, + ) + with fn.open("rb") as fbuf: + pvgis_data = read_pvgis_tmy( + fbuf, pvgis_format="csv", map_variables=False + ) + _compare_pvgis_tmy_csv( + expected, + month_year_expected, + inputs_expected, + meta_expected, + csv_meta, + pvgis_data, + ) def test_read_pvgis_tmy_basic(expected, meta_expected): - fn = DATA_DIR / 'tmy_45.000_8.000_2005_2023.txt' + fn = DATA_DIR / "tmy_45.000_8.000_2005_2023.txt" # XXX: can't infer outputformat from file extensions for basic with pytest.raises(ValueError, match="pvgis format 'txt' was unknown"): read_pvgis_tmy(fn, map_variables=False) # explicit pvgis outputformat - pvgis_data = read_pvgis_tmy(fn, pvgis_format='basic', map_variables=False) + pvgis_data = read_pvgis_tmy(fn, pvgis_format="basic", map_variables=False) _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data) - with fn.open('rb') as fbuf: - pvgis_data = read_pvgis_tmy(fbuf, pvgis_format='basic', - map_variables=False) + with fn.open("rb") as fbuf: + pvgis_data = read_pvgis_tmy( + fbuf, pvgis_format="basic", map_variables=False + ) _compare_pvgis_tmy_basic(expected, meta_expected, pvgis_data) # file buffer raises TypeError if passed to pathlib.Path() with pytest.raises(TypeError): @@ -637,8 +925,9 @@ def test_read_pvgis_tmy_basic(expected, meta_expected): def test_read_pvgis_tmy_exception(): - bad_outputformat = 'bad' + bad_outputformat = "bad" err_msg = f"pvgis format '{bad_outputformat:s}' was unknown" with pytest.raises(ValueError, match=err_msg): - read_pvgis_tmy('filename', pvgis_format=bad_outputformat, - map_variables=False) + read_pvgis_tmy( + "filename", pvgis_format=bad_outputformat, map_variables=False + ) diff --git a/pvlib/tests/iotools/test_sodapro.py b/pvlib/tests/iotools/test_sodapro.py index 83983d2b68..6b2d6199e6 100644 --- a/pvlib/tests/iotools/test_sodapro.py +++ b/pvlib/tests/iotools/test_sodapro.py @@ -11,137 +11,591 @@ from ..conftest import DATA_DIR, assert_frame_equal -testfile_mcclear_verbose = DATA_DIR / 'cams_mcclear_1min_verbose.csv' -testfile_mcclear_monthly = DATA_DIR / 'cams_mcclear_monthly.csv' -testfile_radiation_verbose = DATA_DIR / 'cams_radiation_1min_verbose.csv' -testfile_radiation_monthly = DATA_DIR / 'cams_radiation_monthly.csv' +testfile_mcclear_verbose = DATA_DIR / "cams_mcclear_1min_verbose.csv" +testfile_mcclear_monthly = DATA_DIR / "cams_mcclear_monthly.csv" +testfile_radiation_verbose = DATA_DIR / "cams_radiation_1min_verbose.csv" +testfile_radiation_monthly = DATA_DIR / "cams_radiation_monthly.csv" -index_verbose = pd.date_range('2020-06-01 12', periods=4, freq='1min', - tz='UTC') -index_monthly = pd.date_range('2020-01-01', periods=4, freq='1M') +index_verbose = pd.date_range( + "2020-06-01 12", periods=4, freq="1min", tz="UTC" +) +index_monthly = pd.date_range("2020-01-01", periods=4, freq="1M") dtypes_mcclear_verbose = [ - 'object', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64', 'float64', 'int64', 'float64', - 'float64', 'float64', 'float64'] + "object", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", +] dtypes_mcclear = [ - 'object', 'float64', 'float64', 'float64', 'float64', 'float64'] + "object", + "float64", + "float64", + "float64", + "float64", + "float64", +] dtypes_radiation_verbose = [ - 'object', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64', 'int64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'int64', 'int64', 'float64', 'float64', - 'float64', 'float64'] + "object", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", + "float64", + "int64", + "int64", + "float64", + "float64", + "float64", + "float64", +] dtypes_radiation = [ - 'object', 'float64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64', 'float64', 'float64'] + "object", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", +] columns_mcclear_verbose = [ - 'Observation period', 'ghi_extra', 'ghi_clear', 'bhi_clear', - 'dhi_clear', 'dni_clear', 'solar_zenith', 'summer/winter split', 'tco3', - 'tcwv', 'AOD BC', 'AOD DU', 'AOD SS', 'AOD OR', 'AOD SU', 'AOD NI', - 'AOD AM', 'alpha', 'Aerosol type', 'fiso', 'fvol', 'fgeo', 'albedo'] + "Observation period", + "ghi_extra", + "ghi_clear", + "bhi_clear", + "dhi_clear", + "dni_clear", + "solar_zenith", + "summer/winter split", + "tco3", + "tcwv", + "AOD BC", + "AOD DU", + "AOD SS", + "AOD OR", + "AOD SU", + "AOD NI", + "AOD AM", + "alpha", + "Aerosol type", + "fiso", + "fvol", + "fgeo", + "albedo", +] columns_mcclear = [ - 'Observation period', 'ghi_extra', 'ghi_clear', 'bhi_clear', 'dhi_clear', - 'dni_clear'] + "Observation period", + "ghi_extra", + "ghi_clear", + "bhi_clear", + "dhi_clear", + "dni_clear", +] columns_radiation_verbose = [ - 'Observation period', 'ghi_extra', 'ghi_clear', 'bhi_clear', 'dhi_clear', - 'dni_clear', 'ghi', 'bhi', 'dhi', 'dni', 'Reliability', 'solar_zenith', - 'summer/winter split', 'tco3', 'tcwv', 'AOD BC', 'AOD DU', 'AOD SS', - 'AOD OR', 'AOD SU', 'AOD NI', 'AOD AM', 'alpha', 'Aerosol type', 'fiso', - 'fvol', 'fgeo', 'albedo', 'Cloud optical depth', 'Cloud coverage', - 'Cloud type', 'GHI no corr', 'BHI no corr', 'DHI no corr', 'BNI no corr'] + "Observation period", + "ghi_extra", + "ghi_clear", + "bhi_clear", + "dhi_clear", + "dni_clear", + "ghi", + "bhi", + "dhi", + "dni", + "Reliability", + "solar_zenith", + "summer/winter split", + "tco3", + "tcwv", + "AOD BC", + "AOD DU", + "AOD SS", + "AOD OR", + "AOD SU", + "AOD NI", + "AOD AM", + "alpha", + "Aerosol type", + "fiso", + "fvol", + "fgeo", + "albedo", + "Cloud optical depth", + "Cloud coverage", + "Cloud type", + "GHI no corr", + "BHI no corr", + "DHI no corr", + "BNI no corr", +] columns_radiation_verbose_unmapped = [ - 'Observation period', 'TOA', 'Clear sky GHI', 'Clear sky BHI', - 'Clear sky DHI', 'Clear sky BNI', 'GHI', 'BHI', 'DHI', 'BNI', - 'Reliability', 'sza', 'summer/winter split', 'tco3', 'tcwv', 'AOD BC', - 'AOD DU', 'AOD SS', 'AOD OR', 'AOD SU', 'AOD NI', 'AOD AM', 'alpha', - 'Aerosol type', 'fiso', 'fvol', 'fgeo', 'albedo', 'Cloud optical depth', - 'Cloud coverage', 'Cloud type', 'GHI no corr', 'BHI no corr', - 'DHI no corr', 'BNI no corr'] + "Observation period", + "TOA", + "Clear sky GHI", + "Clear sky BHI", + "Clear sky DHI", + "Clear sky BNI", + "GHI", + "BHI", + "DHI", + "BNI", + "Reliability", + "sza", + "summer/winter split", + "tco3", + "tcwv", + "AOD BC", + "AOD DU", + "AOD SS", + "AOD OR", + "AOD SU", + "AOD NI", + "AOD AM", + "alpha", + "Aerosol type", + "fiso", + "fvol", + "fgeo", + "albedo", + "Cloud optical depth", + "Cloud coverage", + "Cloud type", + "GHI no corr", + "BHI no corr", + "DHI no corr", + "BNI no corr", +] columns_radiation = [ - 'Observation period', 'ghi_extra', 'ghi_clear', 'bhi_clear', 'dhi_clear', - 'dni_clear', 'ghi', 'bhi', 'dhi', 'dni', 'Reliability'] - - -values_mcclear_verbose = np.array([ - ['2020-06-01T12:00:00.0/2020-06-01T12:01:00.0', 1084.194, 848.5020, - 753.564, 94.938, 920.28, 35.0308, 0.9723, 341.0221, 17.7962, 0.0065, - 0.0067, 0.0008, 0.0215, 0.0252, 0.0087, 0.0022, np.nan, -1, 0.1668, - 0.0912, 0.0267, 0.1359], - ['2020-06-01T12:01:00.0/2020-06-01T12:02:00.0', 1083.504, 847.866, 752.904, - 94.962, 920.058, 35.0828, 0.9723, 341.0223, 17.802, 0.0065, 0.0067, - 0.0008, 0.0215, 0.0253, 0.0087, 0.0022, np.nan, -1, 0.1668, 0.0912, - 0.0267, 0.1359], - ['2020-06-01T12:02:00.0/2020-06-01T12:03:00.0', 1082.802, 847.224, 752.232, - 94.986, 919.836, 35.1357, 0.9723, 341.0224, 17.8079, 0.0065, 0.0067, - 0.0008, 0.0216, 0.0253, 0.0087, 0.0022, np.nan, -1, 0.1668, 0.0912, - 0.0267, 0.1359], - ['2020-06-01T12:03:00.0/2020-06-01T12:04:00.0', 1082.088, 846.564, 751.554, - 95.01, 919.614, 35.1896, 0.9723, 341.0226, 17.8137, 0.0065, 0.0067, - 0.0008, 0.0217, 0.0253, 0.0087, 0.0022, np.nan, -1, 0.1668, 0.0912, - 0.0267, 0.1359]]) - -values_mcclear_monthly = np.array([ - ['2020-01-01T00:00:00.0/2020-02-01T00:00:00.0', 67.4314, 39.5494, - 26.1998, 13.3496, 142.1562], - ['2020-02-01T00:00:00.0/2020-03-01T00:00:00.0', 131.2335, 84.7849, - 58.3855, 26.3994, 202.4865], - ['2020-03-01T00:00:00.0/2020-04-01T00:00:00.0', 232.3323, 163.176, - 125.1675, 38.0085, 307.5254], - ['2020-04-01T00:00:00.0/2020-05-01T00:00:00.0', 344.7431, 250.7585, - 197.8757, 52.8829, 387.6707]]) - -values_radiation_verbose = np.array([ - ['2020-06-01T12:00:00.0/2020-06-01T12:01:00.0', 1084.194, 848.502, 753.564, - 94.938, 920.28, 815.358, 702.342, 113.022, 857.724, 1.0, 35.0308, 0.9723, - 341.0221, 17.7962, 0.0065, 0.0067, 0.0008, 0.0215, 0.0252, 0.0087, 0.0022, - np.nan, -1, 0.1668, 0.0912, 0.0267, 0.1359, 0.0, 0, 5, 848.502, 753.564, - 94.938, 920.28], - ['2020-06-01T12:01:00.0/2020-06-01T12:02:00.0', 1083.504, 847.866, 752.904, - 94.962, 920.058, 814.806, 701.73, 113.076, 857.52, 1.0, 35.0828, 0.9723, - 341.0223, 17.802, 0.0065, 0.0067, 0.0008, 0.0215, 0.0253, 0.0087, 0.0022, - np.nan, -1, 0.1668, 0.0912, 0.0267, 0.1359, 0.0, 0, 5, 847.866, 752.904, - 94.962, 920.058], - ['2020-06-01T12:02:00.0/2020-06-01T12:03:00.0', 1082.802, 847.224, 752.232, - 94.986, 919.836, 814.182, 701.094, 113.088, 857.298, 1.0, 35.1357, 0.9723, - 341.0224, 17.8079, 0.0065, 0.0067, 0.0008, 0.0216, 0.0253, 0.0087, 0.0022, - np.nan, -1, 0.1668, 0.0912, 0.0267, 0.1359, 0.0, 0, 5, 847.224, 752.232, - 94.986, 919.836], - ['2020-06-01T12:03:00.0/2020-06-01T12:04:00.0', 1082.088, 846.564, 751.554, - 95.01, 919.614, 813.612, 700.464, 113.148, 857.094, 1.0, 35.1896, 0.9723, - 341.0226, 17.8137, 0.0065, 0.0067, 0.0008, 0.0217, 0.0253, 0.0087, 0.0022, - np.nan, -1, 0.1668, 0.0912, 0.0267, 0.1359, 0.0, 0, 5, 846.564, 751.554, - 95.01, 919.614]]) + "Observation period", + "ghi_extra", + "ghi_clear", + "bhi_clear", + "dhi_clear", + "dni_clear", + "ghi", + "bhi", + "dhi", + "dni", + "Reliability", +] + + +values_mcclear_verbose = np.array( + [ + [ + "2020-06-01T12:00:00.0/2020-06-01T12:01:00.0", + 1084.194, + 848.5020, + 753.564, + 94.938, + 920.28, + 35.0308, + 0.9723, + 341.0221, + 17.7962, + 0.0065, + 0.0067, + 0.0008, + 0.0215, + 0.0252, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + ], + [ + "2020-06-01T12:01:00.0/2020-06-01T12:02:00.0", + 1083.504, + 847.866, + 752.904, + 94.962, + 920.058, + 35.0828, + 0.9723, + 341.0223, + 17.802, + 0.0065, + 0.0067, + 0.0008, + 0.0215, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + ], + [ + "2020-06-01T12:02:00.0/2020-06-01T12:03:00.0", + 1082.802, + 847.224, + 752.232, + 94.986, + 919.836, + 35.1357, + 0.9723, + 341.0224, + 17.8079, + 0.0065, + 0.0067, + 0.0008, + 0.0216, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + ], + [ + "2020-06-01T12:03:00.0/2020-06-01T12:04:00.0", + 1082.088, + 846.564, + 751.554, + 95.01, + 919.614, + 35.1896, + 0.9723, + 341.0226, + 17.8137, + 0.0065, + 0.0067, + 0.0008, + 0.0217, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + ], + ] +) + +values_mcclear_monthly = np.array( + [ + [ + "2020-01-01T00:00:00.0/2020-02-01T00:00:00.0", + 67.4314, + 39.5494, + 26.1998, + 13.3496, + 142.1562, + ], + [ + "2020-02-01T00:00:00.0/2020-03-01T00:00:00.0", + 131.2335, + 84.7849, + 58.3855, + 26.3994, + 202.4865, + ], + [ + "2020-03-01T00:00:00.0/2020-04-01T00:00:00.0", + 232.3323, + 163.176, + 125.1675, + 38.0085, + 307.5254, + ], + [ + "2020-04-01T00:00:00.0/2020-05-01T00:00:00.0", + 344.7431, + 250.7585, + 197.8757, + 52.8829, + 387.6707, + ], + ] +) + +values_radiation_verbose = np.array( + [ + [ + "2020-06-01T12:00:00.0/2020-06-01T12:01:00.0", + 1084.194, + 848.502, + 753.564, + 94.938, + 920.28, + 815.358, + 702.342, + 113.022, + 857.724, + 1.0, + 35.0308, + 0.9723, + 341.0221, + 17.7962, + 0.0065, + 0.0067, + 0.0008, + 0.0215, + 0.0252, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + 0.0, + 0, + 5, + 848.502, + 753.564, + 94.938, + 920.28, + ], + [ + "2020-06-01T12:01:00.0/2020-06-01T12:02:00.0", + 1083.504, + 847.866, + 752.904, + 94.962, + 920.058, + 814.806, + 701.73, + 113.076, + 857.52, + 1.0, + 35.0828, + 0.9723, + 341.0223, + 17.802, + 0.0065, + 0.0067, + 0.0008, + 0.0215, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + 0.0, + 0, + 5, + 847.866, + 752.904, + 94.962, + 920.058, + ], + [ + "2020-06-01T12:02:00.0/2020-06-01T12:03:00.0", + 1082.802, + 847.224, + 752.232, + 94.986, + 919.836, + 814.182, + 701.094, + 113.088, + 857.298, + 1.0, + 35.1357, + 0.9723, + 341.0224, + 17.8079, + 0.0065, + 0.0067, + 0.0008, + 0.0216, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + 0.0, + 0, + 5, + 847.224, + 752.232, + 94.986, + 919.836, + ], + [ + "2020-06-01T12:03:00.0/2020-06-01T12:04:00.0", + 1082.088, + 846.564, + 751.554, + 95.01, + 919.614, + 813.612, + 700.464, + 113.148, + 857.094, + 1.0, + 35.1896, + 0.9723, + 341.0226, + 17.8137, + 0.0065, + 0.0067, + 0.0008, + 0.0217, + 0.0253, + 0.0087, + 0.0022, + np.nan, + -1, + 0.1668, + 0.0912, + 0.0267, + 0.1359, + 0.0, + 0, + 5, + 846.564, + 751.554, + 95.01, + 919.614, + ], + ] +) values_radiation_verbose_integrated = np.copy(values_radiation_verbose) -values_radiation_verbose_integrated[:, 1:10] = \ - values_radiation_verbose_integrated[:, 1:10].astype(float)/60 -values_radiation_verbose_integrated[:, 31:35] = \ - values_radiation_verbose_integrated[:, 31:35].astype(float)/60 - -values_radiation_monthly = np.array([ - ['2020-01-01T00:00:00.0/2020-02-01T00:00:00.0', 67.4317, 39.5496, - 26.2, 13.3496, 142.1567, 20.8763, 3.4526, 17.4357, 16.7595, 0.997], - ['2020-02-01T00:00:00.0/2020-03-01T00:00:00.0', 131.2338, 84.7852, - 58.3858, 26.3994, 202.4871, 47.5197, 13.984, 33.5512, 47.8541, 0.9956], - ['2020-03-01T00:00:00.0/2020-04-01T00:00:00.0', 232.3325, 163.1762, - 125.1677, 38.0085, 307.5256, 120.1659, 69.6217, 50.5653, 159.576, 0.9949], - ['2020-04-01T00:00:00.0/2020-05-01T00:00:00.0', 344.7433, 250.7587, - 197.8758, 52.8829, 387.6709, 196.7015, 123.2593, 73.5152, 233.9675, - 0.9897]]) +values_radiation_verbose_integrated[:, 1:10] = ( + values_radiation_verbose_integrated[:, 1:10].astype(float) / 60 +) +values_radiation_verbose_integrated[:, 31:35] = ( + values_radiation_verbose_integrated[:, 31:35].astype(float) / 60 +) + +values_radiation_monthly = np.array( + [ + [ + "2020-01-01T00:00:00.0/2020-02-01T00:00:00.0", + 67.4317, + 39.5496, + 26.2, + 13.3496, + 142.1567, + 20.8763, + 3.4526, + 17.4357, + 16.7595, + 0.997, + ], + [ + "2020-02-01T00:00:00.0/2020-03-01T00:00:00.0", + 131.2338, + 84.7852, + 58.3858, + 26.3994, + 202.4871, + 47.5197, + 13.984, + 33.5512, + 47.8541, + 0.9956, + ], + [ + "2020-03-01T00:00:00.0/2020-04-01T00:00:00.0", + 232.3325, + 163.1762, + 125.1677, + 38.0085, + 307.5256, + 120.1659, + 69.6217, + 50.5653, + 159.576, + 0.9949, + ], + [ + "2020-04-01T00:00:00.0/2020-05-01T00:00:00.0", + 344.7433, + 250.7587, + 197.8758, + 52.8829, + 387.6709, + 196.7015, + 123.2593, + 73.5152, + 233.9675, + 0.9897, + ], + ] +) # @pytest.fixture @@ -151,24 +605,49 @@ def generate_expected_dataframe(values, columns, index, dtypes): """ expected = pd.DataFrame(values, columns=columns, index=index) expected.index.freq = None - for (col, _dtype) in zip(expected.columns, dtypes): + for col, _dtype in zip(expected.columns, dtypes): expected[col] = expected[col].astype(_dtype) return expected -@pytest.mark.parametrize('testfile,index,columns,values,dtypes', [ - (testfile_mcclear_verbose, index_verbose, columns_mcclear_verbose, - values_mcclear_verbose, dtypes_mcclear_verbose), - (testfile_mcclear_monthly, index_monthly, columns_mcclear, - values_mcclear_monthly, dtypes_mcclear), - (testfile_radiation_verbose, index_verbose, columns_radiation_verbose, - values_radiation_verbose, dtypes_radiation_verbose), - (testfile_radiation_monthly, index_monthly, columns_radiation, - values_radiation_monthly, dtypes_radiation)]) +@pytest.mark.parametrize( + "testfile,index,columns,values,dtypes", + [ + ( + testfile_mcclear_verbose, + index_verbose, + columns_mcclear_verbose, + values_mcclear_verbose, + dtypes_mcclear_verbose, + ), + ( + testfile_mcclear_monthly, + index_monthly, + columns_mcclear, + values_mcclear_monthly, + dtypes_mcclear, + ), + ( + testfile_radiation_verbose, + index_verbose, + columns_radiation_verbose, + values_radiation_verbose, + dtypes_radiation_verbose, + ), + ( + testfile_radiation_monthly, + index_monthly, + columns_radiation, + values_radiation_monthly, + dtypes_radiation, + ), + ], +) def test_read_cams(testfile, index, columns, values, dtypes): expected = generate_expected_dataframe(values, columns, index, dtypes) - out, metadata = sodapro.read_cams(testfile, integrated=False, - map_variables=True) + out, metadata = sodapro.read_cams( + testfile, integrated=False, map_variables=True + ) assert_frame_equal(out, expected, check_less_precise=True) @@ -178,70 +657,97 @@ def test_read_cams_integrated_unmapped_label(): expected = generate_expected_dataframe( values_radiation_verbose_integrated, columns_radiation_verbose_unmapped, - index_verbose+pd.Timedelta(minutes=1), dtypes=dtypes_radiation_verbose) - out, metadata = sodapro.read_cams(testfile_radiation_verbose, - integrated=True, label='right', - map_variables=False) + index_verbose + pd.Timedelta(minutes=1), + dtypes=dtypes_radiation_verbose, + ) + out, metadata = sodapro.read_cams( + testfile_radiation_verbose, + integrated=True, + label="right", + map_variables=False, + ) assert_frame_equal(out, expected, check_less_precise=True) def test_read_cams_metadata(): _, metadata = sodapro.read_cams(testfile_mcclear_monthly, integrated=False) - assert metadata['Time reference'] == 'Universal time (UT)' - assert metadata['noValue'] == 'nan' - assert metadata['latitude'] == 55.7906 - assert metadata['longitude'] == 12.5251 - assert metadata['altitude'] == 39.0 - assert metadata['radiation_unit'] == 'W/m^2' - assert metadata['time_step'] == '1M' - - -@pytest.mark.parametrize('testfile,index,columns,values,dtypes,identifier', [ - (testfile_mcclear_monthly, index_monthly, columns_mcclear, - values_mcclear_monthly, dtypes_mcclear, 'mcclear'), - (testfile_radiation_monthly, index_monthly, columns_radiation, - values_radiation_monthly, dtypes_radiation, 'cams_radiation')]) -def test_get_cams(requests_mock, testfile, index, columns, values, dtypes, - identifier): + assert metadata["Time reference"] == "Universal time (UT)" + assert metadata["noValue"] == "nan" + assert metadata["latitude"] == 55.7906 + assert metadata["longitude"] == 12.5251 + assert metadata["altitude"] == 39.0 + assert metadata["radiation_unit"] == "W/m^2" + assert metadata["time_step"] == "1M" + + +@pytest.mark.parametrize( + "testfile,index,columns,values,dtypes,identifier", + [ + ( + testfile_mcclear_monthly, + index_monthly, + columns_mcclear, + values_mcclear_monthly, + dtypes_mcclear, + "mcclear", + ), + ( + testfile_radiation_monthly, + index_monthly, + columns_radiation, + values_radiation_monthly, + dtypes_radiation, + "cams_radiation", + ), + ], +) +def test_get_cams( + requests_mock, testfile, index, columns, values, dtypes, identifier +): """Test that get_cams generates the correct URI request and that parse_cams is being called correctly""" # Open local test file containing McClear mothly data - with open(testfile, 'r') as test_file: + with open(testfile, "r") as test_file: mock_response = test_file.read() # Specify the full URI of a specific example, this ensures that all of the # inputs are passing on correctly - url_test_cams = f'https://api.soda-solardata.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=80;date_begin=2020-01-01;date_end=2020-05-04;time_ref=UT;summarization=P01M;username=pvlib-admin%2540googlegroups.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_{identifier}&version=1.0.0&RawDataOutput=irradiation' # noqa: E501 + url_test_cams = f"https://api.soda-solardata.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=80;date_begin=2020-01-01;date_end=2020-05-04;time_ref=UT;summarization=P01M;username=pvlib-admin%2540googlegroups.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_{identifier}&version=1.0.0&RawDataOutput=irradiation" # noqa: E501 - requests_mock.get(url_test_cams, text=mock_response, - headers={'Content-Type': 'application/csv'}) + requests_mock.get( + url_test_cams, + text=mock_response, + headers={"Content-Type": "application/csv"}, + ) # Make API call - an error is raised if requested URI does not match out, metadata = sodapro.get_cams( - start=pd.Timestamp('2020-01-01'), - end=pd.Timestamp('2020-05-04'), + start=pd.Timestamp("2020-01-01"), + end=pd.Timestamp("2020-05-04"), latitude=55.7906, longitude=12.5251, - email='pvlib-admin@googlegroups.com', + email="pvlib-admin@googlegroups.com", identifier=identifier, altitude=80, - time_step='1M', + time_step="1M", verbose=False, - integrated=False) + integrated=False, + ) expected = generate_expected_dataframe(values, columns, index, dtypes) assert_frame_equal(out, expected, check_less_precise=True) # Test if Warning is raised if verbose mode is True and time_step != '1min' - with pytest.warns(UserWarning, match='Verbose mode only supports'): + with pytest.warns(UserWarning, match="Verbose mode only supports"): _ = sodapro.get_cams( - start=pd.Timestamp('2020-01-01'), - end=pd.Timestamp('2020-05-04'), + start=pd.Timestamp("2020-01-01"), + end=pd.Timestamp("2020-05-04"), latitude=55.7906, longitude=12.5251, - email='pvlib-admin@googlegroups.com', + email="pvlib-admin@googlegroups.com", identifier=identifier, altitude=80, - time_step='1M', - verbose=True) + time_step="1M", + verbose=True, + ) def test_get_cams_bad_request(requests_mock): @@ -255,44 +761,49 @@ def test_get_cams_bad_request(requests_mock): Please, register yourself at www.soda-pro.com """ - url_cams_bad_request = 'https://pro.soda-is.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=-999;date_begin=2020-01-01;date_end=2020-05-04;time_ref=TST;summarization=PT01H;username=test%2540test.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_mcclear&version=1.0.0&RawDataOutput=irradiation' # noqa: E501 + url_cams_bad_request = "https://pro.soda-is.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=-999;date_begin=2020-01-01;date_end=2020-05-04;time_ref=TST;summarization=PT01H;username=test%2540test.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_mcclear&version=1.0.0&RawDataOutput=irradiation" # noqa: E501 - requests_mock.get(url_cams_bad_request, status_code=400, - text=mock_response_bad_text) + requests_mock.get( + url_cams_bad_request, status_code=400, text=mock_response_bad_text + ) # Test if HTTPError is raised if incorrect input is specified # In the below example a non-registrered email is specified - with pytest.raises(requests.exceptions.HTTPError, - match='Failed to execute WPS process'): + with pytest.raises( + requests.exceptions.HTTPError, match="Failed to execute WPS process" + ): _ = sodapro.get_cams( - start=pd.Timestamp('2020-01-01'), - end=pd.Timestamp('2020-05-04'), + start=pd.Timestamp("2020-01-01"), + end=pd.Timestamp("2020-05-04"), latitude=55.7906, longitude=12.5251, - email='test@test.com', # a non-registrered email - identifier='mcclear', - time_ref='TST', + email="test@test.com", # a non-registrered email + identifier="mcclear", + time_ref="TST", verbose=False, - time_step='1h', - server='pro.soda-is.com') + time_step="1h", + server="pro.soda-is.com", + ) # Test if value error is raised if incorrect identifier is specified - with pytest.raises(ValueError, match='Identifier must be either'): + with pytest.raises(ValueError, match="Identifier must be either"): _ = sodapro.get_cams( - start=pd.Timestamp('2020-01-01'), - end=pd.Timestamp('2020-05-04'), + start=pd.Timestamp("2020-01-01"), + end=pd.Timestamp("2020-05-04"), latitude=55.7906, longitude=12.5251, - email='test@test.com', - identifier='test', # incorrect identifier - server='pro.soda-is.com') + email="test@test.com", + identifier="test", # incorrect identifier + server="pro.soda-is.com", + ) # Test if value error is raised if incorrect time step is specified - with pytest.raises(ValueError, match='Time step not recognized'): + with pytest.raises(ValueError, match="Time step not recognized"): _ = sodapro.get_cams( - start=pd.Timestamp('2020-01-01'), - end=pd.Timestamp('2020-05-04'), + start=pd.Timestamp("2020-01-01"), + end=pd.Timestamp("2020-05-04"), latitude=55.7906, longitude=12.5251, - email='test@test.com', - identifier='mcclear', - time_step='test', # incorrect time step - server='pro.soda-is.com') + email="test@test.com", + identifier="mcclear", + time_step="test", # incorrect time step + server="pro.soda-is.com", + ) diff --git a/pvlib/tests/iotools/test_solaranywhere.py b/pvlib/tests/iotools/test_solaranywhere.py index 018c583be8..e2ea4f4e53 100644 --- a/pvlib/tests/iotools/test_solaranywhere.py +++ b/pvlib/tests/iotools/test_solaranywhere.py @@ -2,14 +2,24 @@ import pytest import pvlib import os -from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, - requires_solaranywhere_credentials) +from ..conftest import ( + DATA_DIR, + RERUNS, + RERUNS_DELAY, + requires_solaranywhere_credentials, +) # High spatial resolution and 5-min data, true dynamics enabled -TESTFILE_HIGH_RESOLUTION = DATA_DIR / 'Burlington, United States SolarAnywhere Time Series 20210101 to 20210103 Lat_44_4675 Lon_-73_2075 SA format.csv' # noqa: E501 +TESTFILE_HIGH_RESOLUTION = ( + DATA_DIR + / "Burlington, United States SolarAnywhere Time Series 20210101 to 20210103 Lat_44_4675 Lon_-73_2075 SA format.csv" +) # noqa: E501 # TGY test file (v3.6) containing GHI/DHI and temperature. # Note, the test file only contains the first three days. -TESTFILE_TMY = DATA_DIR / 'Burlington, United States SolarAnywhere Typical GHI Year Lat_44_465 Lon_-73_205 SA format.csv' # noqa: E501 +TESTFILE_TMY = ( + DATA_DIR + / "Burlington, United States SolarAnywhere Typical GHI Year Lat_44_465 Lon_-73_205 SA format.csv" +) # noqa: E501 @pytest.fixture(scope="module") @@ -22,17 +32,19 @@ def solaranywhere_api_key(): @pytest.fixture def high_resolution_index(): - index = pd.date_range(start='2021-01-01 00:05-0500', - end='2021-01-03 00:00-0500', freq='5min') - index.name = 'ObservationTime' + index = pd.date_range( + start="2021-01-01 00:05-0500", end="2021-01-03 00:00-0500", freq="5min" + ) + index.name = "ObservationTime" return index @pytest.fixture def tmy_index(): index = pd.date_range( - start='2000-01-01 01:00-0500', periods=3*24, freq='1h') - index.name = 'ObservationTime' + start="2000-01-01 01:00-0500", periods=3 * 24, freq="1h" + ) + index.name = "ObservationTime" index.freq = None return index @@ -40,35 +52,104 @@ def tmy_index(): @pytest.fixture def tmy_ghi_series(tmy_index): ghi = [ - 0, 0, 0, 0, 0, 0, 0, 3, 50, 171, 234, 220, 202, 122, 141, 65, 2, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 48, 105, 161, 135, 108, 72, 58, - 33, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 47, 124, 99, 116, - 130, 165, 110, 36, 1, 0, 0, 0, 0, 0, 0, 0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 50, + 171, + 234, + 220, + 202, + 122, + 141, + 65, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2, + 48, + 105, + 161, + 135, + 108, + 72, + 58, + 33, + 2, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4, + 47, + 124, + 99, + 116, + 130, + 165, + 110, + 36, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, ] - return pd.Series(data=ghi, index=tmy_index, name='ghi') + return pd.Series(data=ghi, index=tmy_index, name="ghi") def test_read_solaranywhere_high_resolution(high_resolution_index): - data, meta = pvlib.iotools.read_solaranywhere(TESTFILE_HIGH_RESOLUTION, - map_variables=False) + data, meta = pvlib.iotools.read_solaranywhere( + TESTFILE_HIGH_RESOLUTION, map_variables=False + ) # Check that metadata is parsed correctly - assert meta['latitude'] == 44.4675 - assert meta['longitude'] == -73.2075 - assert meta['altitude'] == 41.0 - assert meta['name'] == 'Burlington United States' - assert meta['TZ'] == -5.0 - assert meta['Data Version'] == '3.6' - assert meta['LatLon Resolution'] == 0.005 + assert meta["latitude"] == 44.4675 + assert meta["longitude"] == -73.2075 + assert meta["altitude"] == 41.0 + assert meta["name"] == "Burlington United States" + assert meta["TZ"] == -5.0 + assert meta["Data Version"] == "3.6" + assert meta["LatLon Resolution"] == 0.005 # Check that columns are parsed correctly - assert 'Albedo' in data.columns - assert 'Global Horizontal Irradiance (GHI) W/m2' in data.columns - assert 'Direct Normal Irradiance (DNI) W/m2' in data.columns - assert 'WindSpeed (m/s)' in data.columns - assert 'WindSpeedObservationType' in data.columns - assert 'Particulate Matter 10 (µg/m3)' in data.columns + assert "Albedo" in data.columns + assert "Global Horizontal Irradiance (GHI) W/m2" in data.columns + assert "Direct Normal Irradiance (DNI) W/m2" in data.columns + assert "WindSpeed (m/s)" in data.columns + assert "WindSpeedObservationType" in data.columns + assert "Particulate Matter 10 (µg/m3)" in data.columns # Check that data is parsed correctly - assert data.loc['2021-01-01 07:00:00-05:00', 'Albedo'] == 0.6 - assert data.loc['2021-01-01 07:00:00-05:00', 'WindSpeed (m/s)'] == 0 + assert data.loc["2021-01-01 07:00:00-05:00", "Albedo"] == 0.6 + assert data.loc["2021-01-01 07:00:00-05:00", "WindSpeed (m/s)"] == 0 # Assert that the index is parsed correctly pd.testing.assert_index_equal(data.index, high_resolution_index) @@ -76,37 +157,46 @@ def test_read_solaranywhere_high_resolution(high_resolution_index): def test_read_solaranywhere_map_variables(): # Check that variables are mapped by default to pvlib names data, meta = pvlib.iotools.read_solaranywhere(TESTFILE_HIGH_RESOLUTION) - mapped_column_names = ['ghi', 'dni', 'dhi', 'temp_air', 'wind_speed', - 'relative_humidity', 'ghi_clear', 'dni_clear', - 'dhi_clear', 'albedo'] + mapped_column_names = [ + "ghi", + "dni", + "dhi", + "temp_air", + "wind_speed", + "relative_humidity", + "ghi_clear", + "dni_clear", + "dhi_clear", + "albedo", + ] for c in mapped_column_names: assert c in data.columns - assert meta['latitude'] == 44.4675 - assert meta['longitude'] == -73.2075 - assert meta['altitude'] == 41.0 + assert meta["latitude"] == 44.4675 + assert meta["longitude"] == -73.2075 + assert meta["altitude"] == 41.0 def test_read_solaranywhere_tmy(tmy_index, tmy_ghi_series): # Check that TMY files are correctly parsed data, meta = pvlib.iotools.read_solaranywhere(TESTFILE_TMY) # Check that columns names are correct and mapped to pvlib names - assert 'ghi' in data.columns - assert 'dni' in data.columns - assert 'dhi' in data.columns - assert 'temp_air' in data.columns + assert "ghi" in data.columns + assert "dni" in data.columns + assert "dhi" in data.columns + assert "temp_air" in data.columns # Check that metadata is parsed correctly - assert meta['latitude'] == 44.465 - assert meta['longitude'] == -73.205 - assert meta['altitude'] == 41.0 - assert meta['name'] == 'Burlington United States' - assert meta['TZ'] == -5.0 - assert meta['Data Version'] == '3.6' - assert meta['LatLon Resolution'] == 0.010 - assert meta['Time Resolution'] == '60 minutes' + assert meta["latitude"] == 44.465 + assert meta["longitude"] == -73.205 + assert meta["altitude"] == 41.0 + assert meta["name"] == "Burlington United States" + assert meta["TZ"] == -5.0 + assert meta["Data Version"] == "3.6" + assert meta["LatLon Resolution"] == 0.010 + assert meta["Time Resolution"] == "60 minutes" # Assert that the index is parsed correctly pd.testing.assert_index_equal(data.index, tmy_index) # Test one column - pd.testing.assert_series_equal(data['ghi'], tmy_ghi_series) + pd.testing.assert_series_equal(data["ghi"], tmy_ghi_series) @pytest.mark.remote_data @@ -115,8 +205,12 @@ def test_get_solaranywhere_bad_probability_of_exceedance(): # Test if ValueError is raised if probability_of_exceedance is not integer with pytest.raises(ValueError, match="must be an integer"): pvlib.iotools.get_solaranywhere( - latitude=44, longitude=-73, api_key='empty', - source='SolarAnywherePOELatest', probability_of_exceedance=0.5) + latitude=44, + longitude=-73, + api_key="empty", + source="SolarAnywherePOELatest", + probability_of_exceedance=0.5, + ) @pytest.mark.remote_data @@ -126,15 +220,19 @@ def test_get_solaranywhere_missing_start_end(solaranywhere_api_key): # Test if ValueError is raised if start/end is missing for non-TMY request with pytest.raises(ValueError, match="simulation start and end time"): pvlib.iotools.get_solaranywhere( - latitude=44, longitude=-73, api_key=solaranywhere_api_key, - source='SolarAnywhereLatest') + latitude=44, + longitude=-73, + api_key=solaranywhere_api_key, + source="SolarAnywhereLatest", + ) @pytest.fixture def time_series_index(): - index = pd.date_range(start='2019-12-31 19:02:30-05:00', periods=288, - freq='5min') - index.name = 'ObservationTime' + index = pd.date_range( + start="2019-12-31 19:02:30-05:00", periods=288, freq="5min" + ) + index.name = "ObservationTime" index.freq = None return index @@ -142,107 +240,394 @@ def time_series_index(): @pytest.fixture def timeseries_temp_air(time_series_index): temp_air = [ - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, ] - return pd.Series(data=temp_air, index=time_series_index, name='temp_air') + return pd.Series(data=temp_air, index=time_series_index, name="temp_air") @requires_solaranywhere_credentials @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_solaranywhere_no_timezone( - solaranywhere_api_key, time_series_index, timeseries_temp_air): + solaranywhere_api_key, time_series_index, timeseries_temp_air +): # Test if data can be retrieved. This test only retrieves one day of data # to minimize the request time. data, meta = pvlib.iotools.get_solaranywhere( - latitude=44.4675, longitude=-73.2075, api_key=solaranywhere_api_key, + latitude=44.4675, + longitude=-73.2075, + api_key=solaranywhere_api_key, # specify start/end without timezone information - start=pd.Timestamp(2020, 1, 1), end=pd.Timestamp(2020, 1, 2), + start=pd.Timestamp(2020, 1, 1), + end=pd.Timestamp(2020, 1, 2), # test specific version of SolarAnywhere - source='SolarAnywhere3_6', - spatial_resolution=0.005, time_resolution=5, true_dynamics=True) + source="SolarAnywhere3_6", + spatial_resolution=0.005, + time_resolution=5, + true_dynamics=True, + ) # Check metadata, including that true-dynamics is set - assert meta['WeatherSiteName'] == 'SolarAnywhere3_6' - assert meta['ApplyTrueDynamics'] is True - assert meta['time_resolution'] == 5 - assert meta['spatial_resolution'] == 0.005 - assert meta['latitude'] == 44.4675 - assert meta['longitude'] == -73.2075 - assert meta['altitude'] == 41.0 + assert meta["WeatherSiteName"] == "SolarAnywhere3_6" + assert meta["ApplyTrueDynamics"] is True + assert meta["time_resolution"] == 5 + assert meta["spatial_resolution"] == 0.005 + assert meta["latitude"] == 44.4675 + assert meta["longitude"] == -73.2075 + assert meta["altitude"] == 41.0 # Check that variables have been mapped (default convention) - assert 'StartTime' in data.columns - assert 'ObservationTime' in data.columns - assert 'EndTime' in data.columns - assert 'ghi' in data.columns - assert 'dni' in data.columns - assert 'dhi' in data.columns - assert 'temp_air' in data.columns - assert 'wind_speed' in data.columns - assert 'albedo' in data.columns - assert 'DataVersion' in data.columns + assert "StartTime" in data.columns + assert "ObservationTime" in data.columns + assert "EndTime" in data.columns + assert "ghi" in data.columns + assert "dni" in data.columns + assert "dhi" in data.columns + assert "temp_air" in data.columns + assert "wind_speed" in data.columns + assert "albedo" in data.columns + assert "DataVersion" in data.columns # Assert index (checks that time resolution is 5 min) pd.testing.assert_index_equal(data.index, time_series_index) # Test one column - pd.testing.assert_series_equal(data['temp_air'], timeseries_temp_air) + pd.testing.assert_series_equal(data["temp_air"], timeseries_temp_air) @requires_solaranywhere_credentials @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_solaranywhere_other_options( - solaranywhere_api_key, time_series_index, timeseries_temp_air): + solaranywhere_api_key, time_series_index, timeseries_temp_air +): # Test if data can be retrieved. This test only retrieves one day of data # to minimize the request time. data, meta = pvlib.iotools.get_solaranywhere( - latitude=44.4675, longitude=-73.2075, api_key=solaranywhere_api_key, + latitude=44.4675, + longitude=-73.2075, + api_key=solaranywhere_api_key, # specify start/end as str with timezone information - start='2020-01-01 00:00:00+0000', - end='2020-01-02 00:00:00+0000', + start="2020-01-01 00:00:00+0000", + end="2020-01-02 00:00:00+0000", # test specific version of SolarAnywhere - source='SolarAnywhere3_7', + source="SolarAnywhere3_7", # test fewer variables variables=[ - 'ObservationTime', - 'GlobalHorizontalIrradiance_WattsPerMeterSquared', + "ObservationTime", + "GlobalHorizontalIrradiance_WattsPerMeterSquared", ], - map_variables=False) + map_variables=False, + ) # Check metadata - assert meta['WeatherSiteName'] == 'SolarAnywhere3_7' - assert meta['ApplyTrueDynamics'] is False # default setting - assert meta['time_resolution'] == 60 # default resolution - assert meta['spatial_resolution'] == 0.01 # default resolution - assert meta['latitude'] == 44.4675 - assert meta['longitude'] == -73.2075 - assert meta['altitude'] == 41.0 + assert meta["WeatherSiteName"] == "SolarAnywhere3_7" + assert meta["ApplyTrueDynamics"] is False # default setting + assert meta["time_resolution"] == 60 # default resolution + assert meta["spatial_resolution"] == 0.01 # default resolution + assert meta["latitude"] == 44.4675 + assert meta["longitude"] == -73.2075 + assert meta["altitude"] == 41.0 # Check that variables have been mapped (default convention) - assert 'StartTime' not in data.columns - assert 'ObservationTime' in data.columns - assert 'EndTime' not in data.columns + assert "StartTime" not in data.columns + assert "ObservationTime" in data.columns + assert "EndTime" not in data.columns # Check that ghi is not mapped - assert 'ghi' not in data.columns - assert 'GlobalHorizontalIrradiance_WattsPerMeterSquared' in data.columns - assert 'dni' not in data.columns - assert 'dhi' not in data.columns - assert 'temp_air' not in data.columns - assert 'wind_speed' not in data.columns - assert 'albedo' not in data.columns - assert 'DataVersion' not in data.columns + assert "ghi" not in data.columns + assert "GlobalHorizontalIrradiance_WattsPerMeterSquared" in data.columns + assert "dni" not in data.columns + assert "dhi" not in data.columns + assert "temp_air" not in data.columns + assert "wind_speed" not in data.columns + assert "albedo" not in data.columns + assert "DataVersion" not in data.columns @requires_solaranywhere_credentials @@ -252,13 +637,15 @@ def test_get_solaranywhere_probability_exceedance_error(solaranywhere_api_key): # Test if ValueError is raised when passing start/end to typical year with pytest.raises(ValueError, match="start and end time must be null"): data, meta = pvlib.iotools.get_solaranywhere( - latitude=44.4675, longitude=-73.2075, + latitude=44.4675, + longitude=-73.2075, api_key=solaranywhere_api_key, # Probabiliy of exceedance year should not have start/end specified - start=pd.Timestamp('2020-01-01 00:00:00+0000'), - end=pd.Timestamp('2020-01-05 12:00:00+0000'), - source='SolarAnywherePOELatest', - probability_of_exceedance=20) + start=pd.Timestamp("2020-01-01 00:00:00+0000"), + end=pd.Timestamp("2020-01-05 12:00:00+0000"), + source="SolarAnywherePOELatest", + probability_of_exceedance=20, + ) @requires_solaranywhere_credentials @@ -268,10 +655,12 @@ def test_get_solaranywhere_timeout_tgy(solaranywhere_api_key): # Test if the service times out when the timeout parameter is close to zero with pytest.raises(TimeoutError, match="Time exceeded"): pvlib.iotools.get_solaranywhere( - latitude=44.4675, longitude=-73.2075, + latitude=44.4675, + longitude=-73.2075, api_key=solaranywhere_api_key, - source='SolarAnywhereTGYLatest', - timeout=0.00001) + source="SolarAnywhereTGYLatest", + timeout=0.00001, + ) @requires_solaranywhere_credentials @@ -281,7 +670,9 @@ def test_get_solaranywhere_not_available(solaranywhere_api_key): # Test if RuntimeError is raised if location in the ocean is requested with pytest.raises(RuntimeError, match="Tile is outside of our coverage"): pvlib.iotools.get_solaranywhere( - latitude=40, longitude=-70, + latitude=40, + longitude=-70, api_key=solaranywhere_api_key, - start=pd.Timestamp('2020-01-01 00:00:00+0000'), - end=pd.Timestamp('2020-01-05 12:00:00+0000')) + start=pd.Timestamp("2020-01-01 00:00:00+0000"), + end=pd.Timestamp("2020-01-05 12:00:00+0000"), + ) diff --git a/pvlib/tests/iotools/test_solargis.py b/pvlib/tests/iotools/test_solargis.py index 55882e91c5..516d3731d2 100644 --- a/pvlib/tests/iotools/test_solargis.py +++ b/pvlib/tests/iotools/test_solargis.py @@ -2,14 +2,22 @@ import pytest import pvlib import requests -from ..conftest import (RERUNS, RERUNS_DELAY, assert_frame_equal, - assert_index_equal) +from ..conftest import ( + RERUNS, + RERUNS_DELAY, + assert_frame_equal, + assert_index_equal, +) @pytest.fixture def hourly_index(): - hourly_index = pd.date_range(start='2022-01-01 00:30+01:00', freq='60min', - periods=24, name='dateTime') + hourly_index = pd.date_range( + start="2022-01-01 00:30+01:00", + freq="60min", + periods=24, + name="dateTime", + ) hourly_index.freq = None return hourly_index @@ -17,29 +25,85 @@ def hourly_index(): @pytest.fixture def hourly_index_start_utc(): hourly_index_left_utc = pd.date_range( - start='2023-01-01 00:00+00:00', freq='30min', periods=24*2, - name='dateTime') + start="2023-01-01 00:00+00:00", + freq="30min", + periods=24 * 2, + name="dateTime", + ) hourly_index_left_utc.freq = None return hourly_index_left_utc @pytest.fixture def hourly_dataframe(hourly_index): - ghi = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 73.0, 152.0, 141.0, 105.0, - 62.0, 65.0, 62.0, 11.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - dni = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0, 233.0, 301.0, 136.0, 32.0, - 0.0, 3.0, 77.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - return pd.DataFrame(data={'ghi': ghi, 'dni': dni}, index=hourly_index) + ghi = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 5.0, + 73.0, + 152.0, + 141.0, + 105.0, + 62.0, + 65.0, + 62.0, + 11.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + dni = [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 30.0, + 233.0, + 301.0, + 136.0, + 32.0, + 0.0, + 3.0, + 77.0, + 5.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + return pd.DataFrame(data={"ghi": ghi, "dni": dni}, index=hourly_index) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_solargis(hourly_dataframe): data, meta = pvlib.iotools.get_solargis( - latitude=48.61259, longitude=20.827079, - start='2022-01-01', end='2022-01-01', - tz='GMT+01', variables=['GHI', 'DNI'], - time_resolution='HOURLY', api_key='demo') + latitude=48.61259, + longitude=20.827079, + start="2022-01-01", + end="2022-01-01", + tz="GMT+01", + variables=["GHI", "DNI"], + time_resolution="HOURLY", + api_key="demo", + ) assert_frame_equal(data, hourly_dataframe) @@ -47,13 +111,17 @@ def test_get_solargis(hourly_dataframe): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_solargis_utc_start_timestamp(hourly_index_start_utc): data, meta = pvlib.iotools.get_solargis( - latitude=48.61259, longitude=20.827079, - start='2023-01-01', end='2023-01-01', - variables=['GTI'], - timestamp_type='start', - time_resolution='MIN_30', - map_variables=False, api_key='demo') - assert 'GTI' in data.columns # assert that variables aren't mapped + latitude=48.61259, + longitude=20.827079, + start="2023-01-01", + end="2023-01-01", + variables=["GTI"], + timestamp_type="start", + time_resolution="MIN_30", + map_variables=False, + api_key="demo", + ) + assert "GTI" in data.columns # assert that variables aren't mapped assert_index_equal(data.index, hourly_index_start_utc) @@ -63,6 +131,11 @@ def test_get_solargis_http_error(): # Test if HTTPError is raised if date outside range is specified with pytest.raises(requests.HTTPError, match="data coverage"): _, _ = pvlib.iotools.get_solargis( - latitude=48.61259, longitude=20.827079, - start='1920-01-01', end='1920-01-01', # date outside range - variables=['GHI', 'DNI'], time_resolution='HOURLY', api_key='demo') + latitude=48.61259, + longitude=20.827079, + start="1920-01-01", + end="1920-01-01", # date outside range + variables=["GHI", "DNI"], + time_resolution="HOURLY", + api_key="demo", + ) diff --git a/pvlib/tests/iotools/test_solcast.py b/pvlib/tests/iotools/test_solcast.py index 3879d88b20..192c22e9b0 100644 --- a/pvlib/tests/iotools/test_solcast.py +++ b/pvlib/tests/iotools/test_solcast.py @@ -4,79 +4,119 @@ import pytest -@pytest.mark.parametrize("endpoint,params,api_key,json_response", [ - ( - "live/radiation_and_weather", - dict( - latitude=-33.856784, - longitude=151.215297, - output_parameters='dni,ghi' +@pytest.mark.parametrize( + "endpoint,params,api_key,json_response", + [ + ( + "live/radiation_and_weather", + dict( + latitude=-33.856784, + longitude=151.215297, + output_parameters="dni,ghi", + ), + "1234", + { + "estimated_actuals": [ + { + "dni": 836, + "ghi": 561, + "period_end": "2023-09-18T05:00:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 866, + "ghi": 643, + "period_end": "2023-09-18T04:30:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 890, + "ghi": 713, + "period_end": "2023-09-18T04:00:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 909, + "ghi": 768, + "period_end": "2023-09-18T03:30:00.0000000Z", + "period": "PT30M", + }, + ] + }, ), - "1234", - {'estimated_actuals': - [{'dni': 836, 'ghi': 561, - 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 866, 'ghi': 643, - 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, - {'dni': 890, 'ghi': 713, - 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 909, 'ghi': 768, - 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] - } - ), -]) + ], +) def test__get_solcast(requests_mock, endpoint, params, api_key, json_response): - mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"latitude={params['latitude']}&" \ - f"longitude={params['longitude']}&" \ - f"output_parameters={params['output_parameters']}" + mock_url = ( + f"https://api.solcast.com.au/data/{endpoint}?" + f"latitude={params['latitude']}&" + f"longitude={params['longitude']}&" + f"output_parameters={params['output_parameters']}" + ) requests_mock.get(mock_url, json=json_response) # with variables remapping pd.testing.assert_frame_equal( - pvlib.iotools.solcast._get_solcast( - endpoint, params, api_key, True - ), + pvlib.iotools.solcast._get_solcast(endpoint, params, api_key, True), pvlib.iotools.solcast._solcast2pvlib( pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]]) - ) + json_response[list(json_response.keys())[0]] + ) + ), ) # no remapping of variables pd.testing.assert_frame_equal( - pvlib.iotools.solcast._get_solcast( - endpoint, params, api_key, False - ), - pd.DataFrame.from_dict( - json_response[list(json_response.keys())[0]]) + pvlib.iotools.solcast._get_solcast(endpoint, params, api_key, False), + pd.DataFrame.from_dict(json_response[list(json_response.keys())[0]]), ) @pytest.mark.parametrize("map_variables", [True, False]) -@pytest.mark.parametrize("endpoint,function,params,json_response", [ - ( - "live/radiation_and_weather", - pvlib.iotools.get_solcast_live, - dict( - api_key="1234", - latitude=-33.856784, - longitude=151.215297, - output_parameters='dni,ghi' +@pytest.mark.parametrize( + "endpoint,function,params,json_response", + [ + ( + "live/radiation_and_weather", + pvlib.iotools.get_solcast_live, + dict( + api_key="1234", + latitude=-33.856784, + longitude=151.215297, + output_parameters="dni,ghi", + ), + { + "estimated_actuals": [ + { + "dni": 836, + "ghi": 561, + "period_end": "2023-09-18T05:00:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 866, + "ghi": 643, + "period_end": "2023-09-18T04:30:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 890, + "ghi": 713, + "period_end": "2023-09-18T04:00:00.0000000Z", + "period": "PT30M", + }, + { + "dni": 909, + "ghi": 768, + "period_end": "2023-09-18T03:30:00.0000000Z", + "period": "PT30M", + }, + ] + }, ), - {'estimated_actuals': - [{'dni': 836, 'ghi': 561, - 'period_end': '2023-09-18T05:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 866, 'ghi': 643, - 'period_end': '2023-09-18T04:30:00.0000000Z', 'period': 'PT30M'}, - {'dni': 890, 'ghi': 713, - 'period_end': '2023-09-18T04:00:00.0000000Z', 'period': 'PT30M'}, - {'dni': 909, 'ghi': 768, - 'period_end': '2023-09-18T03:30:00.0000000Z', 'period': 'PT30M'}] - } - ), -]) + ], +) def test_get_solcast_live( requests_mock, endpoint, function, params, json_response, map_variables ): @@ -108,36 +148,58 @@ def test_get_solcast_live( @pytest.mark.parametrize("map_variables", [True, False]) -@pytest.mark.parametrize("endpoint,function,params,json_response", [ - ( - "tmy/radiation_and_weather", - pvlib.iotools.get_solcast_tmy, - dict( - api_key="1234", - latitude=-33.856784, - longitude=51.215297 +@pytest.mark.parametrize( + "endpoint,function,params,json_response", + [ + ( + "tmy/radiation_and_weather", + pvlib.iotools.get_solcast_tmy, + dict(api_key="1234", latitude=-33.856784, longitude=51.215297), + { + "estimated_actuals": [ + { + "dni": 151, + "ghi": 609, + "period_end": "2059-01-01T01:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 0, + "ghi": 404, + "period_end": "2059-01-01T02:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 0, + "ghi": 304, + "period_end": "2059-01-01T03:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 0, + "ghi": 174, + "period_end": "2059-01-01T04:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 0, + "ghi": 111, + "period_end": "2059-01-01T05:00:00.0000000Z", + "period": "PT60M", + }, + ] + }, ), - {'estimated_actuals': [ - {'dni': 151, 'ghi': 609, - 'period_end': '2059-01-01T01:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 404, - 'period_end': '2059-01-01T02:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 304, - 'period_end': '2059-01-01T03:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 174, - 'period_end': '2059-01-01T04:00:00.0000000Z', 'period': 'PT60M'}, - {'dni': 0, 'ghi': 111, - 'period_end': '2059-01-01T05:00:00.0000000Z', 'period': 'PT60M'}] - } - ), -]) + ], +) def test_get_solcast_tmy( requests_mock, endpoint, function, params, json_response, map_variables ): - - mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&" \ - f"longitude={params['longitude']}&format=json" + mock_url = ( + f"https://api.solcast.com.au/data/{endpoint}?" + f"&latitude={params['latitude']}&" + f"longitude={params['longitude']}&format=json" + ) requests_mock.get(mock_url, json=json_response) @@ -159,72 +221,173 @@ def test_get_solcast_tmy( ) -@pytest.mark.parametrize("in_df,out_df", [ - ( - pd.DataFrame( - [[942, 843, 1017.4, 30, 7.8, 316, 1010, -2, 4.6, 16.4, - '2023-09-20T02:00:00.0000000Z', 'PT30M', 90], - [936, 832, 1017.9, 30, 7.9, 316, 996, -14, 4.5, 16.3, - '2023-09-20T01:30:00.0000000Z', 'PT30M', 0]], - columns=[ - 'dni', 'ghi', 'surface_pressure', 'air_temp', 'wind_speed_10m', - 'wind_direction_10m', 'gti', 'azimuth', 'dewpoint_temp', - 'precipitable_water', 'period_end', 'period', 'zenith'], - index=pd.RangeIndex(start=0, stop=2, step=1) - ), - pd.DataFrame( - [[9.4200e+02, 8.4300e+02, 1.0174e+05, 3.0000e+01, 7.8000e+00, - 3.1600e+02, 1.0100e+03, 2.0000e+00, 4.6000e+00, 1.6400e+00, 90], - [9.3600e+02, 8.3200e+02, 1.0179e+05, 3.0000e+01, 7.9000e+00, - 3.1600e+02, 9.9600e+02, 1.4000e+01, 4.5000e+00, 1.6300e+00, 0]], - columns=[ - 'dni', 'ghi', 'pressure', 'temp_air', 'wind_speed', - 'wind_direction', 'poa_global', 'solar_azimuth', - 'temp_dew', 'precipitable_water', 'solar_zenith'], - index=pd.DatetimeIndex( - ['2023-09-20 01:45:00+00:00', '2023-09-20 01:15:00+00:00'], - dtype='datetime64[ns, UTC]', name='period_mid', freq=None) +@pytest.mark.parametrize( + "in_df,out_df", + [ + ( + pd.DataFrame( + [ + [ + 942, + 843, + 1017.4, + 30, + 7.8, + 316, + 1010, + -2, + 4.6, + 16.4, + "2023-09-20T02:00:00.0000000Z", + "PT30M", + 90, + ], + [ + 936, + 832, + 1017.9, + 30, + 7.9, + 316, + 996, + -14, + 4.5, + 16.3, + "2023-09-20T01:30:00.0000000Z", + "PT30M", + 0, + ], + ], + columns=[ + "dni", + "ghi", + "surface_pressure", + "air_temp", + "wind_speed_10m", + "wind_direction_10m", + "gti", + "azimuth", + "dewpoint_temp", + "precipitable_water", + "period_end", + "period", + "zenith", + ], + index=pd.RangeIndex(start=0, stop=2, step=1), + ), + pd.DataFrame( + [ + [ + 9.4200e02, + 8.4300e02, + 1.0174e05, + 3.0000e01, + 7.8000e00, + 3.1600e02, + 1.0100e03, + 2.0000e00, + 4.6000e00, + 1.6400e00, + 90, + ], + [ + 9.3600e02, + 8.3200e02, + 1.0179e05, + 3.0000e01, + 7.9000e00, + 3.1600e02, + 9.9600e02, + 1.4000e01, + 4.5000e00, + 1.6300e00, + 0, + ], + ], + columns=[ + "dni", + "ghi", + "pressure", + "temp_air", + "wind_speed", + "wind_direction", + "poa_global", + "solar_azimuth", + "temp_dew", + "precipitable_water", + "solar_zenith", + ], + index=pd.DatetimeIndex( + ["2023-09-20 01:45:00+00:00", "2023-09-20 01:15:00+00:00"], + dtype="datetime64[ns, UTC]", + name="period_mid", + freq=None, + ), + ), ) - ) -]) + ], +) def test_solcast2pvlib(in_df, out_df): df = pvlib.iotools.solcast._solcast2pvlib(in_df) pd.testing.assert_frame_equal(df.astype(float), out_df.astype(float)) @pytest.mark.parametrize("map_variables", [True, False]) -@pytest.mark.parametrize("endpoint,function,params,json_response", [ - ( - "historic/radiation_and_weather", - pvlib.iotools.get_solcast_historic, - dict( - api_key="1234", - latitude=-33.856784, - longitude=51.215297, - start="2023-01-01T08:00", - duration="P1D", - period="PT1H", - output_parameters='dni' - ), {'estimated_actuals': [ - {'dni': 822, 'period_end': '2023-01-01T09:00:00.0000000Z', - 'period': 'PT60M'}, - {'dni': 918, 'period_end': '2023-01-01T10:00:00.0000000Z', - 'period': 'PT60M'}, - {'dni': 772, 'period_end': '2023-01-01T11:00:00.0000000Z', - 'period': 'PT60M'}, - {'dni': 574, 'period_end': '2023-01-01T12:00:00.0000000Z', - 'period': 'PT60M'}, - {'dni': 494, 'period_end': '2023-01-01T13:00:00.0000000Z', - 'period': 'PT60M'} - ]} - ), -]) +@pytest.mark.parametrize( + "endpoint,function,params,json_response", + [ + ( + "historic/radiation_and_weather", + pvlib.iotools.get_solcast_historic, + dict( + api_key="1234", + latitude=-33.856784, + longitude=51.215297, + start="2023-01-01T08:00", + duration="P1D", + period="PT1H", + output_parameters="dni", + ), + { + "estimated_actuals": [ + { + "dni": 822, + "period_end": "2023-01-01T09:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 918, + "period_end": "2023-01-01T10:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 772, + "period_end": "2023-01-01T11:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 574, + "period_end": "2023-01-01T12:00:00.0000000Z", + "period": "PT60M", + }, + { + "dni": 494, + "period_end": "2023-01-01T13:00:00.0000000Z", + "period": "PT60M", + }, + ] + }, + ), + ], +) def test_get_solcast_historic( requests_mock, endpoint, function, params, json_response, map_variables ): - mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&" \ - f"longitude={params['longitude']}&format=json" + mock_url = ( + f"https://api.solcast.com.au/data/{endpoint}?" + f"&latitude={params['latitude']}&" + f"longitude={params['longitude']}&format=json" + ) requests_mock.get(mock_url, json=json_response) @@ -247,40 +410,65 @@ def test_get_solcast_historic( @pytest.mark.parametrize("map_variables", [True, False]) -@pytest.mark.parametrize("endpoint,function,params,json_response", [ - ( - "forecast/radiation_and_weather", - pvlib.iotools.get_solcast_forecast, - dict( - api_key="1234", - latitude=-33.856784, - longitude=51.215297, - hours="5", - period="PT1H", - output_parameters='dni' - ), { - 'forecast': [ - {'dni': 0, 'period_end': '2023-12-13T01:00:00.0000000Z', - 'period': 'PT1H'}, - {'dni': 1, 'period_end': '2023-12-13T02:00:00.0000000Z', - 'period': 'PT1H'}, - {'dni': 2, 'period_end': '2023-12-13T03:00:00.0000000Z', - 'period': 'PT1H'}, - {'dni': 3, 'period_end': '2023-12-13T04:00:00.0000000Z', - 'period': 'PT1H'}, - {'dni': 4, 'period_end': '2023-12-13T05:00:00.0000000Z', - 'period': 'PT1H'}, - {'dni': 5, 'period_end': '2023-12-13T06:00:00.0000000Z', - 'period': 'PT1H'} - ]} - ), -]) +@pytest.mark.parametrize( + "endpoint,function,params,json_response", + [ + ( + "forecast/radiation_and_weather", + pvlib.iotools.get_solcast_forecast, + dict( + api_key="1234", + latitude=-33.856784, + longitude=51.215297, + hours="5", + period="PT1H", + output_parameters="dni", + ), + { + "forecast": [ + { + "dni": 0, + "period_end": "2023-12-13T01:00:00.0000000Z", + "period": "PT1H", + }, + { + "dni": 1, + "period_end": "2023-12-13T02:00:00.0000000Z", + "period": "PT1H", + }, + { + "dni": 2, + "period_end": "2023-12-13T03:00:00.0000000Z", + "period": "PT1H", + }, + { + "dni": 3, + "period_end": "2023-12-13T04:00:00.0000000Z", + "period": "PT1H", + }, + { + "dni": 4, + "period_end": "2023-12-13T05:00:00.0000000Z", + "period": "PT1H", + }, + { + "dni": 5, + "period_end": "2023-12-13T06:00:00.0000000Z", + "period": "PT1H", + }, + ] + }, + ), + ], +) def test_get_solcast_forecast( requests_mock, endpoint, function, params, json_response, map_variables ): - mock_url = f"https://api.solcast.com.au/data/{endpoint}?" \ - f"&latitude={params['latitude']}&" \ - f"longitude={params['longitude']}&format=json" + mock_url = ( + f"https://api.solcast.com.au/data/{endpoint}?" + f"&latitude={params['latitude']}&" + f"longitude={params['longitude']}&format=json" + ) requests_mock.get(mock_url, json=json_response) diff --git a/pvlib/tests/iotools/test_solrad.py b/pvlib/tests/iotools/test_solrad.py index 963014302b..8c085b983a 100644 --- a/pvlib/tests/iotools/test_solrad.py +++ b/pvlib/tests/iotools/test_solrad.py @@ -8,99 +8,398 @@ from ..conftest import DATA_DIR, assert_frame_equal, RERUNS, RERUNS_DELAY -testfile = DATA_DIR / 'abq19056.dat' -testfile_mad = DATA_DIR / 'msn19056.dat' -https_testfile = ('https://gml.noaa.gov/aftp/data/radiation/solrad/msn/' - '2019/msn19056.dat') +testfile = DATA_DIR / "abq19056.dat" +testfile_mad = DATA_DIR / "msn19056.dat" +https_testfile = ( + "https://gml.noaa.gov/aftp/data/radiation/solrad/msn/" "2019/msn19056.dat" +) columns = [ - 'year', 'julian_day', 'month', 'day', 'hour', 'minute', 'decimal_time', - 'solar_zenith', 'ghi', 'ghi_flag', 'dni', 'dni_flag', 'dhi', 'dhi_flag', - 'uvb', 'uvb_flag', 'uvb_temp', 'uvb_temp_flag', 'std_dw_psp', 'std_direct', - 'std_diffuse', 'std_uvb'] -index = pd.DatetimeIndex(['2019-02-25 00:00:00', - '2019-02-25 00:01:00', - '2019-02-25 00:02:00', - '2019-02-25 00:03:00'], - freq=None).tz_localize('UTC') -values = np.array([ - [2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, 0.000e+00, - 0.000e+00, 7.930e+01, 1.045e+02, 0.000e+00, 6.050e+01, 0.000e+00, - 9.780e+01, 0.000e+00, 5.900e+00, 0.000e+00, 4.360e+01, 0.000e+00, - 3.820e-01, 2.280e+00, 4.310e-01, 6.000e-02], - [2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, 1.000e+00, - 1.700e-02, 7.949e+01, 1.026e+02, 0.000e+00, 5.970e+01, 0.000e+00, - 9.620e+01, 0.000e+00, 5.700e+00, 0.000e+00, 4.360e+01, 0.000e+00, - 7.640e-01, 1.800e+00, 4.310e-01, 6.000e-02], - [2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, 2.000e+00, - 3.300e-02, 7.968e+01, 1.021e+02, 0.000e+00, 6.580e+01, 0.000e+00, - 9.480e+01, 0.000e+00, 5.500e+00, 0.000e+00, 4.360e+01, 0.000e+00, - 3.820e-01, 4.079e+00, 3.230e-01, 6.000e-02], - [2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, 3.000e+00, - 5.000e-02, 7.987e+01, 1.026e+02, 0.000e+00, 7.630e+01, 0.000e+00, - nan, 0.000e+00, 5.300e+00, 0.000e+00, 4.360e+01, 0.000e+00, - 5.090e-01, 1.920e+00, 2.150e-01, 5.000e-02]]) + "year", + "julian_day", + "month", + "day", + "hour", + "minute", + "decimal_time", + "solar_zenith", + "ghi", + "ghi_flag", + "dni", + "dni_flag", + "dhi", + "dhi_flag", + "uvb", + "uvb_flag", + "uvb_temp", + "uvb_temp_flag", + "std_dw_psp", + "std_direct", + "std_diffuse", + "std_uvb", +] +index = pd.DatetimeIndex( + [ + "2019-02-25 00:00:00", + "2019-02-25 00:01:00", + "2019-02-25 00:02:00", + "2019-02-25 00:03:00", + ], + freq=None, +).tz_localize("UTC") +values = np.array( + [ + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 0.000e00, + 0.000e00, + 7.930e01, + 1.045e02, + 0.000e00, + 6.050e01, + 0.000e00, + 9.780e01, + 0.000e00, + 5.900e00, + 0.000e00, + 4.360e01, + 0.000e00, + 3.820e-01, + 2.280e00, + 4.310e-01, + 6.000e-02, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 1.000e00, + 1.700e-02, + 7.949e01, + 1.026e02, + 0.000e00, + 5.970e01, + 0.000e00, + 9.620e01, + 0.000e00, + 5.700e00, + 0.000e00, + 4.360e01, + 0.000e00, + 7.640e-01, + 1.800e00, + 4.310e-01, + 6.000e-02, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 2.000e00, + 3.300e-02, + 7.968e01, + 1.021e02, + 0.000e00, + 6.580e01, + 0.000e00, + 9.480e01, + 0.000e00, + 5.500e00, + 0.000e00, + 4.360e01, + 0.000e00, + 3.820e-01, + 4.079e00, + 3.230e-01, + 6.000e-02, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 3.000e00, + 5.000e-02, + 7.987e01, + 1.026e02, + 0.000e00, + 7.630e01, + 0.000e00, + nan, + 0.000e00, + 5.300e00, + 0.000e00, + 4.360e01, + 0.000e00, + 5.090e-01, + 1.920e00, + 2.150e-01, + 5.000e-02, + ], + ] +) dtypes = [ - 'int64', 'int64', 'int64', 'int64', 'int64', 'int64', 'float64', - 'float64', 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', - 'float64', 'int64', 'float64', 'int64', 'float64', 'float64', - 'float64', 'float64'] + "int64", + "int64", + "int64", + "int64", + "int64", + "int64", + "float64", + "float64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", +] columns_mad = [ - 'year', 'julian_day', 'month', 'day', 'hour', 'minute', 'decimal_time', - 'solar_zenith', 'ghi', 'ghi_flag', 'dni', 'dni_flag', 'dhi', 'dhi_flag', - 'uvb', 'uvb_flag', 'uvb_temp', 'uvb_temp_flag', 'dpir', 'dpir_flag', - 'dpirc', 'dpirc_flag', 'dpird', 'dpird_flag', 'std_dw_psp', - 'std_direct', 'std_diffuse', 'std_uvb', 'std_dpir', 'std_dpirc', - 'std_dpird'] -values_mad = np.array([ - [ 2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, - 0.000e+00, 0.000e+00, 9.428e+01, -2.300e+00, 0.000e+00, - 0.000e+00, 0.000e+00, 4.000e-01, 0.000e+00, nan, - 1.000e+00, nan, 1.000e+00, 1.872e+02, 0.000e+00, - 2.656e+02, 0.000e+00, 2.653e+02, 0.000e+00, 0.000e+00, - 0.000e+00, 0.000e+00, nan, 2.000e-03, 2.600e+01, - 2.700e+01], - [ 2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, - 1.000e+00, 1.700e-02, 9.446e+01, -2.300e+00, 0.000e+00, - 0.000e+00, 0.000e+00, 1.000e-01, 0.000e+00, nan, - 1.000e+00, nan, 1.000e+00, 1.882e+02, 0.000e+00, - 2.656e+02, 0.000e+00, 2.653e+02, 0.000e+00, 1.330e-01, - 1.280e-01, 2.230e-01, nan, 1.000e-03, 2.600e+01, - 7.200e+01], - [ 2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, - 2.000e+00, 3.300e-02, 9.464e+01, -2.700e+00, 0.000e+00, - -2.000e-01, 0.000e+00, 0.000e+00, 0.000e+00, nan, - 1.000e+00, nan, 1.000e+00, 1.876e+02, 0.000e+00, - 2.656e+02, 0.000e+00, 2.653e+02, 0.000e+00, 0.000e+00, - 2.570e-01, 0.000e+00, nan, 1.000e-03, 2.400e+01, - 4.200e+01], - [ 2.019e+03, 5.600e+01, 2.000e+00, 2.500e+01, 0.000e+00, - 3.000e+00, 5.000e-02, 9.482e+01, -2.500e+00, 0.000e+00, - 4.000e-01, 0.000e+00, 0.000e+00, 0.000e+00, nan, - 1.000e+00, nan, 1.000e+00, 1.873e+02, 0.000e+00, - 2.656e+02, 0.000e+00, 2.653e+02, 0.000e+00, 2.660e-01, - 3.850e-01, 0.000e+00, nan, 1.000e-03, 2.600e+01, - 4.800e+01]]) + "year", + "julian_day", + "month", + "day", + "hour", + "minute", + "decimal_time", + "solar_zenith", + "ghi", + "ghi_flag", + "dni", + "dni_flag", + "dhi", + "dhi_flag", + "uvb", + "uvb_flag", + "uvb_temp", + "uvb_temp_flag", + "dpir", + "dpir_flag", + "dpirc", + "dpirc_flag", + "dpird", + "dpird_flag", + "std_dw_psp", + "std_direct", + "std_diffuse", + "std_uvb", + "std_dpir", + "std_dpirc", + "std_dpird", +] +values_mad = np.array( + [ + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 0.000e00, + 0.000e00, + 9.428e01, + -2.300e00, + 0.000e00, + 0.000e00, + 0.000e00, + 4.000e-01, + 0.000e00, + nan, + 1.000e00, + nan, + 1.000e00, + 1.872e02, + 0.000e00, + 2.656e02, + 0.000e00, + 2.653e02, + 0.000e00, + 0.000e00, + 0.000e00, + 0.000e00, + nan, + 2.000e-03, + 2.600e01, + 2.700e01, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 1.000e00, + 1.700e-02, + 9.446e01, + -2.300e00, + 0.000e00, + 0.000e00, + 0.000e00, + 1.000e-01, + 0.000e00, + nan, + 1.000e00, + nan, + 1.000e00, + 1.882e02, + 0.000e00, + 2.656e02, + 0.000e00, + 2.653e02, + 0.000e00, + 1.330e-01, + 1.280e-01, + 2.230e-01, + nan, + 1.000e-03, + 2.600e01, + 7.200e01, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 2.000e00, + 3.300e-02, + 9.464e01, + -2.700e00, + 0.000e00, + -2.000e-01, + 0.000e00, + 0.000e00, + 0.000e00, + nan, + 1.000e00, + nan, + 1.000e00, + 1.876e02, + 0.000e00, + 2.656e02, + 0.000e00, + 2.653e02, + 0.000e00, + 0.000e00, + 2.570e-01, + 0.000e00, + nan, + 1.000e-03, + 2.400e01, + 4.200e01, + ], + [ + 2.019e03, + 5.600e01, + 2.000e00, + 2.500e01, + 0.000e00, + 3.000e00, + 5.000e-02, + 9.482e01, + -2.500e00, + 0.000e00, + 4.000e-01, + 0.000e00, + 0.000e00, + 0.000e00, + nan, + 1.000e00, + nan, + 1.000e00, + 1.873e02, + 0.000e00, + 2.656e02, + 0.000e00, + 2.653e02, + 0.000e00, + 2.660e-01, + 3.850e-01, + 0.000e00, + nan, + 1.000e-03, + 2.600e01, + 4.800e01, + ], + ] +) dtypes_mad = [ - 'int64', 'int64', 'int64', 'int64', 'int64', 'int64', 'float64', 'float64', - 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', 'float64', - 'int64', 'float64', 'int64', 'float64', 'int64', 'float64', 'int64', - 'float64', 'int64', 'float64', 'float64', 'float64', 'float64', 'float64', - 'float64', 'float64'] -meta = {'station_name': 'Albuquerque', 'latitude': 35.03796, - 'longitude': -106.62211, 'altitude': 1617, 'TZ': -7} -meta_mad = {'station_name': 'Madison', 'latitude': 43.07250, - 'longitude': -89.41133, 'altitude': 271, 'TZ': -6} - - -@pytest.mark.parametrize('testfile,index,columns,values,dtypes,meta', [ - (testfile, index, columns, values, dtypes, meta), - (testfile_mad, index, columns_mad, values_mad, dtypes_mad, meta_mad) -]) + "int64", + "int64", + "int64", + "int64", + "int64", + "int64", + "float64", + "float64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "int64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", + "float64", +] +meta = { + "station_name": "Albuquerque", + "latitude": 35.03796, + "longitude": -106.62211, + "altitude": 1617, + "TZ": -7, +} +meta_mad = { + "station_name": "Madison", + "latitude": 43.07250, + "longitude": -89.41133, + "altitude": 271, + "TZ": -6, +} + + +@pytest.mark.parametrize( + "testfile,index,columns,values,dtypes,meta", + [ + (testfile, index, columns, values, dtypes, meta), + (testfile_mad, index, columns_mad, values_mad, dtypes_mad, meta_mad), + ], +) def test_read_solrad(testfile, index, columns, values, dtypes, meta): expected = pd.DataFrame(values, columns=columns, index=index) - for (col, _dtype) in zip(expected.columns, dtypes): + for col, _dtype in zip(expected.columns, dtypes): expected[col] = expected[col].astype(_dtype) out, m = solrad.read_solrad(testfile) assert_frame_equal(out, expected) @@ -120,19 +419,22 @@ def test_read_solrad_https(): @pytest.mark.remote_data -@pytest.mark.parametrize('testfile, station', [ - (testfile, 'abq'), - (testfile_mad, 'msn'), -]) +@pytest.mark.parametrize( + "testfile, station", + [ + (testfile, "abq"), + (testfile_mad, "msn"), + ], +) def test_get_solrad(testfile, station): df, meta = solrad.get_solrad(station, "2019-02-25", "2019-02-25") - assert meta['station'] == station - assert isinstance(meta['filenames'], list) + assert meta["station"] == station + assert isinstance(meta["filenames"], list) assert len(df) == 1440 - assert df.index[0] == pd.to_datetime('2019-02-25 00:00+00:00') - assert df.index[-1] == pd.to_datetime('2019-02-25 23:59+00:00') + assert df.index[0] == pd.to_datetime("2019-02-25 00:00+00:00") + assert df.index[-1] == pd.to_datetime("2019-02-25 23:59+00:00") expected, _ = solrad.read_solrad(testfile) actual = df.reindex(expected.index) @@ -144,9 +446,9 @@ def test_get_solrad(testfile, station): def test_get_solrad_missing_day(): # data availability begins for ABQ on 2002-02-01 (DOY 32), so requesting # data before that will raise a warning - message = 'The following file was not found: abq/2002/abq02031.dat' + message = "The following file was not found: abq/2002/abq02031.dat" with pytest.warns(UserWarning, match=message): - df, meta = solrad.get_solrad('abq', '2002-01-31', '2002-02-01') + df, meta = solrad.get_solrad("abq", "2002-01-31", "2002-02-01") # but the data for 2022-02-01 is still returned assert not df.empty diff --git a/pvlib/tests/iotools/test_srml.py b/pvlib/tests/iotools/test_srml.py index 80e76d4635..ef4f924d9c 100644 --- a/pvlib/tests/iotools/test_srml.py +++ b/pvlib/tests/iotools/test_srml.py @@ -3,10 +3,15 @@ import pytest from pvlib.iotools import srml -from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, assert_index_equal, - assert_frame_equal) +from ..conftest import ( + DATA_DIR, + RERUNS, + RERUNS_DELAY, + assert_index_equal, + assert_frame_equal, +) -srml_testfile = DATA_DIR / 'SRML-day-EUPO1801.txt' +srml_testfile = DATA_DIR / "SRML-day-EUPO1801.txt" def test_read_srml(): @@ -17,62 +22,69 @@ def test_read_srml(): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_srml_remote(): srml.read_srml( - 'http://solardata.uoregon.edu/download/Archive/EUPO1801.txt' + "http://solardata.uoregon.edu/download/Archive/EUPO1801.txt" ) def test_read_srml_columns_exist(): data = srml.read_srml(srml_testfile) - assert 'ghi_0' in data.columns - assert 'ghi_0_flag' in data.columns - assert 'dni_1' in data.columns - assert 'dni_1_flag' in data.columns - assert '7008' in data.columns - assert '7008_flag' in data.columns + assert "ghi_0" in data.columns + assert "ghi_0_flag" in data.columns + assert "dni_1" in data.columns + assert "dni_1_flag" in data.columns + assert "7008" in data.columns + assert "7008_flag" in data.columns def test_read_srml_map_variables_false(): data = srml.read_srml(srml_testfile, map_variables=False) - assert '1000' in data.columns - assert '1000_flag' in data.columns - assert '2010' in data.columns - assert '2010_flag' in data.columns - assert '7008' in data.columns - assert '7008_flag' in data.columns + assert "1000" in data.columns + assert "1000_flag" in data.columns + assert "2010" in data.columns + assert "2010_flag" in data.columns + assert "7008" in data.columns + assert "7008_flag" in data.columns def test_read_srml_nans_exist(): data = srml.read_srml(srml_testfile) - assert isnan(data['dni_0'].iloc[1119]) - assert data['dni_0_flag'].iloc[1119] == 99 - - -@pytest.mark.parametrize('url,year,month', [ - ('http://solardata.uoregon.edu/download/Archive/EUPO1801.txt', - 2018, 1), - ('http://solardata.uoregon.edu/download/Archive/EUPO1612.txt', - 2016, 12), -]) + assert isnan(data["dni_0"].iloc[1119]) + assert data["dni_0_flag"].iloc[1119] == 99 + + +@pytest.mark.parametrize( + "url,year,month", + [ + ( + "http://solardata.uoregon.edu/download/Archive/EUPO1801.txt", + 2018, + 1, + ), + ( + "http://solardata.uoregon.edu/download/Archive/EUPO1612.txt", + 2016, + 12, + ), + ], +) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_read_srml_dt_index(url, year, month): data = srml.read_srml(url) - start = pd.Timestamp(f'{year:04d}{month:02d}01 00:00') - start = start.tz_localize('Etc/GMT+8') - end = pd.Timestamp(f'{year:04d}{month:02d}31 23:59') - end = end.tz_localize('Etc/GMT+8') + start = pd.Timestamp(f"{year:04d}{month:02d}01 00:00") + start = start.tz_localize("Etc/GMT+8") + end = pd.Timestamp(f"{year:04d}{month:02d}31 23:59") + end = end.tz_localize("Etc/GMT+8") assert data.index[0] == start assert data.index[-1] == end assert (data.index[59::60].minute == 59).all() assert str(year) not in data.columns -@pytest.mark.parametrize('column,expected', [ - ('1001', 'ghi_1'), - ('7324', '7324'), - ('2001', '2001'), - ('2017', 'dni_7') -]) +@pytest.mark.parametrize( + "column,expected", + [("1001", "ghi_1"), ("7324", "7324"), ("2001", "2001"), ("2017", "dni_7")], +) def test__map_columns(column, expected): assert srml._map_columns(column) == expected @@ -80,20 +92,23 @@ def test__map_columns(column, expected): @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_srml(): - url = 'http://solardata.uoregon.edu/download/Archive/EUPO1801.txt' + url = "http://solardata.uoregon.edu/download/Archive/EUPO1801.txt" file_data = srml.read_srml(url) - requested, _ = srml.get_srml(station='EU', start='2018-01-01', - end='2018-01-31') + requested, _ = srml.get_srml( + station="EU", start="2018-01-01", end="2018-01-31" + ) assert_frame_equal(file_data, requested) @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_srml_hourly(): - data, meta = data, meta = srml.get_srml(station='CD', start='1986-04-01', - end='1986-05-31', filetype='PH') - expected_index = pd.date_range(start='1986-04-01', end='1986-05-31 23:59', - freq='1h', tz='Etc/GMT+8') + data, meta = data, meta = srml.get_srml( + station="CD", start="1986-04-01", end="1986-05-31", filetype="PH" + ) + expected_index = pd.date_range( + start="1986-04-01", end="1986-05-31 23:59", freq="1h", tz="Etc/GMT+8" + ) assert_index_equal(data.index, expected_index) @@ -101,24 +116,27 @@ def test_get_srml_hourly(): @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_srml_minute(): data_read = srml.read_srml(srml_testfile) - data_get, meta = srml.get_srml(station='EU', start='2018-01-01', - end='2018-01-31', filetype='PO') - expected_index = pd.date_range(start='2018-01-01', end='2018-01-31 23:59', - freq='1min', tz='Etc/GMT+8') + data_get, meta = srml.get_srml( + station="EU", start="2018-01-01", end="2018-01-31", filetype="PO" + ) + expected_index = pd.date_range( + start="2018-01-01", end="2018-01-31 23:59", freq="1min", tz="Etc/GMT+8" + ) assert_index_equal(data_get.index, expected_index) assert all(c in data_get.columns for c in data_read.columns) # Check that all indices in example file are present in remote file assert data_read.index.isin(data_get.index).all() - assert meta['station'] == 'EU' - assert meta['filetype'] == 'PO' - assert meta['filenames'] == ['EUPO1801.txt'] + assert meta["station"] == "EU" + assert meta["filetype"] == "PO" + assert meta["filenames"] == ["EUPO1801.txt"] @pytest.mark.remote_data @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY) def test_get_srml_nonexisting_month_warning(): - with pytest.warns(UserWarning, match='file was not found: EUPO0912.txt'): + with pytest.warns(UserWarning, match="file was not found: EUPO0912.txt"): # Request data for a period where not all files exist # Eugene (EU) station started reporting 1-minute data in January 2010 data, meta = data, meta = srml.get_srml( - station='EU', start='2009-12-01', end='2010-01-31', filetype='PO') + station="EU", start="2009-12-01", end="2010-01-31", filetype="PO" + ) diff --git a/pvlib/tests/iotools/test_surfrad.py b/pvlib/tests/iotools/test_surfrad.py index 83f7ec7645..ca7b3b0445 100644 --- a/pvlib/tests/iotools/test_surfrad.py +++ b/pvlib/tests/iotools/test_surfrad.py @@ -4,11 +4,15 @@ from pvlib.iotools import surfrad from ..conftest import DATA_DIR, RERUNS, RERUNS_DELAY -testfile = DATA_DIR / 'surfrad-slv16001.dat' -network_testfile = ('ftp://aftp.cmdl.noaa.gov/data/radiation/surfrad/' - 'Alamosa_CO/2016/slv16001.dat') -https_testfile = ('https://gml.noaa.gov/aftp/data/radiation/surfrad/' - 'Alamosa_CO/2016/slv16001.dat') +testfile = DATA_DIR / "surfrad-slv16001.dat" +network_testfile = ( + "ftp://aftp.cmdl.noaa.gov/data/radiation/surfrad/" + "Alamosa_CO/2016/slv16001.dat" +) +https_testfile = ( + "https://gml.noaa.gov/aftp/data/radiation/surfrad/" + "Alamosa_CO/2016/slv16001.dat" +) @pytest.mark.remote_data @@ -34,42 +38,44 @@ def test_read_surfrad_https(): def test_read_surfrad_columns_no_map(): data, _ = surfrad.read_surfrad(testfile, map_variables=False) - assert 'zen' in data.columns - assert 'temp' in data.columns - assert 'par' in data.columns - assert 'pressure' in data.columns + assert "zen" in data.columns + assert "temp" in data.columns + assert "par" in data.columns + assert "pressure" in data.columns def test_read_surfrad_columns_map(): data, _ = surfrad.read_surfrad(testfile) - assert 'solar_zenith' in data.columns - assert 'ghi' in data.columns - assert 'ghi_flag' in data.columns - assert 'dni' in data.columns - assert 'dni_flag' in data.columns - assert 'dhi' in data.columns - assert 'dhi_flag' in data.columns - assert 'wind_direction' in data.columns - assert 'wind_direction_flag' in data.columns - assert 'wind_speed' in data.columns - assert 'wind_speed_flag' in data.columns - assert 'temp_air' in data.columns - assert 'temp_air_flag' in data.columns + assert "solar_zenith" in data.columns + assert "ghi" in data.columns + assert "ghi_flag" in data.columns + assert "dni" in data.columns + assert "dni_flag" in data.columns + assert "dhi" in data.columns + assert "dhi_flag" in data.columns + assert "wind_direction" in data.columns + assert "wind_direction_flag" in data.columns + assert "wind_speed" in data.columns + assert "wind_speed_flag" in data.columns + assert "temp_air" in data.columns + assert "temp_air_flag" in data.columns def test_format_index(): - start = pd.Timestamp('20160101 00:00') - expected = pd.date_range(start=start, periods=1440, freq='1min', tz='UTC') + start = pd.Timestamp("20160101 00:00") + expected = pd.date_range(start=start, periods=1440, freq="1min", tz="UTC") actual, _ = surfrad.read_surfrad(testfile) assert actual.index.equals(expected) def test_read_surfrad_metadata(): - expected = {'name': 'Alamosa', - 'latitude': 37.70, - 'longitude': 105.92, - 'elevation': 2317, - 'surfrad_version': 1, - 'tz': 'UTC'} + expected = { + "name": "Alamosa", + "latitude": 37.70, + "longitude": 105.92, + "elevation": 2317, + "surfrad_version": 1, + "tz": "UTC", + } _, metadata = surfrad.read_surfrad(testfile) assert metadata == expected diff --git a/pvlib/tests/iotools/test_tmy.py b/pvlib/tests/iotools/test_tmy.py index 9107f78b33..49071b9aac 100644 --- a/pvlib/tests/iotools/test_tmy.py +++ b/pvlib/tests/iotools/test_tmy.py @@ -9,12 +9,15 @@ # test the API works from pvlib.iotools import read_tmy3 -TMY2_TESTFILE = DATA_DIR / '12839.tm2' +TMY2_TESTFILE = DATA_DIR / "12839.tm2" # TMY3 format (two files below) represents midnight as 24:00 -TMY3_TESTFILE = DATA_DIR / '703165TY.csv' -TMY3_FEB_LEAPYEAR = DATA_DIR / '723170TYA.CSV' +TMY3_TESTFILE = DATA_DIR / "703165TY.csv" +TMY3_FEB_LEAPYEAR = DATA_DIR / "723170TYA.CSV" # The SolarAnywhere TMY3 format (file below) represents midnight as 00:00 -TMY3_SOLARANYWHERE = DATA_DIR / 'Burlington, United States SolarAnywhere Time Series 2021 Lat_44_465 Lon_-73_205 TMY3 format.csv' # noqa: E501 +TMY3_SOLARANYWHERE = ( + DATA_DIR + / "Burlington, United States SolarAnywhere Time Series 2021 Lat_44_465 Lon_-73_205 TMY3 format.csv" +) # noqa: E501 def test_read_tmy3(): @@ -25,56 +28,58 @@ def test_read_tmy3_recolumn(): with warnings.catch_warnings(): warnings.simplefilter("ignore") data, meta = tmy.read_tmy3(TMY3_TESTFILE, recolumn=True) - assert 'GHISource' in data.columns + assert "GHISource" in data.columns def test_read_tmy3_norecolumn(): data, _ = tmy.read_tmy3(TMY3_TESTFILE, map_variables=False) - assert 'GHI source' in data.columns + assert "GHI source" in data.columns def test_read_tmy3_raise_valueerror(): - with pytest.raises(ValueError, match='`map_variables` and `recolumn`'): + with pytest.raises(ValueError, match="`map_variables` and `recolumn`"): _ = tmy.read_tmy3(TMY3_TESTFILE, recolumn=True, map_variables=True) def test_read_tmy3_map_variables(): data, meta = tmy.read_tmy3(TMY3_TESTFILE, map_variables=True) - assert 'ghi' in data.columns - assert 'dni' in data.columns - assert 'dhi' in data.columns - assert 'pressure' in data.columns - assert 'wind_direction' in data.columns - assert 'wind_speed' in data.columns - assert 'temp_air' in data.columns - assert 'temp_dew' in data.columns - assert 'relative_humidity' in data.columns - assert 'albedo' in data.columns - assert 'ghi_extra' in data.columns - assert 'dni_extra' in data.columns - assert 'precipitable_water' in data.columns + assert "ghi" in data.columns + assert "dni" in data.columns + assert "dhi" in data.columns + assert "pressure" in data.columns + assert "wind_direction" in data.columns + assert "wind_speed" in data.columns + assert "temp_air" in data.columns + assert "temp_dew" in data.columns + assert "relative_humidity" in data.columns + assert "albedo" in data.columns + assert "ghi_extra" in data.columns + assert "dni_extra" in data.columns + assert "precipitable_water" in data.columns def test_read_tmy3_map_variables_deprecating_warning(): - with pytest.warns(pvlibDeprecationWarning, match='names will be renamed'): + with pytest.warns(pvlibDeprecationWarning, match="names will be renamed"): data, meta = tmy.read_tmy3(TMY3_TESTFILE) def test_read_tmy3_coerce_year(): coerce_year = 1987 - data, _ = tmy.read_tmy3(TMY3_TESTFILE, coerce_year=coerce_year, - map_variables=False) + data, _ = tmy.read_tmy3( + TMY3_TESTFILE, coerce_year=coerce_year, map_variables=False + ) assert (data.index[:-1].year == 1987).all() assert data.index[-1].year == 1988 def test_read_tmy3_no_coerce_year(): coerce_year = None - data, _ = tmy.read_tmy3(TMY3_TESTFILE, coerce_year=coerce_year, - map_variables=False) + data, _ = tmy.read_tmy3( + TMY3_TESTFILE, coerce_year=coerce_year, map_variables=False + ) assert 1997 and 1999 in data.index.year - assert data.index[-2] == pd.Timestamp('1998-12-31 23:00:00-09:00') - assert data.index[-1] == pd.Timestamp('1999-01-01 00:00:00-09:00') + assert data.index[-2] == pd.Timestamp("1998-12-31 23:00:00-09:00") + assert data.index[-1] == pd.Timestamp("1999-01-01 00:00:00-09:00") def test_read_tmy2(): @@ -86,21 +91,23 @@ def test_gh865_read_tmy3_feb_leapyear_hr24(): data, meta = read_tmy3(TMY3_FEB_LEAPYEAR, map_variables=False) # just to be safe, make sure this _IS_ the Greensboro file greensboro = { - 'USAF': 723170, - 'Name': '"GREENSBORO PIEDMONT TRIAD INT"', - 'State': 'NC', - 'TZ': -5.0, - 'latitude': 36.1, - 'longitude': -79.95, - 'altitude': 273.0} + "USAF": 723170, + "Name": '"GREENSBORO PIEDMONT TRIAD INT"', + "State": "NC", + "TZ": -5.0, + "latitude": 36.1, + "longitude": -79.95, + "altitude": 273.0, + } assert meta == greensboro # February for Greensboro is 1996, a leap year, so check to make sure there # aren't any rows in the output that contain Feb 29th - assert data.index[1414] == pd.Timestamp('1996-02-28 23:00:00-0500') - assert data.index[1415] == pd.Timestamp('1996-03-01 00:00:00-0500') + assert data.index[1414] == pd.Timestamp("1996-02-28 23:00:00-0500") + assert data.index[1415] == pd.Timestamp("1996-03-01 00:00:00-0500") # now check if it parses correctly when we try to coerce the year - data, _ = read_tmy3(TMY3_FEB_LEAPYEAR, coerce_year=1990, - map_variables=False) + data, _ = read_tmy3( + TMY3_FEB_LEAPYEAR, coerce_year=1990, map_variables=False + ) # if get's here w/o an error, then gh865 is fixed, but let's check anyway assert all(data.index[:-1].year == 1990) assert data.index[-1].year == 1991 @@ -114,20 +121,21 @@ def test_gh865_read_tmy3_feb_leapyear_hr24(): @pytest.fixture def solaranywhere_index(): - return pd.date_range('2021-01-01 01:00:00-05:00', periods=8760, freq='1h') + return pd.date_range("2021-01-01 01:00:00-05:00", periods=8760, freq="1h") def test_solaranywhere_tmy3(solaranywhere_index): # The SolarAnywhere TMY3 format specifies midnight as 00:00 whereas the # NREL TMY3 format utilizes 24:00. The SolarAnywhere file is therefore # included to test files with 00:00 timestamps are parsed correctly - data, meta = tmy.read_tmy3(TMY3_SOLARANYWHERE, encoding='iso-8859-1', - map_variables=False) + data, meta = tmy.read_tmy3( + TMY3_SOLARANYWHERE, encoding="iso-8859-1", map_variables=False + ) pd.testing.assert_index_equal(data.index, solaranywhere_index) - assert meta['USAF'] == 0 - assert meta['Name'] == 'Burlington United States' - assert meta['State'] == 'NA' - assert meta['TZ'] == -5.0 - assert meta['latitude'] == 44.465 - assert meta['longitude'] == -73.205 - assert meta['altitude'] == 41.0 + assert meta["USAF"] == 0 + assert meta["Name"] == "Burlington United States" + assert meta["State"] == "NA" + assert meta["TZ"] == -5.0 + assert meta["latitude"] == 44.465 + assert meta["longitude"] == -73.205 + assert meta["altitude"] == 41.0 diff --git a/pvlib/tests/ivtools/test_sde.py b/pvlib/tests/ivtools/test_sde.py index 66d2e1e54d..6a8c9ac02d 100644 --- a/pvlib/tests/ivtools/test_sde.py +++ b/pvlib/tests/ivtools/test_sde.py @@ -2,39 +2,42 @@ import pytest from pvlib import pvsystem from pvlib.ivtools import sde -from pvlib._deprecation import pvlibDeprecationWarning @pytest.fixture def get_test_iv_params(): - return {'IL': 8.0, 'I0': 5e-10, 'Rs': 0.2, 'Rsh': 1000, 'nNsVth': 1.61864} + return {"IL": 8.0, "I0": 5e-10, "Rs": 0.2, "Rsh": 1000, "nNsVth": 1.61864} def test_fit_sandia_simple(get_test_iv_params, get_bad_iv_curves): test_params = get_test_iv_params - test_params = dict(photocurrent=test_params['IL'], - saturation_current=test_params['I0'], - resistance_series=test_params['Rs'], - resistance_shunt=test_params['Rsh'], - nNsVth=test_params['nNsVth']) + test_params = dict( + photocurrent=test_params["IL"], + saturation_current=test_params["I0"], + resistance_series=test_params["Rs"], + resistance_shunt=test_params["Rsh"], + nNsVth=test_params["nNsVth"], + ) testcurve = pvsystem.singlediode(**test_params) - v = np.linspace(0., testcurve['v_oc'], 300) + v = np.linspace(0.0, testcurve["v_oc"], 300) i = pvsystem.i_from_v(voltage=v, **test_params) expected = tuple(test_params.values()) result = sde.fit_sandia_simple(voltage=v, current=i) assert np.allclose(result, expected, rtol=5e-5) - result = sde.fit_sandia_simple(voltage=v, current=i, - v_oc=testcurve['v_oc'], - i_sc=testcurve['i_sc']) + result = sde.fit_sandia_simple( + voltage=v, current=i, v_oc=testcurve["v_oc"], i_sc=testcurve["i_sc"] + ) assert np.allclose(result, expected, rtol=5e-5) - result = sde.fit_sandia_simple(voltage=v, current=i, - v_oc=testcurve['v_oc'], - i_sc=testcurve['i_sc'], - v_mp_i_mp=(testcurve['v_mp'], - testcurve['i_mp'])) + result = sde.fit_sandia_simple( + voltage=v, + current=i, + v_oc=testcurve["v_oc"], + i_sc=testcurve["i_sc"], + v_mp_i_mp=(testcurve["v_mp"], testcurve["i_mp"]), + ) assert np.allclose(result, expected, rtol=5e-5) result = sde.fit_sandia_simple(voltage=v, current=i, vlim=0.1) @@ -45,186 +48,528 @@ def test_fit_sandia_simple_bad_iv(get_bad_iv_curves): # bad IV curves for coverage of if/then in sde._sandia_simple_params v1, i1, v2, i2 = get_bad_iv_curves result = sde.fit_sandia_simple(voltage=v1, current=i1) - assert np.allclose(result, (-2.4322856072799985, 8.826830831727355, - 111.18558915546389, -63.56227601452038, - -137.9965046659527)) + assert np.allclose( + result, + ( + -2.4322856072799985, + 8.826830831727355, + 111.18558915546389, + -63.56227601452038, + -137.9965046659527, + ), + ) result = sde.fit_sandia_simple(voltage=v2, current=i2) - assert np.allclose(result, (2.62405311949227, 5.075520636620032, - -65.652554411442, 110.35202827739991, - 174.49362093001415)) - - -@pytest.mark.parametrize('i,v,nsvth,expected', [ - (np.array([3., 2.9, 2.8, 2.7, 2.6, 2.5, 2.4, 1.7, 0.8, 0.]), - np.array([0., 0.2, 0.4, 0.6, 0.8, 1., 1.2, 1.4, 1.45, 1.5]), - 10., - (2.3392, 11.6865, -.232, -.2596, -.7119)), - (np.array( - [5., 4.9, 4.8, 4.7, 4.6, 4.5, 4.4, 4.3, 4.2, 4.1, 4., 3.8, 3.5, 1.7, - 0.]), - np.array( - [0., .1, .2, .3, .4, .5, .6, .7, .8, .9, 1., 1.1, 1.18, 1.2, 1.22]), - 15., - (-22.0795, 27.1196, -4.2076, -.0056, -.0498))]) + assert np.allclose( + result, + ( + 2.62405311949227, + 5.075520636620032, + -65.652554411442, + 110.35202827739991, + 174.49362093001415, + ), + ) + + +@pytest.mark.parametrize( + "i,v,nsvth,expected", + [ + ( + np.array([3.0, 2.9, 2.8, 2.7, 2.6, 2.5, 2.4, 1.7, 0.8, 0.0]), + np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.45, 1.5]), + 10.0, + (2.3392, 11.6865, -0.232, -0.2596, -0.7119), + ), + ( + np.array( + [ + 5.0, + 4.9, + 4.8, + 4.7, + 4.6, + 4.5, + 4.4, + 4.3, + 4.2, + 4.1, + 4.0, + 3.8, + 3.5, + 1.7, + 0.0, + ] + ), + np.array( + [ + 0.0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1.0, + 1.1, + 1.18, + 1.2, + 1.22, + ] + ), + 15.0, + (-22.0795, 27.1196, -4.2076, -0.0056, -0.0498), + ), + ], +) def test__fit_sandia_cocontent(i, v, nsvth, expected): # test confirms agreement with Matlab code. The returned parameters # are nonsense iph, io, rsh, rs, n = sde._fit_sandia_cocontent(v, i, nsvth) - np.testing.assert_allclose(iph, np.array(expected[0]), atol=.0001) - np.testing.assert_allclose(io, np.array([expected[1]]), atol=.0001) - np.testing.assert_allclose(rs, np.array([expected[2]]), atol=.0001) - np.testing.assert_allclose(rsh, np.array([expected[3]]), atol=.0001) - np.testing.assert_allclose(n, np.array([expected[4]]), atol=.0001) + np.testing.assert_allclose(iph, np.array(expected[0]), atol=0.0001) + np.testing.assert_allclose(io, np.array([expected[1]]), atol=0.0001) + np.testing.assert_allclose(rs, np.array([expected[2]]), atol=0.0001) + np.testing.assert_allclose(rsh, np.array([expected[3]]), atol=0.0001) + np.testing.assert_allclose(n, np.array([expected[4]]), atol=0.0001) def test__fit_sandia_cocontent_fail(): # tests for ValueError - exc_text = 'voltage and current should have the same length' + exc_text = "voltage and current should have the same length" with pytest.raises(ValueError, match=exc_text): - sde._fit_sandia_cocontent(np.array([0., 1., 2.]), np.array([4., 3.]), - 2.) - exc_text = 'at least 6 voltage points are required; ~50 are recommended' + sde._fit_sandia_cocontent( + np.array([0.0, 1.0, 2.0]), np.array([4.0, 3.0]), 2.0 + ) + exc_text = "at least 6 voltage points are required; ~50 are recommended" with pytest.raises(ValueError, match=exc_text): - sde._fit_sandia_cocontent(np.array([0., 1., 2., 3., 4.]), - np.array([4., 3.9, 3.4, 2., 0.]), - 2.) + sde._fit_sandia_cocontent( + np.array([0.0, 1.0, 2.0, 3.0, 4.0]), + np.array([4.0, 3.9, 3.4, 2.0, 0.0]), + 2.0, + ) @pytest.fixture def get_bad_iv_curves(): # v1, i1 produces a bad value for I0_voc - v1 = np.array([0, 0.338798867469060, 0.677597734938121, 1.01639660240718, - 1.35519546987624, 1.69399433734530, 2.03279320481436, - 2.37159207228342, 2.71039093975248, 3.04918980722154, - 3.38798867469060, 3.72678754215966, 4.06558640962873, - 4.40438527709779, 4.74318414456685, 5.08198301203591, - 5.42078187950497, 5.75958074697403, 6.09837961444309, - 6.43717848191215, 6.77597734938121, 7.11477621685027, - 7.45357508431933, 7.79237395178839, 8.13117281925745, - 8.46997168672651, 8.80877055419557, 9.14756942166463, - 9.48636828913369, 9.82516715660275, 10.1639660240718, - 10.5027648915409, 10.8415637590099, 11.1803626264790, - 11.5191614939481, 11.8579603614171, 12.1967592288862, - 12.5355580963552, 12.8743569638243, 13.2131558312934, - 13.5519546987624, 13.8907535662315, 14.2295524337005, - 14.5683513011696, 14.9071501686387, 15.2459490361077, - 15.5847479035768, 15.9235467710458, 16.2623456385149, - 16.6011445059840, 16.9399433734530, 17.2787422409221, - 17.6175411083911, 17.9563399758602, 18.2951388433293, - 18.6339377107983, 18.9727365782674, 19.3115354457364, - 19.6503343132055, 19.9891331806746, 20.3279320481436, - 20.6667309156127, 21.0055297830817, 21.3443286505508, - 21.6831275180199, 22.0219263854889, 22.3607252529580, - 22.6995241204270, 23.0383229878961, 23.3771218553652, - 23.7159207228342, 24.0547195903033, 24.3935184577724, - 24.7323173252414, 25.0711161927105, 25.4099150601795, - 25.7487139276486, 26.0875127951177, 26.4263116625867, - 26.7651105300558, 27.1039093975248, 27.4427082649939, - 27.7815071324630, 28.1203059999320, 28.4591048674011, - 28.7979037348701, 29.1367026023392, 29.4755014698083, - 29.8143003372773, 30.1530992047464, 30.4918980722154, - 30.8306969396845, 31.1694958071536, 31.5082946746226, - 31.8470935420917, 32.1858924095607, 32.5246912770298, - 32.8634901444989, 33.2022890119679, 33.5410878794370]) - i1 = np.array([3.39430882774470, 2.80864492110761, 3.28358165429196, - 3.41191190551673, 3.11975662808148, 3.35436585834612, - 3.23953272899809, 3.60307083325333, 2.80478101508277, - 2.80505102853845, 3.16918996870373, 3.21088388439857, - 3.46332865310431, 3.09224155015883, 3.17541550741062, - 3.32470179290389, 3.33224664316240, 3.07709000050741, - 2.89141245343405, 3.01365768561537, 3.23265176770231, - 3.32253647634228, 2.97900657569736, 3.31959549243966, - 3.03375461550111, 2.97579298978937, 3.25432831375159, - 2.89178382564454, 3.00341909207567, 3.72637492250097, - 3.28379856976360, 2.96516169245835, 3.25658381110230, - 3.41655911533139, 3.02718097944604, 3.11458376760376, - 3.24617304369762, 3.45935502367636, 3.21557333256913, - 3.27611176482650, 2.86954135732485, 3.32416319254657, - 3.15277467598732, 3.08272557013770, 3.15602202666259, - 3.49432799877150, 3.53863997177632, 3.10602611478455, - 3.05373911151821, 3.09876772570781, 2.97417228624287, - 2.84573593699237, 3.16288578405195, 3.06533173612783, - 3.02118336639575, 3.34374977225502, 2.97255164138821, - 3.19286135682863, 3.10999753817133, 3.26925354620079, - 3.11957809501529, 3.20155017481720, 3.31724984405837, - 3.42879043512927, 3.17933067619240, 3.47777362613969, - 3.20708912539777, 3.48205761174907, 3.16804363684327, - 3.14055472378230, 3.13445657434470, 2.91152696252998, - 3.10984113847427, 2.80443349399489, 3.23146278164875, - 2.94521083406108, 3.17388903141715, 3.05930294897030, - 3.18985234673287, 3.27946609274898, 3.33717523113602, - 2.76394303462702, 3.19375132937510, 2.82628616689450, - 2.85238527394143, 2.82975892599489, 2.79196912313914, - 2.72860792049395, 2.75585977414140, 2.44280222448805, - 2.36052347370628, 2.26785071765738, 2.10868255743462, - 2.06165739407987, 1.90047259509385, 1.39925575828709, - 1.24749015957606, 0.867823806536762, 0.432752457749993, 0]) + v1 = np.array( + [ + 0, + 0.338798867469060, + 0.677597734938121, + 1.01639660240718, + 1.35519546987624, + 1.69399433734530, + 2.03279320481436, + 2.37159207228342, + 2.71039093975248, + 3.04918980722154, + 3.38798867469060, + 3.72678754215966, + 4.06558640962873, + 4.40438527709779, + 4.74318414456685, + 5.08198301203591, + 5.42078187950497, + 5.75958074697403, + 6.09837961444309, + 6.43717848191215, + 6.77597734938121, + 7.11477621685027, + 7.45357508431933, + 7.79237395178839, + 8.13117281925745, + 8.46997168672651, + 8.80877055419557, + 9.14756942166463, + 9.48636828913369, + 9.82516715660275, + 10.1639660240718, + 10.5027648915409, + 10.8415637590099, + 11.1803626264790, + 11.5191614939481, + 11.8579603614171, + 12.1967592288862, + 12.5355580963552, + 12.8743569638243, + 13.2131558312934, + 13.5519546987624, + 13.8907535662315, + 14.2295524337005, + 14.5683513011696, + 14.9071501686387, + 15.2459490361077, + 15.5847479035768, + 15.9235467710458, + 16.2623456385149, + 16.6011445059840, + 16.9399433734530, + 17.2787422409221, + 17.6175411083911, + 17.9563399758602, + 18.2951388433293, + 18.6339377107983, + 18.9727365782674, + 19.3115354457364, + 19.6503343132055, + 19.9891331806746, + 20.3279320481436, + 20.6667309156127, + 21.0055297830817, + 21.3443286505508, + 21.6831275180199, + 22.0219263854889, + 22.3607252529580, + 22.6995241204270, + 23.0383229878961, + 23.3771218553652, + 23.7159207228342, + 24.0547195903033, + 24.3935184577724, + 24.7323173252414, + 25.0711161927105, + 25.4099150601795, + 25.7487139276486, + 26.0875127951177, + 26.4263116625867, + 26.7651105300558, + 27.1039093975248, + 27.4427082649939, + 27.7815071324630, + 28.1203059999320, + 28.4591048674011, + 28.7979037348701, + 29.1367026023392, + 29.4755014698083, + 29.8143003372773, + 30.1530992047464, + 30.4918980722154, + 30.8306969396845, + 31.1694958071536, + 31.5082946746226, + 31.8470935420917, + 32.1858924095607, + 32.5246912770298, + 32.8634901444989, + 33.2022890119679, + 33.5410878794370, + ] + ) + i1 = np.array( + [ + 3.39430882774470, + 2.80864492110761, + 3.28358165429196, + 3.41191190551673, + 3.11975662808148, + 3.35436585834612, + 3.23953272899809, + 3.60307083325333, + 2.80478101508277, + 2.80505102853845, + 3.16918996870373, + 3.21088388439857, + 3.46332865310431, + 3.09224155015883, + 3.17541550741062, + 3.32470179290389, + 3.33224664316240, + 3.07709000050741, + 2.89141245343405, + 3.01365768561537, + 3.23265176770231, + 3.32253647634228, + 2.97900657569736, + 3.31959549243966, + 3.03375461550111, + 2.97579298978937, + 3.25432831375159, + 2.89178382564454, + 3.00341909207567, + 3.72637492250097, + 3.28379856976360, + 2.96516169245835, + 3.25658381110230, + 3.41655911533139, + 3.02718097944604, + 3.11458376760376, + 3.24617304369762, + 3.45935502367636, + 3.21557333256913, + 3.27611176482650, + 2.86954135732485, + 3.32416319254657, + 3.15277467598732, + 3.08272557013770, + 3.15602202666259, + 3.49432799877150, + 3.53863997177632, + 3.10602611478455, + 3.05373911151821, + 3.09876772570781, + 2.97417228624287, + 2.84573593699237, + 3.16288578405195, + 3.06533173612783, + 3.02118336639575, + 3.34374977225502, + 2.97255164138821, + 3.19286135682863, + 3.10999753817133, + 3.26925354620079, + 3.11957809501529, + 3.20155017481720, + 3.31724984405837, + 3.42879043512927, + 3.17933067619240, + 3.47777362613969, + 3.20708912539777, + 3.48205761174907, + 3.16804363684327, + 3.14055472378230, + 3.13445657434470, + 2.91152696252998, + 3.10984113847427, + 2.80443349399489, + 3.23146278164875, + 2.94521083406108, + 3.17388903141715, + 3.05930294897030, + 3.18985234673287, + 3.27946609274898, + 3.33717523113602, + 2.76394303462702, + 3.19375132937510, + 2.82628616689450, + 2.85238527394143, + 2.82975892599489, + 2.79196912313914, + 2.72860792049395, + 2.75585977414140, + 2.44280222448805, + 2.36052347370628, + 2.26785071765738, + 2.10868255743462, + 2.06165739407987, + 1.90047259509385, + 1.39925575828709, + 1.24749015957606, + 0.867823806536762, + 0.432752457749993, + 0, + ] + ) # v2, i2 produces a bad value for I0_vmp - v2 = np.array([0, 0.365686097622586, 0.731372195245173, 1.09705829286776, - 1.46274439049035, 1.82843048811293, 2.19411658573552, - 2.55980268335810, 2.92548878098069, 3.29117487860328, - 3.65686097622586, 4.02254707384845, 4.38823317147104, - 4.75391926909362, 5.11960536671621, 5.48529146433880, - 5.85097756196138, 6.21666365958397, 6.58234975720655, - 6.94803585482914, 7.31372195245173, 7.67940805007431, - 8.04509414769690, 8.41078024531949, 8.77646634294207, - 9.14215244056466, 9.50783853818725, 9.87352463580983, - 10.2392107334324, 10.6048968310550, 10.9705829286776, - 11.3362690263002, 11.7019551239228, 12.0676412215454, - 12.4333273191679, 12.7990134167905, 13.1646995144131, - 13.5303856120357, 13.8960717096583, 14.2617578072809, - 14.6274439049035, 14.9931300025260, 15.3588161001486, - 15.7245021977712, 16.0901882953938, 16.4558743930164, - 16.8215604906390, 17.1872465882616, 17.5529326858841, - 17.9186187835067, 18.2843048811293, 18.6499909787519, - 19.0156770763745, 19.3813631739971, 19.7470492716197, - 20.1127353692422, 20.4784214668648, 20.8441075644874, - 21.2097936621100, 21.5754797597326, 21.9411658573552, - 22.3068519549778, 22.6725380526004, 23.0382241502229, - 23.4039102478455, 23.7695963454681, 24.1352824430907, - 24.5009685407133, 24.8666546383359, 25.2323407359585, - 25.5980268335810, 25.9637129312036, 26.3293990288262, - 26.6950851264488, 27.0607712240714, 27.4264573216940, - 27.7921434193166, 28.1578295169392, 28.5235156145617, - 28.8892017121843, 29.2548878098069, 29.6205739074295, - 29.9862600050521, 30.3519461026747, 30.7176322002973, - 31.0833182979198, 31.4490043955424, 31.8146904931650, - 32.1803765907876, 32.5460626884102, 32.9117487860328, - 33.2774348836554, 33.6431209812779, 34.0088070789005, - 34.3744931765231, 34.7401792741457, 35.1058653717683, - 35.4715514693909, 35.8372375670135, 36.2029236646360]) - i2 = np.array([6.49218806928330, 6.49139336899548, 6.17810697175204, - 6.75197816263663, 6.59529074137515, 6.18164578868300, - 6.38709397931910, 6.30685422248427, 6.44640615548925, - 6.88727230397772, 6.42074852785591, 6.46348580823746, - 6.38642309763941, 5.66356277572311, 6.61010381702082, - 6.33288284311125, 6.22475343933610, 6.30651399433833, - 6.44435022944051, 6.43741711131908, 6.03536180208946, - 6.23814639328170, 5.97229140403242, 6.20790000748341, - 6.22933550182341, 6.22992127804882, 6.13400871899299, - 6.83491312449950, 6.07952797245846, 6.35837746415450, - 6.41972128662324, 6.85256717258275, 6.25807797296759, - 6.25124948151766, 6.22229212812413, 6.72249444167406, - 6.41085549981649, 6.75792874870056, 6.22096181559171, - 6.47839564388996, 6.56010208597432, 6.63300966556949, - 6.34617546039339, 6.79812221146153, 6.14486056194136, - 6.14979256889311, 6.16883037644880, 6.57309183229605, - 6.40064681038509, 6.18861448239873, 6.91340138179698, - 5.94164388433788, 6.23638991745862, 6.31898940411710, - 6.45247884556830, 6.58081455524297, 6.64915284801713, - 6.07122119270245, 6.41398258148256, 6.62144271089614, - 6.36377197712687, 6.51487678829345, 6.53418950147730, - 6.18886469125371, 6.26341063475750, 6.83488211680259, - 6.62699397226695, 6.41286837534735, 6.44060085001851, - 6.48114130629288, 6.18607038456406, 6.16923370572396, - 6.64223126283631, 6.07231852289266, 5.79043710204375, - 6.48463886529882, 6.36263392044401, 6.11212476454494, - 6.14573900812925, 6.12568047243240, 6.43836230231577, - 6.02505694060219, 6.13819468942244, 6.22100593815064, - 6.02394682666345, 5.89016573063789, 5.74448527739202, - 5.50415294280017, 5.31883018164157, 4.87476769510305, - 4.74386713755523, 4.60638346931628, 4.06177345572680, - 3.73334482123538, 3.13848311672243, 2.71638862600768, - 2.02963773590165, 1.49291145092070, 0.818343889647352, 0]) + v2 = np.array( + [ + 0, + 0.365686097622586, + 0.731372195245173, + 1.09705829286776, + 1.46274439049035, + 1.82843048811293, + 2.19411658573552, + 2.55980268335810, + 2.92548878098069, + 3.29117487860328, + 3.65686097622586, + 4.02254707384845, + 4.38823317147104, + 4.75391926909362, + 5.11960536671621, + 5.48529146433880, + 5.85097756196138, + 6.21666365958397, + 6.58234975720655, + 6.94803585482914, + 7.31372195245173, + 7.67940805007431, + 8.04509414769690, + 8.41078024531949, + 8.77646634294207, + 9.14215244056466, + 9.50783853818725, + 9.87352463580983, + 10.2392107334324, + 10.6048968310550, + 10.9705829286776, + 11.3362690263002, + 11.7019551239228, + 12.0676412215454, + 12.4333273191679, + 12.7990134167905, + 13.1646995144131, + 13.5303856120357, + 13.8960717096583, + 14.2617578072809, + 14.6274439049035, + 14.9931300025260, + 15.3588161001486, + 15.7245021977712, + 16.0901882953938, + 16.4558743930164, + 16.8215604906390, + 17.1872465882616, + 17.5529326858841, + 17.9186187835067, + 18.2843048811293, + 18.6499909787519, + 19.0156770763745, + 19.3813631739971, + 19.7470492716197, + 20.1127353692422, + 20.4784214668648, + 20.8441075644874, + 21.2097936621100, + 21.5754797597326, + 21.9411658573552, + 22.3068519549778, + 22.6725380526004, + 23.0382241502229, + 23.4039102478455, + 23.7695963454681, + 24.1352824430907, + 24.5009685407133, + 24.8666546383359, + 25.2323407359585, + 25.5980268335810, + 25.9637129312036, + 26.3293990288262, + 26.6950851264488, + 27.0607712240714, + 27.4264573216940, + 27.7921434193166, + 28.1578295169392, + 28.5235156145617, + 28.8892017121843, + 29.2548878098069, + 29.6205739074295, + 29.9862600050521, + 30.3519461026747, + 30.7176322002973, + 31.0833182979198, + 31.4490043955424, + 31.8146904931650, + 32.1803765907876, + 32.5460626884102, + 32.9117487860328, + 33.2774348836554, + 33.6431209812779, + 34.0088070789005, + 34.3744931765231, + 34.7401792741457, + 35.1058653717683, + 35.4715514693909, + 35.8372375670135, + 36.2029236646360, + ] + ) + i2 = np.array( + [ + 6.49218806928330, + 6.49139336899548, + 6.17810697175204, + 6.75197816263663, + 6.59529074137515, + 6.18164578868300, + 6.38709397931910, + 6.30685422248427, + 6.44640615548925, + 6.88727230397772, + 6.42074852785591, + 6.46348580823746, + 6.38642309763941, + 5.66356277572311, + 6.61010381702082, + 6.33288284311125, + 6.22475343933610, + 6.30651399433833, + 6.44435022944051, + 6.43741711131908, + 6.03536180208946, + 6.23814639328170, + 5.97229140403242, + 6.20790000748341, + 6.22933550182341, + 6.22992127804882, + 6.13400871899299, + 6.83491312449950, + 6.07952797245846, + 6.35837746415450, + 6.41972128662324, + 6.85256717258275, + 6.25807797296759, + 6.25124948151766, + 6.22229212812413, + 6.72249444167406, + 6.41085549981649, + 6.75792874870056, + 6.22096181559171, + 6.47839564388996, + 6.56010208597432, + 6.63300966556949, + 6.34617546039339, + 6.79812221146153, + 6.14486056194136, + 6.14979256889311, + 6.16883037644880, + 6.57309183229605, + 6.40064681038509, + 6.18861448239873, + 6.91340138179698, + 5.94164388433788, + 6.23638991745862, + 6.31898940411710, + 6.45247884556830, + 6.58081455524297, + 6.64915284801713, + 6.07122119270245, + 6.41398258148256, + 6.62144271089614, + 6.36377197712687, + 6.51487678829345, + 6.53418950147730, + 6.18886469125371, + 6.26341063475750, + 6.83488211680259, + 6.62699397226695, + 6.41286837534735, + 6.44060085001851, + 6.48114130629288, + 6.18607038456406, + 6.16923370572396, + 6.64223126283631, + 6.07231852289266, + 5.79043710204375, + 6.48463886529882, + 6.36263392044401, + 6.11212476454494, + 6.14573900812925, + 6.12568047243240, + 6.43836230231577, + 6.02505694060219, + 6.13819468942244, + 6.22100593815064, + 6.02394682666345, + 5.89016573063789, + 5.74448527739202, + 5.50415294280017, + 5.31883018164157, + 4.87476769510305, + 4.74386713755523, + 4.60638346931628, + 4.06177345572680, + 3.73334482123538, + 3.13848311672243, + 2.71638862600768, + 2.02963773590165, + 1.49291145092070, + 0.818343889647352, + 0, + ] + ) return v1, i1, v2, i2 diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index e0def04621..7d734b8f68 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -15,40 +15,58 @@ @pytest.fixture def get_test_iv_params(): - return {'IL': 8.0, 'I0': 5e-10, 'Rsh': 1000, 'Rs': 0.2, 'nNsVth': 1.61864} + return {"IL": 8.0, "I0": 5e-10, "Rsh": 1000, "Rs": 0.2, "nNsVth": 1.61864} @pytest.fixture def cec_params_cansol_cs5p_220p(): - return {'ivcurve': {'V_mp_ref': 46.6, 'I_mp_ref': 4.73, 'V_oc_ref': 58.3, - 'I_sc_ref': 5.05}, - 'specs': {'alpha_sc': 0.0025, 'beta_voc': -0.19659, - 'gamma_pmp': -0.43, 'cells_in_series': 96}, - 'params': {'I_L_ref': 5.056, 'I_o_ref': 1.01e-10, - 'R_sh_ref': 837.51, 'R_s': 1.004, 'a_ref': 2.3674, - 'Adjust': 2.3}} + return { + "ivcurve": { + "V_mp_ref": 46.6, + "I_mp_ref": 4.73, + "V_oc_ref": 58.3, + "I_sc_ref": 5.05, + }, + "specs": { + "alpha_sc": 0.0025, + "beta_voc": -0.19659, + "gamma_pmp": -0.43, + "cells_in_series": 96, + }, + "params": { + "I_L_ref": 5.056, + "I_o_ref": 1.01e-10, + "R_sh_ref": 837.51, + "R_s": 1.004, + "a_ref": 2.3674, + "Adjust": 2.3, + }, + } @requires_pysam def test_fit_cec_sam(cec_params_cansol_cs5p_220p): - input_data = cec_params_cansol_cs5p_220p['ivcurve'] - specs = cec_params_cansol_cs5p_220p['specs'] - I_L_ref, I_o_ref, R_s, R_sh_ref, a_ref, Adjust = \ - sdm.fit_cec_sam( - celltype='polySi', v_mp=input_data['V_mp_ref'], - i_mp=input_data['I_mp_ref'], v_oc=input_data['V_oc_ref'], - i_sc=input_data['I_sc_ref'], alpha_sc=specs['alpha_sc'], - beta_voc=specs['beta_voc'], - gamma_pmp=specs['gamma_pmp'], - cells_in_series=specs['cells_in_series']) - expected = pd.Series(cec_params_cansol_cs5p_220p['params']) + input_data = cec_params_cansol_cs5p_220p["ivcurve"] + specs = cec_params_cansol_cs5p_220p["specs"] + I_L_ref, I_o_ref, R_s, R_sh_ref, a_ref, Adjust = sdm.fit_cec_sam( + celltype="polySi", + v_mp=input_data["V_mp_ref"], + i_mp=input_data["I_mp_ref"], + v_oc=input_data["V_oc_ref"], + i_sc=input_data["I_sc_ref"], + alpha_sc=specs["alpha_sc"], + beta_voc=specs["beta_voc"], + gamma_pmp=specs["gamma_pmp"], + cells_in_series=specs["cells_in_series"], + ) + expected = pd.Series(cec_params_cansol_cs5p_220p["params"]) modeled = pd.Series(index=expected.index, data=np.nan) - modeled['a_ref'] = a_ref - modeled['I_L_ref'] = I_L_ref - modeled['I_o_ref'] = I_o_ref - modeled['R_s'] = R_s - modeled['R_sh_ref'] = R_sh_ref - modeled['Adjust'] = Adjust + modeled["a_ref"] = a_ref + modeled["I_L_ref"] = I_L_ref + modeled["I_o_ref"] = I_o_ref + modeled["R_s"] = R_s + modeled["R_sh_ref"] = R_sh_ref + modeled["Adjust"] = Adjust assert np.allclose(modeled.values, expected.values, rtol=5e-2) @@ -57,54 +75,96 @@ def test_fit_cec_sam_estimation_failure(cec_params_cansol_cs5p_220p): # Failing to estimate the parameters for the CEC SDM model should raise an # exception. with pytest.raises(RuntimeError): - sdm.fit_cec_sam(celltype='polySi', v_mp=0.45, i_mp=5.25, v_oc=0.55, - i_sc=5.5, alpha_sc=0.00275, beta_voc=0.00275, - gamma_pmp=0.0055, cells_in_series=1, temp_ref=25) + sdm.fit_cec_sam( + celltype="polySi", + v_mp=0.45, + i_mp=5.25, + v_oc=0.55, + i_sc=5.5, + alpha_sc=0.00275, + beta_voc=0.00275, + gamma_pmp=0.0055, + cells_in_series=1, + temp_ref=25, + ) def test_fit_desoto(): - result, _ = sdm.fit_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43, - alpha_sc=0.005658, beta_voc=-0.13788, - cells_in_series=60) - result_expected = {'I_L_ref': 9.45232, - 'I_o_ref': 3.22460e-10, - 'R_s': 0.297814, - 'R_sh_ref': 125.798, - 'a_ref': 1.59128, - 'alpha_sc': 0.005658, - 'EgRef': 1.121, - 'dEgdT': -0.0002677, - 'irrad_ref': 1000, - 'temp_ref': 25} - assert np.allclose(pd.Series(result), pd.Series(result_expected), - rtol=1e-4) + result, _ = sdm.fit_desoto( + v_mp=31.0, + i_mp=8.71, + v_oc=38.3, + i_sc=9.43, + alpha_sc=0.005658, + beta_voc=-0.13788, + cells_in_series=60, + ) + result_expected = { + "I_L_ref": 9.45232, + "I_o_ref": 3.22460e-10, + "R_s": 0.297814, + "R_sh_ref": 125.798, + "a_ref": 1.59128, + "alpha_sc": 0.005658, + "EgRef": 1.121, + "dEgdT": -0.0002677, + "irrad_ref": 1000, + "temp_ref": 25, + } + assert np.allclose( + pd.Series(result), pd.Series(result_expected), rtol=1e-4 + ) def test_fit_desoto_init_guess(mocker): - init_guess_array = np.array([9.4, 3.0e-10, 0.3, 125., 1.6]) - init_guess = {k: v for k, v in zip( - ['IL_0', 'Io_0', 'Rs_0', 'Rsh_0', 'a_0'], init_guess_array)} - spy = mocker.spy(optimize, 'root') - result, _ = sdm.fit_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43, - alpha_sc=0.005658, beta_voc=-0.13788, - cells_in_series=60, init_guess=init_guess) - np.testing.assert_array_equal(init_guess_array, spy.call_args[1]['x0']) + init_guess_array = np.array([9.4, 3.0e-10, 0.3, 125.0, 1.6]) + init_guess = { + k: v + for k, v in zip( + ["IL_0", "Io_0", "Rs_0", "Rsh_0", "a_0"], init_guess_array + ) + } + spy = mocker.spy(optimize, "root") + result, _ = sdm.fit_desoto( + v_mp=31.0, + i_mp=8.71, + v_oc=38.3, + i_sc=9.43, + alpha_sc=0.005658, + beta_voc=-0.13788, + cells_in_series=60, + init_guess=init_guess, + ) + np.testing.assert_array_equal(init_guess_array, spy.call_args[1]["x0"]) def test_fit_desoto_init_bad_key(): - init_guess = {'IL_0': 6., 'bad_key': 0} - with pytest.raises(ValueError, match='is not a valid name;'): - result, _ = sdm.fit_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43, - alpha_sc=0.005658, beta_voc=-0.13788, - cells_in_series=60, init_guess=init_guess) + init_guess = {"IL_0": 6.0, "bad_key": 0} + with pytest.raises(ValueError, match="is not a valid name;"): + result, _ = sdm.fit_desoto( + v_mp=31.0, + i_mp=8.71, + v_oc=38.3, + i_sc=9.43, + alpha_sc=0.005658, + beta_voc=-0.13788, + cells_in_series=60, + init_guess=init_guess, + ) def test_fit_desoto_failure(): with pytest.raises(RuntimeError) as exc: - sdm.fit_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43, - alpha_sc=0.005658, beta_voc=-0.13788, - cells_in_series=10) - assert ('Parameter estimation failed') in str(exc.value) + sdm.fit_desoto( + v_mp=31.0, + i_mp=8.71, + v_oc=38.3, + i_sc=9.43, + alpha_sc=0.005658, + beta_voc=-0.13788, + cells_in_series=10, + ) + assert ("Parameter estimation failed") in str(exc.value) @requires_statsmodels @@ -112,56 +172,69 @@ def test_fit_desoto_sandia(cec_params_cansol_cs5p_220p): # this test computes a set of IV curves for the input fixture, fits # the De Soto model to the calculated IV curves, and compares the fitted # parameters to the starting values - params = cec_params_cansol_cs5p_220p['params'] - params.pop('Adjust') - specs = cec_params_cansol_cs5p_220p['specs'] - effective_irradiance = np.array([400., 500., 600., 700., 800., 900., - 1000.]) - temp_cell = np.array([15., 25., 35., 45.]) + params = cec_params_cansol_cs5p_220p["params"] + params.pop("Adjust") + specs = cec_params_cansol_cs5p_220p["specs"] + effective_irradiance = np.array( + [400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0] + ) + temp_cell = np.array([15.0, 25.0, 35.0, 45.0]) ee = np.tile(effective_irradiance, len(temp_cell)) tc = np.repeat(temp_cell, len(effective_irradiance)) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - ee, tc, alpha_sc=specs['alpha_sc'], **params) - ivcurve_params = dict(photocurrent=IL, saturation_current=I0, - resistance_series=Rs, resistance_shunt=Rsh, - nNsVth=nNsVth) - sim_ivcurves = pvsystem.singlediode(**ivcurve_params).to_dict('series') - v = np.linspace(0., sim_ivcurves['v_oc'], 300) + ee, tc, alpha_sc=specs["alpha_sc"], **params + ) + ivcurve_params = dict( + photocurrent=IL, + saturation_current=I0, + resistance_series=Rs, + resistance_shunt=Rsh, + nNsVth=nNsVth, + ) + sim_ivcurves = pvsystem.singlediode(**ivcurve_params).to_dict("series") + v = np.linspace(0.0, sim_ivcurves["v_oc"], 300) i = pvsystem.i_from_v(voltage=v, **ivcurve_params) sim_ivcurves.update(v=v.T, i=i.T, ee=ee, tc=tc) result = sdm.fit_desoto_sandia(sim_ivcurves, specs) modeled = pd.Series(index=params.keys(), data=np.nan) - modeled['a_ref'] = result['a_ref'] - modeled['I_L_ref'] = result['I_L_ref'] - modeled['I_o_ref'] = result['I_o_ref'] - modeled['R_s'] = result['R_s'] - modeled['R_sh_ref'] = result['R_sh_ref'] + modeled["a_ref"] = result["a_ref"] + modeled["I_L_ref"] = result["I_L_ref"] + modeled["I_o_ref"] = result["I_o_ref"] + modeled["R_s"] = result["R_s"] + modeled["R_sh_ref"] = result["R_sh_ref"] expected = pd.Series(params) - assert np.allclose(modeled[params.keys()].values, - expected[params.keys()].values, rtol=5e-2) - assert_allclose(result['dEgdT'], -0.0002677) - assert_allclose(result['EgRef'], 1.3112547292120638) - assert_allclose(result['cells_in_series'], specs['cells_in_series']) + assert np.allclose( + modeled[params.keys()].values, + expected[params.keys()].values, + rtol=5e-2, + ) + assert_allclose(result["dEgdT"], -0.0002677) + assert_allclose(result["EgRef"], 1.3112547292120638) + assert_allclose(result["cells_in_series"], specs["cells_in_series"]) def _read_iv_curves_for_test(datafile, npts): - """ read constants and npts IV curves from datafile """ - iv_specs = dict.fromkeys(['cells_in_series', 'alpha_sc', 'beta_voc', - 'descr']) - ivcurves = dict.fromkeys(['i_sc', 'i_mp', 'v_mp', 'v_oc', 'poa', 'tc', - 'ee']) + """read constants and npts IV curves from datafile""" + iv_specs = dict.fromkeys( + ["cells_in_series", "alpha_sc", "beta_voc", "descr"] + ) + ivcurves = dict.fromkeys( + ["i_sc", "i_mp", "v_mp", "v_oc", "poa", "tc", "ee"] + ) infilen = DATA_DIR / datafile - with infilen.open(mode='r') as f: - - Ns, aIsc, bVoc, descr = f.readline().split(',') + with infilen.open(mode="r") as f: + Ns, aIsc, bVoc, descr = f.readline().split(",") iv_specs.update( - cells_in_series=int(Ns), alpha_sc=float(aIsc), - beta_voc=float(bVoc), descr=descr) + cells_in_series=int(Ns), + alpha_sc=float(aIsc), + beta_voc=float(bVoc), + descr=descr, + ) - strN, strM = f.readline().split(',') + strN, strM = f.readline().split(",") N = int(strN) M = int(strM) @@ -178,54 +251,65 @@ def _read_iv_curves_for_test(datafile, npts): i[:] = np.nan for k in range(N): - tmp = (float(x) for x in f.readline().split(',')) + tmp = (float(x) for x in f.readline().split(",")) isc[k], imp[k], vmp[k], voc[k], poa[k], tc[k], ee[k] = tmp # read voltage and current - tmp = [float(x) for x in f.readline().split(',')] + tmp = [float(x) for x in f.readline().split(",")] while len(tmp) < M: tmp.append(np.nan) v[k, :] = tmp - tmp = [float(x) for x in f.readline().split(',')] + tmp = [float(x) for x in f.readline().split(",")] while len(tmp) < M: tmp.append(np.nan) i[k, :] = tmp - ivcurves['i_sc'] = isc[:npts] - ivcurves['i_mp'] = imp[:npts] - ivcurves['v_oc'] = voc[:npts] - ivcurves['v_mp'] = vmp[:npts] - ivcurves['ee'] = ee[:npts] - ivcurves['tc'] = tc[:npts] - ivcurves['v'] = v[:npts] - ivcurves['i'] = i[:npts] - ivcurves['p_mp'] = ivcurves['v_mp'] * ivcurves['i_mp'] # power + ivcurves["i_sc"] = isc[:npts] + ivcurves["i_mp"] = imp[:npts] + ivcurves["v_oc"] = voc[:npts] + ivcurves["v_mp"] = vmp[:npts] + ivcurves["ee"] = ee[:npts] + ivcurves["tc"] = tc[:npts] + ivcurves["v"] = v[:npts] + ivcurves["i"] = i[:npts] + ivcurves["p_mp"] = ivcurves["v_mp"] * ivcurves["i_mp"] # power return iv_specs, ivcurves def _read_pvsyst_expected(datafile): - """ Read Pvsyst model parameters and diode equation values for each + """Read Pvsyst model parameters and diode equation values for each IV curve """ - pvsyst_specs = dict.fromkeys(['cells_in_series', 'alpha_sc', 'beta_voc', - 'descr']) + pvsyst_specs = dict.fromkeys( + ["cells_in_series", "alpha_sc", "beta_voc", "descr"] + ) # order required to match file being read paramlist = [ - 'I_L_ref', 'I_o_ref', 'EgRef', 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'R_s', - 'gamma_ref', 'mu_gamma'] - varlist = ['iph', 'io', 'rs', 'rsh', 'u'] + "I_L_ref", + "I_o_ref", + "EgRef", + "R_sh_ref", + "R_sh_0", + "R_sh_exp", + "R_s", + "gamma_ref", + "mu_gamma", + ] + varlist = ["iph", "io", "rs", "rsh", "u"] pvsyst = dict.fromkeys(paramlist + varlist) infilen = DATA_DIR / datafile - with infilen.open(mode='r') as f: - - Ns, aIsc, bVoc, descr = f.readline().split(',') + with infilen.open(mode="r") as f: + Ns, aIsc, bVoc, descr = f.readline().split(",") pvsyst_specs.update( - cells_in_series=int(Ns), alpha_sc=float(aIsc), - beta_voc=float(bVoc), descr=descr) + cells_in_series=int(Ns), + alpha_sc=float(aIsc), + beta_voc=float(bVoc), + descr=descr, + ) - tmp = [float(x) for x in f.readline().split(',')] + tmp = [float(x) for x in f.readline().split(",")] # I_L_ref, I_o_ref, EgRef, R_s, R_sh_ref, R_sh_0, R_sh_exp, gamma_ref, # mu_gamma pvsyst.update(zip(paramlist, tmp)) @@ -240,7 +324,7 @@ def _read_pvsyst_expected(datafile): u = np.empty(N) for k in range(N): - tmp = [float(x) for x in f.readline().split(',')] + tmp = [float(x) for x in f.readline().split(",")] Iph[k], Io[k], Rsh[k], Rs[k], u[k] = tmp pvsyst.update(zip(varlist, [Iph, Io, Rs, Rsh, u])) @@ -250,139 +334,183 @@ def _read_pvsyst_expected(datafile): @requires_statsmodels def test_fit_pvsyst_sandia(npts=3000): - # get IV curve data - iv_specs, ivcurves = _read_iv_curves_for_test('PVsyst_demo.csv', npts) + iv_specs, ivcurves = _read_iv_curves_for_test("PVsyst_demo.csv", npts) # get known Pvsyst model parameters and five parameters from each fitted # IV curve - pvsyst_specs, pvsyst = _read_pvsyst_expected('PVsyst_demo_model.csv') + pvsyst_specs, pvsyst = _read_pvsyst_expected("PVsyst_demo_model.csv") modeled = sdm.fit_pvsyst_sandia(ivcurves, iv_specs) # calculate IV curves using the fitted model, for comparison with input # IV curves param_res = pvsystem.calcparams_pvsyst( - effective_irradiance=ivcurves['ee'], temp_cell=ivcurves['tc'], - alpha_sc=iv_specs['alpha_sc'], gamma_ref=modeled['gamma_ref'], - mu_gamma=modeled['mu_gamma'], I_L_ref=modeled['I_L_ref'], - I_o_ref=modeled['I_o_ref'], R_sh_ref=modeled['R_sh_ref'], - R_sh_0=modeled['R_sh_0'], R_s=modeled['R_s'], - cells_in_series=iv_specs['cells_in_series'], EgRef=modeled['EgRef']) + effective_irradiance=ivcurves["ee"], + temp_cell=ivcurves["tc"], + alpha_sc=iv_specs["alpha_sc"], + gamma_ref=modeled["gamma_ref"], + mu_gamma=modeled["mu_gamma"], + I_L_ref=modeled["I_L_ref"], + I_o_ref=modeled["I_o_ref"], + R_sh_ref=modeled["R_sh_ref"], + R_sh_0=modeled["R_sh_0"], + R_s=modeled["R_s"], + cells_in_series=iv_specs["cells_in_series"], + EgRef=modeled["EgRef"], + ) iv_res = pvsystem.singlediode(*param_res) # assertions assert np.allclose( - ivcurves['p_mp'], iv_res['p_mp'], equal_nan=True, rtol=0.038) + ivcurves["p_mp"], iv_res["p_mp"], equal_nan=True, rtol=0.038 + ) assert np.allclose( - ivcurves['v_mp'], iv_res['v_mp'], equal_nan=True, rtol=0.029) + ivcurves["v_mp"], iv_res["v_mp"], equal_nan=True, rtol=0.029 + ) assert np.allclose( - ivcurves['i_mp'], iv_res['i_mp'], equal_nan=True, rtol=0.021) + ivcurves["i_mp"], iv_res["i_mp"], equal_nan=True, rtol=0.021 + ) assert np.allclose( - ivcurves['i_sc'], iv_res['i_sc'], equal_nan=True, rtol=0.003) + ivcurves["i_sc"], iv_res["i_sc"], equal_nan=True, rtol=0.003 + ) assert np.allclose( - ivcurves['v_oc'], iv_res['v_oc'], equal_nan=True, rtol=0.019) + ivcurves["v_oc"], iv_res["v_oc"], equal_nan=True, rtol=0.019 + ) # cells_in_series, alpha_sc, beta_voc, descr assert all((iv_specs[k] == pvsyst_specs[k]) for k in iv_specs.keys()) # I_L_ref, I_o_ref, EgRef, R_sh_ref, R_sh_0, R_sh_exp, R_s, gamma_ref, # mu_gamma - assert np.isclose(modeled['I_L_ref'], pvsyst['I_L_ref'], rtol=6.5e-5) - assert np.isclose(modeled['I_o_ref'], pvsyst['I_o_ref'], rtol=0.15) - assert np.isclose(modeled['R_s'], pvsyst['R_s'], rtol=0.0035) - assert np.isclose(modeled['R_sh_ref'], pvsyst['R_sh_ref'], rtol=0.091) - assert np.isclose(modeled['R_sh_0'], pvsyst['R_sh_0'], rtol=0.013) - assert np.isclose(modeled['EgRef'], pvsyst['EgRef'], rtol=0.037) - assert np.isclose(modeled['gamma_ref'], pvsyst['gamma_ref'], rtol=0.0045) - assert np.isclose(modeled['mu_gamma'], pvsyst['mu_gamma'], rtol=0.064) + assert np.isclose(modeled["I_L_ref"], pvsyst["I_L_ref"], rtol=6.5e-5) + assert np.isclose(modeled["I_o_ref"], pvsyst["I_o_ref"], rtol=0.15) + assert np.isclose(modeled["R_s"], pvsyst["R_s"], rtol=0.0035) + assert np.isclose(modeled["R_sh_ref"], pvsyst["R_sh_ref"], rtol=0.091) + assert np.isclose(modeled["R_sh_0"], pvsyst["R_sh_0"], rtol=0.013) + assert np.isclose(modeled["EgRef"], pvsyst["EgRef"], rtol=0.037) + assert np.isclose(modeled["gamma_ref"], pvsyst["gamma_ref"], rtol=0.0045) + assert np.isclose(modeled["mu_gamma"], pvsyst["mu_gamma"], rtol=0.064) # Iph, Io, Rsh, Rs, u - mask = np.ones(modeled['u'].shape, dtype=bool) + mask = np.ones(modeled["u"].shape, dtype=bool) # exclude one curve with different convergence umask = mask.copy() umask[2540] = False - assert all(modeled['u'][umask] == pvsyst['u'][:npts][umask]) + assert all(modeled["u"][umask] == pvsyst["u"][:npts][umask]) assert np.allclose( - modeled['iph'][modeled['u']], pvsyst['iph'][:npts][modeled['u']], - equal_nan=True, rtol=0.0009) + modeled["iph"][modeled["u"]], + pvsyst["iph"][:npts][modeled["u"]], + equal_nan=True, + rtol=0.0009, + ) assert np.allclose( - modeled['io'][modeled['u']], pvsyst['io'][:npts][modeled['u']], - equal_nan=True, rtol=0.096) + modeled["io"][modeled["u"]], + pvsyst["io"][:npts][modeled["u"]], + equal_nan=True, + rtol=0.096, + ) assert np.allclose( - modeled['rs'][modeled['u']], pvsyst['rs'][:npts][modeled['u']], - equal_nan=True, rtol=0.035) + modeled["rs"][modeled["u"]], + pvsyst["rs"][:npts][modeled["u"]], + equal_nan=True, + rtol=0.035, + ) # exclude one curve with Rsh outside 63% tolerance - rshmask = modeled['u'].copy() + rshmask = modeled["u"].copy() rshmask[2545] = False assert np.allclose( - modeled['rsh'][rshmask], pvsyst['rsh'][:npts][rshmask], - equal_nan=True, rtol=0.63) - - -@pytest.mark.parametrize('vmp, imp, iph, io, rs, rsh, nnsvth, expected', [ - (2., 2., 2., 2., 2., 2., 2., np.nan), - (2., 2., 0., 2., 2., 2., 2., np.nan), - (2., 2., 2., 0., 2., 2., 2., np.nan), - (2., 2., 2., 2., 0., 2., 2., np.nan), - (2., 2., 2., 2., 2., 0., 2., np.nan), - (2., 2., 2., 2., 2., 2., 0., np.nan)]) -def test__update_rsh_fixed_pt_nans(vmp, imp, iph, io, rs, rsh, nnsvth, - expected): + modeled["rsh"][rshmask], + pvsyst["rsh"][:npts][rshmask], + equal_nan=True, + rtol=0.63, + ) + + +@pytest.mark.parametrize( + "vmp, imp, iph, io, rs, rsh, nnsvth, expected", + [ + (2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, np.nan), + (2.0, 2.0, 0.0, 2.0, 2.0, 2.0, 2.0, np.nan), + (2.0, 2.0, 2.0, 0.0, 2.0, 2.0, 2.0, np.nan), + (2.0, 2.0, 2.0, 2.0, 0.0, 2.0, 2.0, np.nan), + (2.0, 2.0, 2.0, 2.0, 2.0, 0.0, 2.0, np.nan), + (2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 0.0, np.nan), + ], +) +def test__update_rsh_fixed_pt_nans( + vmp, imp, iph, io, rs, rsh, nnsvth, expected +): outrsh = sdm._update_rsh_fixed_pt(vmp, imp, iph, io, rs, rsh, nnsvth) assert np.all(np.isnan(outrsh)) def test__update_rsh_fixed_pt_vmp0(): - outrsh = sdm._update_rsh_fixed_pt(vmp=0., imp=2., iph=2., io=2., rs=2., - rsh=2., nnsvth=2.) - assert_allclose(outrsh, np.array([502.]), atol=.0001) + outrsh = sdm._update_rsh_fixed_pt( + vmp=0.0, imp=2.0, iph=2.0, io=2.0, rs=2.0, rsh=2.0, nnsvth=2.0 + ) + assert_allclose(outrsh, np.array([502.0]), atol=0.0001) def test__update_rsh_fixed_pt_vector(): - outrsh = sdm._update_rsh_fixed_pt(rsh=np.array([-1., 3, .5, 2.]), - rs=np.array([1., -.5, 2., 2.]), - io=np.array([.2, .3, -.4, 2.]), - iph=np.array([-.1, 1, 3., 2.]), - nnsvth=np.array([4., -.2, .1, 2.]), - imp=np.array([.2, .2, -1., 2.]), - vmp=np.array([0., -1, 0., 0.])) + outrsh = sdm._update_rsh_fixed_pt( + rsh=np.array([-1.0, 3, 0.5, 2.0]), + rs=np.array([1.0, -0.5, 2.0, 2.0]), + io=np.array([0.2, 0.3, -0.4, 2.0]), + iph=np.array([-0.1, 1, 3.0, 2.0]), + nnsvth=np.array([4.0, -0.2, 0.1, 2.0]), + imp=np.array([0.2, 0.2, -1.0, 2.0]), + vmp=np.array([0.0, -1, 0.0, 0.0]), + ) assert np.all(np.isnan(outrsh[0:3])) - assert_allclose(outrsh[3], np.array([502.]), atol=.0001) - - -@pytest.mark.parametrize('voc, iph, io, rs, rsh, nnsvth, expected', [ - (2., 2., 2., 2., 2., 2., 0.5911), - (2., 2., 2., 0., 2., 2., 0.5911), - (2., 2., 0., 2., 2., 2., 0.), - (2., 0., 2., 2., 2., 2., 1.0161e-4), - (0., 2., 2., 2., 2., 2., 17.9436)]) + assert_allclose(outrsh[3], np.array([502.0]), atol=0.0001) + + +@pytest.mark.parametrize( + "voc, iph, io, rs, rsh, nnsvth, expected", + [ + (2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 0.5911), + (2.0, 2.0, 2.0, 0.0, 2.0, 2.0, 0.5911), + (2.0, 2.0, 0.0, 2.0, 2.0, 2.0, 0.0), + (2.0, 0.0, 2.0, 2.0, 2.0, 2.0, 1.0161e-4), + (0.0, 2.0, 2.0, 2.0, 2.0, 2.0, 17.9436), + ], +) def test__update_io(voc, iph, io, rs, rsh, nnsvth, expected): outio = sdm._update_io(voc, iph, io, rs, rsh, nnsvth) - assert_allclose(outio, expected, atol=.0001) + assert_allclose(outio, expected, atol=0.0001) -@pytest.mark.parametrize('voc, iph, io, rs, rsh, nnsvth', [ - (2., 2., 2., 2., 2., 0.), - (-1., -1., -1., -1., -1., -1.)]) +@pytest.mark.parametrize( + "voc, iph, io, rs, rsh, nnsvth", + [(2.0, 2.0, 2.0, 2.0, 2.0, 0.0), (-1.0, -1.0, -1.0, -1.0, -1.0, -1.0)], +) def test__update_io_nan(voc, iph, io, rs, rsh, nnsvth): outio = sdm._update_io(voc, iph, io, rs, rsh, nnsvth) assert np.isnan(outio) -@pytest.mark.parametrize('vmp, imp, iph, io, rs, rsh, nnsvth, expected', [ - (2., 2., 2., 2., 2., 2., 2., (1.8726, 2.)), - (2., 0., 2., 2., 2., 2., 2., (1.8726, 3.4537)), - (2., 2., 0., 2., 2., 2., 2., (1.2650, 0.8526)), - (0., 2., 2., 2., 2., 2., 2., (1.5571, 2.))]) +@pytest.mark.parametrize( + "vmp, imp, iph, io, rs, rsh, nnsvth, expected", + [ + (2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, (1.8726, 2.0)), + (2.0, 0.0, 2.0, 2.0, 2.0, 2.0, 2.0, (1.8726, 3.4537)), + (2.0, 2.0, 0.0, 2.0, 2.0, 2.0, 2.0, (1.2650, 0.8526)), + (0.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, (1.5571, 2.0)), + ], +) def test__calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth, expected): theta, phi = sdm._calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth) - assert_allclose(theta, expected[0], atol=.0001) - assert_allclose(phi, expected[1], atol=.0001) - - -@pytest.mark.parametrize('vmp, imp, iph, io, rs, rsh, nnsvth', [ - (2., 2., 2., 0., 2., 2., 2.), - (2., 2., 2., 2., 2., 2., 0.), - (2., 0., 2., 2., 2., 0., 2.)]) + assert_allclose(theta, expected[0], atol=0.0001) + assert_allclose(phi, expected[1], atol=0.0001) + + +@pytest.mark.parametrize( + "vmp, imp, iph, io, rs, rsh, nnsvth", + [ + (2.0, 2.0, 2.0, 0.0, 2.0, 2.0, 2.0), + (2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 0.0), + (2.0, 0.0, 2.0, 2.0, 2.0, 0.0, 2.0), + ], +) def test__calc_theta_phi_exact_both_nan(vmp, imp, iph, io, rs, rsh, nnsvth): theta, phi = sdm._calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth) assert np.isnan(theta) @@ -390,31 +518,42 @@ def test__calc_theta_phi_exact_both_nan(vmp, imp, iph, io, rs, rsh, nnsvth): def test__calc_theta_phi_exact_one_nan(): - theta, phi = sdm._calc_theta_phi_exact(imp=2., iph=2., vmp=2., io=2., - nnsvth=2., rs=0., rsh=2.) + theta, phi = sdm._calc_theta_phi_exact( + imp=2.0, iph=2.0, vmp=2.0, io=2.0, nnsvth=2.0, rs=0.0, rsh=2.0 + ) assert np.isnan(theta) - assert_allclose(phi, 2., atol=.0001) + assert_allclose(phi, 2.0, atol=0.0001) def test__calc_theta_phi_exact_vector(): - theta, phi = sdm._calc_theta_phi_exact(imp=np.array([1., -1.]), - iph=np.array([-1., 1.]), - vmp=np.array([1., -1.]), - io=np.array([-1., 1.]), - nnsvth=np.array([1., -1.]), - rs=np.array([-1., 1.]), - rsh=np.array([1., -1.])) + theta, phi = sdm._calc_theta_phi_exact( + imp=np.array([1.0, -1.0]), + iph=np.array([-1.0, 1.0]), + vmp=np.array([1.0, -1.0]), + io=np.array([-1.0, 1.0]), + nnsvth=np.array([1.0, -1.0]), + rs=np.array([-1.0, 1.0]), + rsh=np.array([1.0, -1.0]), + ) assert np.isnan(theta[0]) assert np.isnan(theta[1]) assert np.isnan(phi[0]) - assert_allclose(phi[1], 2.2079, atol=.0001) + assert_allclose(phi[1], 2.2079, atol=0.0001) def test_pvsyst_temperature_coeff(): # test for consistency with dP/dT estimated with secant rule - params = {'alpha_sc': 0., 'gamma_ref': 1.1, 'mu_gamma': 0., - 'I_L_ref': 6., 'I_o_ref': 5.e-9, 'R_sh_ref': 200., - 'R_sh_0': 2000., 'R_s': 0.5, 'cells_in_series': 60} + params = { + "alpha_sc": 0.0, + "gamma_ref": 1.1, + "mu_gamma": 0.0, + "I_L_ref": 6.0, + "I_o_ref": 5.0e-9, + "R_sh_ref": 200.0, + "R_sh_0": 2000.0, + "R_s": 0.5, + "cells_in_series": 60, + } expected = -0.004886706494879083 # params defines a Pvsyst model for a notional module. # expected value is created by calculating power at 1000 W/m2, and cell @@ -423,7 +562,14 @@ def test_pvsyst_temperature_coeff(): # as the slope (p_mp at 26C - p_mp at 24C) / 2 # using the secant rule for derivatives. gamma_pdc = sdm.pvsyst_temperature_coeff( - params['alpha_sc'], params['gamma_ref'], params['mu_gamma'], - params['I_L_ref'], params['I_o_ref'], params['R_sh_ref'], - params['R_sh_0'], params['R_s'], params['cells_in_series']) + params["alpha_sc"], + params["gamma_ref"], + params["mu_gamma"], + params["I_L_ref"], + params["I_o_ref"], + params["R_sh_ref"], + params["R_sh_0"], + params["R_s"], + params["cells_in_series"], + ) assert_allclose(gamma_pdc, expected, rtol=0.0005) diff --git a/pvlib/tests/ivtools/test_utils.py b/pvlib/tests/ivtools/test_utils.py index d8a35e554d..eeafdc96aa 100644 --- a/pvlib/tests/ivtools/test_utils.py +++ b/pvlib/tests/ivtools/test_utils.py @@ -9,16 +9,49 @@ @pytest.fixture def ivcurve(): - voltage = np.array([0., 1., 5., 10., 25., 25.00001, 30., 28., 45., 47., - 49., 51., np.nan]) - current = np.array([7., 6., 6., 5., 4., 3., 2.7, 2.5, np.nan, 0.5, -1., 0., - np.nan]) + voltage = np.array( + [ + 0.0, + 1.0, + 5.0, + 10.0, + 25.0, + 25.00001, + 30.0, + 28.0, + 45.0, + 47.0, + 49.0, + 51.0, + np.nan, + ] + ) + current = np.array( + [ + 7.0, + 6.0, + 6.0, + 5.0, + 4.0, + 3.0, + 2.7, + 2.5, + np.nan, + 0.5, + -1.0, + 0.0, + np.nan, + ] + ) return voltage, current def test__numdiff(): - iv = pd.read_csv(DATA_DIR / 'ivtools_numdiff.csv', - names=['I', 'V', 'dIdV', 'd2IdV2'], dtype=float) + iv = pd.read_csv( + DATA_DIR / "ivtools_numdiff.csv", + names=["I", "V", "dIdV", "d2IdV2"], + dtype=float, + ) df, d2f = _numdiff(iv.V, iv.I) assert np.allclose(iv.dIdV, df, equal_nan=True) assert np.allclose(iv.d2IdV2, d2f, equal_nan=True) @@ -27,49 +60,151 @@ def test__numdiff(): def test_rectify_iv_curve(ivcurve): voltage, current = ivcurve - vexp_no_dec = np.array([0., 1., 5., 10., 25., 25.00001, 28., 30., 47., - 51.]) - iexp_no_dec = np.array([7., 6., 6., 5., 4., 3., 2.5, 2.7, 0.5, 0.]) + vexp_no_dec = np.array( + [0.0, 1.0, 5.0, 10.0, 25.0, 25.00001, 28.0, 30.0, 47.0, 51.0] + ) + iexp_no_dec = np.array([7.0, 6.0, 6.0, 5.0, 4.0, 3.0, 2.5, 2.7, 0.5, 0.0]) v, i = rectify_iv_curve(voltage, current) - np.testing.assert_allclose(v, vexp_no_dec, atol=.0001) - np.testing.assert_allclose(i, iexp_no_dec, atol=.0001) + np.testing.assert_allclose(v, vexp_no_dec, atol=0.0001) + np.testing.assert_allclose(i, iexp_no_dec, atol=0.0001) - vexp = np.array([0., 1., 5., 10., 25., 28., 30., 47., 51.]) - iexp = np.array([7., 6., 6., 5., 3.5, 2.5, 2.7, 0.5, 0.]) + vexp = np.array([0.0, 1.0, 5.0, 10.0, 25.0, 28.0, 30.0, 47.0, 51.0]) + iexp = np.array([7.0, 6.0, 6.0, 5.0, 3.5, 2.5, 2.7, 0.5, 0.0]) v, i = rectify_iv_curve(voltage, current, decimals=4) - np.testing.assert_allclose(v, vexp, atol=.0001) - np.testing.assert_allclose(i, iexp, atol=.0001) - - -@pytest.mark.parametrize('x,y,expected', [ - (np.array([0., 1., 2., 3., 4., 1., 2., 3., 4., 5.]), - np.array([2., 1., 0., 1., 2., 3., 2., 1., 2., 3.]), - (np.array([[0., -1., 2.], [-0.5, -1., 1.], [-0.75, -0.5, 3.], - [0.75, -1.5, 0.375], [0.125, -1.25, 2.5625], [1.5, 0., 0.], - [-0.5, -1., 2.], [-0.25, 1.5, 0.375], [0.75, -1.5, 1.375], - [0.5, 1., 1.], [1.5, 0., 1.], [0.0278, -0.3333, 2.1667], - [-0.75, 1.5, 1.625], [-0.25, 1.5, 1.375], [0.1667, 0., 2.], - [0., 1., 2.]]), - np.array([0., 1., 1., 1.5, 1.5, 2., 2., 2.5, 2.5, 3., 3., 3., 3.5, 3.5, - 4., 4., 5.]), - np.array([2., 1., 3., 0.375, 2.5625, 0., 2., 0.375, 1.375, 1., 1., - 2.1667, 1.625, 1.375, 2., 2., 3.]), - np.array([0., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 1., 0., 0., - 0.]))), - (np.array([1., 2., 3., 4., 5.]), - np.array([-2., -1., 0., 1., 2.]), - (np.array([[0., 1., -2.], [0., 1., -1.], [0., 1., 0.], [0., 1., 1.]]), - np.array([1., 2., 3., 4., 5.]), - np.array([-2., -1., 0., 1., 2.]), - np.array([0., 0., 0., 0., 0.]))), - (np.array([-.5, -.1, 0., .2, .3]), - np.array([-5., -1., .2, .5, 2.]), - (np.array([[2.2727, 9.0909, -5.], [63.0303, 10.9091, -1.], - [-72.7273, 17.2121, -.297], [-11.8182, 2.6667, .2], - [6.0606, .303, .3485], [122.7273, 2.7273, .5]]), - np.array([-.5, -.1, -.05, 0., .1, .2, .3]), - np.array([-5., -1., -.297, .2, .3485, .5, 2.]), - np.array([0., 0., 1., 0., 1., 0., 0.])))]) + np.testing.assert_allclose(v, vexp, atol=0.0001) + np.testing.assert_allclose(i, iexp, atol=0.0001) + + +@pytest.mark.parametrize( + "x,y,expected", + [ + ( + np.array([0.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 5.0]), + np.array([2.0, 1.0, 0.0, 1.0, 2.0, 3.0, 2.0, 1.0, 2.0, 3.0]), + ( + np.array( + [ + [0.0, -1.0, 2.0], + [-0.5, -1.0, 1.0], + [-0.75, -0.5, 3.0], + [0.75, -1.5, 0.375], + [0.125, -1.25, 2.5625], + [1.5, 0.0, 0.0], + [-0.5, -1.0, 2.0], + [-0.25, 1.5, 0.375], + [0.75, -1.5, 1.375], + [0.5, 1.0, 1.0], + [1.5, 0.0, 1.0], + [0.0278, -0.3333, 2.1667], + [-0.75, 1.5, 1.625], + [-0.25, 1.5, 1.375], + [0.1667, 0.0, 2.0], + [0.0, 1.0, 2.0], + ] + ), + np.array( + [ + 0.0, + 1.0, + 1.0, + 1.5, + 1.5, + 2.0, + 2.0, + 2.5, + 2.5, + 3.0, + 3.0, + 3.0, + 3.5, + 3.5, + 4.0, + 4.0, + 5.0, + ] + ), + np.array( + [ + 2.0, + 1.0, + 3.0, + 0.375, + 2.5625, + 0.0, + 2.0, + 0.375, + 1.375, + 1.0, + 1.0, + 2.1667, + 1.625, + 1.375, + 2.0, + 2.0, + 3.0, + ] + ), + np.array( + [ + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + ] + ), + ), + ), + ( + np.array([1.0, 2.0, 3.0, 4.0, 5.0]), + np.array([-2.0, -1.0, 0.0, 1.0, 2.0]), + ( + np.array( + [ + [0.0, 1.0, -2.0], + [0.0, 1.0, -1.0], + [0.0, 1.0, 0.0], + [0.0, 1.0, 1.0], + ] + ), + np.array([1.0, 2.0, 3.0, 4.0, 5.0]), + np.array([-2.0, -1.0, 0.0, 1.0, 2.0]), + np.array([0.0, 0.0, 0.0, 0.0, 0.0]), + ), + ), + ( + np.array([-0.5, -0.1, 0.0, 0.2, 0.3]), + np.array([-5.0, -1.0, 0.2, 0.5, 2.0]), + ( + np.array( + [ + [2.2727, 9.0909, -5.0], + [63.0303, 10.9091, -1.0], + [-72.7273, 17.2121, -0.297], + [-11.8182, 2.6667, 0.2], + [6.0606, 0.303, 0.3485], + [122.7273, 2.7273, 0.5], + ] + ), + np.array([-0.5, -0.1, -0.05, 0.0, 0.1, 0.2, 0.3]), + np.array([-5.0, -1.0, -0.297, 0.2, 0.3485, 0.5, 2.0]), + np.array([0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0]), + ), + ), + ], +) def test__schmumaker_qspline(x, y, expected): [t, c, yhat, kflag] = _schumaker_qspline(x, y) np.testing.assert_allclose(c, expected[0], atol=0.0001) @@ -80,82 +215,149 @@ def test__schmumaker_qspline(x, y, expected): @pytest.fixture def i_array(): - i = np.array([8.09403993, 8.09382549, 8.09361103, 8.09339656, 8.09318205, - 8.09296748, 8.09275275, 8.09253771, 8.09232204, 8.09210506, - 8.09188538, 8.09166014, 8.09142342, 8.09116305, 8.09085392, - 8.09044425, 8.08982734, 8.08878333, 8.08685945, 8.08312463, - 8.07566926, 8.06059856, 8.03005836, 7.96856869, 7.8469714, - 7.61489584, 7.19789314, 6.51138396, 5.49373476, 4.13267172, - 2.46021487, 0.52838624, -1.61055289]) + i = np.array( + [ + 8.09403993, + 8.09382549, + 8.09361103, + 8.09339656, + 8.09318205, + 8.09296748, + 8.09275275, + 8.09253771, + 8.09232204, + 8.09210506, + 8.09188538, + 8.09166014, + 8.09142342, + 8.09116305, + 8.09085392, + 8.09044425, + 8.08982734, + 8.08878333, + 8.08685945, + 8.08312463, + 8.07566926, + 8.06059856, + 8.03005836, + 7.96856869, + 7.8469714, + 7.61489584, + 7.19789314, + 6.51138396, + 5.49373476, + 4.13267172, + 2.46021487, + 0.52838624, + -1.61055289, + ] + ) return i @pytest.fixture def v_array(): - v = np.array([-0.005, 0.015, 0.035, 0.055, 0.075, 0.095, 0.115, 0.135, - 0.155, 0.175, 0.195, 0.215, 0.235, 0.255, 0.275, 0.295, - 0.315, 0.335, 0.355, 0.375, 0.395, 0.415, 0.435, 0.455, - 0.475, 0.495, 0.515, 0.535, 0.555, 0.575, 0.595, 0.615, - 0.635]) + v = np.array( + [ + -0.005, + 0.015, + 0.035, + 0.055, + 0.075, + 0.095, + 0.115, + 0.135, + 0.155, + 0.175, + 0.195, + 0.215, + 0.235, + 0.255, + 0.275, + 0.295, + 0.315, + 0.335, + 0.355, + 0.375, + 0.395, + 0.415, + 0.435, + 0.455, + 0.475, + 0.495, + 0.515, + 0.535, + 0.555, + 0.575, + 0.595, + 0.615, + 0.635, + ] + ) return v # astm_e1036 tests def test_astm_e1036(v_array, i_array): result = astm_e1036(v_array, i_array) - expected = {'voc': 0.6195097477985162, - 'isc': 8.093986320386227, - 'vmp': 0.494283417170082, - 'imp': 7.626088301548568, - 'pmp': 3.7694489853302127, - 'ff': 0.7517393078504361} - fit = result.pop('mp_fit') + expected = { + "voc": 0.6195097477985162, + "isc": 8.093986320386227, + "vmp": 0.494283417170082, + "imp": 7.626088301548568, + "pmp": 3.7694489853302127, + "ff": 0.7517393078504361, + } + fit = result.pop("mp_fit") expected_fit = np.array( - [3.6260726, 0.49124176, -0.24644747, -0.26442383, -0.1223237]) + [3.6260726, 0.49124176, -0.24644747, -0.26442383, -0.1223237] + ) assert fit.coef == pytest.approx(expected_fit) assert result == pytest.approx(expected) def test_astm_e1036_fit_order(v_array, i_array): result = astm_e1036(v_array, i_array, mp_fit_order=3) - fit = result.pop('mp_fit') - expected_fit = np.array( - [3.64081697, 0.49124176, -0.3720477, -0.26442383]) + fit = result.pop("mp_fit") + expected_fit = np.array([3.64081697, 0.49124176, -0.3720477, -0.26442383]) assert fit.coef == pytest.approx(expected_fit) def test_astm_e1036_est_isc_voc(v_array, i_array): - ''' + """ Test the case in which Isc and Voc estimates are valid without a linear fit - ''' + """ v = v_array i = i_array v = np.append(v, [0.001, 0.6201]) - i = np.append(i, [8.09397560e+00, 7.10653445e-04]) + i = np.append(i, [8.09397560e00, 7.10653445e-04]) result = astm_e1036(v, i) - expected = {'voc': 0.6201, - 'isc': 8.093975598317805, - 'vmp': 0.494283417170082, - 'imp': 7.626088301548568, - 'pmp': 3.7694489853302127, - 'ff': 0.751024747526615} - result.pop('mp_fit') + expected = { + "voc": 0.6201, + "isc": 8.093975598317805, + "vmp": 0.494283417170082, + "imp": 7.626088301548568, + "pmp": 3.7694489853302127, + "ff": 0.751024747526615, + } + result.pop("mp_fit") assert result == pytest.approx(expected) def test_astm_e1036_mpfit_limits(v_array, i_array): - result = astm_e1036(v_array, - i_array, - imax_limits=(0.85, 1.1), - vmax_limits=(0.85, 1.1)) - expected = {'voc': 0.6195097477985162, - 'isc': 8.093986320386227, - 'vmp': 0.49464214190725303, - 'imp': 7.620032530519718, - 'pmp': 3.769189212299219, - 'ff': 0.7516875014460312} - result.pop('mp_fit') + result = astm_e1036( + v_array, i_array, imax_limits=(0.85, 1.1), vmax_limits=(0.85, 1.1) + ) + expected = { + "voc": 0.6195097477985162, + "isc": 8.093986320386227, + "vmp": 0.49464214190725303, + "imp": 7.620032530519718, + "pmp": 3.769189212299219, + "ff": 0.7516875014460312, + } + result.pop("mp_fit") assert result == pytest.approx(expected) @@ -163,11 +365,13 @@ def test_astm_e1036_fit_points(v_array, i_array): i = i_array i[3] = 8.1 # ensure an interesting change happens result = astm_e1036(v_array, i, voc_points=4, isc_points=4) - expected = {'voc': 0.619337073271274, - 'isc': 8.093160893325297, - 'vmp': 0.494283417170082, - 'imp': 7.626088301548568, - 'pmp': 3.7694489853302127, - 'ff': 0.7520255886236707} - result.pop('mp_fit') + expected = { + "voc": 0.619337073271274, + "isc": 8.093160893325297, + "vmp": 0.494283417170082, + "imp": 7.626088301548568, + "pmp": 3.7694489853302127, + "ff": 0.7520255886236707, + } + result.pop("mp_fit") assert result == pytest.approx(expected) diff --git a/pvlib/tests/spectrum/conftest.py b/pvlib/tests/spectrum/conftest.py index 6e2a42c05c..f2dad42dc1 100644 --- a/pvlib/tests/spectrum/conftest.py +++ b/pvlib/tests/spectrum/conftest.py @@ -4,7 +4,7 @@ from ..conftest import DATA_DIR -SPECTRL2_TEST_DATA = DATA_DIR / 'spectrl2_example_spectra.csv' +SPECTRL2_TEST_DATA = DATA_DIR / "spectrl2_example_spectra.csv" @pytest.fixture @@ -22,19 +22,19 @@ def spectrl2_data(): ) """ kwargs = { - 'surface_tilt': 0, - 'relative_airmass': 1.4899535986910446, - 'apparent_zenith': 47.912086486816406, - 'aoi': 47.91208648681641, - 'ground_albedo': 0.2, - 'surface_pressure': 101300, - 'ozone': 0.344, - 'precipitable_water': 1.42, - 'aerosol_turbidity_500nm': 0.1, - 'dayofyear': 75 + "surface_tilt": 0, + "relative_airmass": 1.4899535986910446, + "apparent_zenith": 47.912086486816406, + "aoi": 47.91208648681641, + "ground_albedo": 0.2, + "surface_pressure": 101300, + "ozone": 0.344, + "precipitable_water": 1.42, + "aerosol_turbidity_500nm": 0.1, + "dayofyear": 75, } df = pd.read_csv(SPECTRL2_TEST_DATA, index_col=0) # convert um to nm - df['wavelength'] = np.round(df['wavelength'] * 1000, 1) - df[['specdif', 'specdir', 'specetr', 'specglo']] /= 1000 + df["wavelength"] = np.round(df["wavelength"] * 1000, 1) + df[["specdif", "specdir", "specetr", "specglo"]] /= 1000 return kwargs, df diff --git a/pvlib/tests/spectrum/test_irradiance.py b/pvlib/tests/spectrum/test_irradiance.py index 63b0bc95d2..e22379f60a 100644 --- a/pvlib/tests/spectrum/test_irradiance.py +++ b/pvlib/tests/spectrum/test_irradiance.py @@ -8,11 +8,12 @@ from ..conftest import assert_series_equal, fail_on_pvlib_version -@fail_on_pvlib_version('0.12') +@fail_on_pvlib_version("0.12") def test_get_am15g(): # test that the reference spectrum is read and interpolated correctly - with pytest.warns(pvlibDeprecationWarning, - match="get_reference_spectra instead"): + with pytest.warns( + pvlibDeprecationWarning, match="get_reference_spectra instead" + ): e = spectrum.get_am15g() assert_equal(len(e), 2002) assert_equal(np.sum(e.index), 2761442) @@ -21,8 +22,9 @@ def test_get_am15g(): wavelength = [270, 850, 950, 1200, 1201.25, 4001] expected = [0.0, 0.893720, 0.147260, 0.448250, 0.4371025, 0.0] - with pytest.warns(pvlibDeprecationWarning, - match="get_reference_spectra instead"): + with pytest.warns( + pvlibDeprecationWarning, match="get_reference_spectra instead" + ): e = spectrum.get_am15g(wavelength) assert_equal(len(e), len(wavelength)) assert_allclose(e, expected, rtol=1e-6) @@ -79,7 +81,7 @@ def test_average_photon_energy_series(): # series input spectra = spectrum.get_reference_spectra() - spectra = spectra['global'] + spectra = spectra["global"] ape = spectrum.average_photon_energy(spectra) expected = 1.45017 assert_allclose(ape, expected, rtol=1e-4) @@ -99,8 +101,9 @@ def test_average_photon_energy_dataframe(): def test_average_photon_energy_invalid_type(): # test that spectrum argument is either a pandas Series or dataframe spectra = 5 - with pytest.raises(TypeError, match='must be either a pandas Series or' - ' DataFrame'): + with pytest.raises( + TypeError, match="must be either a pandas Series or" " DataFrame" + ): spectrum.average_photon_energy(spectra) @@ -108,8 +111,8 @@ def test_average_photon_energy_neg_irr_series(): # test for handling of negative spectral irradiance values with a # pandas Series input - spectra = spectrum.get_reference_spectra()['global']*-1 - with pytest.raises(ValueError, match='must be positive'): + spectra = spectrum.get_reference_spectra()["global"] * -1 + with pytest.raises(ValueError, match="must be positive"): spectrum.average_photon_energy(spectra) @@ -117,9 +120,9 @@ def test_average_photon_energy_neg_irr_dataframe(): # test for handling of negative spectral irradiance values with a # pandas DataFrame input - spectra = spectrum.get_reference_spectra().T*-1 + spectra = spectrum.get_reference_spectra().T * -1 - with pytest.raises(ValueError, match='must be positive'): + with pytest.raises(ValueError, match="must be positive"): spectrum.average_photon_energy(spectra) @@ -129,7 +132,7 @@ def test_average_photon_energy_zero_irr(): spectra_df_zero = spectrum.get_reference_spectra().T spectra_df_zero.iloc[1] = 0 - spectra_series_zero = spectrum.get_reference_spectra()['global']*0 + spectra_series_zero = spectrum.get_reference_spectra()["global"] * 0 out_1 = spectrum.average_photon_energy(spectra_df_zero) out_2 = spectrum.average_photon_energy(spectra_series_zero) expected_1 = np.array([1.36848, np.nan, 1.40885]) diff --git a/pvlib/tests/spectrum/test_mismatch.py b/pvlib/tests/spectrum/test_mismatch.py index 5397a81f46..9bb2aeefce 100644 --- a/pvlib/tests/spectrum/test_mismatch.py +++ b/pvlib/tests/spectrum/test_mismatch.py @@ -14,10 +14,10 @@ def test_calc_spectral_mismatch_field(spectrl2_data): # sample data _, e_sun = spectrl2_data - e_sun = e_sun.set_index('wavelength') + e_sun = e_sun.set_index("wavelength") e_sun = e_sun.transpose() - e_ref = spectrum.get_reference_spectra(standard='ASTM G173-03')["global"] + e_ref = spectrum.get_reference_spectra(standard="ASTM G173-03")["global"] sr = spectrum.get_example_spectral_response() # test with single sun spectrum, same as ref spectrum @@ -25,13 +25,13 @@ def test_calc_spectral_mismatch_field(spectrl2_data): assert_approx_equal(mm, 1.0, significant=6) # test with single sun spectrum - mm = spectrum.calc_spectral_mismatch_field(sr, e_sun=e_sun.loc['specglo']) + mm = spectrum.calc_spectral_mismatch_field(sr, e_sun=e_sun.loc["specglo"]) assert_approx_equal(mm, 0.992397, significant=6) # test with single sun spectrum, also used as reference spectrum - mm = spectrum.calc_spectral_mismatch_field(sr, - e_sun=e_sun.loc['specglo'], - e_ref=e_sun.loc['specglo']) + mm = spectrum.calc_spectral_mismatch_field( + sr, e_sun=e_sun.loc["specglo"], e_ref=e_sun.loc["specglo"] + ) assert_approx_equal(mm, 1.0, significant=6) # test with multiple sun spectra @@ -42,28 +42,61 @@ def test_calc_spectral_mismatch_field(spectrl2_data): assert_allclose(mm, expected, rtol=1e-6) -@pytest.mark.parametrize("module_type,expect", [ - ('cdte', np.array( - [[0.99051020, 0.97640320, 0.93975028], - [1.02928735, 1.01881074, 0.98578821], - [1.04750335, 1.03814456, 1.00623986]])), - ('monosi', np.array( - [[0.97769770, 1.02043409, 1.03574032], - [0.98630905, 1.03055092, 1.04736262], - [0.98828494, 1.03299036, 1.05026561]])), - ('polysi', np.array( - [[0.97704080, 1.01705849, 1.02613202], - [0.98992828, 1.03173953, 1.04260662], - [0.99352435, 1.03588785, 1.04730718]])), - ('cigs', np.array( - [[0.97459190, 1.02821696, 1.05067895], - [0.97529378, 1.02967497, 1.05289307], - [0.97269159, 1.02730558, 1.05075651]])), - ('asi', np.array( - [[1.05552750, 0.87707583, 0.72243772], - [1.11225204, 0.93665901, 0.78487953], - [1.14555295, 0.97084011, 0.81994083]])) -]) +@pytest.mark.parametrize( + "module_type,expect", + [ + ( + "cdte", + np.array( + [ + [0.99051020, 0.97640320, 0.93975028], + [1.02928735, 1.01881074, 0.98578821], + [1.04750335, 1.03814456, 1.00623986], + ] + ), + ), + ( + "monosi", + np.array( + [ + [0.97769770, 1.02043409, 1.03574032], + [0.98630905, 1.03055092, 1.04736262], + [0.98828494, 1.03299036, 1.05026561], + ] + ), + ), + ( + "polysi", + np.array( + [ + [0.97704080, 1.01705849, 1.02613202], + [0.98992828, 1.03173953, 1.04260662], + [0.99352435, 1.03588785, 1.04730718], + ] + ), + ), + ( + "cigs", + np.array( + [ + [0.97459190, 1.02821696, 1.05067895], + [0.97529378, 1.02967497, 1.05289307], + [0.97269159, 1.02730558, 1.05075651], + ] + ), + ), + ( + "asi", + np.array( + [ + [1.05552750, 0.87707583, 0.72243772], + [1.11225204, 0.93665901, 0.78487953], + [1.14555295, 0.97084011, 0.81994083], + ] + ), + ), + ], +) def test_spectral_factor_firstsolar(module_type, expect): ams = np.array([1, 3, 5]) pws = np.array([1, 3, 5]) @@ -82,17 +115,19 @@ def test_spectral_factor_firstsolar_supplied(): def test_spectral_factor_firstsolar_large_airmass_supplied_max(): # test airmass > user-defined maximum is treated same as airmass=maximum - m_eq11 = spectrum.spectral_factor_firstsolar(1, 11, 'monosi', - max_airmass_absolute=11) - m_gt11 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi', - max_airmass_absolute=11) + m_eq11 = spectrum.spectral_factor_firstsolar( + 1, 11, "monosi", max_airmass_absolute=11 + ) + m_gt11 = spectrum.spectral_factor_firstsolar( + 1, 15, "monosi", max_airmass_absolute=11 + ) assert_allclose(m_eq11, m_gt11) def test_spectral_factor_firstsolar_large_airmass(): # test that airmass > 10 is treated same as airmass=10 - m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, 'monosi') - m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, 'monosi') + m_eq10 = spectrum.spectral_factor_firstsolar(1, 10, "monosi") + m_gt10 = spectrum.spectral_factor_firstsolar(1, 15, "monosi") assert_allclose(m_eq10, m_gt10) @@ -105,49 +140,54 @@ def test_spectral_factor_firstsolar_ambiguous_both(): # use the cdte coeffs coeffs = (0.87102, -0.040543, -0.00929202, 0.10052, 0.073062, -0.0034187) with pytest.raises(TypeError): - spectrum.spectral_factor_firstsolar(1, 1, 'cdte', coefficients=coeffs) + spectrum.spectral_factor_firstsolar(1, 1, "cdte", coefficients=coeffs) def test_spectral_factor_firstsolar_low_airmass(): - m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, 'monosi') - m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') + m_eq58 = spectrum.spectral_factor_firstsolar(1, 0.58, "monosi") + m_lt58 = spectrum.spectral_factor_firstsolar(1, 0.1, "monosi") assert_allclose(m_eq58, m_lt58) - with pytest.warns(UserWarning, match='Low airmass values replaced'): - _ = spectrum.spectral_factor_firstsolar(1, 0.1, 'monosi') + with pytest.warns(UserWarning, match="Low airmass values replaced"): + _ = spectrum.spectral_factor_firstsolar(1, 0.1, "monosi") def test_spectral_factor_firstsolar_range(): - out = spectrum.spectral_factor_firstsolar(np.array([.1, 3, 10]), - np.array([1, 3, 5]), - module_type='monosi') + out = spectrum.spectral_factor_firstsolar( + np.array([0.1, 3, 10]), np.array([1, 3, 5]), module_type="monosi" + ) expected = np.array([0.96080878, 1.03055092, np.nan]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='High precipitable water values ' - 'replaced'): - out = spectrum.spectral_factor_firstsolar(6, 1.5, - max_precipitable_water=5, - module_type='monosi') - with pytest.warns(UserWarning, match='Low precipitable water values ' - 'replaced'): - out = spectrum.spectral_factor_firstsolar(np.array([0, 3, 8]), - np.array([1, 3, 5]), - module_type='monosi') + with pytest.warns( + UserWarning, match="High precipitable water values " "replaced" + ): + out = spectrum.spectral_factor_firstsolar( + 6, 1.5, max_precipitable_water=5, module_type="monosi" + ) + with pytest.warns( + UserWarning, match="Low precipitable water values " "replaced" + ): + out = spectrum.spectral_factor_firstsolar( + np.array([0, 3, 8]), np.array([1, 3, 5]), module_type="monosi" + ) expected = np.array([0.96080878, 1.03055092, 1.04932727]) assert_allclose(out, expected, atol=1e-3) - with pytest.warns(UserWarning, match='Low precipitable water values ' - 'replaced'): - out = spectrum.spectral_factor_firstsolar(0.2, 1.5, - min_precipitable_water=1, - module_type='monosi') - - -@pytest.mark.parametrize('airmass,expected', [ - (1.5, 1.00028714375), - (np.array([[10, np.nan]]), np.array([[0.999535, 0]])), - (pd.Series([5]), pd.Series([1.0387675])) -]) + with pytest.warns( + UserWarning, match="Low precipitable water values " "replaced" + ): + out = spectrum.spectral_factor_firstsolar( + 0.2, 1.5, min_precipitable_water=1, module_type="monosi" + ) + + +@pytest.mark.parametrize( + "airmass,expected", + [ + (1.5, 1.00028714375), + (np.array([[10, np.nan]]), np.array([[0.999535, 0]])), + (pd.Series([5]), pd.Series([1.0387675])), + ], +) def test_spectral_factor_sapm(sapm_module_params, airmass, expected): - out = spectrum.spectral_factor_sapm(airmass, sapm_module_params) if isinstance(airmass, pd.Series): @@ -156,28 +196,46 @@ def test_spectral_factor_sapm(sapm_module_params, airmass, expected): assert_allclose(out, expected, atol=1e-4) -@pytest.mark.parametrize("module_type,expected", [ - ('asi', np.array([0.9108, 0.9897, 0.9707, 1.0265, 1.0798, 0.9537])), - ('perovskite', np.array([0.9422, 0.9932, 0.9868, 1.0183, 1.0604, 0.9737])), - ('cdte', np.array([0.9824, 1.0000, 1.0065, 1.0117, 1.042, 0.9979])), - ('multisi', np.array([0.9907, 0.9979, 1.0203, 1.0081, 1.0058, 1.019])), - ('monosi', np.array([0.9935, 0.9987, 1.0264, 1.0074, 0.9999, 1.0263])), - ('cigs', np.array([1.0014, 1.0011, 1.0270, 1.0082, 1.0029, 1.026])), -]) +@pytest.mark.parametrize( + "module_type,expected", + [ + ("asi", np.array([0.9108, 0.9897, 0.9707, 1.0265, 1.0798, 0.9537])), + ( + "perovskite", + np.array([0.9422, 0.9932, 0.9868, 1.0183, 1.0604, 0.9737]), + ), + ("cdte", np.array([0.9824, 1.0000, 1.0065, 1.0117, 1.042, 0.9979])), + ("multisi", np.array([0.9907, 0.9979, 1.0203, 1.0081, 1.0058, 1.019])), + ("monosi", np.array([0.9935, 0.9987, 1.0264, 1.0074, 0.9999, 1.0263])), + ("cigs", np.array([1.0014, 1.0011, 1.0270, 1.0082, 1.0029, 1.026])), + ], +) def test_spectral_factor_caballero(module_type, expected): ams = np.array([3.0, 1.5, 3.0, 1.5, 1.5, 3.0]) aods = np.array([1.0, 1.0, 0.02, 0.02, 0.08, 0.08]) pws = np.array([1.42, 1.42, 1.42, 1.42, 4.0, 1.0]) - out = spectrum.spectral_factor_caballero(pws, ams, aods, - module_type=module_type) + out = spectrum.spectral_factor_caballero( + pws, ams, aods, module_type=module_type + ) assert np.allclose(expected, out, atol=1e-3) def test_spectral_factor_caballero_supplied(): # use the cdte coeffs coeffs = ( - 1.0044, 0.0095, -0.0037, 0.0002, 0.0000, -0.0046, - -0.0182, 0, 0.0095, 0.0068, 0, 1) + 1.0044, + 0.0095, + -0.0037, + 0.0002, + 0.0000, + -0.0046, + -0.0182, + 0, + 0.0095, + 0.0068, + 0, + 1, + ) out = spectrum.spectral_factor_caballero(1, 1, 1, coefficients=coeffs) expected = 1.0021964 assert_allclose(out, expected, atol=1e-3) @@ -186,49 +244,75 @@ def test_spectral_factor_caballero_supplied(): def test_spectral_factor_caballero_supplied_redundant(): # Error when specifying both module_type and coefficients coeffs = ( - 1.0044, 0.0095, -0.0037, 0.0002, 0.0000, -0.0046, - -0.0182, 0, 0.0095, 0.0068, 0, 1) + 1.0044, + 0.0095, + -0.0037, + 0.0002, + 0.0000, + -0.0046, + -0.0182, + 0, + 0.0095, + 0.0068, + 0, + 1, + ) with pytest.raises(ValueError): - spectrum.spectral_factor_caballero(1, 1, 1, module_type='cdte', - coefficients=coeffs) + spectrum.spectral_factor_caballero( + 1, 1, 1, module_type="cdte", coefficients=coeffs + ) def test_spectral_factor_caballero_supplied_ambiguous(): # Error when specifying neither module_type nor coefficients with pytest.raises(ValueError): - spectrum.spectral_factor_caballero(1, 1, 1, module_type=None, - coefficients=None) - - -@pytest.mark.parametrize("module_type,expected", [ - ('asi', np.array([1.15534029, 1.1123772, 1.08286684, 1.01915462])), - ('fs-2', np.array([1.0694323, 1.04948777, 1.03556288, 0.9881471])), - ('fs-4', np.array([1.05234725, 1.037771, 1.0275516, 0.98820533])), - ('multisi', np.array([1.03310403, 1.02391703, 1.01744833, 0.97947605])), - ('monosi', np.array([1.03225083, 1.02335353, 1.01708734, 0.97950110])), - ('cigs', np.array([1.01475834, 1.01143927, 1.00909094, 0.97852966])), -]) + spectrum.spectral_factor_caballero( + 1, 1, 1, module_type=None, coefficients=None + ) + + +@pytest.mark.parametrize( + "module_type,expected", + [ + ("asi", np.array([1.15534029, 1.1123772, 1.08286684, 1.01915462])), + ("fs-2", np.array([1.0694323, 1.04948777, 1.03556288, 0.9881471])), + ("fs-4", np.array([1.05234725, 1.037771, 1.0275516, 0.98820533])), + ( + "multisi", + np.array([1.03310403, 1.02391703, 1.01744833, 0.97947605]), + ), + ("monosi", np.array([1.03225083, 1.02335353, 1.01708734, 0.97950110])), + ("cigs", np.array([1.01475834, 1.01143927, 1.00909094, 0.97852966])), + ], +) def test_spectral_factor_pvspec(module_type, expected): ams = np.array([1.0, 1.5, 2.0, 1.5]) kcs = np.array([0.4, 0.6, 0.8, 1.4]) - out = spectrum.spectral_factor_pvspec(ams, kcs, - module_type=module_type) + out = spectrum.spectral_factor_pvspec(ams, kcs, module_type=module_type) assert np.allclose(expected, out, atol=1e-8) -@pytest.mark.parametrize("module_type,expected", [ - ('asi', pd.Series([1.15534029, 1.1123772, 1.08286684, 1.01915462])), - ('fs-2', pd.Series([1.0694323, 1.04948777, 1.03556288, 0.9881471])), - ('fs-4', pd.Series([1.05234725, 1.037771, 1.0275516, 0.98820533])), - ('multisi', pd.Series([1.03310403, 1.02391703, 1.01744833, 0.97947605])), - ('monosi', pd.Series([1.03225083, 1.02335353, 1.01708734, 0.97950110])), - ('cigs', pd.Series([1.01475834, 1.01143927, 1.00909094, 0.97852966])), -]) +@pytest.mark.parametrize( + "module_type,expected", + [ + ("asi", pd.Series([1.15534029, 1.1123772, 1.08286684, 1.01915462])), + ("fs-2", pd.Series([1.0694323, 1.04948777, 1.03556288, 0.9881471])), + ("fs-4", pd.Series([1.05234725, 1.037771, 1.0275516, 0.98820533])), + ( + "multisi", + pd.Series([1.03310403, 1.02391703, 1.01744833, 0.97947605]), + ), + ( + "monosi", + pd.Series([1.03225083, 1.02335353, 1.01708734, 0.97950110]), + ), + ("cigs", pd.Series([1.01475834, 1.01143927, 1.00909094, 0.97852966])), + ], +) def test_spectral_factor_pvspec_series(module_type, expected): ams = pd.Series([1.0, 1.5, 2.0, 1.5]) kcs = pd.Series([0.4, 0.6, 0.8, 1.4]) - out = spectrum.spectral_factor_pvspec(ams, kcs, - module_type=module_type) + out = spectrum.spectral_factor_pvspec(ams, kcs, module_type=module_type) assert isinstance(out, pd.Series) assert np.allclose(expected, out, atol=1e-8) @@ -244,39 +328,45 @@ def test_spectral_factor_pvspec_supplied(): def test_spectral_factor_pvspec_supplied_redundant(): # Error when specifying both module_type and coefficients coeffs = (0.9847, -0.05237, 0.03034) - with pytest.raises(ValueError, match='supply only one of'): - spectrum.spectral_factor_pvspec(1.5, 0.8, module_type='multisi', - coefficients=coeffs) + with pytest.raises(ValueError, match="supply only one of"): + spectrum.spectral_factor_pvspec( + 1.5, 0.8, module_type="multisi", coefficients=coeffs + ) def test_spectral_factor_pvspec_supplied_ambiguous(): # Error when specifying neither module_type nor coefficients - with pytest.raises(ValueError, match='No valid input provided'): - spectrum.spectral_factor_pvspec(1.5, 0.8, module_type=None, - coefficients=None) - - -@pytest.mark.parametrize("module_type,expected", [ - ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])), - ('cdte', np.array([1.09657, 1.05594, 1.02763, 0.97740])), -]) + with pytest.raises(ValueError, match="No valid input provided"): + spectrum.spectral_factor_pvspec( + 1.5, 0.8, module_type=None, coefficients=None + ) + + +@pytest.mark.parametrize( + "module_type,expected", + [ + ("multisi", np.array([1.06129, 1.03098, 1.01155, 0.99849])), + ("cdte", np.array([1.09657, 1.05594, 1.02763, 0.97740])), + ], +) def test_spectral_factor_jrc(module_type, expected): ams = np.array([1.0, 1.5, 2.0, 1.5]) kcs = np.array([0.4, 0.6, 0.8, 1.4]) - out = spectrum.spectral_factor_jrc(ams, kcs, - module_type=module_type) + out = spectrum.spectral_factor_jrc(ams, kcs, module_type=module_type) assert np.allclose(expected, out, atol=1e-4) -@pytest.mark.parametrize("module_type,expected", [ - ('multisi', np.array([1.06129, 1.03098, 1.01155, 0.99849])), - ('cdte', np.array([1.09657, 1.05594, 1.02763, 0.97740])), -]) +@pytest.mark.parametrize( + "module_type,expected", + [ + ("multisi", np.array([1.06129, 1.03098, 1.01155, 0.99849])), + ("cdte", np.array([1.09657, 1.05594, 1.02763, 0.97740])), + ], +) def test_spectral_factor_jrc_series(module_type, expected): ams = pd.Series([1.0, 1.5, 2.0, 1.5]) kcs = pd.Series([0.4, 0.6, 0.8, 1.4]) - out = spectrum.spectral_factor_jrc(ams, kcs, - module_type=module_type) + out = spectrum.spectral_factor_jrc(ams, kcs, module_type=module_type) assert isinstance(out, pd.Series) assert np.allclose(expected, out, atol=1e-4) @@ -292,13 +382,15 @@ def test_spectral_factor_jrc_supplied(): def test_spectral_factor_jrc_supplied_redundant(): # Error when specifying both module_type and coefficients coeffs = (0.494, 0.146, 0.00103) - with pytest.raises(ValueError, match='supply only one of'): - spectrum.spectral_factor_jrc(1.0, 0.8, module_type='multisi', - coefficients=coeffs) + with pytest.raises(ValueError, match="supply only one of"): + spectrum.spectral_factor_jrc( + 1.0, 0.8, module_type="multisi", coefficients=coeffs + ) def test_spectral_factor_jrc_supplied_ambiguous(): # Error when specifying neither module_type nor coefficients - with pytest.raises(ValueError, match='No valid input provided'): - spectrum.spectral_factor_jrc(1.0, 0.8, module_type=None, - coefficients=None) + with pytest.raises(ValueError, match="No valid input provided"): + spectrum.spectral_factor_jrc( + 1.0, 0.8, module_type=None, coefficients=None + ) diff --git a/pvlib/tests/spectrum/test_response.py b/pvlib/tests/spectrum/test_response.py index 2ffe572dce..19ee492fbf 100644 --- a/pvlib/tests/spectrum/test_response.py +++ b/pvlib/tests/spectrum/test_response.py @@ -58,12 +58,9 @@ def test_sr_to_qe(sr_and_eqe_fixture): assert_allclose(qe, sr_and_eqe_fixture["quantum_efficiency"]) # pandas series type # note: output Series' name should match the input - qe = spectrum.sr_to_qe( - sr_and_eqe_fixture["spectral_response"] - ) + qe = spectrum.sr_to_qe(sr_and_eqe_fixture["spectral_response"]) pd.testing.assert_series_equal( - qe, sr_and_eqe_fixture["quantum_efficiency"], - check_names=False + qe, sr_and_eqe_fixture["quantum_efficiency"], check_names=False ) assert qe.name == "spectral_response" # series normalization @@ -90,12 +87,9 @@ def test_qe_to_sr(sr_and_eqe_fixture): assert_allclose(sr, sr_and_eqe_fixture["spectral_response"]) # pandas series type # note: output Series' name should match the input - sr = spectrum.qe_to_sr( - sr_and_eqe_fixture["quantum_efficiency"] - ) + sr = spectrum.qe_to_sr(sr_and_eqe_fixture["quantum_efficiency"]) pd.testing.assert_series_equal( - sr, sr_and_eqe_fixture["spectral_response"], - check_names=False + sr, sr_and_eqe_fixture["spectral_response"], check_names=False ) assert sr.name == "quantum_efficiency" # series normalization @@ -110,9 +104,7 @@ def test_qe_to_sr(sr_and_eqe_fixture): ) # error on lack of wavelength parameter if no pandas object is provided with pytest.raises(TypeError, match="must have an '.index' attribute"): - _ = spectrum.qe_to_sr( - sr_and_eqe_fixture["quantum_efficiency"].values - ) + _ = spectrum.qe_to_sr(sr_and_eqe_fixture["quantum_efficiency"].values) def test_qe_and_sr_reciprocal_conversion(sr_and_eqe_fixture): diff --git a/pvlib/tests/spectrum/test_spectrl2.py b/pvlib/tests/spectrum/test_spectrl2.py index 38df01830f..5b142130e3 100644 --- a/pvlib/tests/spectrum/test_spectrl2.py +++ b/pvlib/tests/spectrum/test_spectrl2.py @@ -9,15 +9,19 @@ def test_spectrl2(spectrl2_data): # compare against output from solar_utils wrapper around NREL spectrl2_2.c kwargs, expected = spectrl2_data actual = spectrum.spectrl2(**kwargs) - assert_allclose(expected['wavelength'].values, actual['wavelength']) - assert_allclose(expected['specdif'].values, actual['dhi'].ravel(), - atol=7e-5) - assert_allclose(expected['specdir'].values, actual['dni'].ravel(), - atol=1.5e-4) - assert_allclose(expected['specetr'], actual['dni_extra'].ravel(), - atol=2e-4) - assert_allclose(expected['specglo'], actual['poa_global'].ravel(), - atol=1e-4) + assert_allclose(expected["wavelength"].values, actual["wavelength"]) + assert_allclose( + expected["specdif"].values, actual["dhi"].ravel(), atol=7e-5 + ) + assert_allclose( + expected["specdir"].values, actual["dni"].ravel(), atol=1.5e-4 + ) + assert_allclose( + expected["specetr"], actual["dni_extra"].ravel(), atol=2e-4 + ) + assert_allclose( + expected["specglo"], actual["poa_global"].ravel(), atol=1e-4 + ) def test_spectrl2_array(spectrl2_data): @@ -26,10 +30,17 @@ def test_spectrl2_array(spectrl2_data): kwargs = {k: np.array([v, v, v]) for k, v in kwargs.items()} actual = spectrum.spectrl2(**kwargs) - assert actual['wavelength'].shape == (122,) + assert actual["wavelength"].shape == (122,) - keys = ['dni_extra', 'dhi', 'dni', 'poa_sky_diffuse', 'poa_ground_diffuse', - 'poa_direct', 'poa_global'] + keys = [ + "dni_extra", + "dhi", + "dni", + "poa_sky_diffuse", + "poa_ground_diffuse", + "poa_direct", + "poa_global", + ] for key in keys: assert actual[key].shape == (122, 3) @@ -37,15 +48,22 @@ def test_spectrl2_array(spectrl2_data): def test_spectrl2_series(spectrl2_data): # test that supplying Series instead of scalars works kwargs, expected = spectrl2_data - kwargs.pop('dayofyear') - index = pd.to_datetime(['2020-03-15 10:45:59']*3) + kwargs.pop("dayofyear") + index = pd.to_datetime(["2020-03-15 10:45:59"] * 3) kwargs = {k: pd.Series([v, v, v], index=index) for k, v in kwargs.items()} actual = spectrum.spectrl2(**kwargs) - assert actual['wavelength'].shape == (122,) + assert actual["wavelength"].shape == (122,) - keys = ['dni_extra', 'dhi', 'dni', 'poa_sky_diffuse', 'poa_ground_diffuse', - 'poa_direct', 'poa_global'] + keys = [ + "dni_extra", + "dhi", + "dni", + "poa_sky_diffuse", + "poa_ground_diffuse", + "poa_direct", + "poa_global", + ] for key in keys: assert actual[key].shape == (122, 3) @@ -53,8 +71,8 @@ def test_spectrl2_series(spectrl2_data): def test_dayofyear_missing(spectrl2_data): # test that not specifying dayofyear with non-pandas inputs raises error kwargs, expected = spectrl2_data - kwargs.pop('dayofyear') - with pytest.raises(ValueError, match='dayofyear must be specified'): + kwargs.pop("dayofyear") + with pytest.raises(ValueError, match="dayofyear must be specified"): _ = spectrum.spectrl2(**kwargs) @@ -62,11 +80,11 @@ def test_aoi_gt_90(spectrl2_data): # test that returned irradiance values are non-negative when aoi > 90 # see GH #1348 kwargs, _ = spectrl2_data - kwargs['apparent_zenith'] = 70 - kwargs['aoi'] = 130 - kwargs['surface_tilt'] = 60 + kwargs["apparent_zenith"] = 70 + kwargs["aoi"] = 130 + kwargs["surface_tilt"] = 60 spectra = spectrum.spectrl2(**kwargs) - for key in ['poa_direct', 'poa_global']: - message = f'{key} contains negative values for aoi>90' + for key in ["poa_direct", "poa_global"]: + message = f"{key} contains negative values for aoi>90" assert np.all(spectra[key] >= 0), message diff --git a/pvlib/tests/test__deprecation.py b/pvlib/tests/test__deprecation.py index 31263c8a25..ba25f73f7c 100644 --- a/pvlib/tests/test__deprecation.py +++ b/pvlib/tests/test__deprecation.py @@ -10,20 +10,21 @@ import warnings -@pytest.mark.xfail(strict=True, - reason='fail_on_pvlib_version should cause test to fail') -@fail_on_pvlib_version('0.0') +@pytest.mark.xfail( + strict=True, reason="fail_on_pvlib_version should cause test to fail" +) +@fail_on_pvlib_version("0.0") def test_fail_on_pvlib_version(): pass # pragma: no cover -@fail_on_pvlib_version('100000.0') +@fail_on_pvlib_version("100000.0") def test_fail_on_pvlib_version_pass(): pass -@pytest.mark.xfail(strict=True, reason='ensure that the test is called') -@fail_on_pvlib_version('100000.0') +@pytest.mark.xfail(strict=True, reason="ensure that the test is called") +@fail_on_pvlib_version("100000.0") def test_fail_on_pvlib_version_fail_in_test(): raise Exception @@ -46,7 +47,7 @@ def deprec_func(): )(alt_func) -@fail_on_pvlib_version('350.9') +@fail_on_pvlib_version("350.9") def test_use_fixture_with_decorator(some_data, deprec_func): # test that the correct data is returned by the some_data fixture assert some_data == "some data" diff --git a/pvlib/tests/test_albedo.py b/pvlib/tests/test_albedo.py index 5e4c35258a..6fa6bfdfc9 100644 --- a/pvlib/tests/test_albedo.py +++ b/pvlib/tests/test_albedo.py @@ -8,22 +8,23 @@ def test_inland_water_dvoracek_default(): - result = albedo.inland_water_dvoracek(solar_elevation=90, - color_coeff=0.13, - wave_roughness_coeff=0.29) + result = albedo.inland_water_dvoracek( + solar_elevation=90, color_coeff=0.13, wave_roughness_coeff=0.29 + ) assert_allclose(result, 0.072, 0.001) def test_inland_water_dvoracek_negative_elevation(): - result = albedo.inland_water_dvoracek(solar_elevation=-60, - color_coeff=0.13, - wave_roughness_coeff=0.29) + result = albedo.inland_water_dvoracek( + solar_elevation=-60, color_coeff=0.13, wave_roughness_coeff=0.29 + ) assert_allclose(result, 0.13, 0.01) def test_inland_water_dvoracek_string_surface_condition(): - result = albedo.inland_water_dvoracek(solar_elevation=90, - surface_condition='clear_water_no_waves') # noqa: E501 + result = albedo.inland_water_dvoracek( + solar_elevation=90, surface_condition="clear_water_no_waves" + ) # noqa: E501 assert_allclose(result, 0.072, 0.001) @@ -31,54 +32,72 @@ def test_inland_water_dvoracek_ndarray(): solar_elevs = np.array([-50, 0, 20, 60, 90]) color_coeffs = np.array([0.1, 0.1, 0.2, 0.3, 0.4]) roughness_coeffs = np.array([0.3, 0.3, 0.8, 1.5, 2]) - result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs, - color_coeff=color_coeffs, - wave_roughness_coeff=roughness_coeffs) # noqa: E501 + result = albedo.inland_water_dvoracek( + solar_elevation=solar_elevs, + color_coeff=color_coeffs, + wave_roughness_coeff=roughness_coeffs, + ) # noqa: E501 expected = np.array([0.1, 0.1, 0.12875, 0.06278, 0.064]) assert_allclose(expected, result, atol=1e-5) def test_inland_water_dvoracek_series(): - times = pd.date_range(start="2015-01-01 00:00", end="2015-01-02 00:00", - freq="6h") + times = pd.date_range( + start="2015-01-01 00:00", end="2015-01-02 00:00", freq="6h" + ) solar_elevs = pd.Series([-50, 0, 20, 60, 90], index=times) color_coeffs = pd.Series([0.1, 0.1, 0.2, 0.3, 0.4], index=times) roughness_coeffs = pd.Series([0.1, 0.3, 0.8, 1.5, 2], index=times) - result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs, - color_coeff=color_coeffs, - wave_roughness_coeff=roughness_coeffs) # noqa: E501 + result = albedo.inland_water_dvoracek( + solar_elevation=solar_elevs, + color_coeff=color_coeffs, + wave_roughness_coeff=roughness_coeffs, + ) # noqa: E501 expected = pd.Series([0.1, 0.1, 0.12875, 0.06278, 0.064], index=times) assert_series_equal(expected, result, atol=1e-5) def test_inland_water_dvoracek_series_mix_with_array(): - times = pd.date_range(start="2015-01-01 00:00", end="2015-01-01 06:00", - freq="6h") + times = pd.date_range( + start="2015-01-01 00:00", end="2015-01-01 06:00", freq="6h" + ) solar_elevs = pd.Series([45, 60], index=times) color_coeffs = 0.13 roughness_coeffs = 0.29 - result = albedo.inland_water_dvoracek(solar_elevation=solar_elevs, - color_coeff=color_coeffs, - wave_roughness_coeff=roughness_coeffs) # noqa: E501 + result = albedo.inland_water_dvoracek( + solar_elevation=solar_elevs, + color_coeff=color_coeffs, + wave_roughness_coeff=roughness_coeffs, + ) # noqa: E501 expected = pd.Series([0.08555, 0.07787], index=times) assert_series_equal(expected, result, atol=1e-5) def test_inland_water_dvoracek_invalid(): - with pytest.raises(ValueError, match='Either a `surface_condition` has to ' - 'be chosen or a combination of `color_coeff` and' - ' `wave_roughness_coeff`.'): # no surface info given + with pytest.raises( + ValueError, + match="Either a `surface_condition` has to " + "be chosen or a combination of `color_coeff` and" + " `wave_roughness_coeff`.", + ): # no surface info given albedo.inland_water_dvoracek(solar_elevation=45) - with pytest.raises(KeyError, match='not_a_surface_type'): # invalid type - albedo.inland_water_dvoracek(solar_elevation=45, - surface_condition='not_a_surface_type') - with pytest.raises(ValueError, match='Either a `surface_condition` has to ' - 'be chosen or a combination of `color_coeff` and' - ' `wave_roughness_coeff`.'): # only one coeff given - albedo.inland_water_dvoracek(solar_elevation=45, - color_coeff=0.13) - with pytest.raises(ValueError, match='Either a `surface_condition` has to ' - 'be chosen or a combination of `color_coeff` and' - ' `wave_roughness_coeff`.'): # only one coeff given - albedo.inland_water_dvoracek(solar_elevation=45, - wave_roughness_coeff=0.29) + with pytest.raises(KeyError, match="not_a_surface_type"): # invalid type + albedo.inland_water_dvoracek( + solar_elevation=45, surface_condition="not_a_surface_type" + ) + with pytest.raises( + ValueError, + match="Either a `surface_condition` has to " + "be chosen or a combination of `color_coeff` and" + " `wave_roughness_coeff`.", + ): # only one coeff given + albedo.inland_water_dvoracek(solar_elevation=45, color_coeff=0.13) + with pytest.raises( + ValueError, + match="Either a `surface_condition` has to " + "be chosen or a combination of `color_coeff` and" + " `wave_roughness_coeff`.", + ): # only one coeff given + albedo.inland_water_dvoracek( + solar_elevation=45, wave_roughness_coeff=0.29 + ) diff --git a/pvlib/tests/test_atmosphere.py b/pvlib/tests/test_atmosphere.py index 2f0b5cadc2..225e7eb95b 100644 --- a/pvlib/tests/test_atmosphere.py +++ b/pvlib/tests/test_atmosphere.py @@ -20,7 +20,7 @@ def test_pres2alt(): def test_alt2pres(): out = atmosphere.alt2pres(np.array([-100, 0, 1000, 8000])) - expected = np.array([102532.073, 101324.999, 89874.750, 35600.496]) + expected = np.array([102532.073, 101324.999, 89874.750, 35600.496]) assert_allclose(out, expected, atol=0.001) @@ -29,23 +29,26 @@ def zeniths(): return np.array([100, 89.9, 80, 0]) -@pytest.mark.parametrize("model,expected", - [['simple', [nan, 572.958, 5.759, 1.000]], - ['kasten1966', [nan, 35.365, 5.580, 0.999]], - ['youngirvine1967', [ - nan, -2.251358367165932e+05, 5.5365, 1.0000]], - ['kastenyoung1989', [nan, 36.467, 5.586, 1.000]], - ['gueymard1993', [nan, 36.431, 5.581, 1.000]], - ['young1994', [nan, 30.733, 5.541, 1.000]], - ['pickering2002', [nan, 37.064, 5.581, 1.000]], - ['gueymard2003', [nan, 36.676, 5.590, 1.000]]]) +@pytest.mark.parametrize( + "model,expected", + [ + ["simple", [nan, 572.958, 5.759, 1.000]], + ["kasten1966", [nan, 35.365, 5.580, 0.999]], + ["youngirvine1967", [nan, -2.251358367165932e05, 5.5365, 1.0000]], + ["kastenyoung1989", [nan, 36.467, 5.586, 1.000]], + ["gueymard1993", [nan, 36.431, 5.581, 1.000]], + ["young1994", [nan, 30.733, 5.541, 1.000]], + ["pickering2002", [nan, 37.064, 5.581, 1.000]], + ["gueymard2003", [nan, 36.676, 5.590, 1.000]], + ], +) def test_airmass(model, expected, zeniths): out = atmosphere.get_relative_airmass(zeniths, model) expected = np.array(expected) assert_allclose(out, expected, equal_nan=True, atol=0.001) # test series in/out. index does not matter # hits the isinstance() block in get_relative_airmass - times = pd.date_range(start='20180101', periods=len(zeniths), freq='1s') + times = pd.date_range(start="20180101", periods=len(zeniths), freq="1s") zeniths = pd.Series(zeniths, index=times) expected = pd.Series(expected, index=times) out = atmosphere.get_relative_airmass(zeniths, model) @@ -58,15 +61,15 @@ def test_airmass_scalar(): def test_airmass_invalid(): with pytest.raises(ValueError): - atmosphere.get_relative_airmass(0, 'invalid') + atmosphere.get_relative_airmass(0, "invalid") def test_get_absolute_airmass(): # input am - relative_am = np.array([nan, 40, 2, .999]) + relative_am = np.array([nan, 40, 2, 0.999]) # call without pressure kwarg out = atmosphere.get_absolute_airmass(relative_am) - expected = np.array([nan, 40., 2., 0.999]) + expected = np.array([nan, 40.0, 2.0, 0.999]) assert_allclose(out, expected, equal_nan=True, atol=0.001) # call with pressure kwarg out = atmosphere.get_absolute_airmass(relative_am, pressure=90000) @@ -78,72 +81,80 @@ def test_gueymard94_pw(): temp_air = np.array([0, 20, 40]) relative_humidity = np.array([0, 30, 100]) temps_humids = np.array( - list(itertools.product(temp_air, relative_humidity))) + list(itertools.product(temp_air, relative_humidity)) + ) pws = atmosphere.gueymard94_pw(temps_humids[:, 0], temps_humids[:, 1]) expected = np.array( - [ 0.1 , 0.33702061, 1.12340202, 0.1 , - 1.12040963, 3.73469877, 0.1 , 3.44859767, 11.49532557]) + [ + 0.1, + 0.33702061, + 1.12340202, + 0.1, + 1.12040963, + 3.73469877, + 0.1, + 3.44859767, + 11.49532557, + ] + ) assert_allclose(pws, expected, atol=0.01) def test_tdew_to_rh_to_tdew(): - # dewpoint temp calculated with wmo and aekr coefficients - dewpoint_original = pd.Series([ - 15.0, 20.0, 25.0, 12.0, 8.0 - ]) + dewpoint_original = pd.Series([15.0, 20.0, 25.0, 12.0, 8.0]) temperature_ambient = pd.Series([20.0, 25.0, 30.0, 15.0, 10.0]) # Calculate relative humidity using pandas series as input relative_humidity = atmosphere.rh_from_tdew( - temp_air=temperature_ambient, - temp_dew=dewpoint_original + temp_air=temperature_ambient, temp_dew=dewpoint_original ) dewpoint_calculated = atmosphere.tdew_from_rh( - temp_air=temperature_ambient, - relative_humidity=relative_humidity + temp_air=temperature_ambient, relative_humidity=relative_humidity ) # test pd.testing.assert_series_equal( - dewpoint_original, - dewpoint_calculated, - check_names=False + dewpoint_original, dewpoint_calculated, check_names=False ) def test_rh_from_tdew(): - - dewpoint = pd.Series([ - 15.0, 20.0, 25.0, 12.0, 8.0 - ]) + dewpoint = pd.Series([15.0, 20.0, 25.0, 12.0, 8.0]) # relative humidity calculated with wmo and aekr coefficients - relative_humidity_wmo = pd.Series([ - 72.95185312581116, 73.81500029087906, 74.6401272083123, - 82.27063889868842, 87.39018119185337 - ]) - relative_humidity_aekr = pd.Series([ - 72.93876680928582, 73.8025121880607, 74.62820502423823, - 82.26135295757305, 87.38323744820416 - ]) + relative_humidity_wmo = pd.Series( + [ + 72.95185312581116, + 73.81500029087906, + 74.6401272083123, + 82.27063889868842, + 87.39018119185337, + ] + ) + relative_humidity_aekr = pd.Series( + [ + 72.93876680928582, + 73.8025121880607, + 74.62820502423823, + 82.26135295757305, + 87.38323744820416, + ] + ) temperature_ambient = pd.Series([20.0, 25.0, 30.0, 15.0, 10.0]) # Calculate relative humidity using pandas series as input rh_series = atmosphere.rh_from_tdew( - temp_air=temperature_ambient, - temp_dew=dewpoint + temp_air=temperature_ambient, temp_dew=dewpoint ) pd.testing.assert_series_equal( - rh_series, - relative_humidity_wmo, - check_names=False + rh_series, relative_humidity_wmo, check_names=False ) # Calulate relative humidity using pandas series as input @@ -151,27 +162,23 @@ def test_rh_from_tdew(): rh_series_aekr = atmosphere.rh_from_tdew( temp_air=temperature_ambient, temp_dew=dewpoint, - coeff=(6.1094, 17.625, 243.04) + coeff=(6.1094, 17.625, 243.04), ) pd.testing.assert_series_equal( - rh_series_aekr, - relative_humidity_aekr, - check_names=False + rh_series_aekr, relative_humidity_aekr, check_names=False ) # Calculate relative humidity using array as input rh_array = atmosphere.rh_from_tdew( - temp_air=temperature_ambient.to_numpy(), - temp_dew=dewpoint.to_numpy() + temp_air=temperature_ambient.to_numpy(), temp_dew=dewpoint.to_numpy() ) np.testing.assert_allclose(rh_array, relative_humidity_wmo.to_numpy()) # Calculate relative humidity using float as input rh_float = atmosphere.rh_from_tdew( - temp_air=temperature_ambient.iloc[0], - temp_dew=dewpoint.iloc[0] + temp_air=temperature_ambient.iloc[0], temp_dew=dewpoint.iloc[0] ) assert np.isclose(rh_float, relative_humidity_wmo.iloc[0]) @@ -179,27 +186,33 @@ def test_rh_from_tdew(): # Unit tests def test_tdew_from_rh(): - - dewpoint = pd.Series([ - 15.0, 20.0, 25.0, 12.0, 8.0 - ]) + dewpoint = pd.Series([15.0, 20.0, 25.0, 12.0, 8.0]) # relative humidity calculated with wmo and aekr coefficients - relative_humidity_wmo = pd.Series([ - 72.95185312581116, 73.81500029087906, 74.6401272083123, - 82.27063889868842, 87.39018119185337 - ]) - relative_humidity_aekr = pd.Series([ - 72.93876680928582, 73.8025121880607, 74.62820502423823, - 82.26135295757305, 87.38323744820416 - ]) + relative_humidity_wmo = pd.Series( + [ + 72.95185312581116, + 73.81500029087906, + 74.6401272083123, + 82.27063889868842, + 87.39018119185337, + ] + ) + relative_humidity_aekr = pd.Series( + [ + 72.93876680928582, + 73.8025121880607, + 74.62820502423823, + 82.26135295757305, + 87.38323744820416, + ] + ) temperature_ambient = pd.Series([20.0, 25.0, 30.0, 15.0, 10.0]) # test as series dewpoint_series = atmosphere.tdew_from_rh( - temp_air=temperature_ambient, - relative_humidity=relative_humidity_wmo + temp_air=temperature_ambient, relative_humidity=relative_humidity_wmo ) pd.testing.assert_series_equal( @@ -210,18 +223,17 @@ def test_tdew_from_rh(): dewpoint_series_aekr = atmosphere.tdew_from_rh( temp_air=temperature_ambient, relative_humidity=relative_humidity_aekr, - coeff=(6.1094, 17.625, 243.04) + coeff=(6.1094, 17.625, 243.04), ) pd.testing.assert_series_equal( - dewpoint_series_aekr, dewpoint, - check_names=False + dewpoint_series_aekr, dewpoint, check_names=False ) # test as numpy array dewpoint_array = atmosphere.tdew_from_rh( temp_air=temperature_ambient.to_numpy(), - relative_humidity=relative_humidity_wmo.to_numpy() + relative_humidity=relative_humidity_wmo.to_numpy(), ) np.testing.assert_allclose(dewpoint_array, dewpoint.to_numpy()) @@ -229,16 +241,18 @@ def test_tdew_from_rh(): # test as float dewpoint_float = atmosphere.tdew_from_rh( temp_air=temperature_ambient.iloc[0], - relative_humidity=relative_humidity_wmo.iloc[0] + relative_humidity=relative_humidity_wmo.iloc[0], ) assert np.isclose(dewpoint_float, dewpoint.iloc[0]) def test_first_solar_spectral_correction_deprecated(): - with pytest.warns(pvlibDeprecationWarning, - match='Use pvlib.spectrum.spectral_factor_firstsolar'): - atmosphere.first_solar_spectral_correction(1, 1, 'cdte') + with pytest.warns( + pvlibDeprecationWarning, + match="Use pvlib.spectrum.spectral_factor_firstsolar", + ): + atmosphere.first_solar_spectral_correction(1, 1, "cdte") def test_kasten96_lt(): @@ -247,17 +261,23 @@ def test_kasten96_lt(): pwat = np.array([0, 2.5, 5]) aod_bb = np.array([0, 0.1, 1]) lt_expected = np.array( - [[[1.3802, 2.4102, 11.6802], - [1.16303976, 2.37303976, 13.26303976], - [1.12101907, 2.51101907, 15.02101907]], - - [[2.95546945, 3.98546945, 13.25546945], - [2.17435443, 3.38435443, 14.27435443], - [1.99821967, 3.38821967, 15.89821967]], - - [[3.37410769, 4.40410769, 13.67410769], - [2.44311797, 3.65311797, 14.54311797], - [2.23134152, 3.62134152, 16.13134152]]] + [ + [ + [1.3802, 2.4102, 11.6802], + [1.16303976, 2.37303976, 13.26303976], + [1.12101907, 2.51101907, 15.02101907], + ], + [ + [2.95546945, 3.98546945, 13.25546945], + [2.17435443, 3.38435443, 14.27435443], + [1.99821967, 3.38821967, 15.89821967], + ], + [ + [3.37410769, 4.40410769, 13.67410769], + [2.44311797, 3.65311797, 14.54311797], + [2.23134152, 3.62134152, 16.13134152], + ], + ] ) lt = atmosphere.kasten96_lt(*np.meshgrid(amp, pwat, aod_bb)) assert np.allclose(lt, lt_expected, 1e-3) @@ -283,8 +303,9 @@ def test_bird_hulstrom80_aod_bb(): @pytest.fixture def windspeeds_data_powerlaw(): data = pd.DataFrame( - index=pd.date_range(start="2015-01-01 00:00", end="2015-01-01 05:00", - freq="1h"), + index=pd.date_range( + start="2015-01-01 00:00", end="2015-01-01 05:00", freq="1h" + ), columns=["wind_ref", "height_ref", "height_desired", "wind_calc"], data=[ (10, -2, 5, np.nan), @@ -292,8 +313,8 @@ def windspeeds_data_powerlaw(): (5, 4, 5, 5.067393209486324), (7, 6, 10, 7.2178684911195905), (10, 8, 20, 10.565167835216586), - (12, 10, 30, 12.817653329393977) - ] + (12, 10, 30, 12.817653329393977), + ], ) return data @@ -304,18 +325,22 @@ def test_windspeed_powerlaw_ndarray(windspeeds_data_powerlaw): windspeeds_data_powerlaw["wind_ref"].to_numpy(), windspeeds_data_powerlaw["height_ref"], windspeeds_data_powerlaw["height_desired"], - surface_type='unstable_air_above_open_water_surface') - assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(), - result_surface) + surface_type="unstable_air_above_open_water_surface", + ) + assert_allclose( + windspeeds_data_powerlaw["wind_calc"].to_numpy(), result_surface + ) # test wind speed estimation by passing in the exponent corresponding # to the surface_type above result_exponent = atmosphere.windspeed_powerlaw( windspeeds_data_powerlaw["wind_ref"].to_numpy(), windspeeds_data_powerlaw["height_ref"], windspeeds_data_powerlaw["height_desired"], - exponent=0.06) - assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(), - result_exponent) + exponent=0.06, + ) + assert_allclose( + windspeeds_data_powerlaw["wind_calc"].to_numpy(), result_exponent + ) def test_windspeed_powerlaw_series(windspeeds_data_powerlaw): @@ -323,29 +348,41 @@ def test_windspeed_powerlaw_series(windspeeds_data_powerlaw): windspeeds_data_powerlaw["wind_ref"], windspeeds_data_powerlaw["height_ref"], windspeeds_data_powerlaw["height_desired"], - surface_type='unstable_air_above_open_water_surface') - assert_series_equal(windspeeds_data_powerlaw["wind_calc"], - result, check_names=False) + surface_type="unstable_air_above_open_water_surface", + ) + assert_series_equal( + windspeeds_data_powerlaw["wind_calc"], result, check_names=False + ) def test_windspeed_powerlaw_invalid(): - with pytest.raises(ValueError, match="Either a 'surface_type' or an " - "'exponent' parameter must be given"): + with pytest.raises( + ValueError, + match="Either a 'surface_type' or an " + "'exponent' parameter must be given", + ): # no exponent or surface_type given - atmosphere.windspeed_powerlaw(wind_speed_reference=10, - height_reference=5, - height_desired=10) - with pytest.raises(ValueError, match="Either a 'surface_type' or an " - "'exponent' parameter must be given"): + atmosphere.windspeed_powerlaw( + wind_speed_reference=10, height_reference=5, height_desired=10 + ) + with pytest.raises( + ValueError, + match="Either a 'surface_type' or an " + "'exponent' parameter must be given", + ): # no exponent or surface_type given - atmosphere.windspeed_powerlaw(wind_speed_reference=10, - height_reference=5, - height_desired=10, - exponent=1.2, - surface_type="surf") - with pytest.raises(KeyError, match='not_an_exponent'): + atmosphere.windspeed_powerlaw( + wind_speed_reference=10, + height_reference=5, + height_desired=10, + exponent=1.2, + surface_type="surf", + ) + with pytest.raises(KeyError, match="not_an_exponent"): # invalid surface_type - atmosphere.windspeed_powerlaw(wind_speed_reference=10, - height_reference=5, - height_desired=10, - surface_type='not_an_exponent') + atmosphere.windspeed_powerlaw( + wind_speed_reference=10, + height_reference=5, + height_desired=10, + surface_type="not_an_exponent", + ) diff --git a/pvlib/tests/test_clearsky.py b/pvlib/tests/test_clearsky.py index f7300b98a9..be3483e3f1 100644 --- a/pvlib/tests/test_clearsky.py +++ b/pvlib/tests/test_clearsky.py @@ -20,56 +20,116 @@ def test_ineichen_series(): - times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h', - tz='America/Phoenix') - apparent_zenith = pd.Series(np.array( - [124.0390863, 113.38779941, 82.85457044, 46.0467599, 10.56413562, - 34.86074109, 72.41687122, 105.69538659, 124.05614124]), - index=times) - am = pd.Series(np.array( - [nan, nan, 6.97935524, 1.32355476, 0.93527685, - 1.12008114, 3.01614096, nan, nan]), - index=times) - expected = pd.DataFrame(np. - array([[ 0. , 0. , 0. ], - [ 0. , 0. , 0. ], - [ 65.49426624, 321.16092181, 25.54562017], - [ 704.6968125 , 888.90147035, 87.73601277], - [1044.1230677 , 953.24925854, 107.03109696], - [ 853.02065704, 922.06124712, 96.42909484], - [ 251.99427693, 655.44925241, 53.9901349 ], - [ 0. , 0. , 0. ], - [ 0. , 0. , 0. ]]), - columns=['ghi', 'dni', 'dhi'], - index=times) + times = pd.date_range( + start="2014-06-24", end="2014-06-25", freq="3h", tz="America/Phoenix" + ) + apparent_zenith = pd.Series( + np.array( + [ + 124.0390863, + 113.38779941, + 82.85457044, + 46.0467599, + 10.56413562, + 34.86074109, + 72.41687122, + 105.69538659, + 124.05614124, + ] + ), + index=times, + ) + am = pd.Series( + np.array( + [ + nan, + nan, + 6.97935524, + 1.32355476, + 0.93527685, + 1.12008114, + 3.01614096, + nan, + nan, + ] + ), + index=times, + ) + expected = pd.DataFrame( + np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [65.49426624, 321.16092181, 25.54562017], + [704.6968125, 888.90147035, 87.73601277], + [1044.1230677, 953.24925854, 107.03109696], + [853.02065704, 922.06124712, 96.42909484], + [251.99427693, 655.44925241, 53.9901349], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + ), + columns=["ghi", "dni", "dhi"], + index=times, + ) out = clearsky.ineichen(apparent_zenith, am, 3) assert_frame_equal(expected, out) def test_ineichen_series_perez_enhancement(): - times = pd.date_range(start='2014-06-24', end='2014-06-25', freq='3h', - tz='America/Phoenix') - apparent_zenith = pd.Series(np.array( - [124.0390863, 113.38779941, 82.85457044, 46.0467599, 10.56413562, - 34.86074109, 72.41687122, 105.69538659, 124.05614124]), - index=times) - am = pd.Series(np.array( - [nan, nan, 6.97935524, 1.32355476, 0.93527685, - 1.12008114, 3.01614096, nan, nan]), - index=times) - expected = pd.DataFrame(np. - array([[ 0. , 0. , 0. ], - [ 0. , 0. , 0. ], - [ 91.1249279 , 321.16092171, 51.17628184], - [ 716.46580547, 888.9014706 , 99.50500553], - [1053.42066073, 953.24925905, 116.3286895 ], - [ 863.54692748, 922.06124652, 106.9553658 ], - [ 271.06382275, 655.44925213, 73.05968076], - [ 0. , 0. , 0. ], - [ 0. , 0. , 0. ]]), - columns=['ghi', 'dni', 'dhi'], - index=times) + times = pd.date_range( + start="2014-06-24", end="2014-06-25", freq="3h", tz="America/Phoenix" + ) + apparent_zenith = pd.Series( + np.array( + [ + 124.0390863, + 113.38779941, + 82.85457044, + 46.0467599, + 10.56413562, + 34.86074109, + 72.41687122, + 105.69538659, + 124.05614124, + ] + ), + index=times, + ) + am = pd.Series( + np.array( + [ + nan, + nan, + 6.97935524, + 1.32355476, + 0.93527685, + 1.12008114, + 3.01614096, + nan, + nan, + ] + ), + index=times, + ) + expected = pd.DataFrame( + np.array( + [ + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [91.1249279, 321.16092171, 51.17628184], + [716.46580547, 888.9014706, 99.50500553], + [1053.42066073, 953.24925905, 116.3286895], + [863.54692748, 922.06124652, 106.9553658], + [271.06382275, 655.44925213, 73.05968076], + [0.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + ] + ), + columns=["ghi", "dni", "dhi"], + index=times, + ) out = clearsky.ineichen(apparent_zenith, am, 3, perez_enhancement=True) assert_frame_equal(expected, out) @@ -77,11 +137,11 @@ def test_ineichen_series_perez_enhancement(): def test_ineichen_scalar_input(): expected = OrderedDict() - expected['ghi'] = 1038.159219 - expected['dni'] = 942.2081860378344 - expected['dhi'] = 110.26529293612793 + expected["ghi"] = 1038.159219 + expected["dni"] = 942.2081860378344 + expected["dhi"] = 110.26529293612793 - out = clearsky.ineichen(10., 1., 3.) + out = clearsky.ineichen(10.0, 1.0, 3.0) for k, v in expected.items(): assert_allclose(expected[k], out[k]) @@ -89,28 +149,29 @@ def test_ineichen_scalar_input(): def test_ineichen_nans(): length = 4 - apparent_zenith = np.full(length, 10.) + apparent_zenith = np.full(length, 10.0) apparent_zenith[0] = np.nan - linke_turbidity = np.full(length, 3.) + linke_turbidity = np.full(length, 3.0) linke_turbidity[1] = np.nan - dni_extra = np.full(length, 1370.) + dni_extra = np.full(length, 1370.0) dni_extra[2] = np.nan - airmass_absolute = np.full(length, 1.) + airmass_absolute = np.full(length, 1.0) expected = OrderedDict() - expected['ghi'] = np.full(length, np.nan) - expected['dni'] = np.full(length, np.nan) - expected['dhi'] = np.full(length, np.nan) + expected["ghi"] = np.full(length, np.nan) + expected["dni"] = np.full(length, np.nan) + expected["dhi"] = np.full(length, np.nan) - expected['ghi'][length-1] = 1042.72590228 - expected['dni'][length-1] = 946.35279683 - expected['dhi'][length-1] = 110.75033088 + expected["ghi"][length - 1] = 1042.72590228 + expected["dni"][length - 1] = 946.35279683 + expected["dhi"][length - 1] = 110.75033088 - out = clearsky.ineichen(apparent_zenith, airmass_absolute, - linke_turbidity, dni_extra=dni_extra) + out = clearsky.ineichen( + apparent_zenith, airmass_absolute, linke_turbidity, dni_extra=dni_extra + ) for k, v in expected.items(): assert_allclose(expected[k], out[k]) @@ -119,51 +180,73 @@ def test_ineichen_nans(): def test_ineichen_arrays(): expected = OrderedDict() - expected['ghi'] = (np. - array([[[1095.77074798, 1054.17449885, 1014.15727338], - [ 839.40909243, 807.54451692, 776.88954373], - [ 190.27859353, 183.05548067, 176.10656239]], - - [[ 773.49041181, 625.19479557, 505.33080493], - [ 592.52803177, 478.92699901, 387.10585505], - [ 134.31520045, 108.56393694, 87.74977339]], - - [[ 545.9968869 , 370.78162375, 251.79449885], - [ 418.25788117, 284.03520249, 192.88577665], - [ 94.81136442, 64.38555328, 43.72365587]]])) - - expected['dni'] = (np. - array([[[1014.38807396, 942.20818604, 861.11344424], - [1014.38807396, 942.20818604, 861.11344424], - [1014.38807396, 942.20818604, 861.11344424]], - - [[ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235], - [ 687.61305142, 419.14891162, 255.50098235]], - - [[ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012], - [ 458.62196014, 186.46177428, 75.80970012]]])) - - expected['dhi'] = (np. - array([[[ 81.38267402, 111.96631281, 153.04382915], - [ 62.3427452 , 85.77117175, 117.23837487], - [ 14.13195304, 19.44274618, 26.57578203]], + expected["ghi"] = np.array( + [ + [ + [1095.77074798, 1054.17449885, 1014.15727338], + [839.40909243, 807.54451692, 776.88954373], + [190.27859353, 183.05548067, 176.10656239], + ], + [ + [773.49041181, 625.19479557, 505.33080493], + [592.52803177, 478.92699901, 387.10585505], + [134.31520045, 108.56393694, 87.74977339], + ], + [ + [545.9968869, 370.78162375, 251.79449885], + [418.25788117, 284.03520249, 192.88577665], + [94.81136442, 64.38555328, 43.72365587], + ], + ] + ) - [[ 85.87736039, 206.04588395, 249.82982258], - [ 65.78587472, 157.84030442, 191.38074731], - [ 14.91244713, 35.77949226, 43.38249342]], + expected["dni"] = np.array( + [ + [ + [1014.38807396, 942.20818604, 861.11344424], + [1014.38807396, 942.20818604, 861.11344424], + [1014.38807396, 942.20818604, 861.11344424], + ], + [ + [687.61305142, 419.14891162, 255.50098235], + [687.61305142, 419.14891162, 255.50098235], + [687.61305142, 419.14891162, 255.50098235], + ], + [ + [458.62196014, 186.46177428, 75.80970012], + [458.62196014, 186.46177428, 75.80970012], + [458.62196014, 186.46177428, 75.80970012], + ], + ] + ) - [[ 87.37492676, 184.31984947, 175.98479873], - [ 66.93307711, 141.19719644, 134.81217714], - [ 15.17249681, 32.00680597, 30.5594396 ]]])) + expected["dhi"] = np.array( + [ + [ + [81.38267402, 111.96631281, 153.04382915], + [62.3427452, 85.77117175, 117.23837487], + [14.13195304, 19.44274618, 26.57578203], + ], + [ + [85.87736039, 206.04588395, 249.82982258], + [65.78587472, 157.84030442, 191.38074731], + [14.91244713, 35.77949226, 43.38249342], + ], + [ + [87.37492676, 184.31984947, 175.98479873], + [66.93307711, 141.19719644, 134.81217714], + [15.17249681, 32.00680597, 30.5594396], + ], + ] + ) apparent_zenith = np.linspace(0, 80, 3) airmass_absolute = np.linspace(1, 10, 3) linke_turbidity = np.linspace(2, 4, 3) - apparent_zenith, airmass_absolute, linke_turbidity = \ - np.meshgrid(apparent_zenith, airmass_absolute, linke_turbidity) + apparent_zenith, airmass_absolute, linke_turbidity = np.meshgrid( + apparent_zenith, airmass_absolute, linke_turbidity + ) out = clearsky.ineichen(apparent_zenith, airmass_absolute, linke_turbidity) @@ -173,8 +256,9 @@ def test_ineichen_arrays(): def test_ineichen_dni_extra(): expected = pd.DataFrame( - np.array([[1042.72590228, 946.35279683, 110.75033088]]), - columns=['ghi', 'dni', 'dhi']) + np.array([[1042.72590228, 946.35279683, 110.75033088]]), + columns=["ghi", "dni", "dhi"], + ) out = clearsky.ineichen(10, 1, 3, dni_extra=pd.Series(1370)) assert_frame_equal(expected, out) @@ -182,16 +266,18 @@ def test_ineichen_dni_extra(): def test_ineichen_altitude(): expected = pd.DataFrame( - np.array([[1134.24312405, 994.95377835, 154.40492924]]), - columns=['ghi', 'dni', 'dhi']) + np.array([[1134.24312405, 994.95377835, 154.40492924]]), + columns=["ghi", "dni", "dhi"], + ) out = clearsky.ineichen(10, 1, 3, altitude=pd.Series(2000)) assert_frame_equal(expected, out) def test_lookup_linke_turbidity(): - times = pd.date_range(start='2014-06-24', end='2014-06-25', - freq='12h', tz='America/Phoenix') + times = pd.date_range( + start="2014-06-24", end="2014-06-25", freq="12h", tz="America/Phoenix" + ) # expect same value on 2014-06-24 0000 and 1200, and # diff value on 2014-06-25 expected = pd.Series( @@ -202,8 +288,9 @@ def test_lookup_linke_turbidity(): def test_lookup_linke_turbidity_leapyear(): - times = pd.date_range(start='2016-06-24', end='2016-06-25', - freq='12h', tz='America/Phoenix') + times = pd.date_range( + start="2016-06-24", end="2016-06-25", freq="12h", tz="America/Phoenix" + ) # expect same value on 2016-06-24 0000 and 1200, and # diff value on 2016-06-25 expected = pd.Series( @@ -214,18 +301,21 @@ def test_lookup_linke_turbidity_leapyear(): def test_lookup_linke_turbidity_nointerp(): - times = pd.date_range(start='2014-06-24', end='2014-06-25', - freq='12h', tz='America/Phoenix') + times = pd.date_range( + start="2014-06-24", end="2014-06-25", freq="12h", tz="America/Phoenix" + ) # expect same value for all days - expected = pd.Series(np.array([3., 3., 3.]), index=times) - out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875, - interp_turbidity=False) + expected = pd.Series(np.array([3.0, 3.0, 3.0]), index=times) + out = clearsky.lookup_linke_turbidity( + times, 32.125, -110.875, interp_turbidity=False + ) assert_series_equal(expected, out) def test_lookup_linke_turbidity_months(): - times = pd.date_range(start='2014-04-01', end='2014-07-01', - freq='1M', tz='America/Phoenix') + times = pd.date_range( + start="2014-04-01", end="2014-07-01", freq="1M", tz="America/Phoenix" + ) expected = pd.Series( np.array([2.89918032787, 2.97540983607, 3.19672131148]), index=times ) @@ -234,8 +324,9 @@ def test_lookup_linke_turbidity_months(): def test_lookup_linke_turbidity_months_leapyear(): - times = pd.date_range(start='2016-04-01', end='2016-07-01', - freq='1M', tz='America/Phoenix') + times = pd.date_range( + start="2016-04-01", end="2016-07-01", freq="1M", tz="America/Phoenix" + ) expected = pd.Series( np.array([2.89918032787, 2.97540983607, 3.19672131148]), index=times ) @@ -244,45 +335,59 @@ def test_lookup_linke_turbidity_months_leapyear(): def test_lookup_linke_turbidity_nointerp_months(): - times = pd.date_range(start='2014-04-10', end='2014-07-10', - freq='1M', tz='America/Phoenix') - expected = pd.Series(np.array([2.85, 2.95, 3.]), index=times) - out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875, - interp_turbidity=False) + times = pd.date_range( + start="2014-04-10", end="2014-07-10", freq="1M", tz="America/Phoenix" + ) + expected = pd.Series(np.array([2.85, 2.95, 3.0]), index=times) + out = clearsky.lookup_linke_turbidity( + times, 32.125, -110.875, interp_turbidity=False + ) assert_series_equal(expected, out) # changing the dates shouldn't matter if interp=False - times = pd.date_range(start='2014-04-05', end='2014-07-05', - freq='1M', tz='America/Phoenix') - out = clearsky.lookup_linke_turbidity(times, 32.125, -110.875, - interp_turbidity=False) + times = pd.date_range( + start="2014-04-05", end="2014-07-05", freq="1M", tz="America/Phoenix" + ) + out = clearsky.lookup_linke_turbidity( + times, 32.125, -110.875, interp_turbidity=False + ) assert_series_equal(expected, out) def test_haurwitz(): - apparent_solar_elevation = np.array([-20, -0.05, -0.001, 5, 10, 30, 50, 90]) + apparent_solar_elevation = np.array( + [-20, -0.05, -0.001, 5, 10, 30, 50, 90] + ) apparent_solar_zenith = 90 - apparent_solar_elevation - data_in = pd.DataFrame(data=apparent_solar_zenith, - index=apparent_solar_zenith, - columns=['apparent_zenith']) - expected = pd.DataFrame(np.array([0., - 0., - 0., - 48.6298687941956, - 135.741748091813, - 487.894132885425, - 778.766689344363, - 1035.09203253450]), - columns=['ghi'], - index=apparent_solar_zenith) - out = clearsky.haurwitz(data_in['apparent_zenith']) + data_in = pd.DataFrame( + data=apparent_solar_zenith, + index=apparent_solar_zenith, + columns=["apparent_zenith"], + ) + expected = pd.DataFrame( + np.array( + [ + 0.0, + 0.0, + 0.0, + 48.6298687941956, + 135.741748091813, + 487.894132885425, + 778.766689344363, + 1035.09203253450, + ] + ), + columns=["ghi"], + index=apparent_solar_zenith, + ) + out = clearsky.haurwitz(data_in["apparent_zenith"]) assert_frame_equal(expected, out) def test_simplified_solis_scalar_elevation(): expected = OrderedDict() - expected['ghi'] = 1064.653145 - expected['dni'] = 959.335463 - expected['dhi'] = 129.125602 + expected["ghi"] = 1064.653145 + expected["dni"] = 959.335463 + expected["dhi"] = 129.125602 out = clearsky.simplified_solis(80) for k, v in expected.items(): @@ -291,9 +396,9 @@ def test_simplified_solis_scalar_elevation(): def test_simplified_solis_scalar_neg_elevation(): expected = OrderedDict() - expected['ghi'] = 0 - expected['dni'] = 0 - expected['dhi'] = 0 + expected["ghi"] = 0 + expected["dni"] = 0 + expected["dhi"] = 0 out = clearsky.simplified_solis(-10) for k, v in expected.items(): @@ -302,45 +407,59 @@ def test_simplified_solis_scalar_neg_elevation(): def test_simplified_solis_series_elevation(): expected = pd.DataFrame( - np.array([[959.335463, 1064.653145, 129.125602]]), - columns=['dni', 'ghi', 'dhi']) - expected = expected[['ghi', 'dni', 'dhi']] + np.array([[959.335463, 1064.653145, 129.125602]]), + columns=["dni", "ghi", "dhi"], + ) + expected = expected[["ghi", "dni", "dhi"]] out = clearsky.simplified_solis(pd.Series(80)) assert_frame_equal(expected, out) def test_simplified_solis_dni_extra(): - expected = pd.DataFrame(np.array([[963.555414, 1069.33637, 129.693603]]), - columns=['dni', 'ghi', 'dhi']) - expected = expected[['ghi', 'dni', 'dhi']] + expected = pd.DataFrame( + np.array([[963.555414, 1069.33637, 129.693603]]), + columns=["dni", "ghi", "dhi"], + ) + expected = expected[["ghi", "dni", "dhi"]] out = clearsky.simplified_solis(80, dni_extra=pd.Series(1370)) assert_frame_equal(expected, out) def test_simplified_solis_pressure(): - expected = pd.DataFrame(np. - array([[ 964.26930718, 1067.96543669, 127.22841797], - [ 961.88811874, 1066.36847963, 128.1402539 ], - [ 959.58112234, 1064.81837558, 129.0304193 ]]), - columns=['dni', 'ghi', 'dhi']) - expected = expected[['ghi', 'dni', 'dhi']] + expected = pd.DataFrame( + np.array( + [ + [964.26930718, 1067.96543669, 127.22841797], + [961.88811874, 1066.36847963, 128.1402539], + [959.58112234, 1064.81837558, 129.0304193], + ] + ), + columns=["dni", "ghi", "dhi"], + ) + expected = expected[["ghi", "dni", "dhi"]] out = clearsky.simplified_solis( - 80, pressure=pd.Series([95000, 98000, 101000])) + 80, pressure=pd.Series([95000, 98000, 101000]) + ) assert_frame_equal(expected, out) def test_simplified_solis_aod700(): - expected = pd.DataFrame(np. - array([[ 1056.61710493, 1105.7229086 , 64.41747323], - [ 1007.50558875, 1085.74139063, 102.96233698], - [ 959.3354628 , 1064.65314509, 129.12560167], - [ 342.45810926, 638.63409683, 77.71786575], - [ 55.24140911, 7.5413313 , 0. ]]), - columns=['dni', 'ghi', 'dhi']) - expected = expected[['ghi', 'dni', 'dhi']] + expected = pd.DataFrame( + np.array( + [ + [1056.61710493, 1105.7229086, 64.41747323], + [1007.50558875, 1085.74139063, 102.96233698], + [959.3354628, 1064.65314509, 129.12560167], + [342.45810926, 638.63409683, 77.71786575], + [55.24140911, 7.5413313, 0.0], + ] + ), + columns=["dni", "ghi", "dhi"], + ) + expected = expected[["ghi", "dni", "dhi"]] aod700 = pd.Series([0.0, 0.05, 0.1, 1, 10]) out = clearsky.simplified_solis(80, aod700=aod700) @@ -348,26 +467,31 @@ def test_simplified_solis_aod700(): def test_simplified_solis_precipitable_water(): - expected = pd.DataFrame(np. - array([[ 1001.15353307, 1107.84678941, 128.58887606], - [ 1001.15353307, 1107.84678941, 128.58887606], - [ 983.51027357, 1089.62306672, 129.08755996], - [ 959.3354628 , 1064.65314509, 129.12560167], - [ 872.02335029, 974.18046717, 125.63581346]]), - columns=['dni', 'ghi', 'dhi']) - expected = expected[['ghi', 'dni', 'dhi']] + expected = pd.DataFrame( + np.array( + [ + [1001.15353307, 1107.84678941, 128.58887606], + [1001.15353307, 1107.84678941, 128.58887606], + [983.51027357, 1089.62306672, 129.08755996], + [959.3354628, 1064.65314509, 129.12560167], + [872.02335029, 974.18046717, 125.63581346], + ] + ), + columns=["dni", "ghi", "dhi"], + ) + expected = expected[["ghi", "dni", "dhi"]] out = clearsky.simplified_solis( - 80, precipitable_water=pd.Series([0.0, 0.2, 0.5, 1.0, 5.0])) + 80, precipitable_water=pd.Series([0.0, 0.2, 0.5, 1.0, 5.0]) + ) assert_frame_equal(expected, out) def test_simplified_solis_small_scalar_pw(): - expected = OrderedDict() - expected['ghi'] = 1107.84678941 - expected['dni'] = 1001.15353307 - expected['dhi'] = 128.58887606 + expected["ghi"] = 1107.84678941 + expected["dni"] = 1001.15353307 + expected["dhi"] = 128.58887606 out = clearsky.simplified_solis(80, precipitable_water=0.1) for k, v in expected.items(): @@ -377,14 +501,17 @@ def test_simplified_solis_small_scalar_pw(): def test_simplified_solis_return_arrays(): expected = OrderedDict() - expected['ghi'] = np.array([[ 1148.40081325, 913.42330823], - [ 965.48550828, 760.04527609]]) + expected["ghi"] = np.array( + [[1148.40081325, 913.42330823], [965.48550828, 760.04527609]] + ) - expected['dni'] = np.array([[ 1099.25706525, 656.24601381], - [ 915.31689149, 530.31697378]]) + expected["dni"] = np.array( + [[1099.25706525, 656.24601381], [915.31689149, 530.31697378]] + ) - expected['dhi'] = np.array([[ 64.1063074 , 254.6186615 ], - [ 62.75642216, 232.21931597]]) + expected["dhi"] = np.array( + [[64.1063074, 254.6186615], [62.75642216, 232.21931597]] + ) aod700 = np.linspace(0, 0.5, 2) precipitable_water = np.linspace(0, 10, 2) @@ -398,13 +525,12 @@ def test_simplified_solis_return_arrays(): def test_simplified_solis_nans_arrays(): - # construct input arrays that each have 1 nan offset from each other, # the last point is valid for all arrays length = 6 - apparent_elevation = np.full(length, 80.) + apparent_elevation = np.full(length, 80.0) apparent_elevation[0] = np.nan aod700 = np.full(length, 0.1) @@ -413,36 +539,36 @@ def test_simplified_solis_nans_arrays(): precipitable_water = np.full(length, 0.5) precipitable_water[2] = np.nan - pressure = np.full(length, 98000.) + pressure = np.full(length, 98000.0) pressure[3] = np.nan - dni_extra = np.full(length, 1370.) + dni_extra = np.full(length, 1370.0) dni_extra[4] = np.nan expected = OrderedDict() - expected['ghi'] = np.full(length, np.nan) - expected['dni'] = np.full(length, np.nan) - expected['dhi'] = np.full(length, np.nan) + expected["ghi"] = np.full(length, np.nan) + expected["dni"] = np.full(length, np.nan) + expected["dhi"] = np.full(length, np.nan) - expected['ghi'][length-1] = 1096.022736 - expected['dni'][length-1] = 990.306854 - expected['dhi'][length-1] = 128.664594 + expected["ghi"][length - 1] = 1096.022736 + expected["dni"][length - 1] = 990.306854 + expected["dhi"][length - 1] = 128.664594 - out = clearsky.simplified_solis(apparent_elevation, aod700, - precipitable_water, pressure, dni_extra) + out = clearsky.simplified_solis( + apparent_elevation, aod700, precipitable_water, pressure, dni_extra + ) for k, v in expected.items(): assert_allclose(expected[k], out[k]) def test_simplified_solis_nans_series(): - # construct input arrays that each have 1 nan offset from each other, # the last point is valid for all arrays length = 6 - apparent_elevation = pd.Series(np.full(length, 80.)) + apparent_elevation = pd.Series(np.full(length, 80.0)) apparent_elevation[0] = np.nan aod700 = np.full(length, 0.1) @@ -451,32 +577,33 @@ def test_simplified_solis_nans_series(): precipitable_water = np.full(length, 0.5) precipitable_water[2] = np.nan - pressure = np.full(length, 98000.) + pressure = np.full(length, 98000.0) pressure[3] = np.nan - dni_extra = np.full(length, 1370.) + dni_extra = np.full(length, 1370.0) dni_extra[4] = np.nan expected = OrderedDict() - expected['ghi'] = np.full(length, np.nan) - expected['dni'] = np.full(length, np.nan) - expected['dhi'] = np.full(length, np.nan) + expected["ghi"] = np.full(length, np.nan) + expected["dni"] = np.full(length, np.nan) + expected["dhi"] = np.full(length, np.nan) - expected['ghi'][length-1] = 1096.022736 - expected['dni'][length-1] = 990.306854 - expected['dhi'][length-1] = 128.664594 + expected["ghi"][length - 1] = 1096.022736 + expected["dni"][length - 1] = 990.306854 + expected["dhi"][length - 1] = 128.664594 expected = pd.DataFrame.from_dict(expected) - out = clearsky.simplified_solis(apparent_elevation, aod700, - precipitable_water, pressure, dni_extra) + out = clearsky.simplified_solis( + apparent_elevation, aod700, precipitable_water, pressure, dni_extra + ) assert_frame_equal(expected, out) def test_linke_turbidity_corners(): """Test Linke turbidity corners out of bounds.""" - months = pd.DatetimeIndex('%d/1/2016' % (m + 1) for m in range(12)) + months = pd.DatetimeIndex("%d/1/2016" % (m + 1) for m in range(12)) def monthly_lt_nointerp(lat, lon, time=months): """monthly Linke turbidity factor without time interpolation""" @@ -487,19 +614,23 @@ def monthly_lt_nointerp(lat, lon, time=months): # Northwest assert np.allclose( monthly_lt_nointerp(90, -180), - [1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9]) + [1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9], + ) # Southwest assert np.allclose( monthly_lt_nointerp(-90, -180), - [1.35, 1.3, 1.45, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.4, 1.4, 1.3]) + [1.35, 1.3, 1.45, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.4, 1.4, 1.3], + ) # Northeast assert np.allclose( monthly_lt_nointerp(90, 180), - [1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9]) + [1.9, 1.9, 1.9, 2.0, 2.05, 2.05, 2.1, 2.1, 2.0, 1.95, 1.9, 1.9], + ) # Southeast assert np.allclose( monthly_lt_nointerp(-90, 180), - [1.35, 1.7, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.7]) + [1.35, 1.7, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.35, 1.7], + ) # test out of range exceptions at corners with pytest.raises(IndexError): monthly_lt_nointerp(91, -122) # exceeds max latitude @@ -513,21 +644,25 @@ def monthly_lt_nointerp(lat, lon, time=months): @pytest.fixture def detect_clearsky_data(): - data_file = DATA_DIR / 'detect_clearsky_data.csv' + data_file = DATA_DIR / "detect_clearsky_data.csv" expected = pd.read_csv( - data_file, index_col=0, parse_dates=True, comment='#') - expected = expected.tz_localize('UTC').tz_convert('Etc/GMT+7') + data_file, index_col=0, parse_dates=True, comment="#" + ) + expected = expected.tz_localize("UTC").tz_convert("Etc/GMT+7") metadata = {} with data_file.open() as f: for line in f: - if line.startswith('#'): - key, value = line.strip('# \n').split(':') + if line.startswith("#"): + key, value = line.strip("# \n").split(":") metadata[key] = float(value) else: break - metadata['window_length'] = int(metadata['window_length']) - loc = Location(metadata['latitude'], metadata['longitude'], - altitude=metadata['elevation']) + metadata["window_length"] = int(metadata["window_length"]) + loc = Location( + metadata["latitude"], + metadata["longitude"], + altitude=metadata["elevation"], + ) # specify turbidity to guard against future lookup changes cs = loc.get_clearsky(expected.index, linke_turbidity=2.658197) return expected, cs @@ -537,21 +672,25 @@ def detect_clearsky_data(): def detect_clearsky_threshold_data(): # this is (roughly) just a 2 hour period of the same data in # detect_clearsky_data (which only spans 30 minutes) - data_file = DATA_DIR / 'detect_clearsky_threshold_data.csv' + data_file = DATA_DIR / "detect_clearsky_threshold_data.csv" expected = pd.read_csv( - data_file, index_col=0, parse_dates=True, comment='#') - expected = expected.tz_localize('UTC').tz_convert('Etc/GMT+7') + data_file, index_col=0, parse_dates=True, comment="#" + ) + expected = expected.tz_localize("UTC").tz_convert("Etc/GMT+7") metadata = {} with data_file.open() as f: for line in f: - if line.startswith('#'): - key, value = line.strip('# \n').split(':') + if line.startswith("#"): + key, value = line.strip("# \n").split(":") metadata[key] = float(value) else: break - metadata['window_length'] = int(metadata['window_length']) - loc = Location(metadata['latitude'], metadata['longitude'], - altitude=metadata['elevation']) + metadata["window_length"] = int(metadata["window_length"]) + loc = Location( + metadata["latitude"], + metadata["longitude"], + altitude=metadata["elevation"], + ) # specify turbidity to guard against future lookup changes cs = loc.get_clearsky(expected.index, linke_turbidity=2.658197) return expected, cs @@ -564,41 +703,60 @@ def test_clearsky_get_threshold(): def test_clearsky_get_threshold_raises_error(): - with pytest.raises(ValueError, match='can only be used for inputs'): + with pytest.raises(ValueError, match="can only be used for inputs"): clearsky._clearsky_get_threshold(0.5) -def test_detect_clearsky_calls_threshold(mocker, detect_clearsky_threshold_data): - threshold_spy = mocker.spy(clearsky, '_clearsky_get_threshold') +def test_detect_clearsky_calls_threshold( + mocker, detect_clearsky_threshold_data +): + threshold_spy = mocker.spy(clearsky, "_clearsky_get_threshold") expected, cs = detect_clearsky_threshold_data - threshold_actual = clearsky.detect_clearsky(expected['GHI'], cs['ghi'], - infer_limits=True) + threshold_actual = clearsky.detect_clearsky( + expected["GHI"], cs["ghi"], infer_limits=True + ) assert threshold_spy.call_count == 1 def test_detect_clearsky(detect_clearsky_data): expected, cs = detect_clearsky_data clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi'], times=cs.index, window_length=10) - assert_series_equal(expected['Clear or not'], clear_samples, - check_dtype=False, check_names=False) + expected["GHI"], cs["ghi"], times=cs.index, window_length=10 + ) + assert_series_equal( + expected["Clear or not"], + clear_samples, + check_dtype=False, + check_names=False, + ) def test_detect_clearsky_defaults(detect_clearsky_data): expected, cs = detect_clearsky_data - clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi']) - assert_series_equal(expected['Clear or not'], clear_samples, - check_dtype=False, check_names=False) + clear_samples = clearsky.detect_clearsky(expected["GHI"], cs["ghi"]) + assert_series_equal( + expected["Clear or not"], + clear_samples, + check_dtype=False, + check_names=False, + ) def test_detect_clearsky_components(detect_clearsky_data): expected, cs = detect_clearsky_data clear_samples, components, alpha = clearsky.detect_clearsky( - expected['GHI'], cs['ghi'], times=cs.index, window_length=10, - return_components=True) - assert_series_equal(expected['Clear or not'], clear_samples, - check_dtype=False, check_names=False) + expected["GHI"], + cs["ghi"], + times=cs.index, + window_length=10, + return_components=True, + ) + assert_series_equal( + expected["Clear or not"], + clear_samples, + check_dtype=False, + check_names=False, + ) assert isinstance(components, OrderedDict) assert np.allclose(alpha, 0.9633903181941296) @@ -608,32 +766,48 @@ def test_detect_clearsky_iterations(detect_clearsky_data): alpha = 1.0448 with pytest.warns(RuntimeWarning): clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi']*alpha, max_iterations=1) - assert clear_samples[:'2012-04-01 10:41:00'].all() - assert not clear_samples['2012-04-01 10:42:00':].any() # expected False + expected["GHI"], cs["ghi"] * alpha, max_iterations=1 + ) + assert clear_samples[:"2012-04-01 10:41:00"].all() + assert not clear_samples["2012-04-01 10:42:00":].any() # expected False clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi']*alpha, max_iterations=20) - assert_series_equal(expected['Clear or not'], clear_samples, - check_dtype=False, check_names=False) + expected["GHI"], cs["ghi"] * alpha, max_iterations=20 + ) + assert_series_equal( + expected["Clear or not"], + clear_samples, + check_dtype=False, + check_names=False, + ) def test_detect_clearsky_kwargs(detect_clearsky_data): expected, cs = detect_clearsky_data clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi'], times=cs.index, window_length=10, - mean_diff=1000, max_diff=1000, lower_line_length=-1000, - upper_line_length=1000, var_diff=10, slope_dev=1000) + expected["GHI"], + cs["ghi"], + times=cs.index, + window_length=10, + mean_diff=1000, + max_diff=1000, + lower_line_length=-1000, + upper_line_length=1000, + var_diff=10, + slope_dev=1000, + ) assert clear_samples.all() def test_detect_clearsky_window(detect_clearsky_data): expected, cs = detect_clearsky_data clear_samples = clearsky.detect_clearsky( - expected['GHI'], cs['ghi'], window_length=3) - expected = expected['Clear or not'].copy() + expected["GHI"], cs["ghi"], window_length=3 + ) + expected = expected["Clear or not"].copy() expected.iloc[-3:] = 1 - assert_series_equal(expected, clear_samples, - check_dtype=False, check_names=False) + assert_series_equal( + expected, clear_samples, check_dtype=False, check_names=False + ) def test_detect_clearsky_time_interval(detect_clearsky_data): @@ -642,18 +816,26 @@ def test_detect_clearsky_time_interval(detect_clearsky_data): cs2 = cs.iloc[u] expected2 = expected.iloc[u] clear_samples = clearsky.detect_clearsky( - expected2['GHI'], cs2['ghi'], window_length=6) - assert_series_equal(expected2['Clear or not'], clear_samples, - check_dtype=False, check_names=False) + expected2["GHI"], cs2["ghi"], window_length=6 + ) + assert_series_equal( + expected2["Clear or not"], + clear_samples, + check_dtype=False, + check_names=False, + ) def test_detect_clearsky_arrays(detect_clearsky_data): expected, cs = detect_clearsky_data clear_samples = clearsky.detect_clearsky( - expected['GHI'].values, cs['ghi'].values, times=cs.index, - window_length=10) + expected["GHI"].values, + cs["ghi"].values, + times=cs.index, + window_length=10, + ) assert isinstance(clear_samples, np.ndarray) - assert (clear_samples == expected['Clear or not'].values).all() + assert (clear_samples == expected["Clear or not"].values).all() def test_detect_clearsky_irregular_times(detect_clearsky_data): @@ -662,26 +844,27 @@ def test_detect_clearsky_irregular_times(detect_clearsky_data): times[0] += 10**9 times = pd.DatetimeIndex(times) with pytest.raises(NotImplementedError): - clearsky.detect_clearsky(expected['GHI'].values, cs['ghi'].values, - times, 10) + clearsky.detect_clearsky( + expected["GHI"].values, cs["ghi"].values, times, 10 + ) def test_detect_clearsky_missing_index(detect_clearsky_data): expected, cs = detect_clearsky_data with pytest.raises(ValueError): - clearsky.detect_clearsky(expected['GHI'].values, cs['ghi'].values) + clearsky.detect_clearsky(expected["GHI"].values, cs["ghi"].values) def test_detect_clearsky_not_enough_data(detect_clearsky_data): expected, cs = detect_clearsky_data - with pytest.raises(ValueError, match='times has only'): - clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=60) + with pytest.raises(ValueError, match="times has only"): + clearsky.detect_clearsky(expected["GHI"], cs["ghi"], window_length=60) def test_detect_clearsky_window_too_short(detect_clearsky_data): expected, cs = detect_clearsky_data with pytest.raises(ValueError, match="Samples per window of "): - clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=2) + clearsky.detect_clearsky(expected["GHI"], cs["ghi"], window_length=2) @pytest.mark.parametrize("window_length", [5, 10, 15, 20, 25]) @@ -699,11 +882,15 @@ def test_detect_clearsky_optimizer_not_failed( def detect_clearsky_helper_data(): samples_per_window = 3 sample_interval = 1 - x = pd.Series(np.arange(0, 7)**2.) + x = pd.Series(np.arange(0, 7) ** 2.0) # line length between adjacent points - sqt = pd.Series(np.sqrt(np.array([np.nan, 2., 10., 26., 50., 82, 122.]))) - H = hankel(np.arange(samples_per_window), - np.arange(samples_per_window-1, len(sqt))) + sqt = pd.Series( + np.sqrt(np.array([np.nan, 2.0, 10.0, 26.0, 50.0, 82, 122.0])) + ) + H = hankel( + np.arange(samples_per_window), + np.arange(samples_per_window - 1, len(sqt)), + ) return x, samples_per_window, sample_interval, H @@ -711,60 +898,73 @@ def test__line_length_windowed(detect_clearsky_helper_data): x, samples_per_window, sample_interval, H = detect_clearsky_helper_data # sqt is hand-calculated assuming window=3 # line length between adjacent points - sqt = pd.Series(np.sqrt(np.array([np.nan, 2., 10., 26., 50., 82, 122.]))) + sqt = pd.Series( + np.sqrt(np.array([np.nan, 2.0, 10.0, 26.0, 50.0, 82, 122.0])) + ) expected = {} - expected['line_length'] = sqt + sqt.shift(-1) + expected["line_length"] = sqt + sqt.shift(-1) result = clearsky._line_length_windowed( - x, H, samples_per_window, sample_interval) - assert_series_equal(result, expected['line_length']) + x, H, samples_per_window, sample_interval + ) + assert_series_equal(result, expected["line_length"]) def test__max_diff_windowed(detect_clearsky_helper_data): x, samples_per_window, sample_interval, H = detect_clearsky_helper_data expected = {} - expected['max_diff'] = pd.Series( - data=[np.nan, 3., 5., 7., 9., 11., np.nan], index=x.index) + expected["max_diff"] = pd.Series( + data=[np.nan, 3.0, 5.0, 7.0, 9.0, 11.0, np.nan], index=x.index + ) result = clearsky._max_diff_windowed(x, H, samples_per_window) - assert_series_equal(result, expected['max_diff']) + assert_series_equal(result, expected["max_diff"]) def test__calc_stats(detect_clearsky_helper_data): x, samples_per_window, sample_interval, H = detect_clearsky_helper_data # stats are hand-computed assuming window = 3, sample_interval = 1, # and right-aligned labels - mean_x = pd.Series(np.array([np.nan, np.nan, 5, 14, 29, 50, 77]) / 3.) + mean_x = pd.Series(np.array([np.nan, np.nan, 5, 14, 29, 50, 77]) / 3.0) max_x = pd.Series(np.array([np.nan, np.nan, 4, 9, 16, 25, 36])) - diff_std = np.array([np.nan, np.nan, np.sqrt(2), np.sqrt(2), np.sqrt(2), - np.sqrt(2), np.sqrt(2)]) + diff_std = np.array( + [ + np.nan, + np.nan, + np.sqrt(2), + np.sqrt(2), + np.sqrt(2), + np.sqrt(2), + np.sqrt(2), + ] + ) slope_nstd = diff_std / mean_x slope = x.diff().shift(-1) expected = {} - expected['mean'] = mean_x.shift(-1) # shift to align to center - expected['max'] = max_x.shift(-1) + expected["mean"] = mean_x.shift(-1) # shift to align to center + expected["max"] = max_x.shift(-1) # slope between adjacent points - expected['slope'] = slope - expected['slope_nstd'] = slope_nstd.shift(-1) - result = clearsky._calc_stats( - x, samples_per_window, sample_interval, H) + expected["slope"] = slope + expected["slope_nstd"] = slope_nstd.shift(-1) + result = clearsky._calc_stats(x, samples_per_window, sample_interval, H) res_mean, res_max, res_slope_nstd, res_slope = result - assert_series_equal(res_mean, expected['mean']) - assert_series_equal(res_max, expected['max']) - assert_series_equal(res_slope_nstd, expected['slope_nstd']) - assert_series_equal(res_slope, expected['slope']) + assert_series_equal(res_mean, expected["mean"]) + assert_series_equal(res_max, expected["max"]) + assert_series_equal(res_slope_nstd, expected["slope_nstd"]) + assert_series_equal(res_slope, expected["slope"]) def test_bird(): """Test Bird/Hulstrom Clearsky Model""" - times = pd.date_range(start='1/1/2015 0:00', end='12/31/2015 23:00', - freq='h') + times = pd.date_range( + start="1/1/2015 0:00", end="12/31/2015 23:00", freq="h" + ) tz = -7 # test timezone - gmt_tz = pytz.timezone('Etc/GMT%+d' % -(tz)) + gmt_tz = pytz.timezone("Etc/GMT%+d" % -(tz)) times = times.tz_localize(gmt_tz) # set timezone - times_utc = times.tz_convert('UTC') + times_utc = times.tz_convert("UTC") # match test data from BIRD_08_16_2012.xls - latitude = 40. - longitude = -105. - press_mB = 840. + latitude = 40.0 + longitude = -105.0 + press_mB = 840.0 o3_cm = 0.3 h2o_cm = 1.5 aod_500nm = 0.1 @@ -772,75 +972,91 @@ def test_bird(): b_a = 0.85 alb = 0.2 eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) - hour_angle = solarposition.hour_angle(times, longitude, eot) - 0.5 * 15. + hour_angle = solarposition.hour_angle(times, longitude, eot) - 0.5 * 15.0 declination = solarposition.declination_spencer71(times_utc.dayofyear) zenith = solarposition.solar_zenith_analytical( np.deg2rad(latitude), np.deg2rad(hour_angle), declination ) zenith = np.rad2deg(zenith) - airmass = atmosphere.get_relative_airmass(zenith, model='kasten1966') + airmass = atmosphere.get_relative_airmass(zenith, model="kasten1966") etr = irradiance.get_extra_radiation(times) # test Bird with time series data - field_names = ('dni', 'direct_horizontal', 'ghi', 'dhi') + field_names = ("dni", "direct_horizontal", "ghi", "dhi") irrads = clearsky.bird( - zenith, airmass, aod_380nm, aod_500nm, h2o_cm, o3_cm, press_mB * 100., - etr, b_a, alb + zenith, + airmass, + aod_380nm, + aod_500nm, + h2o_cm, + o3_cm, + press_mB * 100.0, + etr, + b_a, + alb, ) Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names) - data_path = DATA_DIR / 'BIRD_08_16_2012.csv' + data_path = DATA_DIR / "BIRD_08_16_2012.csv" testdata = pd.read_csv(data_path, usecols=range(1, 26), header=1).dropna() - testdata[['DEC', 'EQT']] = testdata[['DEC', 'EQT']].shift(tz) + testdata[["DEC", "EQT"]] = testdata[["DEC", "EQT"]].shift(tz) testdata = testdata[:tz] end = 48 + tz testdata.index = times[1:end] - assert np.allclose(testdata['DEC'], np.rad2deg(declination[1:end])) - assert np.allclose(testdata['EQT'], eot[1:end], rtol=1e-4) - assert np.allclose(testdata['Hour Angle'], hour_angle[1:end], rtol=1e-2) - assert np.allclose(testdata['Zenith Ang'], zenith[1:end], rtol=1e-2) - dawn = zenith < 88. - dusk = testdata['Zenith Ang'] < 88. - am = pd.Series(np.where(dawn, airmass, 0.), index=times).fillna(0.0) + assert np.allclose(testdata["DEC"], np.rad2deg(declination[1:end])) + assert np.allclose(testdata["EQT"], eot[1:end], rtol=1e-4) + assert np.allclose(testdata["Hour Angle"], hour_angle[1:end], rtol=1e-2) + assert np.allclose(testdata["Zenith Ang"], zenith[1:end], rtol=1e-2) + dawn = zenith < 88.0 + dusk = testdata["Zenith Ang"] < 88.0 + am = pd.Series(np.where(dawn, airmass, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Air Mass'].where(dusk, 0.), am[1:end], rtol=1e-3 + testdata["Air Mass"].where(dusk, 0.0), am[1:end], rtol=1e-3 ) - direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.) + direct_beam = pd.Series(np.where(dawn, Eb, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:end], rtol=1e-3 + testdata["Direct Beam"].where(dusk, 0.0), direct_beam[1:end], rtol=1e-3 ) - direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.) + direct_horz = pd.Series(np.where(dawn, Ebh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:end], rtol=1e-3 + testdata["Direct Hz"].where(dusk, 0.0), direct_horz[1:end], rtol=1e-3 ) - global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.) + global_horz = pd.Series(np.where(dawn, Gh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Global Hz'].where(dusk, 0.), global_horz[1:end], rtol=1e-3 + testdata["Global Hz"].where(dusk, 0.0), global_horz[1:end], rtol=1e-3 ) - diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.) + diffuse_horz = pd.Series(np.where(dawn, Dh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:end], rtol=1e-3 + testdata["Dif Hz"].where(dusk, 0.0), diffuse_horz[1:end], rtol=1e-3 ) # repeat test with albedo as a Series alb_series = pd.Series(0.2, index=times) irrads = clearsky.bird( - zenith, airmass, aod_380nm, aod_500nm, h2o_cm, o3_cm, press_mB * 100., - etr, b_a, alb_series + zenith, + airmass, + aod_380nm, + aod_500nm, + h2o_cm, + o3_cm, + press_mB * 100.0, + etr, + b_a, + alb_series, ) Eb, Ebh, Gh, Dh = (irrads[_] for _ in field_names) - direct_beam = pd.Series(np.where(dawn, Eb, 0.), index=times).fillna(0.) + direct_beam = pd.Series(np.where(dawn, Eb, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Direct Beam'].where(dusk, 0.), direct_beam[1:end], rtol=1e-3 + testdata["Direct Beam"].where(dusk, 0.0), direct_beam[1:end], rtol=1e-3 ) - direct_horz = pd.Series(np.where(dawn, Ebh, 0.), index=times).fillna(0.) + direct_horz = pd.Series(np.where(dawn, Ebh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Direct Hz'].where(dusk, 0.), direct_horz[1:end], rtol=1e-3 + testdata["Direct Hz"].where(dusk, 0.0), direct_horz[1:end], rtol=1e-3 ) - global_horz = pd.Series(np.where(dawn, Gh, 0.), index=times).fillna(0.) + global_horz = pd.Series(np.where(dawn, Gh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Global Hz'].where(dusk, 0.), global_horz[1:end], rtol=1e-3 + testdata["Global Hz"].where(dusk, 0.0), global_horz[1:end], rtol=1e-3 ) - diffuse_horz = pd.Series(np.where(dawn, Dh, 0.), index=times).fillna(0.) + diffuse_horz = pd.Series(np.where(dawn, Dh, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata['Dif Hz'].where(dusk, 0.), diffuse_horz[1:end], rtol=1e-3 + testdata["Dif Hz"].where(dusk, 0.0), diffuse_horz[1:end], rtol=1e-3 ) # test keyword parameters @@ -848,37 +1064,49 @@ def test_bird(): zenith, airmass, aod_380nm, aod_500nm, h2o_cm, dni_extra=etr ) Eb2, Ebh2, Gh2, Dh2 = (irrads2[_] for _ in field_names) - data_path = DATA_DIR / 'BIRD_08_16_2012_patm.csv' + data_path = DATA_DIR / "BIRD_08_16_2012_patm.csv" testdata2 = pd.read_csv(data_path, usecols=range(1, 26), header=1).dropna() - testdata2[['DEC', 'EQT']] = testdata2[['DEC', 'EQT']].shift(tz) + testdata2[["DEC", "EQT"]] = testdata2[["DEC", "EQT"]].shift(tz) testdata2 = testdata2[:tz] testdata2.index = times[1:end] - direct_beam2 = pd.Series(np.where(dawn, Eb2, 0.), index=times).fillna(0.) + direct_beam2 = pd.Series(np.where(dawn, Eb2, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata2['Direct Beam'].where(dusk, 0.), direct_beam2[1:end], - rtol=1e-3 + testdata2["Direct Beam"].where(dusk, 0.0), + direct_beam2[1:end], + rtol=1e-3, + ) + direct_horz2 = pd.Series(np.where(dawn, Ebh2, 0.0), index=times).fillna( + 0.0 ) - direct_horz2 = pd.Series(np.where(dawn, Ebh2, 0.), index=times).fillna(0.) assert np.allclose( - testdata2['Direct Hz'].where(dusk, 0.), direct_horz2[1:end], rtol=1e-3 + testdata2["Direct Hz"].where(dusk, 0.0), direct_horz2[1:end], rtol=1e-3 ) - global_horz2 = pd.Series(np.where(dawn, Gh2, 0.), index=times).fillna(0.) + global_horz2 = pd.Series(np.where(dawn, Gh2, 0.0), index=times).fillna(0.0) assert np.allclose( - testdata2['Global Hz'].where(dusk, 0.), global_horz2[1:end], rtol=1e-3 + testdata2["Global Hz"].where(dusk, 0.0), global_horz2[1:end], rtol=1e-3 + ) + diffuse_horz2 = pd.Series(np.where(dawn, Dh2, 0.0), index=times).fillna( + 0.0 ) - diffuse_horz2 = pd.Series(np.where(dawn, Dh2, 0.), index=times).fillna(0.) assert np.allclose( - testdata2['Dif Hz'].where(dusk, 0.), diffuse_horz2[1:end], rtol=1e-3 + testdata2["Dif Hz"].where(dusk, 0.0), diffuse_horz2[1:end], rtol=1e-3 ) # test scalars just at noon # XXX: calculations start at 12am so noon is at index = 12 irrads3 = clearsky.bird( - zenith[12], airmass[12], aod_380nm, aod_500nm, h2o_cm, - dni_extra=etr.iloc[12] + zenith[12], + airmass[12], + aod_380nm, + aod_500nm, + h2o_cm, + dni_extra=etr.iloc[12], ) Eb3, Ebh3, Gh3, Dh3 = (irrads3[_] for _ in field_names) # XXX: testdata starts at 1am so noon is at index = 11 np.allclose( [Eb3, Ebh3, Gh3, Dh3], - testdata2[['Direct Beam', 'Direct Hz', 'Global Hz', 'Dif Hz']].iloc[11], - rtol=1e-3) + testdata2[["Direct Beam", "Direct Hz", "Global Hz", "Dif Hz"]].iloc[ + 11 + ], + rtol=1e-3, + ) diff --git a/pvlib/tests/test_conftest.py b/pvlib/tests/test_conftest.py index c0baf7654d..14a7b43e6b 100644 --- a/pvlib/tests/test_conftest.py +++ b/pvlib/tests/test_conftest.py @@ -3,13 +3,15 @@ from pvlib.tests import conftest -@pytest.mark.parametrize('function_name', ['assert_index_equal', - 'assert_series_equal', - 'assert_frame_equal']) -@pytest.mark.parametrize('pd_version', ['1.0.0', '1.1.0']) -@pytest.mark.parametrize('check_less_precise', [True, False]) -def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, - check_less_precise): +@pytest.mark.parametrize( + "function_name", + ["assert_index_equal", "assert_series_equal", "assert_frame_equal"], +) +@pytest.mark.parametrize("pd_version", ["1.0.0", "1.1.0"]) +@pytest.mark.parametrize("check_less_precise", [True, False]) +def test__check_pandas_assert_kwargs( + mocker, function_name, pd_version, check_less_precise +): # test that conftest._check_pandas_assert_kwargs returns appropriate # kwargs for the assert_x_equal functions @@ -19,19 +21,19 @@ def test__check_pandas_assert_kwargs(mocker, function_name, pd_version, # patch the pandas assert; not interested in actually calling them, # plus we want to spy on how they get called. - spy = mocker.patch('pandas.testing.' + function_name) + spy = mocker.patch("pandas.testing." + function_name) # patch pd.__version__ to exercise the two branches in # conftest._check_pandas_assert_kwargs - mocker.patch('pandas.__version__', new=pd_version) + mocker.patch("pandas.__version__", new=pd_version) # finally, run the function and check what args got passed to pandas: assert_function = getattr(conftest, function_name) args = [None, None] assert_function(*args, check_less_precise=check_less_precise) - if pd_version == '1.1.0': + if pd_version == "1.1.0": tol = 1e-3 if check_less_precise else 1e-5 - expected_kwargs = {'atol': tol, 'rtol': tol} + expected_kwargs = {"atol": tol, "rtol": tol} else: - expected_kwargs = {'check_less_precise': check_less_precise} + expected_kwargs = {"check_less_precise": check_less_precise} spy.assert_called_once_with(*args, **expected_kwargs) diff --git a/pvlib/tests/test_iam.py b/pvlib/tests/test_iam.py index f5ca231bd4..8ff7970cbc 100644 --- a/pvlib/tests/test_iam.py +++ b/pvlib/tests/test_iam.py @@ -15,32 +15,59 @@ def test_ashrae(): - thetas = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 89., 90., - np.nan]) - expected = np.array([0, 0.9193437, 0.97928932, 0.99588039, 1., 0.99588039, - 0.97928932, 0.9193437, 0, 0, np.nan]) - iam = _iam.ashrae(thetas, .05) + thetas = np.array( + [-90.0, -67.5, -45.0, -22.5, 0.0, 22.5, 45.0, 67.5, 89.0, 90.0, np.nan] + ) + expected = np.array( + [ + 0, + 0.9193437, + 0.97928932, + 0.99588039, + 1.0, + 0.99588039, + 0.97928932, + 0.9193437, + 0, + 0, + np.nan, + ] + ) + iam = _iam.ashrae(thetas, 0.05) assert_allclose(iam, expected, equal_nan=True) iam_series = _iam.ashrae(pd.Series(thetas)) assert_series_equal(iam_series, pd.Series(expected)) def test_ashrae_scalar(): - thetas = -45. - iam = _iam.ashrae(thetas, .05) + thetas = -45.0 + iam = _iam.ashrae(thetas, 0.05) expected = 0.97928932 assert_allclose(iam, expected, equal_nan=True) thetas = np.nan - iam = _iam.ashrae(thetas, .05) + iam = _iam.ashrae(thetas, 0.05) expected = np.nan assert_allclose(iam, expected, equal_nan=True) def test_physical(): - aoi = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 90., - np.nan]) - expected = np.array([0, 0.8893998, 0.98797788, 0.99926198, 1, 0.99926198, - 0.98797788, 0.8893998, 0, np.nan]) + aoi = np.array( + [-90.0, -67.5, -45.0, -22.5, 0.0, 22.5, 45.0, 67.5, 90.0, np.nan] + ) + expected = np.array( + [ + 0, + 0.8893998, + 0.98797788, + 0.99926198, + 1, + 0.99926198, + 0.98797788, + 0.8893998, + 0, + np.nan, + ] + ) iam = _iam.physical(aoi, 1.526, 0.002, 4) assert_allclose(iam, expected, atol=1e-7, equal_nan=True) @@ -80,7 +107,7 @@ def test_physical_noar(): def test_physical_scalar(): - aoi = -45. + aoi = -45.0 iam = _iam.physical(aoi, 1.526, 0.002, 4) expected = 0.98797788 assert_allclose(iam, expected, equal_nan=True) @@ -91,8 +118,7 @@ def test_physical_scalar(): def test_martin_ruiz(): - - aoi = 45. + aoi = 45.0 a_r = 0.16 expected = 0.98986965 @@ -124,14 +150,12 @@ def test_martin_ruiz(): def test_martin_ruiz_exception(): - with pytest.raises(ValueError): _iam.martin_ruiz(0.0, a_r=0.0) def test_martin_ruiz_diffuse(): - - surface_tilt = 30. + surface_tilt = 30.0 a_r = 0.16 expected = (0.9549735, 0.7944426) @@ -145,10 +169,24 @@ def test_martin_ruiz_diffuse(): a_r = 0.18 surface_tilt = [0, 30, 90, 120, 180, np.nan, np.inf] - expected_sky = [0.9407678, 0.9452250, 0.9407678, 0.9055541, 0.0000000, - np.nan, np.nan] - expected_gnd = [0.0000000, 0.7610849, 0.9407678, 0.9483508, 0.9407678, - np.nan, np.nan] + expected_sky = [ + 0.9407678, + 0.9452250, + 0.9407678, + 0.9055541, + 0.0000000, + np.nan, + np.nan, + ] + expected_gnd = [ + 0.0000000, + 0.7610849, + 0.9407678, + 0.9483508, + 0.9407678, + np.nan, + np.nan, + ] # check various inputs as list iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r) @@ -162,17 +200,16 @@ def test_martin_ruiz_diffuse(): # check various inputs as Series surface_tilt = pd.Series(surface_tilt) - expected_sky = pd.Series(expected_sky, name='iam_sky') - expected_gnd = pd.Series(expected_gnd, name='iam_ground') + expected_sky = pd.Series(expected_sky, name="iam_sky") + expected_gnd = pd.Series(expected_gnd, name="iam_ground") iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r) assert_series_equal(iam[0], expected_sky) assert_series_equal(iam[1], expected_gnd) def test_iam_interp(): - aoi_meas = [0.0, 45.0, 65.0, 75.0] - iam_meas = [1.0, 0.9, 0.8, 0.6] + iam_meas = [1.0, 0.9, 0.8, 0.6] # simple default linear method aoi = 55.0 @@ -183,7 +220,7 @@ def test_iam_interp(): # simple non-default method aoi = 55.0 expected = 0.8878062 - iam = _iam.interp(aoi, aoi_meas, iam_meas, method='cubic') + iam = _iam.interp(aoi, aoi_meas, iam_meas, method="cubic") assert_allclose(iam, expected) # check with all reference values @@ -214,14 +251,18 @@ def test_iam_interp(): _iam.interp(0.0, [0, 90], [1, -1]) -@pytest.mark.parametrize('aoi,expected', [ - (45, 0.9975036250000002), - (np.array([[-30, 30, 100, np.nan]]), - np.array([[0, 1.007572, 0, np.nan]])), - (pd.Series([80]), pd.Series([0.597472])) -]) +@pytest.mark.parametrize( + "aoi,expected", + [ + (45, 0.9975036250000002), + ( + np.array([[-30, 30, 100, np.nan]]), + np.array([[0, 1.007572, 0, np.nan]]), + ), + (pd.Series([80]), pd.Series([0.597472])), + ], +) def test_sapm(sapm_module_params, aoi, expected): - out = _iam.sapm(aoi, sapm_module_params) if isinstance(aoi, pd.Series): @@ -231,13 +272,13 @@ def test_sapm(sapm_module_params, aoi, expected): def test_sapm_limits(): - module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + module_parameters = {"B0": 5, "B1": 0, "B2": 0, "B3": 0, "B4": 0, "B5": 0} assert _iam.sapm(1, module_parameters) == 5 - module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + module_parameters = {"B0": 5, "B1": 0, "B2": 0, "B3": 0, "B4": 0, "B5": 0} assert _iam.sapm(1, module_parameters, upper=1) == 1 - module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0} + module_parameters = {"B0": -5, "B1": 0, "B2": 0, "B3": 0, "B4": 0, "B5": 0} assert _iam.sapm(1, module_parameters) == 0 @@ -245,22 +286,22 @@ def test_marion_diffuse_model(mocker): # 1: return values are correct # 2: the underlying models are called appropriately ashrae_expected = { - 'sky': 0.9596085829811408, - 'horizon': 0.8329070417832541, - 'ground': 0.719823559106309 + "sky": 0.9596085829811408, + "horizon": 0.8329070417832541, + "ground": 0.719823559106309, } physical_expected = { - 'sky': 0.9539178294437575, - 'horizon': 0.7652650139134007, - 'ground': 0.6387140117795903 + "sky": 0.9539178294437575, + "horizon": 0.7652650139134007, + "ground": 0.6387140117795903, } - ashrae_spy = mocker.spy(_iam, 'ashrae') - physical_spy = mocker.spy(_iam, 'physical') + ashrae_spy = mocker.spy(_iam, "ashrae") + physical_spy = mocker.spy(_iam, "physical") - ashrae_actual = _iam.marion_diffuse('ashrae', 20) + ashrae_actual = _iam.marion_diffuse("ashrae", 20) assert ashrae_spy.call_count == 3 # one call for each of the 3 regions assert physical_spy.call_count == 0 - physical_actual = _iam.marion_diffuse('physical', 20) + physical_actual = _iam.marion_diffuse("physical", 20) assert ashrae_spy.call_count == 3 assert physical_spy.call_count == 3 @@ -274,11 +315,11 @@ def test_marion_diffuse_model(mocker): def test_marion_diffuse_kwargs(): # kwargs get passed to underlying model expected = { - 'sky': 0.967489994422575, - 'horizon': 0.8647842827418412, - 'ground': 0.7700443455928433 + "sky": 0.967489994422575, + "horizon": 0.8647842827418412, + "ground": 0.7700443455928433, } - actual = _iam.marion_diffuse('ashrae', 20, b=0.04) + actual = _iam.marion_diffuse("ashrae", 20, b=0.04) for k, v in expected.items(): assert_allclose(actual[k], v) @@ -286,45 +327,62 @@ def test_marion_diffuse_kwargs(): def test_marion_diffuse_invalid(): with pytest.raises(ValueError): - _iam.marion_diffuse('not_a_model', 20) + _iam.marion_diffuse("not_a_model", 20) -@pytest.mark.parametrize('region,N,expected', [ - ('sky', 180, 0.9596085829811408), - ('horizon', 1800, 0.8329070417832541), - ('ground', 180, 0.719823559106309) -]) +@pytest.mark.parametrize( + "region,N,expected", + [ + ("sky", 180, 0.9596085829811408), + ("horizon", 1800, 0.8329070417832541), + ("ground", 180, 0.719823559106309), + ], +) def test_marion_integrate_scalar(region, N, expected): actual = _iam.marion_integrate(_iam.ashrae, 20, region, N) assert_allclose(actual, expected) - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): actual = _iam.marion_integrate(_iam.ashrae, np.nan, region, N) expected = np.nan assert_allclose(actual, expected) -@pytest.mark.parametrize('region,N,expected', [ - ('sky', 180, [0.9523611991069362, 0.9596085829811408, 0.9619811198105501]), - ('horizon', 1800, [0.0, 0.8329070417832541, 0.8987287652347437]), - ('ground', 180, [0.0, 0.719823559106309, 0.8186360238536674]) -]) +@pytest.mark.parametrize( + "region,N,expected", + [ + ( + "sky", + 180, + [0.9523611991069362, 0.9596085829811408, 0.9619811198105501], + ), + ("horizon", 1800, [0.0, 0.8329070417832541, 0.8987287652347437]), + ("ground", 180, [0.0, 0.719823559106309, 0.8186360238536674]), + ], +) def test_marion_integrate_list(region, N, expected): actual = _iam.marion_integrate(_iam.ashrae, [0, 20, 30], region, N) assert_allclose(actual, expected) - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): actual = _iam.marion_integrate(_iam.ashrae, [0, 20, np.nan], region, N) assert_allclose(actual, [expected[0], expected[1], np.nan]) -@pytest.mark.parametrize('region,N,expected', [ - ('sky', 180, [0.9523611991069362, 0.9596085829811408, 0.9619811198105501]), - ('horizon', 1800, [0.0, 0.8329070417832541, 0.8987287652347437]), - ('ground', 180, [0.0, 0.719823559106309, 0.8186360238536674]) -]) +@pytest.mark.parametrize( + "region,N,expected", + [ + ( + "sky", + 180, + [0.9523611991069362, 0.9596085829811408, 0.9619811198105501], + ), + ("horizon", 1800, [0.0, 0.8329070417832541, 0.8987287652347437]), + ("ground", 180, [0.0, 0.719823559106309, 0.8186360238536674]), + ], +) def test_marion_integrate_series(region, N, expected): - idx = pd.date_range('2019-01-01', periods=3, freq='h') + idx = pd.date_range("2019-01-01", periods=3, freq="h") tilt = pd.Series([0, 20, 30], index=idx) expected = pd.Series(expected, index=idx) actual = _iam.marion_integrate(_iam.ashrae, tilt, region, N) @@ -332,13 +390,13 @@ def test_marion_integrate_series(region, N, expected): tilt.iloc[1] = np.nan expected.iloc[1] = np.nan - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): actual = _iam.marion_integrate(_iam.ashrae, tilt, region, N) assert_allclose(actual, expected) def test_marion_integrate_ground_flat(): - iam = _iam.marion_integrate(_iam.ashrae, 0, 'horizon', 1800) + iam = _iam.marion_integrate(_iam.ashrae, 0, "horizon", 1800) assert_allclose(iam, 0) @@ -346,14 +404,14 @@ def test_marion_integrate_invalid(): # check for invalid region string. this actually gets checked twice, # with the difference being whether `num` is specified or not. with pytest.raises(ValueError): - _iam.marion_integrate(_iam.ashrae, 0, 'bad') + _iam.marion_integrate(_iam.ashrae, 0, "bad") with pytest.raises(ValueError): - _iam.marion_integrate(_iam.ashrae, 0, 'bad', 180) + _iam.marion_integrate(_iam.ashrae, 0, "bad", 180) def test_schlick(): - idx = pd.date_range('2019-01-01', freq='h', periods=9) + idx = pd.date_range("2019-01-01", freq="h", periods=9) aoi = pd.Series([-180, -135, -90, -45, 0, 45, 90, 135, 180], idx) expected = pd.Series([0, 0, 0, 0.99784451, 1.0, 0.99784451, 0, 0, 0], idx) @@ -389,25 +447,47 @@ def test_schlick_diffuse(): assert_allclose(expected_ground[i], actual_ground, rtol=1e-6) # pandas Series - idx = pd.date_range('2019-01-01', freq='h', periods=len(surface_tilt)) - actual_sky, actual_ground = _iam.schlick_diffuse(pd.Series(surface_tilt, - idx)) + idx = pd.date_range("2019-01-01", freq="h", periods=len(surface_tilt)) + actual_sky, actual_ground = _iam.schlick_diffuse( + pd.Series(surface_tilt, idx) + ) assert_series_equal(pd.Series(expected_sky, idx), actual_sky) - assert_series_equal(pd.Series(expected_ground, idx), actual_ground, - rtol=1e-6) - - -@pytest.mark.parametrize('source,source_params,target,expected', [ - ('physical', {'n': 1.5, 'K': 4.5, 'L': 0.004}, 'martin_ruiz', - {'a_r': 0.174037}), - ('physical', {'n': 1.5, 'K': 4.5, 'L': 0.004}, 'ashrae', - {'b': 0.042896}), - ('ashrae', {'b': 0.15}, 'physical', - {'n': 0.991457, 'K': 4, 'L': 0.037813}), - ('ashrae', {'b': 0.15}, 'martin_ruiz', {'a_r': 0.302390}), - ('martin_ruiz', {'a_r': 0.15}, 'physical', - {'n': 1.240190, 'K': 4, 'L': 0.002791055}), - ('martin_ruiz', {'a_r': 0.15}, 'ashrae', {'b': 0.025458})]) + assert_series_equal( + pd.Series(expected_ground, idx), actual_ground, rtol=1e-6 + ) + + +@pytest.mark.parametrize( + "source,source_params,target,expected", + [ + ( + "physical", + {"n": 1.5, "K": 4.5, "L": 0.004}, + "martin_ruiz", + {"a_r": 0.174037}, + ), + ( + "physical", + {"n": 1.5, "K": 4.5, "L": 0.004}, + "ashrae", + {"b": 0.042896}, + ), + ( + "ashrae", + {"b": 0.15}, + "physical", + {"n": 0.991457, "K": 4, "L": 0.037813}, + ), + ("ashrae", {"b": 0.15}, "martin_ruiz", {"a_r": 0.302390}), + ( + "martin_ruiz", + {"a_r": 0.15}, + "physical", + {"n": 1.240190, "K": 4, "L": 0.002791055}, + ), + ("martin_ruiz", {"a_r": 0.15}, "ashrae", {"b": 0.025458}), + ], +) def test_convert(source, source_params, target, expected): target_params = _iam.convert(source, source_params, target) exp = [expected[k] for k in expected] @@ -415,10 +495,14 @@ def test_convert(source, source_params, target, expected): assert_allclose(exp, tar, rtol=1e-05) -@pytest.mark.parametrize('source,source_params', [ - ('ashrae', {'b': 0.15}), - ('ashrae', {'b': 0.05}), - ('martin_ruiz', {'a_r': 0.15})]) +@pytest.mark.parametrize( + "source,source_params", + [ + ("ashrae", {"b": 0.15}), + ("ashrae", {"b": 0.05}), + ("martin_ruiz", {"a_r": 0.15}), + ], +) def test_convert_recover(source, source_params): # convert isn't set up to handle both source and target = 'physical' target_params = _iam.convert(source, source_params, source, xtol=1e-7) @@ -429,29 +513,31 @@ def test_convert_recover(source, source_params): def test_convert_ashrae_physical_no_fix_n(): # convert ashrae to physical, without fixing n - source_params = {'b': 0.15} - target_params = _iam.convert('ashrae', source_params, 'physical', - fix_n=False) - expected = {'n': 0.989019, 'K': 4, 'L': 0.037382} + source_params = {"b": 0.15} + target_params = _iam.convert( + "ashrae", source_params, "physical", fix_n=False + ) + expected = {"n": 0.989019, "K": 4, "L": 0.037382} exp = [expected[k] for k in expected] tar = [target_params[k] for k in expected] assert_allclose(exp, tar, rtol=1e-05) def test_convert_reverse_order_in_physical(): - source_params = {'a_r': 0.25} - target_params = _iam.convert('martin_ruiz', source_params, 'physical') - expected = {'n': 1.691398, 'K': 4, 'L': 0.071633} + source_params = {"a_r": 0.25} + target_params = _iam.convert("martin_ruiz", source_params, "physical") + expected = {"n": 1.691398, "K": 4, "L": 0.071633} exp = [expected[k] for k in expected] tar = [target_params[k] for k in expected] assert_allclose(exp, tar, rtol=1e-05) def test_convert_xtol(): - source_params = {'b': 0.15} - target_params = _iam.convert('ashrae', source_params, 'physical', - xtol=1e-8) - expected = {'n': 0.9914568914, 'K': 4, 'L': 0.0378126985} + source_params = {"b": 0.15} + target_params = _iam.convert( + "ashrae", source_params, "physical", xtol=1e-8 + ) + expected = {"n": 0.9914568914, "K": 4, "L": 0.0378126985} exp = [expected[k] for k in expected] tar = [target_params[k] for k in expected] assert_allclose(exp, tar, rtol=1e-6) @@ -461,33 +547,35 @@ def test_convert_custom_weight_func(): aoi = np.linspace(0, 90, 91) # convert physical to martin_ruiz, using custom weight function - source_params = {'n': 1.5, 'K': 4.5, 'L': 0.004} + source_params = {"n": 1.5, "K": 4.5, "L": 0.004} source_iam = _iam.physical(aoi, **source_params) # define custom weight function that takes in other arguments def scaled_weight(aoi): - return 2. * aoi + return 2.0 * aoi # expected value calculated from computing residual function over # a range of inputs, and taking minimum of these values expected_min_res = 16.39724 - actual_dict = _iam.convert('physical', source_params, 'martin_ruiz', - weight=scaled_weight) - actual_min_res = _iam._residual(aoi, source_iam, _iam.martin_ruiz, - [actual_dict['a_r']], scaled_weight) + actual_dict = _iam.convert( + "physical", source_params, "martin_ruiz", weight=scaled_weight + ) + actual_min_res = _iam._residual( + aoi, source_iam, _iam.martin_ruiz, [actual_dict["a_r"]], scaled_weight + ) assert np.isclose(expected_min_res, actual_min_res, atol=1e-06) def test_convert_model_not_implemented(): - with pytest.raises(NotImplementedError, match='model has not been'): - _iam.convert('ashrae', {'b': 0.1}, 'foo') + with pytest.raises(NotImplementedError, match="model has not been"): + _iam.convert("ashrae", {"b": 0.1}, "foo") def test_convert_wrong_model_parameters(): - with pytest.raises(ValueError, match='model was expecting'): - _iam.convert('ashrae', {'B': 0.1}, 'physical') + with pytest.raises(ValueError, match="model was expecting"): + _iam.convert("ashrae", {"B": 0.1}, "physical") def test_convert__minimize_fails(): @@ -496,8 +584,8 @@ def test_convert__minimize_fails(): def nan_weight(aoi): return np.nan - with pytest.raises(RuntimeError, match='Optimizer exited unsuccessfully'): - _iam.convert('ashrae', {'b': 0.1}, 'physical', weight=nan_weight) + with pytest.raises(RuntimeError, match="Optimizer exited unsuccessfully"): + _iam.convert("ashrae", {"b": 0.1}, "physical", weight=nan_weight) def test_fit(): @@ -507,8 +595,8 @@ def test_fit(): expected_a_r = 0.14 - actual_dict = _iam.fit(aoi, perturbed_iam, 'martin_ruiz') - actual_a_r = actual_dict['a_r'] + actual_dict = _iam.fit(aoi, perturbed_iam, "martin_ruiz") + actual_a_r = actual_dict["a_r"] assert np.isclose(expected_a_r, actual_a_r, atol=1e-04) @@ -516,7 +604,7 @@ def test_fit(): def test_fit_custom_weight_func(): # define custom weight function that takes in other arguments def scaled_weight(aoi): - return 2. * aoi + return 2.0 * aoi aoi = np.linspace(0, 90, 5) perturb = np.array([1.2, 1.01, 0.95, 1, 0.98]) @@ -524,16 +612,17 @@ def scaled_weight(aoi): expected_a_r = 0.14 - actual_dict = _iam.fit(aoi, perturbed_iam, 'martin_ruiz', - weight=scaled_weight) - actual_a_r = actual_dict['a_r'] + actual_dict = _iam.fit( + aoi, perturbed_iam, "martin_ruiz", weight=scaled_weight + ) + actual_a_r = actual_dict["a_r"] assert np.isclose(expected_a_r, actual_a_r, atol=1e-04) def test_fit_model_not_implemented(): - with pytest.raises(NotImplementedError, match='model has not been'): - _iam.fit(np.array([0, 10]), np.array([1, 0.99]), 'foo') + with pytest.raises(NotImplementedError, match="model has not been"): + _iam.fit(np.array([0, 10]), np.array([1, 0.99]), "foo") def test_fit__minimize_fails(): @@ -542,9 +631,13 @@ def test_fit__minimize_fails(): def nan_weight(aoi): return np.nan - with pytest.raises(RuntimeError, match='Optimizer exited unsuccessfully'): - _iam.fit(np.array([0, 10]), np.array([1, 0.99]), 'physical', - weight=nan_weight) + with pytest.raises(RuntimeError, match="Optimizer exited unsuccessfully"): + _iam.fit( + np.array([0, 10]), + np.array([1, 0.99]), + "physical", + weight=nan_weight, + ) def test__residual_zero_outside_range(): diff --git a/pvlib/tests/test_inverter.py b/pvlib/tests/test_inverter.py index 6c854efba2..85d588b741 100644 --- a/pvlib/tests/test_inverter.py +++ b/pvlib/tests/test_inverter.py @@ -15,8 +15,9 @@ def test_adr(adr_inverter_parameters): pdcs = pd.Series([135, 1232, 1170, 420, 551]) pacs = inverter.adr(vdcs, pdcs, adr_inverter_parameters) - assert_series_equal(pacs, pd.Series([np.nan, 1161.5745, 1116.4459, - 382.6679, np.nan])) + assert_series_equal( + pacs, pd.Series([np.nan, 1161.5745, 1116.4459, 382.6679, np.nan]) + ) def test_adr_vtol(adr_inverter_parameters): @@ -24,13 +25,14 @@ def test_adr_vtol(adr_inverter_parameters): pdcs = pd.Series([135, 1232, 1170, 420, 551]) pacs = inverter.adr(vdcs, pdcs, adr_inverter_parameters, vtol=0.20) - assert_series_equal(pacs, pd.Series([104.8223, 1161.5745, 1116.4459, - 382.6679, 513.3385])) + assert_series_equal( + pacs, pd.Series([104.8223, 1161.5745, 1116.4459, 382.6679, 513.3385]) + ) def test_adr_float(adr_inverter_parameters): - vdcs = 154. - pdcs = 1232. + vdcs = 154.0 + pdcs = 1232.0 pacs = inverter.adr(vdcs, pdcs, adr_inverter_parameters) assert_allclose(pacs, 1161.5745) @@ -38,10 +40,10 @@ def test_adr_float(adr_inverter_parameters): def test_adr_invalid_and_night(sam_data): # also tests if inverter.adr can read the output from pvsystem.retrieve_sam - inverters = sam_data['adrinverter'] - testinv = 'Zigor__Sunzet_3_TL_US_240V__CEC_2011_' - vdcs = np.array([39.873036, 0., np.nan, 420]) - pdcs = np.array([188.09182, 0., 420, np.nan]) + inverters = sam_data["adrinverter"] + testinv = "Zigor__Sunzet_3_TL_US_240V__CEC_2011_" + vdcs = np.array([39.873036, 0.0, np.nan, 420]) + pdcs = np.array([188.09182, 0.0, 420, np.nan]) pacs = inverter.adr(vdcs, pdcs, inverters[testinv]) assert_allclose(pacs, np.array([np.nan, -0.25, np.nan, np.nan])) @@ -57,17 +59,17 @@ def test_sandia(cec_inverter_parameters): def test_sandia_float(cec_inverter_parameters): - vdcs = 25. + vdcs = 25.0 idcs = 5.5 pdcs = idcs * vdcs pacs = inverter.sandia(vdcs, pdcs, cec_inverter_parameters) assert_allclose(pacs, 132.004278, 1e-5) # test at low power condition - vdcs = 25. + vdcs = 25.0 idcs = 0 pdcs = idcs * vdcs pacs = inverter.sandia(vdcs, pdcs, cec_inverter_parameters) - assert_allclose(pacs, -1. * cec_inverter_parameters['Pnt'], 1e-5) + assert_allclose(pacs, -1.0 * cec_inverter_parameters["Pnt"], 1e-5) def test_sandia_Pnt_micro(): @@ -76,21 +78,21 @@ def test_sandia_Pnt_micro(): power output when the DC power was 0. """ inverter_parameters = { - 'Name': 'Enphase Energy: M250-60-2LL-S2x (-ZC) (-NA) 208V [CEC 2013]', - 'Vac': 208.0, - 'Paco': 240.0, - 'Pdco': 250.5311318, - 'Vdco': 32.06160667, - 'Pso': 1.12048857, - 'C0': -5.76E-05, - 'C1': -6.24E-04, - 'C2': 8.09E-02, - 'C3': -0.111781106, - 'Pnt': 0.043, - 'Vdcmax': 48.0, - 'Idcmax': 9.8, - 'Mppt_low': 27.0, - 'Mppt_high': 39.0, + "Name": "Enphase Energy: M250-60-2LL-S2x (-ZC) (-NA) 208V [CEC 2013]", + "Vac": 208.0, + "Paco": 240.0, + "Pdco": 250.5311318, + "Vdco": 32.06160667, + "Pso": 1.12048857, + "C0": -5.76e-05, + "C1": -6.24e-04, + "C2": 8.09e-02, + "C3": -0.111781106, + "Pnt": 0.043, + "Vdcmax": 48.0, + "Idcmax": 9.8, + "Mppt_low": 27.0, + "Mppt_high": 39.0, } vdcs = pd.Series(np.linspace(0, 50, 3)) idcs = pd.Series(np.linspace(0, 11, 3)) @@ -104,17 +106,19 @@ def test_sandia_multi(cec_inverter_parameters): vdcs = pd.Series(np.linspace(0, 50, 3)) idcs = pd.Series(np.linspace(0, 11, 3)) / 2 pdcs = idcs * vdcs - pacs = inverter.sandia_multi((vdcs, vdcs), (pdcs, pdcs), - cec_inverter_parameters) + pacs = inverter.sandia_multi( + (vdcs, vdcs), (pdcs, pdcs), cec_inverter_parameters + ) assert_series_equal(pacs, pd.Series([-0.020000, 132.004278, 250.000000])) # with lists instead of tuples - pacs = inverter.sandia_multi([vdcs, vdcs], [pdcs, pdcs], - cec_inverter_parameters) + pacs = inverter.sandia_multi( + [vdcs, vdcs], [pdcs, pdcs], cec_inverter_parameters + ) assert_series_equal(pacs, pd.Series([-0.020000, 132.004278, 250.000000])) # with arrays instead of tuples - pacs = inverter.sandia_multi(np.array([vdcs, vdcs]), - np.array([pdcs, pdcs]), - cec_inverter_parameters) + pacs = inverter.sandia_multi( + np.array([vdcs, vdcs]), np.array([pdcs, pdcs]), cec_inverter_parameters + ) assert_allclose(pacs, np.array([-0.020000, 132.004278, 250.000000])) @@ -122,7 +126,7 @@ def test_sandia_multi_length_error(cec_inverter_parameters): vdcs = pd.Series(np.linspace(0, 50, 3)) idcs = pd.Series(np.linspace(0, 11, 3)) pdcs = idcs * vdcs - with pytest.raises(ValueError, match='p_dc and v_dc have different'): + with pytest.raises(ValueError, match="p_dc and v_dc have different"): inverter.sandia_multi((vdcs,), (pdcs, pdcs), cec_inverter_parameters) @@ -139,8 +143,8 @@ def test_pvwatts_scalars(): out = inverter.pvwatts(90, 100, 0.95) assert_allclose(out, expected) # GH 675 - expected = 0. - out = inverter.pvwatts(0., 100) + expected = 0.0 + out = inverter.pvwatts(0.0, 100) assert_allclose(out, expected) @@ -155,10 +159,7 @@ def test_pvwatts_possible_negative(): def test_pvwatts_arrays(): pdc = np.array([[np.nan], [0], [50], [100]]) pdc0 = 100 - expected = np.array([[np.nan], - [0.], - [47.60843624], - [95.]]) + expected = np.array([[np.nan], [0.0], [47.60843624], [95.0]]) out = inverter.pvwatts(pdc, pdc0, 0.95) assert_allclose(out, expected, equal_nan=True) @@ -166,7 +167,7 @@ def test_pvwatts_arrays(): def test_pvwatts_series(): pdc = pd.Series([np.nan, 0, 50, 100]) pdc0 = 100 - expected = pd.Series(np.array([np.nan, 0., 47.608436, 95.])) + expected = pd.Series(np.array([np.nan, 0.0, 47.608436, 95.0])) out = inverter.pvwatts(pdc, pdc0, 0.95) assert_series_equal(expected, out) @@ -174,7 +175,7 @@ def test_pvwatts_series(): def test_pvwatts_multi(): pdc = np.array([np.nan, 0, 50, 100]) / 2 pdc0 = 100 - expected = np.array([np.nan, 0., 47.608436, 95.]) + expected = np.array([np.nan, 0.0, 47.608436, 95.0]) out = inverter.pvwatts_multi((pdc, pdc), pdc0, 0.95) assert_allclose(expected, out) # with 2D array @@ -190,24 +191,52 @@ def test_pvwatts_multi(): assert_series_equal(pd.Series(expected), out) -INVERTER_TEST_MEAS = DATA_DIR / 'inverter_fit_snl_meas.csv' -INVERTER_TEST_SIM = DATA_DIR / 'inverter_fit_snl_sim.csv' - - -@pytest.mark.parametrize('infilen, expected', [ - (INVERTER_TEST_MEAS, {'Paco': 333000., 'Pdco': 343251., 'Vdco': 740., - 'Pso': 1427.746, 'C0': -5.768e-08, 'C1': 3.596e-05, - 'C2': 1.038e-03, 'C3': 2.978e-05, 'Pnt': 1.}), - (INVERTER_TEST_SIM, {'Paco': 1000., 'Pdco': 1050., 'Vdco': 240., - 'Pso': 10., 'C0': 1e-6, 'C1': 1e-4, 'C2': 1e-2, - 'C3': 1e-3, 'Pnt': 1.}), -]) +INVERTER_TEST_MEAS = DATA_DIR / "inverter_fit_snl_meas.csv" +INVERTER_TEST_SIM = DATA_DIR / "inverter_fit_snl_sim.csv" + + +@pytest.mark.parametrize( + "infilen, expected", + [ + ( + INVERTER_TEST_MEAS, + { + "Paco": 333000.0, + "Pdco": 343251.0, + "Vdco": 740.0, + "Pso": 1427.746, + "C0": -5.768e-08, + "C1": 3.596e-05, + "C2": 1.038e-03, + "C3": 2.978e-05, + "Pnt": 1.0, + }, + ), + ( + INVERTER_TEST_SIM, + { + "Paco": 1000.0, + "Pdco": 1050.0, + "Vdco": 240.0, + "Pso": 10.0, + "C0": 1e-6, + "C1": 1e-4, + "C2": 1e-2, + "C3": 1e-3, + "Pnt": 1.0, + }, + ), + ], +) def test_fit_sandia(infilen, expected): curves = pd.read_csv(infilen) - dc_power = curves['ac_power'] / curves['efficiency'] - result = inverter.fit_sandia(ac_power=curves['ac_power'], - dc_power=dc_power, - dc_voltage=curves['dc_voltage'], - dc_voltage_level=curves['dc_voltage_level'], - p_ac_0=expected['Paco'], p_nt=expected['Pnt']) + dc_power = curves["ac_power"] / curves["efficiency"] + result = inverter.fit_sandia( + ac_power=curves["ac_power"], + dc_power=dc_power, + dc_voltage=curves["dc_voltage"], + dc_voltage_level=curves["dc_voltage_level"], + p_ac_0=expected["Paco"], + p_nt=expected["Pnt"], + ) assert expected == pytest.approx(result, rel=1e-3) diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py index 9352e978e6..724119560a 100644 --- a/pvlib/tests/test_irradiance.py +++ b/pvlib/tests/test_irradiance.py @@ -7,8 +7,7 @@ import pandas as pd import pytest -from numpy.testing import (assert_almost_equal, - assert_allclose) +from numpy.testing import assert_almost_equal, assert_allclose from pvlib import irradiance, albedo from .conftest import ( @@ -29,41 +28,88 @@ @pytest.fixture def times(): # must include night values - return pd.date_range(start='20140624', freq='6h', periods=4, - tz='US/Arizona') + return pd.date_range( + start="20140624", freq="6h", periods=4, tz="US/Arizona" + ) @pytest.fixture def irrad_data(times): - return pd.DataFrame(np.array( - [[0., 0., 0.], - [79.73860422, 316.1949056, 40.46149818], - [1042.48031487, 939.95469881, 118.45831879], - [257.20751138, 646.22886049, 62.03376265]]), - columns=['ghi', 'dni', 'dhi'], index=times) + return pd.DataFrame( + np.array( + [ + [0.0, 0.0, 0.0], + [79.73860422, 316.1949056, 40.46149818], + [1042.48031487, 939.95469881, 118.45831879], + [257.20751138, 646.22886049, 62.03376265], + ] + ), + columns=["ghi", "dni", "dhi"], + index=times, + ) @pytest.fixture def ephem_data(times): - return pd.DataFrame(np.array( - [[124.0390863, 124.0390863, -34.0390863, -34.0390863, - 352.69550699, -2.36677158], - [82.85457044, 82.97705621, 7.14542956, 7.02294379, - 66.71410338, -2.42072165], - [10.56413562, 10.56725766, 79.43586438, 79.43274234, - 144.76567754, -2.47457321], - [72.41687122, 72.46903556, 17.58312878, 17.53096444, - 287.04104128, -2.52831909]]), - columns=['apparent_zenith', 'zenith', 'apparent_elevation', - 'elevation', 'azimuth', 'equation_of_time'], - index=times) + return pd.DataFrame( + np.array( + [ + [ + 124.0390863, + 124.0390863, + -34.0390863, + -34.0390863, + 352.69550699, + -2.36677158, + ], + [ + 82.85457044, + 82.97705621, + 7.14542956, + 7.02294379, + 66.71410338, + -2.42072165, + ], + [ + 10.56413562, + 10.56725766, + 79.43586438, + 79.43274234, + 144.76567754, + -2.47457321, + ], + [ + 72.41687122, + 72.46903556, + 17.58312878, + 17.53096444, + 287.04104128, + -2.52831909, + ], + ] + ), + columns=[ + "apparent_zenith", + "zenith", + "apparent_elevation", + "elevation", + "azimuth", + "equation_of_time", + ], + index=times, + ) @pytest.fixture def dni_et(): return np.array( - [1321.1655834833093, 1321.1655834833093, 1321.1655834833093, - 1321.1655834833093]) + [ + 1321.1655834833093, + 1321.1655834833093, + 1321.1655834833093, + 1321.1655834833093, + ] + ) @pytest.fixture @@ -72,7 +118,7 @@ def relative_airmass(times): # setup for et rad test. put it here for readability -timestamp = pd.Timestamp('20161026') +timestamp = pd.Timestamp("20161026") dt_index = pd.DatetimeIndex([timestamp]) doy = timestamp.dayofyear dt_date = timestamp.date() @@ -81,26 +127,31 @@ def relative_airmass(times): value = 1383.636203 -@pytest.mark.parametrize('testval, expected', [ - (doy, value), - (np.float64(doy), value), - (dt_date, value), - (dt_datetime, value), - (dt_np64, value), - (np.array([doy]), np.array([value])), - (pd.Series([doy]), np.array([value])), - (dt_index, pd.Series([value], index=dt_index)), - (timestamp, value) -]) -@pytest.mark.parametrize('method', [ - 'asce', 'spencer', 'nrel', pytest.param('pyephem', marks=requires_ephem)]) +@pytest.mark.parametrize( + "testval, expected", + [ + (doy, value), + (np.float64(doy), value), + (dt_date, value), + (dt_datetime, value), + (dt_np64, value), + (np.array([doy]), np.array([value])), + (pd.Series([doy]), np.array([value])), + (dt_index, pd.Series([value], index=dt_index)), + (timestamp, value), + ], +) +@pytest.mark.parametrize( + "method", + ["asce", "spencer", "nrel", pytest.param("pyephem", marks=requires_ephem)], +) def test_get_extra_radiation(testval, expected, method): out = irradiance.get_extra_radiation(testval, method=method) assert_allclose(out, expected, atol=10) def test_get_extra_radiation_epoch_year(): - out = irradiance.get_extra_radiation(doy, method='nrel', epoch_year=2012) + out = irradiance.get_extra_radiation(doy, method="nrel", epoch_year=2012) assert_allclose(out, 1382.4926804890767, atol=0.1) @@ -110,16 +161,18 @@ def test_get_extra_radiation_nrel_numba(times): # don't warn on method reload warnings.simplefilter("ignore") result = irradiance.get_extra_radiation( - times, method='nrel', how='numba', numthreads=4) + times, method="nrel", how="numba", numthreads=4 + ) # and reset to no-numba state - irradiance.get_extra_radiation(times, method='nrel') - assert_allclose(result, - [1322.332316, 1322.296282, 1322.261205, 1322.227091]) + irradiance.get_extra_radiation(times, method="nrel") + assert_allclose( + result, [1322.332316, 1322.296282, 1322.261205, 1322.227091] + ) def test_get_extra_radiation_invalid(): with pytest.raises(ValueError): - irradiance.get_extra_radiation(300, method='invalid') + irradiance.get_extra_radiation(300, method="invalid") def test_get_ground_diffuse_simple_float(): @@ -128,34 +181,38 @@ def test_get_ground_diffuse_simple_float(): def test_get_ground_diffuse_simple_series(irrad_data): - ground_irrad = irradiance.get_ground_diffuse(40, irrad_data['ghi']) - assert ground_irrad.name == 'diffuse_ground' + ground_irrad = irradiance.get_ground_diffuse(40, irrad_data["ghi"]) + assert ground_irrad.name == "diffuse_ground" def test_get_ground_diffuse_albedo_0(irrad_data): ground_irrad = irradiance.get_ground_diffuse( - 40, irrad_data['ghi'], albedo=0) + 40, irrad_data["ghi"], albedo=0 + ) assert (0 == ground_irrad).all() def test_get_ground_diffuse_albedo_series(times): albedo = pd.Series(0.2, index=times) ground_irrad = irradiance.get_ground_diffuse( - 45, pd.Series(1000, index=times), albedo) - expected = albedo * 0.5 * (1 - np.sqrt(2) / 2.) * 1000 - expected.name = 'diffuse_ground' + 45, pd.Series(1000, index=times), albedo + ) + expected = albedo * 0.5 * (1 - np.sqrt(2) / 2.0) * 1000 + expected.name = "diffuse_ground" assert_series_equal(ground_irrad, expected) def test_grounddiffuse_albedo_invalid_surface(irrad_data): with pytest.raises(KeyError): irradiance.get_ground_diffuse( - 40, irrad_data['ghi'], surface_type='invalid') + 40, irrad_data["ghi"], surface_type="invalid" + ) def test_get_ground_diffuse_albedo_surface(irrad_data): - result = irradiance.get_ground_diffuse(40, irrad_data['ghi'], - surface_type='sand') + result = irradiance.get_ground_diffuse( + 40, irrad_data["ghi"], surface_type="sand" + ) assert_allclose(result, [0, 3.731058, 48.778813, 12.035025], atol=1e-4) @@ -165,7 +222,7 @@ def test_isotropic_float(): def test_isotropic_series(irrad_data): - result = irradiance.isotropic(40, irrad_data['dhi']) + result = irradiance.isotropic(40, irrad_data["dhi"]) assert_allclose(result, [0, 35.728402, 104.601328, 54.777191], atol=1e-4) @@ -176,9 +233,12 @@ def test_klucher_series_float(): solar_zenith, solar_azimuth = 20.0, 180.0 # expect same result for floats and pd.Series expected = irradiance.klucher( - surface_tilt, surface_azimuth, - pd.Series(dhi), pd.Series(ghi), - pd.Series(solar_zenith), pd.Series(solar_azimuth) + surface_tilt, + surface_azimuth, + pd.Series(dhi), + pd.Series(ghi), + pd.Series(solar_zenith), + pd.Series(solar_azimuth), ) # 94.99429931664851 result = irradiance.klucher( surface_tilt, surface_azimuth, dhi, ghi, solar_zenith, solar_azimuth @@ -187,248 +247,380 @@ def test_klucher_series_float(): def test_klucher_series(irrad_data, ephem_data): - result = irradiance.klucher(40, 180, irrad_data['dhi'], irrad_data['ghi'], - ephem_data['apparent_zenith'], - ephem_data['azimuth']) + result = irradiance.klucher( + 40, + 180, + irrad_data["dhi"], + irrad_data["ghi"], + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + ) # pvlib matlab 1.4 does not contain the max(cos_tt, 0) correction # so, these values are different - assert_allclose(result, [0., 36.789794, 109.209347, 56.965916], atol=1e-4) + assert_allclose(result, [0.0, 36.789794, 109.209347, 56.965916], atol=1e-4) # expect same result for np.array and pd.Series expected = irradiance.klucher( - 40, 180, irrad_data['dhi'].values, irrad_data['ghi'].values, - ephem_data['apparent_zenith'].values, ephem_data['azimuth'].values + 40, + 180, + irrad_data["dhi"].values, + irrad_data["ghi"].values, + ephem_data["apparent_zenith"].values, + ephem_data["azimuth"].values, ) assert_allclose(result, expected, atol=1e-4) def test_haydavies(irrad_data, ephem_data, dni_et): result = irradiance.haydavies( - 40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, - ephem_data['apparent_zenith'], ephem_data['azimuth']) + 40, + 180, + irrad_data["dhi"], + irrad_data["dni"], + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + ) # values from matlab 1.4 code assert_allclose(result, [0, 27.1775, 102.9949, 33.1909], atol=1e-4) def test_haydavies_components(irrad_data, ephem_data, dni_et): - expected = pd.DataFrame(np.array( - [[0, 27.1775, 102.9949, 33.1909], - [0, 27.1775, 30.1818, 27.9837], - [0, 0, 72.8130, 5.2071], - [0, 0, 0, 0]]).T, - columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], - index=irrad_data.index + expected = pd.DataFrame( + np.array( + [ + [0, 27.1775, 102.9949, 33.1909], + [0, 27.1775, 30.1818, 27.9837], + [0, 0, 72.8130, 5.2071], + [0, 0, 0, 0], + ] + ).T, + columns=["sky_diffuse", "isotropic", "circumsolar", "horizon"], + index=irrad_data.index, ) # pandas result = irradiance.haydavies( - 40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, - ephem_data['apparent_zenith'], ephem_data['azimuth'], - return_components=True) + 40, + 180, + irrad_data["dhi"], + irrad_data["dni"], + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + return_components=True, + ) assert_frame_equal(result, expected, check_less_precise=4) # numpy result = irradiance.haydavies( - 40, 180, irrad_data['dhi'].values, irrad_data['dni'].values, dni_et, - ephem_data['apparent_zenith'].values, ephem_data['azimuth'].values, - return_components=True) - assert_allclose(result['sky_diffuse'], expected['sky_diffuse'], atol=1e-4) - assert_allclose(result['isotropic'], expected['isotropic'], atol=1e-4) - assert_allclose(result['circumsolar'], expected['circumsolar'], atol=1e-4) - assert_allclose(result['horizon'], expected['horizon'], atol=1e-4) + 40, + 180, + irrad_data["dhi"].values, + irrad_data["dni"].values, + dni_et, + ephem_data["apparent_zenith"].values, + ephem_data["azimuth"].values, + return_components=True, + ) + assert_allclose(result["sky_diffuse"], expected["sky_diffuse"], atol=1e-4) + assert_allclose(result["isotropic"], expected["isotropic"], atol=1e-4) + assert_allclose(result["circumsolar"], expected["circumsolar"], atol=1e-4) + assert_allclose(result["horizon"], expected["horizon"], atol=1e-4) assert isinstance(result, dict) # scalar result = irradiance.haydavies( - 40, 180, irrad_data['dhi'].values[-1], irrad_data['dni'].values[-1], - dni_et[-1], ephem_data['apparent_zenith'].values[-1], - ephem_data['azimuth'].values[-1], return_components=True) - assert_allclose(result['sky_diffuse'], expected['sky_diffuse'].iloc[-1], - atol=1e-4) - assert_allclose(result['isotropic'], expected['isotropic'].iloc[-1], - atol=1e-4) - assert_allclose(result['circumsolar'], expected['circumsolar'].iloc[-1], - atol=1e-4) - assert_allclose(result['horizon'], expected['horizon'].iloc[-1], atol=1e-4) + 40, + 180, + irrad_data["dhi"].values[-1], + irrad_data["dni"].values[-1], + dni_et[-1], + ephem_data["apparent_zenith"].values[-1], + ephem_data["azimuth"].values[-1], + return_components=True, + ) + assert_allclose( + result["sky_diffuse"], expected["sky_diffuse"].iloc[-1], atol=1e-4 + ) + assert_allclose( + result["isotropic"], expected["isotropic"].iloc[-1], atol=1e-4 + ) + assert_allclose( + result["circumsolar"], expected["circumsolar"].iloc[-1], atol=1e-4 + ) + assert_allclose(result["horizon"], expected["horizon"].iloc[-1], atol=1e-4) assert isinstance(result, dict) def test_reindl(irrad_data, ephem_data, dni_et): result = irradiance.reindl( - 40, 180, irrad_data['dhi'], irrad_data['dni'], irrad_data['ghi'], - dni_et, ephem_data['apparent_zenith'], ephem_data['azimuth']) + 40, + 180, + irrad_data["dhi"], + irrad_data["dni"], + irrad_data["ghi"], + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + ) # values from matlab 1.4 code - assert_allclose(result, [0., 27.9412, 104.1317, 34.1663], atol=1e-4) + assert_allclose(result, [0.0, 27.9412, 104.1317, 34.1663], atol=1e-4) def test_king(irrad_data, ephem_data): - result = irradiance.king(40, irrad_data['dhi'], irrad_data['ghi'], - ephem_data['apparent_zenith']) + result = irradiance.king( + 40, irrad_data["dhi"], irrad_data["ghi"], ephem_data["apparent_zenith"] + ) assert_allclose(result, [0, 44.629352, 115.182626, 79.719855], atol=1e-4) def test_perez(irrad_data, ephem_data, dni_et, relative_airmass): - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez(40, 180, irrad_data['dhi'], dni, - dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], relative_airmass) - expected = pd.Series(np.array( - [0., 31.46046871, np.nan, 45.45539877]), - index=irrad_data.index) + out = irradiance.perez( + 40, + 180, + irrad_data["dhi"], + dni, + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + relative_airmass, + ) + expected = pd.Series( + np.array([0.0, 31.46046871, np.nan, 45.45539877]), + index=irrad_data.index, + ) assert_series_equal(out, expected, check_less_precise=2) def test_perez_driesse(irrad_data, ephem_data, dni_et, relative_airmass): - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez_driesse(40, 180, irrad_data['dhi'], dni, - dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], relative_airmass) - expected = pd.Series(np.array( - [0., 29.991, np.nan, 47.397]), - index=irrad_data.index) + out = irradiance.perez_driesse( + 40, + 180, + irrad_data["dhi"], + dni, + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + relative_airmass, + ) + expected = pd.Series( + np.array([0.0, 29.991, np.nan, 47.397]), index=irrad_data.index + ) assert_series_equal(out, expected, check_less_precise=2) def test_perez_driesse_airmass(irrad_data, ephem_data, dni_et): - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez_driesse(40, 180, irrad_data['dhi'], dni, - dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], airmass=None) + out = irradiance.perez_driesse( + 40, + 180, + irrad_data["dhi"], + dni, + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + airmass=None, + ) print(out) - expected = pd.Series(np.array( - [0., 29.991, np.nan, 47.397]), - index=irrad_data.index) + expected = pd.Series( + np.array([0.0, 29.991, np.nan, 47.397]), index=irrad_data.index + ) assert_series_equal(out, expected, check_less_precise=2) def test_perez_components(irrad_data, ephem_data, dni_et, relative_airmass): - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez(40, 180, irrad_data['dhi'], dni, - dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], relative_airmass, - return_components=True) - expected = pd.DataFrame(np.array( - [[0., 31.46046871, np.nan, 45.45539877], - [0., 26.84138589, np.nan, 31.72696071], - [0., 0., np.nan, 4.47966439], - [0., 4.62212181, np.nan, 9.25316454]]).T, - columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], - index=irrad_data.index - ) - expected_for_sum = expected['sky_diffuse'].copy() + out = irradiance.perez( + 40, + 180, + irrad_data["dhi"], + dni, + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + relative_airmass, + return_components=True, + ) + expected = pd.DataFrame( + np.array( + [ + [0.0, 31.46046871, np.nan, 45.45539877], + [0.0, 26.84138589, np.nan, 31.72696071], + [0.0, 0.0, np.nan, 4.47966439], + [0.0, 4.62212181, np.nan, 9.25316454], + ] + ).T, + columns=["sky_diffuse", "isotropic", "circumsolar", "horizon"], + index=irrad_data.index, + ) + expected_for_sum = expected["sky_diffuse"].copy() expected_for_sum.iloc[2] = 0 sum_components = out.iloc[:, 1:].sum(axis=1) - sum_components.name = 'sky_diffuse' + sum_components.name = "sky_diffuse" assert_frame_equal(out, expected, check_less_precise=2) assert_series_equal(sum_components, expected_for_sum, check_less_precise=2) -def test_perez_driesse_components(irrad_data, ephem_data, dni_et, - relative_airmass): - dni = irrad_data['dni'].copy() +def test_perez_driesse_components( + irrad_data, ephem_data, dni_et, relative_airmass +): + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez_driesse(40, 180, irrad_data['dhi'], dni, - dni_et, ephem_data['apparent_zenith'], - ephem_data['azimuth'], relative_airmass, - return_components=True) - - expected = pd.DataFrame(np.array( - [[0., 29.991, np.nan, 47.397], - [0., 25.806, np.nan, 33.181], - [0., 0.000, np.nan, 4.197], - [0., 4.184, np.nan, 10.018]]).T, - columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], - index=irrad_data.index - ) - expected_for_sum = expected['sky_diffuse'].copy() + out = irradiance.perez_driesse( + 40, + 180, + irrad_data["dhi"], + dni, + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + relative_airmass, + return_components=True, + ) + + expected = pd.DataFrame( + np.array( + [ + [0.0, 29.991, np.nan, 47.397], + [0.0, 25.806, np.nan, 33.181], + [0.0, 0.000, np.nan, 4.197], + [0.0, 4.184, np.nan, 10.018], + ] + ).T, + columns=["sky_diffuse", "isotropic", "circumsolar", "horizon"], + index=irrad_data.index, + ) + expected_for_sum = expected["sky_diffuse"].copy() expected_for_sum.iloc[2] = 0 sum_components = out.iloc[:, 1:].sum(axis=1) - sum_components.name = 'sky_diffuse' + sum_components.name = "sky_diffuse" assert_frame_equal(out, expected, check_less_precise=2) assert_series_equal(sum_components, expected_for_sum, check_less_precise=2) def test_perez_negative_horizon(): - times = pd.date_range(start='20190101 11:30:00', freq='1h', - periods=5, tz='US/Central') + times = pd.date_range( + start="20190101 11:30:00", freq="1h", periods=5, tz="US/Central" + ) # Avoid test dependencies on functionality not being tested by hard-coding # the inputs. This data corresponds to Goodwin Creek in the afternoon on # 1/1/2019. # dni_e is slightly rounded from irradiance.get_extra_radiation # airmass from atmosphere.get_relative_airmas - inputs = pd.DataFrame(np.array( - [[158, 19, 1, 0, 0], - [249, 165, 136, 93, 50], - [57.746951, 57.564205, 60.813841, 66.989435, 75.353368], - [171.003315, 187.346924, 202.974357, 216.725599, 228.317233], - [1414, 1414, 1414, 1414, 1414], - [1.869315, 1.859981, 2.044429, 2.544943, 3.900136]]).T, - columns=['dni', 'dhi', 'solar_zenith', - 'solar_azimuth', 'dni_extra', 'airmass'], - index=times - ) - - out = irradiance.perez(34, 180, inputs['dhi'], inputs['dni'], - inputs['dni_extra'], inputs['solar_zenith'], - inputs['solar_azimuth'], inputs['airmass'], - model='allsitescomposite1990', - return_components=True) + inputs = pd.DataFrame( + np.array( + [ + [158, 19, 1, 0, 0], + [249, 165, 136, 93, 50], + [57.746951, 57.564205, 60.813841, 66.989435, 75.353368], + [171.003315, 187.346924, 202.974357, 216.725599, 228.317233], + [1414, 1414, 1414, 1414, 1414], + [1.869315, 1.859981, 2.044429, 2.544943, 3.900136], + ] + ).T, + columns=[ + "dni", + "dhi", + "solar_zenith", + "solar_azimuth", + "dni_extra", + "airmass", + ], + index=times, + ) + + out = irradiance.perez( + 34, + 180, + inputs["dhi"], + inputs["dni"], + inputs["dni_extra"], + inputs["solar_zenith"], + inputs["solar_azimuth"], + inputs["airmass"], + model="allsitescomposite1990", + return_components=True, + ) # sky_diffuse can be less than isotropic under certain conditions as # horizon goes negative - expected = pd.DataFrame(np.array( - [[281.410185, 152.20879, 123.867898, 82.836412, 43.517015], - [166.785419, 142.24475, 119.173875, 83.525150, 45.725931], - [113.548755, 16.09757, 9.956174, 3.142467, 0], - [1.076010, -6.13353, -5.262151, -3.831230, -2.208923]]).T, - columns=['sky_diffuse', 'isotropic', 'circumsolar', 'horizon'], - index=times + expected = pd.DataFrame( + np.array( + [ + [281.410185, 152.20879, 123.867898, 82.836412, 43.517015], + [166.785419, 142.24475, 119.173875, 83.525150, 45.725931], + [113.548755, 16.09757, 9.956174, 3.142467, 0], + [1.076010, -6.13353, -5.262151, -3.831230, -2.208923], + ] + ).T, + columns=["sky_diffuse", "isotropic", "circumsolar", "horizon"], + index=times, ) - expected_for_sum = expected['sky_diffuse'].copy() + expected_for_sum = expected["sky_diffuse"].copy() sum_components = out.iloc[:, 1:].sum(axis=1) - sum_components.name = 'sky_diffuse' + sum_components.name = "sky_diffuse" assert_frame_equal(out, expected, check_less_precise=2) assert_series_equal(sum_components, expected_for_sum, check_less_precise=2) def test_perez_arrays(irrad_data, ephem_data, dni_et, relative_airmass): - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez(40, 180, irrad_data['dhi'].values, dni.values, - dni_et, ephem_data['apparent_zenith'].values, - ephem_data['azimuth'].values, - relative_airmass.values) - expected = np.array( - [0., 31.46046871, np.nan, 45.45539877]) + out = irradiance.perez( + 40, + 180, + irrad_data["dhi"].values, + dni.values, + dni_et, + ephem_data["apparent_zenith"].values, + ephem_data["azimuth"].values, + relative_airmass.values, + ) + expected = np.array([0.0, 31.46046871, np.nan, 45.45539877]) assert_allclose(out, expected, atol=1e-2) assert isinstance(out, np.ndarray) -def test_perez_driesse_arrays(irrad_data, ephem_data, dni_et, - relative_airmass): - dni = irrad_data['dni'].copy() +def test_perez_driesse_arrays( + irrad_data, ephem_data, dni_et, relative_airmass +): + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan - out = irradiance.perez_driesse(40, 180, irrad_data['dhi'].values, - dni.values, dni_et, - ephem_data['apparent_zenith'].values, - ephem_data['azimuth'].values, - relative_airmass.values) - expected = np.array( - [0., 29.990, np.nan, 47.396]) + out = irradiance.perez_driesse( + 40, + 180, + irrad_data["dhi"].values, + dni.values, + dni_et, + ephem_data["apparent_zenith"].values, + ephem_data["azimuth"].values, + relative_airmass.values, + ) + expected = np.array([0.0, 29.990, np.nan, 47.396]) assert_allclose(out, expected, atol=1e-2) assert isinstance(out, np.ndarray) def test_perez_scalar(): # copied values from fixtures - out = irradiance.perez(40, 180, 118.45831879, 939.95469881, - 1321.1655834833093, 10.56413562, 144.76567754, - 1.01688136) + out = irradiance.perez( + 40, + 180, + 118.45831879, + 939.95469881, + 1321.1655834833093, + 10.56413562, + 144.76567754, + 1.01688136, + ) # this will fail. out is ndarry with ndim == 0. fix in future version. # assert np.isscalar(out) assert_allclose(out, 109.084332) @@ -436,185 +628,321 @@ def test_perez_scalar(): def test_perez_driesse_scalar(): # copied values from fixtures - out = irradiance.perez_driesse(40, 180, 118.458, 939.954, - 1321.165, 10.564, 144.765, 1.016) + out = irradiance.perez_driesse( + 40, 180, 118.458, 939.954, 1321.165, 10.564, 144.765, 1.016 + ) # this will fail. out is ndarry with ndim == 0. fix in future version. # assert np.isscalar(out) assert_allclose(out, 110.341, atol=1e-2) -@pytest.mark.parametrize('model', ['isotropic', 'klucher', 'haydavies', - 'reindl', 'king', 'perez', 'perez-driesse']) +@pytest.mark.parametrize( + "model", + [ + "isotropic", + "klucher", + "haydavies", + "reindl", + "king", + "perez", + "perez-driesse", + ], +) def test_sky_diffuse_zenith_close_to_90(model): # GH 432 sky_diffuse = irradiance.get_sky_diffuse( - 30, 180, 89.999, 230, - dni=10, ghi=51, dhi=50, dni_extra=1360, airmass=12, model=model) + 30, + 180, + 89.999, + 230, + dni=10, + ghi=51, + dhi=50, + dni_extra=1360, + airmass=12, + model=model, + ) assert sky_diffuse < 100 def test_get_sky_diffuse_model_invalid(): with pytest.raises(ValueError): irradiance.get_sky_diffuse( - 30, 180, 0, 180, 1000, 1100, 100, dni_extra=1360, airmass=1, - model='invalid') + 30, + 180, + 0, + 180, + 1000, + 1100, + 100, + dni_extra=1360, + airmass=1, + model="invalid", + ) def test_get_sky_diffuse_missing_dni_extra(): - msg = 'dni_extra is required' + msg = "dni_extra is required" with pytest.raises(ValueError, match=msg): irradiance.get_sky_diffuse( - 30, 180, 0, 180, 1000, 1100, 100, airmass=1, - model='haydavies') + 30, 180, 0, 180, 1000, 1100, 100, airmass=1, model="haydavies" + ) def test_get_sky_diffuse_missing_airmass(irrad_data, ephem_data, dni_et): # test assumes location is Tucson, AZ # calculated airmass should be the equivalent to fixture airmass - dni = irrad_data['dni'].copy() + dni = irrad_data["dni"].copy() dni.iloc[2] = np.nan out = irradiance.get_sky_diffuse( - 40, 180, ephem_data['apparent_zenith'], ephem_data['azimuth'], dni, - irrad_data['ghi'], irrad_data['dhi'], dni_et, model='perez') - expected = pd.Series(np.array( - [0., 31.46046871, np.nan, 45.45539877]), - index=irrad_data.index) + 40, + 180, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + dni, + irrad_data["ghi"], + irrad_data["dhi"], + dni_et, + model="perez", + ) + expected = pd.Series( + np.array([0.0, 31.46046871, np.nan, 45.45539877]), + index=irrad_data.index, + ) assert_series_equal(out, expected, check_less_precise=2) def test_campbell_norman(): - expected = pd.DataFrame(np.array( - [[863.859736967, 653.123094076, 220.65905025]]), - columns=['ghi', 'dni', 'dhi'], - index=[0]) + expected = pd.DataFrame( + np.array([[863.859736967, 653.123094076, 220.65905025]]), + columns=["ghi", "dni", "dhi"], + index=[0], + ) out = irradiance.campbell_norman( - pd.Series([10]), pd.Series([0.5]), pd.Series([109764.21013135818]), - dni_extra=1400) + pd.Series([10]), + pd.Series([0.5]), + pd.Series([109764.21013135818]), + dni_extra=1400, + ) assert_frame_equal(out, expected) -def test_get_total_irradiance(irrad_data, ephem_data, dni_et, - relative_airmass): - models = ['isotropic', 'klucher', - 'haydavies', 'reindl', 'king', 'perez', 'perez-driesse'] +def test_get_total_irradiance( + irrad_data, ephem_data, dni_et, relative_airmass +): + models = [ + "isotropic", + "klucher", + "haydavies", + "reindl", + "king", + "perez", + "perez-driesse", + ] for model in models: total = irradiance.get_total_irradiance( - 32, 180, - ephem_data['apparent_zenith'], ephem_data['azimuth'], - dni=irrad_data['dni'], ghi=irrad_data['ghi'], - dhi=irrad_data['dhi'], - dni_extra=dni_et, airmass=relative_airmass, + 32, + 180, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + dni=irrad_data["dni"], + ghi=irrad_data["ghi"], + dhi=irrad_data["dhi"], + dni_extra=dni_et, + airmass=relative_airmass, model=model, - surface_type='urban') + surface_type="urban", + ) - assert total.columns.tolist() == ['poa_global', 'poa_direct', - 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'] + assert total.columns.tolist() == [ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ] -@pytest.mark.parametrize('model', ['isotropic', 'klucher', - 'haydavies', 'reindl', 'king', - 'perez', 'perez-driesse']) +@pytest.mark.parametrize( + "model", + [ + "isotropic", + "klucher", + "haydavies", + "reindl", + "king", + "perez", + "perez-driesse", + ], +) def test_get_total_irradiance_albedo( - irrad_data, ephem_data, dni_et, relative_airmass, model): + irrad_data, ephem_data, dni_et, relative_airmass, model +): albedo = pd.Series(0.2, index=ephem_data.index) total = irradiance.get_total_irradiance( - 32, 180, - ephem_data['apparent_zenith'], ephem_data['azimuth'], - dni=irrad_data['dni'], ghi=irrad_data['ghi'], - dhi=irrad_data['dhi'], - dni_extra=dni_et, airmass=relative_airmass, + 32, + 180, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + dni=irrad_data["dni"], + ghi=irrad_data["ghi"], + dhi=irrad_data["dhi"], + dni_extra=dni_et, + airmass=relative_airmass, model=model, - albedo=albedo) + albedo=albedo, + ) - assert total.columns.tolist() == ['poa_global', 'poa_direct', - 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'] + assert total.columns.tolist() == [ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ] -@pytest.mark.parametrize('model', ['isotropic', 'klucher', - 'haydavies', 'reindl', 'king', - 'perez', 'perez-driesse']) +@pytest.mark.parametrize( + "model", + [ + "isotropic", + "klucher", + "haydavies", + "reindl", + "king", + "perez", + "perez-driesse", + ], +) def test_get_total_irradiance_scalars(model): total = irradiance.get_total_irradiance( - 32, 180, - 10, 180, - dni=1000, ghi=1100, + 32, + 180, + 10, + 180, + dni=1000, + ghi=1100, dhi=100, - dni_extra=1400, airmass=1, + dni_extra=1400, + airmass=1, model=model, - surface_type='urban') + surface_type="urban", + ) - assert list(total.keys()) == ['poa_global', 'poa_direct', - 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'] + assert list(total.keys()) == [ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ] # test that none of the values are nan assert np.isnan(np.array(list(total.values()))).sum() == 0 def test_get_total_irradiance_missing_dni_extra(): - msg = 'dni_extra is required' + msg = "dni_extra is required" with pytest.raises(ValueError, match=msg): irradiance.get_total_irradiance( - 32, 180, - 10, 180, - dni=1000, ghi=1100, - dhi=100, - model='haydavies') + 32, 180, 10, 180, dni=1000, ghi=1100, dhi=100, model="haydavies" + ) def test_get_total_irradiance_missing_airmass(): total = irradiance.get_total_irradiance( - 32, 180, - 10, 180, - dni=1000, ghi=1100, + 32, + 180, + 10, + 180, + dni=1000, + ghi=1100, dhi=100, dni_extra=1400, - model='perez') - assert list(total.keys()) == ['poa_global', 'poa_direct', - 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'] + model="perez", + ) + assert list(total.keys()) == [ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ] def test_poa_components(irrad_data, ephem_data, dni_et, relative_airmass): - aoi = irradiance.aoi(40, 180, ephem_data['apparent_zenith'], - ephem_data['azimuth']) - gr_sand = irradiance.get_ground_diffuse(40, irrad_data['ghi'], - surface_type='sand') + aoi = irradiance.aoi( + 40, 180, ephem_data["apparent_zenith"], ephem_data["azimuth"] + ) + gr_sand = irradiance.get_ground_diffuse( + 40, irrad_data["ghi"], surface_type="sand" + ) diff_perez = irradiance.perez( - 40, 180, irrad_data['dhi'], irrad_data['dni'], dni_et, - ephem_data['apparent_zenith'], ephem_data['azimuth'], relative_airmass) + 40, + 180, + irrad_data["dhi"], + irrad_data["dni"], + dni_et, + ephem_data["apparent_zenith"], + ephem_data["azimuth"], + relative_airmass, + ) out = irradiance.poa_components( - aoi, irrad_data['dni'], diff_perez, gr_sand) - expected = pd.DataFrame(np.array( - [[0., -0., 0., 0., - 0.], - [35.19456561, 0., 35.19456561, 31.4635077, - 3.73105791], - [956.18253696, 798.31939281, 157.86314414, 109.08433162, - 48.77881252], - [90.99624896, 33.50143401, 57.49481495, 45.45978964, - 12.03502531]]), - columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'], - index=irrad_data.index) + aoi, irrad_data["dni"], diff_perez, gr_sand + ) + expected = pd.DataFrame( + np.array( + [ + [0.0, -0.0, 0.0, 0.0, 0.0], + [35.19456561, 0.0, 35.19456561, 31.4635077, 3.73105791], + [ + 956.18253696, + 798.31939281, + 157.86314414, + 109.08433162, + 48.77881252, + ], + [ + 90.99624896, + 33.50143401, + 57.49481495, + 45.45978964, + 12.03502531, + ], + ] + ), + columns=[ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ], + index=irrad_data.index, + ) assert_frame_equal(out, expected) -@pytest.mark.parametrize('pressure,expected', [ - (93193, [[830.46567, 0.79742, 0.93505], - [676.18340, 0.63782, 3.02102]]), - (None, [[868.72425, 0.79742, 1.01664], - [680.73800, 0.63782, 3.28463]]), - (101325, [[868.72425, 0.79742, 1.01664], - [680.73800, 0.63782, 3.28463]]) -]) +@pytest.mark.parametrize( + "pressure,expected", + [ + ( + 93193, + [[830.46567, 0.79742, 0.93505], [676.18340, 0.63782, 3.02102]], + ), + (None, [[868.72425, 0.79742, 1.01664], [680.73800, 0.63782, 3.28463]]), + ( + 101325, + [[868.72425, 0.79742, 1.01664], [680.73800, 0.63782, 3.28463]], + ), + ], +) def test_disc_value(pressure, expected): # see GH 449 for pressure=None vs. 101325. - columns = ['dni', 'kt', 'airmass'] - times = pd.DatetimeIndex(['2014-06-24T1200', '2014-06-24T1800'], - tz='America/Phoenix') + columns = ["dni", "kt", "airmass"] + times = pd.DatetimeIndex( + ["2014-06-24T1200", "2014-06-24T1800"], tz="America/Phoenix" + ) ghi = pd.Series([1038.62, 254.53], index=times) zenith = pd.Series([10.567, 72.469], index=times) out = irradiance.disc(ghi, zenith, times, pressure=pressure) @@ -627,123 +955,166 @@ def test_disc_value(pressure, expected): def test_disc_overirradiance(): - columns = ['dni', 'kt', 'airmass'] + columns = ["dni", "kt", "airmass"] ghi = np.array([3000]) solar_zenith = np.full_like(ghi, 0) - times = pd.date_range(start='2016-07-19 12:00:00', freq='1s', - periods=len(ghi), tz='America/Phoenix') - out = irradiance.disc(ghi=ghi, solar_zenith=solar_zenith, - datetime_or_doy=times) - expected = pd.DataFrame(np.array( - [[8.72544336e+02, 1.00000000e+00, 9.99493933e-01]]), - columns=columns, index=times) + times = pd.date_range( + start="2016-07-19 12:00:00", + freq="1s", + periods=len(ghi), + tz="America/Phoenix", + ) + out = irradiance.disc( + ghi=ghi, solar_zenith=solar_zenith, datetime_or_doy=times + ) + expected = pd.DataFrame( + np.array([[8.72544336e02, 1.00000000e00, 9.99493933e-01]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) def test_disc_min_cos_zenith_max_zenith(): # map out behavior under difficult conditions with various # limiting kwargs settings - columns = ['dni', 'kt', 'airmass'] - times = pd.DatetimeIndex(['2016-07-19 06:11:00'], tz='America/Phoenix') + columns = ["dni", "kt", "airmass"] + times = pd.DatetimeIndex(["2016-07-19 06:11:00"], tz="America/Phoenix") out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times) - expected = pd.DataFrame(np.array( - [[0.00000000e+00, 1.16046346e-02, 12.0]]), - columns=columns, index=times) + expected = pd.DataFrame( + np.array([[0.00000000e00, 1.16046346e-02, 12.0]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) # max_zenith and/or max_airmass keep these results reasonable - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - min_cos_zenith=0) - expected = pd.DataFrame(np.array( - [[0.00000000e+00, 1.0, 12.0]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, min_cos_zenith=0 + ) + expected = pd.DataFrame( + np.array([[0.00000000e00, 1.0, 12.0]]), columns=columns, index=times + ) assert_frame_equal(out, expected) # still get reasonable values because of max_airmass=12 limit - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - max_zenith=100) - expected = pd.DataFrame(np.array( - [[0., 1.16046346e-02, 12.0]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, max_zenith=100 + ) + expected = pd.DataFrame( + np.array([[0.0, 1.16046346e-02, 12.0]]), columns=columns, index=times + ) assert_frame_equal(out, expected) # still get reasonable values because of max_airmass=12 limit - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - min_cos_zenith=0, max_zenith=100) - expected = pd.DataFrame(np.array( - [[277.50185968, 1.0, 12.0]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, + solar_zenith=89.99, + datetime_or_doy=times, + min_cos_zenith=0, + max_zenith=100, + ) + expected = pd.DataFrame( + np.array([[277.50185968, 1.0, 12.0]]), columns=columns, index=times + ) assert_frame_equal(out, expected) # max_zenith keeps this result reasonable - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - min_cos_zenith=0, max_airmass=100) - expected = pd.DataFrame(np.array( - [[0.00000000e+00, 1.0, 36.39544757]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, + solar_zenith=89.99, + datetime_or_doy=times, + min_cos_zenith=0, + max_airmass=100, + ) + expected = pd.DataFrame( + np.array([[0.00000000e00, 1.0, 36.39544757]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) # allow zenith to be close to 90 and airmass to be infinite # and we get crazy values - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - max_zenith=100, max_airmass=100) - expected = pd.DataFrame(np.array( - [[6.68577449e+03, 1.16046346e-02, 3.63954476e+01]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, + solar_zenith=89.99, + datetime_or_doy=times, + max_zenith=100, + max_airmass=100, + ) + expected = pd.DataFrame( + np.array([[6.68577449e03, 1.16046346e-02, 3.63954476e01]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) # allow min cos zenith to be 0, zenith to be close to 90, # and airmass to be very big and we get even higher DNI values - out = irradiance.disc(ghi=1.0, solar_zenith=89.99, datetime_or_doy=times, - min_cos_zenith=0, max_zenith=100, max_airmass=100) - expected = pd.DataFrame(np.array( - [[7.21238390e+03, 1., 3.63954476e+01]]), - columns=columns, index=times) + out = irradiance.disc( + ghi=1.0, + solar_zenith=89.99, + datetime_or_doy=times, + min_cos_zenith=0, + max_zenith=100, + max_airmass=100, + ) + expected = pd.DataFrame( + np.array([[7.21238390e03, 1.0, 3.63954476e01]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) def test_dirint_value(): - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([1038.62, 254.53], index=times) zenith = pd.Series([10.567, 72.469], index=times) - pressure = 93193. + pressure = 93193.0 dirint_data = irradiance.dirint(ghi, zenith, times, pressure=pressure) - assert_almost_equal(dirint_data.values, - np.array([868.8, 699.7]), 1) + assert_almost_equal(dirint_data.values, np.array([868.8, 699.7]), 1) def test_dirint_nans(): - times = pd.date_range(start='2014-06-24T12-0700', periods=5, freq='6h') + times = pd.date_range(start="2014-06-24T12-0700", periods=5, freq="6h") ghi = pd.Series([np.nan, 1038.62, 1038.62, 1038.62, 1038.62], index=times) zenith = pd.Series([10.567, np.nan, 10.567, 10.567, 10.567], index=times) - pressure = pd.Series([93193., 93193., np.nan, 93193., 93193.], index=times) + pressure = pd.Series( + [93193.0, 93193.0, np.nan, 93193.0, 93193.0], index=times + ) temp_dew = pd.Series([10, 10, 10, np.nan, 10], index=times) - dirint_data = irradiance.dirint(ghi, zenith, times, pressure=pressure, - temp_dew=temp_dew) - assert_almost_equal(dirint_data.values, - np.array([np.nan, np.nan, np.nan, np.nan, 893.1]), 1) + dirint_data = irradiance.dirint( + ghi, zenith, times, pressure=pressure, temp_dew=temp_dew + ) + assert_almost_equal( + dirint_data.values, + np.array([np.nan, np.nan, np.nan, np.nan, 893.1]), + 1, + ) def test_dirint_tdew(): - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([1038.62, 254.53], index=times) zenith = pd.Series([10.567, 72.469], index=times) - pressure = 93193. - dirint_data = irradiance.dirint(ghi, zenith, times, pressure=pressure, - temp_dew=10) - assert_almost_equal(dirint_data.values, - np.array([882.1, 672.6]), 1) + pressure = 93193.0 + dirint_data = irradiance.dirint( + ghi, zenith, times, pressure=pressure, temp_dew=10 + ) + assert_almost_equal(dirint_data.values, np.array([882.1, 672.6]), 1) def test_dirint_no_delta_kt(): - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([1038.62, 254.53], index=times) zenith = pd.Series([10.567, 72.469], index=times) - pressure = 93193. - dirint_data = irradiance.dirint(ghi, zenith, times, pressure=pressure, - use_delta_kt_prime=False) - assert_almost_equal(dirint_data.values, - np.array([861.9, 670.4]), 1) + pressure = 93193.0 + dirint_data = irradiance.dirint( + ghi, zenith, times, pressure=pressure, use_delta_kt_prime=False + ) + assert_almost_equal(dirint_data.values, np.array([861.9, 670.4]), 1) def test_dirint_coeffs(): @@ -757,37 +1128,40 @@ def test_dirint_min_cos_zenith_max_zenith(): # map out behavior under difficult conditions with various # limiting kwargs settings # times don't have any physical relevance - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([0, 1], index=times) solar_zenith = pd.Series([90, 89.99], index=times) out = irradiance.dirint(ghi, solar_zenith, times) - expected = pd.Series([0.0, 0.0], index=times, name='dni') + expected = pd.Series([0.0, 0.0], index=times, name="dni") assert_series_equal(out, expected) out = irradiance.dirint(ghi, solar_zenith, times, min_cos_zenith=0) - expected = pd.Series([0.0, 0.0], index=times, name='dni') + expected = pd.Series([0.0, 0.0], index=times, name="dni") assert_series_equal(out, expected) out = irradiance.dirint(ghi, solar_zenith, times, max_zenith=90) - expected = pd.Series([0.0, 0.0], index=times, name='dni') + expected = pd.Series([0.0, 0.0], index=times, name="dni") assert_series_equal(out, expected, check_less_precise=True) - out = irradiance.dirint(ghi, solar_zenith, times, min_cos_zenith=0, - max_zenith=90) - expected = pd.Series([0.0, 144.264507], index=times, name='dni') + out = irradiance.dirint( + ghi, solar_zenith, times, min_cos_zenith=0, max_zenith=90 + ) + expected = pd.Series([0.0, 144.264507], index=times, name="dni") assert_series_equal(out, expected, check_less_precise=True) - out = irradiance.dirint(ghi, solar_zenith, times, min_cos_zenith=0, - max_zenith=100) - expected = pd.Series([0.0, 144.264507], index=times, name='dni') + out = irradiance.dirint( + ghi, solar_zenith, times, min_cos_zenith=0, max_zenith=100 + ) + expected = pd.Series([0.0, 144.264507], index=times, name="dni") assert_series_equal(out, expected, check_less_precise=True) def test_ghi_from_poa_driesse(mocker): # inputs copied from test_gti_dirint times = pd.DatetimeIndex( - ['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700']) + ["2014-06-24T06-0700", "2014-06-24T09-0700", "2014-06-24T12-0700"] + ) poa_global = np.array([20, 300, 1000]) zenith = np.array([80, 45, 20]) azimuth = np.array([90, 135, 180]) @@ -796,8 +1170,13 @@ def test_ghi_from_poa_driesse(mocker): # test core function output = irradiance.ghi_from_poa_driesse_2023( - surface_tilt, surface_azimuth, zenith, azimuth, - poa_global, dni_extra=1366.1) + surface_tilt, + surface_azimuth, + zenith, + azimuth, + poa_global, + dni_extra=1366.1, + ) expected = [22.089, 304.088, 931.143] assert_allclose(expected, output, atol=0.001) @@ -806,8 +1185,13 @@ def test_ghi_from_poa_driesse(mocker): poa_global = pd.Series([20, 300, 1000], index=times) output = irradiance.ghi_from_poa_driesse_2023( - surface_tilt, surface_azimuth, zenith, azimuth, - poa_global, dni_extra=1366.1) + surface_tilt, + surface_azimuth, + zenith, + azimuth, + poa_global, + dni_extra=1366.1, + ) assert isinstance(output, pd.Series) @@ -815,8 +1199,14 @@ def test_ghi_from_poa_driesse(mocker): poa_global = np.array([0, 1500, np.nan]) ghi, conv, niter = irradiance.ghi_from_poa_driesse_2023( - surface_tilt, surface_azimuth, zenith, azimuth, - poa_global, dni_extra=1366.1, full_output=True) + surface_tilt, + surface_azimuth, + zenith, + azimuth, + poa_global, + dni_extra=1366.1, + full_output=True, + ) expected = [0, np.nan, np.nan] assert_allclose(expected, ghi, atol=0.001) @@ -833,20 +1223,33 @@ def test_ghi_from_poa_driesse(mocker): xtol = -3.14159 # negative value raises exception in scipy.optimize.bisect with pytest.raises(ValueError, match=rf"xtol too small \({xtol:g} <= 0\)"): output = irradiance.ghi_from_poa_driesse_2023( - surface_tilt, surface_azimuth, zenith, azimuth, - poa_global, dni_extra=1366.1, xtol=xtol) + surface_tilt, + surface_azimuth, + zenith, + azimuth, + poa_global, + dni_extra=1366.1, + xtol=xtol, + ) # test propagation xtol = 3.141592 bisect_spy = mocker.spy(irradiance, "bisect") output = irradiance.ghi_from_poa_driesse_2023( - surface_tilt, surface_azimuth, zenith, azimuth, - poa_global, dni_extra=1366.1, xtol=xtol) + surface_tilt, + surface_azimuth, + zenith, + azimuth, + poa_global, + dni_extra=1366.1, + xtol=xtol, + ) assert bisect_spy.call_args[1]["xtol"] == xtol def test_gti_dirint(): times = pd.DatetimeIndex( - ['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700']) + ["2014-06-24T06-0700", "2014-06-24T09-0700", "2014-06-24T12-0700"] + ) poa_global = np.array([20, 300, 1000]) aoi = np.array([100, 70, 10]) zenith = np.array([80, 45, 20]) @@ -856,21 +1259,35 @@ def test_gti_dirint(): # test defaults output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth) + poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth + ) - expected_col_order = ['ghi', 'dni', 'dhi'] - expected = pd.DataFrame(array( - [[21.05796198, 0., 21.05796198], - [291.40037163, 63.41290679, 246.56067523], - [931.04078010, 695.94965324, 277.06172442]]), - columns=expected_col_order, index=times) + expected_col_order = ["ghi", "dni", "dhi"] + expected = pd.DataFrame( + array( + [ + [21.05796198, 0.0, 21.05796198], + [291.40037163, 63.41290679, 246.56067523], + [931.04078010, 695.94965324, 277.06172442], + ] + ), + columns=expected_col_order, + index=times, + ) assert_frame_equal(output, expected) # test ignore calculate_gt_90 output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, - calculate_gt_90=False) + poa_global, + aoi, + zenith, + azimuth, + times, + surface_tilt, + surface_azimuth, + calculate_gt_90=False, + ) expected_no_90 = expected.copy() expected_no_90.iloc[0, :] = np.nan @@ -878,66 +1295,118 @@ def test_gti_dirint(): assert_frame_equal(output, expected_no_90) # test pressure input - pressure = 93193. + pressure = 93193.0 output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, - pressure=pressure) + poa_global, + aoi, + zenith, + azimuth, + times, + surface_tilt, + surface_azimuth, + pressure=pressure, + ) - expected = pd.DataFrame(array( - [[21.05796198, 0., 21.05796198], - [293.21310935, 63.27500913, 248.47092131], - [932.46756378, 648.05001357, 323.49974813]]), - columns=expected_col_order, index=times) + expected = pd.DataFrame( + array( + [ + [21.05796198, 0.0, 21.05796198], + [293.21310935, 63.27500913, 248.47092131], + [932.46756378, 648.05001357, 323.49974813], + ] + ), + columns=expected_col_order, + index=times, + ) assert_frame_equal(output, expected) # test albedo input albedo = 0.05 output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, - albedo=albedo) + poa_global, + aoi, + zenith, + azimuth, + times, + surface_tilt, + surface_azimuth, + albedo=albedo, + ) - expected = pd.DataFrame(array( - [[21.3592591, 0., 21.3592591], - [294.4985420, 66.25848451, 247.64671830], - [941.7943404, 727.50552952, 258.16276278]]), - columns=expected_col_order, index=times) + expected = pd.DataFrame( + array( + [ + [21.3592591, 0.0, 21.3592591], + [294.4985420, 66.25848451, 247.64671830], + [941.7943404, 727.50552952, 258.16276278], + ] + ), + columns=expected_col_order, + index=times, + ) assert_frame_equal(output, expected) # test with albedo as a Series albedo = pd.Series(0.05, index=times) output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, - albedo=albedo) + poa_global, + aoi, + zenith, + azimuth, + times, + surface_tilt, + surface_azimuth, + albedo=albedo, + ) assert_frame_equal(output, expected) # test temp_dew input temp_dew = np.array([70, 80, 20]) output = irradiance.gti_dirint( - poa_global, aoi, zenith, azimuth, times, surface_tilt, surface_azimuth, - temp_dew=temp_dew) + poa_global, + aoi, + zenith, + azimuth, + times, + surface_tilt, + surface_azimuth, + temp_dew=temp_dew, + ) - expected = pd.DataFrame(array( - [[21.05796198, 0., 21.05796198], - [295.06070190, 38.20346345, 268.0467738], - [931.79627208, 689.81549269, 283.5817439]]), - columns=expected_col_order, index=times) + expected = pd.DataFrame( + array( + [ + [21.05796198, 0.0, 21.05796198], + [295.06070190, 38.20346345, 268.0467738], + [931.79627208, 689.81549269, 283.5817439], + ] + ), + columns=expected_col_order, + index=times, + ) assert_frame_equal(output, expected) def test_erbs(): - index = pd.DatetimeIndex(['20190101']*3 + ['20190620']) + index = pd.DatetimeIndex(["20190101"] * 3 + ["20190620"]) ghi = pd.Series([0, 50, 1000, 1000], index=index) zenith = pd.Series([120, 85, 10, 10], index=index) - expected = pd.DataFrame(np.array( - [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [9.67192672e+01, 4.15703604e+01, 4.05723511e-01], - [7.94205651e+02, 2.17860117e+02, 7.18132729e-01], - [8.42001578e+02, 1.70790318e+02, 7.68214312e-01]]), - columns=['dni', 'dhi', 'kt'], index=index) + expected = pd.DataFrame( + np.array( + [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [9.67192672e01, 4.15703604e01, 4.05723511e-01], + [7.94205651e02, 2.17860117e02, 7.18132729e-01], + [8.42001578e02, 1.70790318e02, 7.68214312e-01], + ] + ), + columns=["dni", "dhi", "kt"], + index=index, + ) out = irradiance.erbs(ghi, zenith, index) @@ -945,16 +1414,22 @@ def test_erbs(): def test_erbs_driesse(): - index = pd.DatetimeIndex(['20190101']*3 + ['20190620']) + index = pd.DatetimeIndex(["20190101"] * 3 + ["20190620"]) ghi = pd.Series([0, 50, 1000, 1000], index=index) zenith = pd.Series([120, 85, 10, 10], index=index) # expected values are the same as for erbs original test - expected = pd.DataFrame(np.array( - [[0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [9.67192672e+01, 4.15703604e+01, 4.05723511e-01], - [7.94205651e+02, 2.17860117e+02, 7.18132729e-01], - [8.42001578e+02, 1.70790318e+02, 7.68214312e-01]]), - columns=['dni', 'dhi', 'kt'], index=index) + expected = pd.DataFrame( + np.array( + [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [9.67192672e01, 4.15703604e01, 4.05723511e-01], + [7.94205651e02, 2.17860117e02, 7.18132729e-01], + [8.42001578e02, 1.70790318e02, 7.68214312e-01], + ] + ), + columns=["dni", "dhi", "kt"], + index=index, + ) out = irradiance.erbs_driesse(ghi, zenith, index) @@ -973,15 +1448,21 @@ def test_erbs_driesse(): def test_boland(): - index = pd.DatetimeIndex(['20190101']*3 + ['20190620']) + index = pd.DatetimeIndex(["20190101"] * 3 + ["20190620"]) ghi = pd.Series([0, 50, 1000, 1000], index=index) zenith = pd.Series([120, 85, 10, 10], index=index) - expected = pd.DataFrame(np.array( - [[0.0, 0.0, 0.0], - [81.9448546, 42.8580353, 0.405723511], - [723.764990, 287.230626, 0.718132729], - [805.020419, 207.209650, 0.768214312]]), - columns=['dni', 'dhi', 'kt'], index=index) + expected = pd.DataFrame( + np.array( + [ + [0.0, 0.0, 0.0], + [81.9448546, 42.8580353, 0.405723511], + [723.764990, 287.230626, 0.718132729], + [805.020419, 207.209650, 0.768214312], + ] + ), + columns=["dni", "dhi", "kt"], + index=index, + ) out = irradiance.boland(ghi, zenith, index) @@ -989,15 +1470,21 @@ def test_boland(): def test_orgill_hollands(): - index = pd.DatetimeIndex(['20190101']*3 + ['20190620']) + index = pd.DatetimeIndex(["20190101"] * 3 + ["20190620"]) ghi = pd.Series([0, 50, 1000, 1000], index=index) zenith = pd.Series([120, 85, 10, 10], index=index) - expected = pd.DataFrame(np.array( - [[0.0, 0.0, 0.0], - [108.731366, 40.5234370, 0.405723511], - [776.155771, 235.635779, 0.718132729], - [835.696102, 177.000000, 0.768214312]]), - columns=['dni', 'dhi', 'kt'], index=index) + expected = pd.DataFrame( + np.array( + [ + [0.0, 0.0, 0.0], + [108.731366, 40.5234370, 0.405723511], + [776.155771, 235.635779, 0.718132729], + [835.696102, 177.000000, 0.768214312], + ] + ), + columns=["dni", "dhi", "kt"], + index=index, + ) out = irradiance.orgill_hollands(ghi, zenith, index) @@ -1007,38 +1494,49 @@ def test_orgill_hollands(): def test_erbs_min_cos_zenith_max_zenith(): # map out behavior under difficult conditions with various # limiting kwargs settings - columns = ['dni', 'dhi', 'kt'] - times = pd.DatetimeIndex(['2016-07-19 06:11:00'], tz='America/Phoenix') + columns = ["dni", "dhi", "kt"] + times = pd.DatetimeIndex(["2016-07-19 06:11:00"], tz="America/Phoenix") # max_zenith keeps these results reasonable - out = irradiance.erbs(ghi=1.0, zenith=89.99999, - datetime_or_doy=times, min_cos_zenith=0) - expected = pd.DataFrame(np.array( - [[0., 1., 1.]]), - columns=columns, index=times) + out = irradiance.erbs( + ghi=1.0, zenith=89.99999, datetime_or_doy=times, min_cos_zenith=0 + ) + expected = pd.DataFrame( + np.array([[0.0, 1.0, 1.0]]), columns=columns, index=times + ) assert_frame_equal(out, expected) # 4-5 9s will produce bad behavior without max_zenith limit - out = irradiance.erbs(ghi=1.0, zenith=89.99999, - datetime_or_doy=times, max_zenith=100) - expected = pd.DataFrame(np.array( - [[6.00115286e+03, 9.98952601e-01, 1.16377640e-02]]), - columns=columns, index=times) + out = irradiance.erbs( + ghi=1.0, zenith=89.99999, datetime_or_doy=times, max_zenith=100 + ) + expected = pd.DataFrame( + np.array([[6.00115286e03, 9.98952601e-01, 1.16377640e-02]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) # 1-2 9s will produce bad behavior without either limit - out = irradiance.erbs(ghi=1.0, zenith=89.99, datetime_or_doy=times, - min_cos_zenith=0, max_zenith=100) - expected = pd.DataFrame(np.array( - [[4.78419761e+03, 1.65000000e-01, 1.00000000e+00]]), - columns=columns, index=times) + out = irradiance.erbs( + ghi=1.0, + zenith=89.99, + datetime_or_doy=times, + min_cos_zenith=0, + max_zenith=100, + ) + expected = pd.DataFrame( + np.array([[4.78419761e03, 1.65000000e-01, 1.00000000e00]]), + columns=columns, + index=times, + ) assert_frame_equal(out, expected) # check default behavior under hardest condition out = irradiance.erbs(ghi=1.0, zenith=90, datetime_or_doy=times) - expected = pd.DataFrame(np.array( - [[0., 1., 0.01163776]]), - columns=columns, index=times) + expected = pd.DataFrame( + np.array([[0.0, 1.0, 0.01163776]]), columns=columns, index=times + ) assert_frame_equal(out, expected) @@ -1048,9 +1546,9 @@ def test_erbs_all_scalar(): doy = 180 expected = OrderedDict() - expected['dni'] = 8.42358014e+02 - expected['dhi'] = 1.70439297e+02 - expected['kt'] = 7.68919470e-01 + expected["dni"] = 8.42358014e02 + expected["dhi"] = 1.70439297e02 + expected["kt"] = 7.68919470e-01 out = irradiance.erbs(ghi, zenith, doy) @@ -1061,96 +1559,127 @@ def test_erbs_all_scalar(): def test_dirindex(times): ghi = pd.Series([0, 0, 1038.62, 254.53], index=times) ghi_clearsky = pd.Series( - np.array([0., 79.73860422, 1042.48031487, 257.20751138]), - index=times + np.array([0.0, 79.73860422, 1042.48031487, 257.20751138]), index=times ) dni_clear = pd.Series( - np.array([0., 316.1949056, 939.95469881, 646.22886049]), - index=times + np.array([0.0, 316.1949056, 939.95469881, 646.22886049]), index=times ) zenith = pd.Series( np.array([124.0390863, 82.85457044, 10.56413562, 72.41687122]), - index=times - ) - pressure = 93193. - tdew = 10. - out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, - zenith, times, pressure=pressure, - temp_dew=tdew) - dirint_close_values = irradiance.dirint(ghi, zenith, times, - pressure=pressure, - use_delta_kt_prime=True, - temp_dew=tdew).values - expected_out = np.array([np.nan, 0., 748.31562800, 630.73752100]) + index=times, + ) + pressure = 93193.0 + tdew = 10.0 + out = irradiance.dirindex( + ghi, + ghi_clearsky, + dni_clear, + zenith, + times, + pressure=pressure, + temp_dew=tdew, + ) + dirint_close_values = irradiance.dirint( + ghi, + zenith, + times, + pressure=pressure, + use_delta_kt_prime=True, + temp_dew=tdew, + ).values + expected_out = np.array([np.nan, 0.0, 748.31562800, 630.73752100]) tolerance = 1e-8 - assert np.allclose(out, expected_out, rtol=tolerance, atol=0, - equal_nan=True) + assert np.allclose( + out, expected_out, rtol=tolerance, atol=0, equal_nan=True + ) tol_dirint = 0.2 assert np.allclose( out.values, dirint_close_values, rtol=tol_dirint, atol=0, - equal_nan=True) + equal_nan=True, + ) @fail_on_pvlib_version("0.13") def test_dirindex_ghi_clearsky_deprecation(): - times = pd.DatetimeIndex(['2014-06-24T18-1200']) + times = pd.DatetimeIndex(["2014-06-24T18-1200"]) ghi = pd.Series([1038.62], index=times) ghi_clearsky = pd.Series([1042.48031487], index=times) dni_clearsky = pd.Series([939.95469881], index=times) zenith = pd.Series([10.56413562], index=times) pressure, tdew = 93193, 10 - with pytest.warns(pvlibDeprecationWarning, match='ghi_clear'): + with pytest.warns(pvlibDeprecationWarning, match="ghi_clear"): irradiance.dirindex( - ghi=ghi, ghi_clearsky=ghi_clearsky, dni_clear=dni_clearsky, - zenith=zenith, times=times, pressure=pressure, temp_dew=tdew) + ghi=ghi, + ghi_clearsky=ghi_clearsky, + dni_clear=dni_clearsky, + zenith=zenith, + times=times, + pressure=pressure, + temp_dew=tdew, + ) def test_dirindex_min_cos_zenith_max_zenith(): # map out behavior under difficult conditions with various # limiting kwargs settings # times don't have any physical relevance - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([0, 1], index=times) ghi_clearsky = pd.Series([0, 1], index=times) dni_clear = pd.Series([0, 5], index=times) solar_zenith = pd.Series([90, 89.99], index=times) - out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith, - times) + out = irradiance.dirindex( + ghi, ghi_clearsky, dni_clear, solar_zenith, times + ) expected = pd.Series([nan, nan], index=times) assert_series_equal(out, expected) - out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith, - times, min_cos_zenith=0) + out = irradiance.dirindex( + ghi, ghi_clearsky, dni_clear, solar_zenith, times, min_cos_zenith=0 + ) expected = pd.Series([nan, nan], index=times) assert_series_equal(out, expected) - out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith, - times, max_zenith=90) + out = irradiance.dirindex( + ghi, ghi_clearsky, dni_clear, solar_zenith, times, max_zenith=90 + ) expected = pd.Series([nan, nan], index=times) assert_series_equal(out, expected) - out = irradiance.dirindex(ghi, ghi_clearsky, dni_clear, solar_zenith, - times, min_cos_zenith=0, max_zenith=100) - expected = pd.Series([nan, 5.], index=times) + out = irradiance.dirindex( + ghi, + ghi_clearsky, + dni_clear, + solar_zenith, + times, + min_cos_zenith=0, + max_zenith=100, + ) + expected = pd.Series([nan, 5.0], index=times) assert_series_equal(out, expected) @fail_on_pvlib_version("0.13") def test_dirindex_dni_clearsky_deprecation(): - times = pd.DatetimeIndex(['2014-06-24T12-0700', '2014-06-24T18-0700']) + times = pd.DatetimeIndex(["2014-06-24T12-0700", "2014-06-24T18-0700"]) ghi = pd.Series([0, 1], index=times) ghi_clearsky = pd.Series([0, 1], index=times) dni_clear = pd.Series([0, 5], index=times) solar_zenith = pd.Series([90, 89.99], index=times) - with pytest.warns(pvlibDeprecationWarning, match='dni_clear'): - irradiance.dirindex(ghi, ghi_clearsky, dni_clearsky=dni_clear, - zenith=solar_zenith, times=times, - min_cos_zenith=0) + with pytest.warns(pvlibDeprecationWarning, match="dni_clear"): + irradiance.dirindex( + ghi, + ghi_clearsky, + dni_clearsky=dni_clear, + zenith=solar_zenith, + times=times, + min_cos_zenith=0, + ) def test_dni(): @@ -1159,16 +1688,29 @@ def test_dni(): zenith = pd.Series([80, 100, 85, 70, 85]) dni_clear = pd.Series([50, 50, 200, 50, 300]) - dni = irradiance.dni(ghi, dhi, zenith, - dni_clear=dni_clear, clearsky_tolerance=2) - assert_series_equal(dni, - pd.Series([float('nan'), float('nan'), 400, - 146.190220008, 573.685662283])) + dni = irradiance.dni( + ghi, dhi, zenith, dni_clear=dni_clear, clearsky_tolerance=2 + ) + assert_series_equal( + dni, + pd.Series( + [float("nan"), float("nan"), 400, 146.190220008, 573.685662283] + ), + ) dni = irradiance.dni(ghi, dhi, zenith) - assert_series_equal(dni, - pd.Series([float('nan'), float('nan'), 573.685662283, - 146.190220008, 573.685662283])) + assert_series_equal( + dni, + pd.Series( + [ + float("nan"), + float("nan"), + 573.685662283, + 146.190220008, + 573.685662283, + ] + ), + ) @fail_on_pvlib_version("0.13") @@ -1177,28 +1719,39 @@ def test_dni_dni_clearsky_deprecation(): dhi = pd.Series([100, 90, 50, 50, 50]) zenith = pd.Series([80, 100, 85, 70, 85]) dni_clear = pd.Series([50, 50, 200, 50, 300]) - with pytest.warns(pvlibDeprecationWarning, match='dni_clear'): - irradiance.dni(ghi, dhi, zenith, - clearsky_dni=dni_clear, clearsky_tolerance=2) + with pytest.warns(pvlibDeprecationWarning, match="dni_clear"): + irradiance.dni( + ghi, dhi, zenith, clearsky_dni=dni_clear, clearsky_tolerance=2 + ) @pytest.mark.parametrize( - 'surface_tilt,surface_azimuth,solar_zenith,' + - 'solar_azimuth,aoi_expected,aoi_proj_expected', - [(0, 0, 0, 0, 0, 1), - (30, 180, 30, 180, 0, 1), - (30, 180, 150, 0, 180, -1), - (90, 0, 30, 60, 75.5224878, 0.25), - (90, 0, 30, 170, 119.4987042, -0.4924038)]) -def test_aoi_and_aoi_projection(surface_tilt, surface_azimuth, solar_zenith, - solar_azimuth, aoi_expected, - aoi_proj_expected): - aoi = irradiance.aoi(surface_tilt, surface_azimuth, solar_zenith, - solar_azimuth) + "surface_tilt,surface_azimuth,solar_zenith," + + "solar_azimuth,aoi_expected,aoi_proj_expected", + [ + (0, 0, 0, 0, 0, 1), + (30, 180, 30, 180, 0, 1), + (30, 180, 150, 0, 180, -1), + (90, 0, 30, 60, 75.5224878, 0.25), + (90, 0, 30, 170, 119.4987042, -0.4924038), + ], +) +def test_aoi_and_aoi_projection( + surface_tilt, + surface_azimuth, + solar_zenith, + solar_azimuth, + aoi_expected, + aoi_proj_expected, +): + aoi = irradiance.aoi( + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) assert_allclose(aoi, aoi_expected, atol=1e-5) aoi_projection = irradiance.aoi_projection( - surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) + surface_tilt, surface_azimuth, solar_zenith, solar_azimuth + ) assert_allclose(aoi_projection, aoi_proj_expected, atol=1e-6) @@ -1216,11 +1769,12 @@ def test_aoi_projection_precision(): # arrays zeniths = np.array([zenith]) azimuths = np.array([azimuth]) - projections = irradiance.aoi_projection(zeniths, azimuths, - zeniths, azimuths) + projections = irradiance.aoi_projection( + zeniths, azimuths, zeniths, azimuths + ) assert all(projections <= 1) assert all(np.isclose(projections, 1)) - assert projections.dtype == np.dtype('float64') + assert projections.dtype == np.dtype("float64") @pytest.fixture @@ -1231,43 +1785,50 @@ def airmass_kt(): def test_kt_kt_prime_factor(airmass_kt): out = irradiance._kt_kt_prime_factor(airmass_kt) - expected = np.array([0.999971, 0.723088, 0.548811, 0.471068]) + expected = np.array([0.999971, 0.723088, 0.548811, 0.471068]) assert_allclose(out, expected, atol=1e-5) def test_clearsky_index(): - ghi = np.array([-1., 0., 1., 500., 1000., np.nan]) + ghi = np.array([-1.0, 0.0, 1.0, 500.0, 1000.0, np.nan]) ghi_measured, ghi_modeled = np.meshgrid(ghi, ghi) # default max_clearsky_index - with np.errstate(invalid='ignore', divide='ignore'): + with np.errstate(invalid="ignore", divide="ignore"): out = irradiance.clearsky_index(ghi_measured, ghi_modeled) expected = np.array( - [[1., 0., 0., 0., 0., np.nan], - [0., 0., 0., 0., 0., np.nan], - [0., 0., 1., 2., 2., np.nan], - [0., 0., 0.002, 1., 2., np.nan], - [0., 0., 0.001, 0.5, 1., np.nan], - [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]) + [ + [1.0, 0.0, 0.0, 0.0, 0.0, np.nan], + [0.0, 0.0, 0.0, 0.0, 0.0, np.nan], + [0.0, 0.0, 1.0, 2.0, 2.0, np.nan], + [0.0, 0.0, 0.002, 1.0, 2.0, np.nan], + [0.0, 0.0, 0.001, 0.5, 1.0, np.nan], + [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], + ] + ) assert_allclose(out, expected, atol=0.001) # specify max_clearsky_index - with np.errstate(invalid='ignore', divide='ignore'): - out = irradiance.clearsky_index(ghi_measured, ghi_modeled, - max_clearsky_index=1.5) + with np.errstate(invalid="ignore", divide="ignore"): + out = irradiance.clearsky_index( + ghi_measured, ghi_modeled, max_clearsky_index=1.5 + ) expected = np.array( - [[1., 0., 0., 0., 0., np.nan], - [0., 0., 0., 0., 0., np.nan], - [0., 0., 1., 1.5, 1.5, np.nan], - [0., 0., 0.002, 1., 1.5, np.nan], - [0., 0., 0.001, 0.5, 1., np.nan], - [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]) + [ + [1.0, 0.0, 0.0, 0.0, 0.0, np.nan], + [0.0, 0.0, 0.0, 0.0, 0.0, np.nan], + [0.0, 0.0, 1.0, 1.5, 1.5, np.nan], + [0.0, 0.0, 0.002, 1.0, 1.5, np.nan], + [0.0, 0.0, 0.001, 0.5, 1.0, np.nan], + [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], + ] + ) assert_allclose(out, expected, atol=0.001) # scalars out = irradiance.clearsky_index(10, 1000) expected = 0.01 assert_allclose(out, expected, atol=0.001) # series - times = pd.date_range(start='20180601', periods=2, freq='12h') - ghi_measured = pd.Series([100, 500], index=times) + times = pd.date_range(start="20180601", periods=2, freq="12h") + ghi_measured = pd.Series([100, 500], index=times) ghi_modeled = pd.Series([500, 1000], index=times) out = irradiance.clearsky_index(ghi_measured, ghi_modeled) expected = pd.Series([0.2, 0.5], index=times) @@ -1276,7 +1837,7 @@ def test_clearsky_index(): @fail_on_pvlib_version("0.13") def test_clearsky_index_clearsky_ghi_deprecation(): - with pytest.warns(pvlibDeprecationWarning, match='ghi_clear'): + with pytest.warns(pvlibDeprecationWarning, match="ghi_clear"): ghi, clearsky_ghi = 200, 300 irradiance.clearsky_index(ghi, clearsky_ghi=clearsky_ghi) @@ -1289,47 +1850,61 @@ def test_clearness_index(): out = irradiance.clearness_index(ghi, solar_zenith, 1370) # np.set_printoptions(precision=3, floatmode='maxprec', suppress=True) expected = np.array( - [[0., 0., 0.011, 2.], - [0., 0., 0.011, 2.], - [0., 0., 0.011, 2.], - [0., 0., 0.001, 0.73]]) + [ + [0.0, 0.0, 0.011, 2.0], + [0.0, 0.0, 0.011, 2.0], + [0.0, 0.0, 0.011, 2.0], + [0.0, 0.0, 0.001, 0.73], + ] + ) assert_allclose(out, expected, atol=0.001) # specify min_cos_zenith - with np.errstate(invalid='ignore', divide='ignore'): - out = irradiance.clearness_index(ghi, solar_zenith, 1400, - min_cos_zenith=0) + with np.errstate(invalid="ignore", divide="ignore"): + out = irradiance.clearness_index( + ghi, solar_zenith, 1400, min_cos_zenith=0 + ) expected = np.array( - [[0., nan, 2., 2.], - [0., 0., 2., 2.], - [0., 0., 2., 2.], - [0., 0., 0.001, 0.714]]) + [ + [0.0, nan, 2.0, 2.0], + [0.0, 0.0, 2.0, 2.0], + [0.0, 0.0, 2.0, 2.0], + [0.0, 0.0, 0.001, 0.714], + ] + ) assert_allclose(out, expected, atol=0.001) # specify max_clearness_index - out = irradiance.clearness_index(ghi, solar_zenith, 1370, - max_clearness_index=0.82) + out = irradiance.clearness_index( + ghi, solar_zenith, 1370, max_clearness_index=0.82 + ) expected = np.array( - [[0., 0., 0.011, 0.82], - [0., 0., 0.011, 0.82], - [0., 0., 0.011, 0.82], - [0., 0., 0.001, 0.73]]) + [ + [0.0, 0.0, 0.011, 0.82], + [0.0, 0.0, 0.011, 0.82], + [0.0, 0.0, 0.011, 0.82], + [0.0, 0.0, 0.001, 0.73], + ] + ) assert_allclose(out, expected, atol=0.001) # specify min_cos_zenith and max_clearness_index - with np.errstate(invalid='ignore', divide='ignore'): - out = irradiance.clearness_index(ghi, solar_zenith, 1400, - min_cos_zenith=0, - max_clearness_index=0.82) + with np.errstate(invalid="ignore", divide="ignore"): + out = irradiance.clearness_index( + ghi, solar_zenith, 1400, min_cos_zenith=0, max_clearness_index=0.82 + ) expected = np.array( - [[0., nan, 0.82, 0.82], - [0., 0., 0.82, 0.82], - [0., 0., 0.82, 0.82], - [0., 0., 0.001, 0.714]]) + [ + [0.0, nan, 0.82, 0.82], + [0.0, 0.0, 0.82, 0.82], + [0.0, 0.0, 0.82, 0.82], + [0.0, 0.0, 0.001, 0.714], + ] + ) assert_allclose(out, expected, atol=0.001) # scalars out = irradiance.clearness_index(1000, 10, 1400) expected = 0.725 assert_allclose(out, expected, atol=0.001) # series - times = pd.date_range(start='20180601', periods=2, freq='12h') + times = pd.date_range(start="20180601", periods=2, freq="12h") ghi = pd.Series([0, 1000], index=times) solar_zenith = pd.Series([90, 0], index=times) extra_radiation = pd.Series([1360, 1400], index=times) @@ -1339,66 +1914,87 @@ def test_clearness_index(): def test_clearness_index_zenith_independent(airmass_kt): - clearness_index = np.array([-1, 0, .1, 1]) + clearness_index = np.array([-1, 0, 0.1, 1]) clearness_index, airmass_kt = np.meshgrid(clearness_index, airmass_kt) - out = irradiance.clearness_index_zenith_independent(clearness_index, - airmass_kt) + out = irradiance.clearness_index_zenith_independent( + clearness_index, airmass_kt + ) expected = np.array( - [[0., 0., 0.1, 1.], - [0., 0., 0.138, 1.383], - [0., 0., 0.182, 1.822], - [0., 0., 0.212, 2.]]) + [ + [0.0, 0.0, 0.1, 1.0], + [0.0, 0.0, 0.138, 1.383], + [0.0, 0.0, 0.182, 1.822], + [0.0, 0.0, 0.212, 2.0], + ] + ) assert_allclose(out, expected, atol=0.001) # test max_clearness_index out = irradiance.clearness_index_zenith_independent( - clearness_index, airmass_kt, max_clearness_index=0.82) + clearness_index, airmass_kt, max_clearness_index=0.82 + ) expected = np.array( - [[0., 0., 0.1, 0.82], - [0., 0., 0.138, 0.82], - [0., 0., 0.182, 0.82], - [0., 0., 0.212, 0.82]]) + [ + [0.0, 0.0, 0.1, 0.82], + [0.0, 0.0, 0.138, 0.82], + [0.0, 0.0, 0.182, 0.82], + [0.0, 0.0, 0.212, 0.82], + ] + ) assert_allclose(out, expected, atol=0.001) # scalars - out = irradiance.clearness_index_zenith_independent(.4, 2) + out = irradiance.clearness_index_zenith_independent(0.4, 2) expected = 0.443 assert_allclose(out, expected, atol=0.001) # series - times = pd.date_range(start='20180601', periods=2, freq='12h') - clearness_index = pd.Series([0, .5], index=times) + times = pd.date_range(start="20180601", periods=2, freq="12h") + clearness_index = pd.Series([0, 0.5], index=times) airmass = pd.Series([np.nan, 2], index=times) - out = irradiance.clearness_index_zenith_independent(clearness_index, - airmass) + out = irradiance.clearness_index_zenith_independent( + clearness_index, airmass + ) expected = pd.Series([np.nan, 0.553744437562], index=times) assert_series_equal(out, expected) def test_complete_irradiance(): # Generate dataframe to test on - times = pd.date_range('2010-07-05 7:00:00-0700', periods=2, freq='h') - i = pd.DataFrame({'ghi': [372.103976116, 497.087579068], - 'dhi': [356.543700, 465.44400], - 'dni': [49.63565561689957, 62.10624908037814]}, - index=times) + times = pd.date_range("2010-07-05 7:00:00-0700", periods=2, freq="h") + i = pd.DataFrame( + { + "ghi": [372.103976116, 497.087579068], + "dhi": [356.543700, 465.44400], + "dni": [49.63565561689957, 62.10624908037814], + }, + index=times, + ) # Define the solar position and clearsky dataframe - solar_position = pd.DataFrame({'apparent_zenith': [71.7303262449161, - 59.369], - 'zenith': [71.7764, 59.395]}, - index=pd.DatetimeIndex([ - '2010-07-05 07:00:00-0700', - '2010-07-05 08:00:00-0700'])) - clearsky = pd.DataFrame({'dni': [625.5254880160008, 778.7766443075865], - 'ghi': [246.3508023804681, 469.461381740857], - 'dhi': [50.25488725346631, 72.66909939636372]}, - index=pd.DatetimeIndex([ - '2010-07-05 07:00:00-0700', - '2010-07-05 08:00:00-0700'])) + solar_position = pd.DataFrame( + { + "apparent_zenith": [71.7303262449161, 59.369], + "zenith": [71.7764, 59.395], + }, + index=pd.DatetimeIndex( + ["2010-07-05 07:00:00-0700", "2010-07-05 08:00:00-0700"] + ), + ) + clearsky = pd.DataFrame( + { + "dni": [625.5254880160008, 778.7766443075865], + "ghi": [246.3508023804681, 469.461381740857], + "dhi": [50.25488725346631, 72.66909939636372], + }, + index=pd.DatetimeIndex( + ["2010-07-05 07:00:00-0700", "2010-07-05 08:00:00-0700"] + ), + ) # Test scenario where DNI is generated via component sum equation complete_df = irradiance.complete_irradiance( solar_position.apparent_zenith, ghi=i.ghi, dhi=i.dhi, dni=None, - dni_clear=clearsky.dni) + dni_clear=clearsky.dni, + ) # Assert that the ghi, dhi, and dni series match the original dataframe # values assert_frame_equal(complete_df, i) @@ -1408,7 +2004,8 @@ def test_complete_irradiance(): ghi=None, dhi=i.dhi, dni=i.dni, - dni_clear=clearsky.dni) + dni_clear=clearsky.dni, + ) # Assert that the ghi, dhi, and dni series match the original dataframe # values assert_frame_equal(complete_df, i) @@ -1418,37 +2015,47 @@ def test_complete_irradiance(): ghi=i.ghi, dhi=None, dni=i.dni, - dni_clear=clearsky.dni) + dni_clear=clearsky.dni, + ) # Assert that the ghi, dhi, and dni series match the original dataframe # values assert_frame_equal(complete_df, i) # Test scenario where all parameters are passed (throw error) with pytest.raises(ValueError): - irradiance.complete_irradiance(solar_position.apparent_zenith, - ghi=i.ghi, - dhi=i.dhi, - dni=i.dni, - dni_clear=clearsky.dni) + irradiance.complete_irradiance( + solar_position.apparent_zenith, + ghi=i.ghi, + dhi=i.dhi, + dni=i.dni, + dni_clear=clearsky.dni, + ) # Test scenario where only one parameter is passed (throw error) with pytest.raises(ValueError): - irradiance.complete_irradiance(solar_position.apparent_zenith, - ghi=None, - dhi=None, - dni=i.dni, - dni_clear=clearsky.dni) + irradiance.complete_irradiance( + solar_position.apparent_zenith, + ghi=None, + dhi=None, + dni=i.dni, + dni_clear=clearsky.dni, + ) def test_louche(): - - index = pd.DatetimeIndex(['20190101']*3 + ['20190620']*1) + index = pd.DatetimeIndex(["20190101"] * 3 + ["20190620"] * 1) ghi = pd.Series([0, 50, 1000, 1000], index=index) zenith = pd.Series([91, 85, 10, 10], index=index) - expected = pd.DataFrame(np.array( - [[0, 0, 0], - [130.089669, 38.661938, 0.405724], - [828.498650, 184.088106, 0.718133], - [887.407348, 126.074364, 0.768214]]), - columns=['dni', 'dhi', 'kt'], index=index) + expected = pd.DataFrame( + np.array( + [ + [0, 0, 0], + [130.089669, 38.661938, 0.405724], + [828.498650, 184.088106, 0.718133], + [887.407348, 126.074364, 0.768214], + ] + ), + columns=["dni", "dhi", "kt"], + index=index, + ) out = irradiance.louche(ghi, zenith, index) @@ -1456,9 +2063,12 @@ def test_louche(): def test_SURFACE_ALBEDOS_deprecated(): - with pytest.warns(pvlibDeprecationWarning, match='SURFACE_ALBEDOS has been' - ' moved to the albedo module as of v0.11.0. Please use' - ' pvlib.albedo.SURFACE_ALBEDOS.'): + with pytest.warns( + pvlibDeprecationWarning, + match="SURFACE_ALBEDOS has been" + " moved to the albedo module as of v0.11.0. Please use" + " pvlib.albedo.SURFACE_ALBEDOS.", + ): irradiance.SURFACE_ALBEDOS diff --git a/pvlib/tests/test_location.py b/pvlib/tests/test_location.py index e04b10ab4c..3d0eb2ddfd 100644 --- a/pvlib/tests/test_location.py +++ b/pvlib/tests/test_location.py @@ -24,20 +24,26 @@ def test_location_required(): def test_location_all(): - Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - - -@pytest.mark.parametrize('tz', [ - pytz.timezone('US/Arizona'), 'America/Phoenix', -7, -7.0, - datetime.timezone.utc -]) + Location(32.2, -111, "US/Arizona", 700, "Tucson") + + +@pytest.mark.parametrize( + "tz", + [ + pytz.timezone("US/Arizona"), + "America/Phoenix", + -7, + -7.0, + datetime.timezone.utc, + ], +) def test_location_tz(tz): Location(32.2, -111, tz) def test_location_invalid_tz(): with pytest.raises(UnknownTimeZoneError): - Location(32.2, -111, 'invalid') + Location(32.2, -111, "invalid") def test_location_invalid_tz_type(): @@ -46,41 +52,45 @@ def test_location_invalid_tz_type(): def test_location_print_all(): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - expected_str = '\n'.join([ - 'Location: ', - ' name: Tucson', - ' latitude: 32.2', - ' longitude: -111', - ' altitude: 700', - ' tz: US/Arizona' - ]) + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + expected_str = "\n".join( + [ + "Location: ", + " name: Tucson", + " latitude: 32.2", + " longitude: -111", + " altitude: 700", + " tz: US/Arizona", + ] + ) assert tus.__str__() == expected_str def test_location_print_pytz(): - tus = Location(32.2, -111, pytz.timezone('US/Arizona'), 700, 'Tucson') - expected_str = '\n'.join([ - 'Location: ', - ' name: Tucson', - ' latitude: 32.2', - ' longitude: -111', - ' altitude: 700', - ' tz: US/Arizona' - ]) + tus = Location(32.2, -111, pytz.timezone("US/Arizona"), 700, "Tucson") + expected_str = "\n".join( + [ + "Location: ", + " name: Tucson", + " latitude: 32.2", + " longitude: -111", + " altitude: 700", + " tz: US/Arizona", + ] + ) assert tus.__str__() == expected_str @pytest.fixture def times(): - return pd.date_range(start='20160101T0600-0700', - end='20160101T1800-0700', - freq='3h') + return pd.date_range( + start="20160101T0600-0700", end="20160101T1800-0700", freq="3h" + ) def test_get_clearsky(mocker, times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - m = mocker.spy(pvlib.clearsky, 'ineichen') + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + m = mocker.spy(pvlib.clearsky, "ineichen") out = tus.get_clearsky(times) assert m.call_count == 1 assert_index_equal(out.index, times) @@ -89,14 +99,15 @@ def test_get_clearsky(mocker, times): assert out.iloc[-1:, :].sum().sum() == 0 # check that values are > 0 during the day assert (out.iloc[1:-1, :] > 0).all().all() - assert (out.columns.values == ['ghi', 'dni', 'dhi']).all() + assert (out.columns.values == ["ghi", "dni", "dhi"]).all() def test_get_clearsky_ineichen_supply_linke(mocker): - tus = Location(32.2, -111, 'US/Arizona', 700) - times = pd.date_range(start='2014-06-24-0700', end='2014-06-25-0700', - freq='3h') - mocker.spy(pvlib.clearsky, 'ineichen') + tus = Location(32.2, -111, "US/Arizona", 700) + times = pd.date_range( + start="2014-06-24-0700", end="2014-06-25-0700", freq="3h" + ) + mocker.spy(pvlib.clearsky, "ineichen") out = tus.get_clearsky(times, linke_turbidity=3) # we only care that the LT is passed in this test pvlib.clearsky.ineichen.assert_called_once_with(ANY, ANY, 3, ANY, ANY) @@ -106,146 +117,183 @@ def test_get_clearsky_ineichen_supply_linke(mocker): assert out.iloc[-2:, :].sum().sum() == 0 # check that values are > 0 during the day assert (out.iloc[2:-2, :] > 0).all().all() - assert (out.columns.values == ['ghi', 'dni', 'dhi']).all() + assert (out.columns.values == ["ghi", "dni", "dhi"]).all() def test_get_clearsky_haurwitz(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - clearsky = tus.get_clearsky(times, model='haurwitz') - expected = pd.DataFrame(data=np.array( - [[ 0. ], - [ 242.30085588], - [ 559.38247117], - [ 384.6873791 ], - [ 0. ]]), - columns=['ghi'], - index=times) + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + clearsky = tus.get_clearsky(times, model="haurwitz") + expected = pd.DataFrame( + data=np.array( + [[0.0], [242.30085588], [559.38247117], [384.6873791], [0.0]] + ), + columns=["ghi"], + index=times, + ) assert_frame_equal(expected, clearsky) def test_get_clearsky_simplified_solis(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - clearsky = tus.get_clearsky(times, model='simplified_solis') - expected = pd.DataFrame(data=np. - array([[ 0. , 0. , 0. ], - [ 70.00146271, 638.01145669, 236.71136245], - [ 101.69729217, 852.51950946, 577.1117803 ], - [ 86.1679965 , 755.98048017, 385.59586091], - [ 0. , 0. , 0. ]]), - columns=['dhi', 'dni', 'ghi'], - index=times) - expected = expected[['ghi', 'dni', 'dhi']] + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + clearsky = tus.get_clearsky(times, model="simplified_solis") + expected = pd.DataFrame( + data=np.array( + [ + [0.0, 0.0, 0.0], + [70.00146271, 638.01145669, 236.71136245], + [101.69729217, 852.51950946, 577.1117803], + [86.1679965, 755.98048017, 385.59586091], + [0.0, 0.0, 0.0], + ] + ), + columns=["dhi", "dni", "ghi"], + index=times, + ) + expected = expected[["ghi", "dni", "dhi"]] assert_frame_equal(expected, clearsky, check_less_precise=2) def test_get_clearsky_simplified_solis_apparent_elevation(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - solar_position = {'apparent_elevation': pd.Series(80, index=times), - 'apparent_zenith': pd.Series(10, index=times)} - clearsky = tus.get_clearsky(times, model='simplified_solis', - solar_position=solar_position) - expected = pd.DataFrame(data=np. - array([[ 131.3124497 , 1001.14754036, 1108.14147919], - [ 131.3124497 , 1001.14754036, 1108.14147919], - [ 131.3124497 , 1001.14754036, 1108.14147919], - [ 131.3124497 , 1001.14754036, 1108.14147919], - [ 131.3124497 , 1001.14754036, 1108.14147919]]), - columns=['dhi', 'dni', 'ghi'], - index=times) - expected = expected[['ghi', 'dni', 'dhi']] + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + solar_position = { + "apparent_elevation": pd.Series(80, index=times), + "apparent_zenith": pd.Series(10, index=times), + } + clearsky = tus.get_clearsky( + times, model="simplified_solis", solar_position=solar_position + ) + expected = pd.DataFrame( + data=np.array( + [ + [131.3124497, 1001.14754036, 1108.14147919], + [131.3124497, 1001.14754036, 1108.14147919], + [131.3124497, 1001.14754036, 1108.14147919], + [131.3124497, 1001.14754036, 1108.14147919], + [131.3124497, 1001.14754036, 1108.14147919], + ] + ), + columns=["dhi", "dni", "ghi"], + index=times, + ) + expected = expected[["ghi", "dni", "dhi"]] assert_frame_equal(expected, clearsky, check_less_precise=2) def test_get_clearsky_simplified_solis_dni_extra(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - clearsky = tus.get_clearsky(times, model='simplified_solis', - dni_extra=1370) - expected = pd.DataFrame(data=np. - array([[ 0. , 0. , 0. ], - [ 67.82281485, 618.15469596, 229.34422063], - [ 98.53217848, 825.98663808, 559.15039353], - [ 83.48619937, 732.45218243, 373.59500313], - [ 0. , 0. , 0. ]]), - columns=['dhi', 'dni', 'ghi'], - index=times) - expected = expected[['ghi', 'dni', 'dhi']] + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + clearsky = tus.get_clearsky( + times, model="simplified_solis", dni_extra=1370 + ) + expected = pd.DataFrame( + data=np.array( + [ + [0.0, 0.0, 0.0], + [67.82281485, 618.15469596, 229.34422063], + [98.53217848, 825.98663808, 559.15039353], + [83.48619937, 732.45218243, 373.59500313], + [0.0, 0.0, 0.0], + ] + ), + columns=["dhi", "dni", "ghi"], + index=times, + ) + expected = expected[["ghi", "dni", "dhi"]] assert_frame_equal(expected, clearsky) def test_get_clearsky_simplified_solis_pressure(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - clearsky = tus.get_clearsky(times, model='simplified_solis', - pressure=95000) - expected = pd.DataFrame(data=np. - array([[ 0. , 0. , 0. ], - [ 70.20556637, 635.53091983, 236.17716435], - [ 102.08954904, 850.49502085, 576.28465815], - [ 86.46561686, 753.70744638, 384.90537859], - [ 0. , 0. , 0. ]]), - columns=['dhi', 'dni', 'ghi'], - index=times) - expected = expected[['ghi', 'dni', 'dhi']] + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + clearsky = tus.get_clearsky( + times, model="simplified_solis", pressure=95000 + ) + expected = pd.DataFrame( + data=np.array( + [ + [0.0, 0.0, 0.0], + [70.20556637, 635.53091983, 236.17716435], + [102.08954904, 850.49502085, 576.28465815], + [86.46561686, 753.70744638, 384.90537859], + [0.0, 0.0, 0.0], + ] + ), + columns=["dhi", "dni", "ghi"], + index=times, + ) + expected = expected[["ghi", "dni", "dhi"]] assert_frame_equal(expected, clearsky, check_less_precise=2) def test_get_clearsky_simplified_solis_aod_pw(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - clearsky = tus.get_clearsky(times, model='simplified_solis', - aod700=0.25, precipitable_water=2.) - expected = pd.DataFrame(data=np. - array([[ 0. , 0. , 0. ], - [ 85.77821205, 374.58084365, 179.48483117], - [ 143.52743364, 625.91745295, 490.06254157], - [ 114.63275842, 506.52275195, 312.24711495], - [ 0. , 0. , 0. ]]), - columns=['dhi', 'dni', 'ghi'], - index=times) - expected = expected[['ghi', 'dni', 'dhi']] + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + clearsky = tus.get_clearsky( + times, model="simplified_solis", aod700=0.25, precipitable_water=2.0 + ) + expected = pd.DataFrame( + data=np.array( + [ + [0.0, 0.0, 0.0], + [85.77821205, 374.58084365, 179.48483117], + [143.52743364, 625.91745295, 490.06254157], + [114.63275842, 506.52275195, 312.24711495], + [0.0, 0.0, 0.0], + ] + ), + columns=["dhi", "dni", "ghi"], + index=times, + ) + expected = expected[["ghi", "dni", "dhi"]] assert_frame_equal(expected, clearsky, check_less_precise=2) def test_get_clearsky_valueerror(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") with pytest.raises(ValueError): - tus.get_clearsky(times, model='invalid_model') + tus.get_clearsky(times, model="invalid_model") def test_from_tmy_3(): from pvlib.tests.iotools.test_tmy import TMY3_TESTFILE from pvlib.iotools import read_tmy3 + data, meta = read_tmy3(TMY3_TESTFILE, map_variables=True) loc = Location.from_tmy(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' + assert loc.tz != "UTC" assert_frame_equal(loc.weather, data) def test_from_tmy_2(): from pvlib.tests.iotools.test_tmy import TMY2_TESTFILE from pvlib.iotools import read_tmy2 + data, meta = read_tmy2(TMY2_TESTFILE) loc = Location.from_tmy(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' + assert loc.tz != "UTC" assert_frame_equal(loc.weather, data) def test_from_epw(): from pvlib.tests.iotools.test_epw import epw_testfile from pvlib.iotools import read_epw + data, meta = read_epw(epw_testfile) loc = Location.from_epw(meta, data) assert loc.name is not None assert loc.altitude != 0 - assert loc.tz != 'UTC' + assert loc.tz != "UTC" assert_frame_equal(loc.weather, data) def test_get_solarposition(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) ephem_data = golden_mst.get_solarposition(times, temperature=11) ephem_data = np.round(ephem_data, 3) expected_solpos.index = times @@ -254,103 +302,123 @@ def test_get_solarposition(expected_solpos, golden_mst): def test_get_airmass(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") airmass = tus.get_airmass(times) - expected = pd.DataFrame(data=np.array( - [[ nan, nan], - [ 3.61046506, 3.32072602], - [ 1.76470864, 1.62309115], - [ 2.45582153, 2.25874238], - [ nan, nan]]), - columns=['airmass_relative', 'airmass_absolute'], - index=times) + expected = pd.DataFrame( + data=np.array( + [ + [nan, nan], + [3.61046506, 3.32072602], + [1.76470864, 1.62309115], + [2.45582153, 2.25874238], + [nan, nan], + ] + ), + columns=["airmass_relative", "airmass_absolute"], + index=times, + ) assert_frame_equal(expected, airmass) - airmass = tus.get_airmass(times, model='young1994') - expected = pd.DataFrame(data=np.array( - [[ nan, nan], - [ 3.6075018 , 3.31800056], - [ 1.7641033 , 1.62253439], - [ 2.45413091, 2.25718744], - [ nan, nan]]), - columns=['airmass_relative', 'airmass_absolute'], - index=times) + airmass = tus.get_airmass(times, model="young1994") + expected = pd.DataFrame( + data=np.array( + [ + [nan, nan], + [3.6075018, 3.31800056], + [1.7641033, 1.62253439], + [2.45413091, 2.25718744], + [nan, nan], + ] + ), + columns=["airmass_relative", "airmass_absolute"], + index=times, + ) assert_frame_equal(expected, airmass) def test_get_airmass_valueerror(times): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") with pytest.raises(ValueError): - tus.get_airmass(times, model='invalid_model') + tus.get_airmass(times, model="invalid_model") def test_Location___repr__(): - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') - - expected = '\n'.join([ - 'Location: ', - ' name: Tucson', - ' latitude: 32.2', - ' longitude: -111', - ' altitude: 700', - ' tz: US/Arizona' - ]) + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") + + expected = "\n".join( + [ + "Location: ", + " name: Tucson", + " latitude: 32.2", + " longitude: -111", + " altitude: 700", + " tz: US/Arizona", + ] + ) assert tus.__repr__() == expected @requires_ephem def test_get_sun_rise_set_transit(golden): - times = pd.DatetimeIndex(['2015-01-01 07:00:00', '2015-01-01 23:00:00'], - tz='MST') - result = golden.get_sun_rise_set_transit(times, method='pyephem') - assert all(result.columns == ['sunrise', 'sunset', 'transit']) + times = pd.DatetimeIndex( + ["2015-01-01 07:00:00", "2015-01-01 23:00:00"], tz="MST" + ) + result = golden.get_sun_rise_set_transit(times, method="pyephem") + assert all(result.columns == ["sunrise", "sunset", "transit"]) - result = golden.get_sun_rise_set_transit(times, method='spa') - assert all(result.columns == ['sunrise', 'sunset', 'transit']) + result = golden.get_sun_rise_set_transit(times, method="spa") + assert all(result.columns == ["sunrise", "sunset", "transit"]) dayofyear = 1 declination = declination_spencer71(dayofyear) eot = equation_of_time_spencer71(dayofyear) - result = golden.get_sun_rise_set_transit(times, method='geometric', - declination=declination, - equation_of_time=eot) - assert all(result.columns == ['sunrise', 'sunset', 'transit']) + result = golden.get_sun_rise_set_transit( + times, + method="geometric", + declination=declination, + equation_of_time=eot, + ) + assert all(result.columns == ["sunrise", "sunset", "transit"]) def test_get_sun_rise_set_transit_valueerror(golden): - times = pd.DatetimeIndex(['2015-01-01 07:00:00', '2015-01-01 23:00:00'], - tz='MST') + times = pd.DatetimeIndex( + ["2015-01-01 07:00:00", "2015-01-01 23:00:00"], tz="MST" + ) with pytest.raises(ValueError): - golden.get_sun_rise_set_transit(times, method='eyeball') + golden.get_sun_rise_set_transit(times, method="eyeball") def test_extra_kwargs(): - with pytest.raises(TypeError, match='arbitrary_kwarg'): - Location(32.2, -111, arbitrary_kwarg='value') - - -@pytest.mark.parametrize('lat,lon,expected_alt', [ - pytest.param(32.2540, -110.9742, 724, id='Tucson, USA'), - pytest.param(-15.3875, 28.3228, 1253, id='Lusaka, Zambia'), - pytest.param(35.6762, 139.6503, 40, id='Tokyo, Japan'), - pytest.param(-35.2802, 149.1310, 566, id='Canberra, Australia'), - pytest.param(4.7110, -74.0721, 2555, id='Bogota, Colombia'), - pytest.param(31.525849, 35.449214, -415, id='Dead Sea, West Bank'), - pytest.param(28.6139, 77.2090, 214, id='New Delhi, India'), - pytest.param(0, 0, 0, id='Null Island, Atlantic Ocean'), -]) + with pytest.raises(TypeError, match="arbitrary_kwarg"): + Location(32.2, -111, arbitrary_kwarg="value") + + +@pytest.mark.parametrize( + "lat,lon,expected_alt", + [ + pytest.param(32.2540, -110.9742, 724, id="Tucson, USA"), + pytest.param(-15.3875, 28.3228, 1253, id="Lusaka, Zambia"), + pytest.param(35.6762, 139.6503, 40, id="Tokyo, Japan"), + pytest.param(-35.2802, 149.1310, 566, id="Canberra, Australia"), + pytest.param(4.7110, -74.0721, 2555, id="Bogota, Colombia"), + pytest.param(31.525849, 35.449214, -415, id="Dead Sea, West Bank"), + pytest.param(28.6139, 77.2090, 214, id="New Delhi, India"), + pytest.param(0, 0, 0, id="Null Island, Atlantic Ocean"), + ], +) def test_lookup_altitude(lat, lon, expected_alt): alt_found = lookup_altitude(lat, lon) assert alt_found == pytest.approx(expected_alt, abs=125) def test_location_lookup_altitude(mocker): - mocker.spy(location, 'lookup_altitude') - tus = Location(32.2, -111, 'US/Arizona', 700, 'Tucson') + mocker.spy(location, "lookup_altitude") + tus = Location(32.2, -111, "US/Arizona", 700, "Tucson") location.lookup_altitude.assert_not_called() assert tus.altitude == 700 location.lookup_altitude.reset_mock() - tus = Location(32.2, -111, 'US/Arizona') + tus = Location(32.2, -111, "US/Arizona") location.lookup_altitude.assert_called_once_with(32.2, -111) assert tus.altitude == location.lookup_altitude(32.2, -111) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index dcbd820f16..f85ed345a5 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -7,257 +7,306 @@ from pvlib.modelchain import ModelChain from pvlib.pvsystem import PVSystem from pvlib.location import Location -from pvlib._deprecation import pvlibDeprecationWarning from .conftest import assert_series_equal, assert_frame_equal import pytest -from .conftest import fail_on_pvlib_version - -@pytest.fixture(scope='function') -def sapm_dc_snl_ac_system(sapm_module_params, cec_inverter_parameters, - sapm_temperature_cs5p_220m): - module = 'Canadian_Solar_CS5P_220M___2009_' +@pytest.fixture(scope="function") +def sapm_dc_snl_ac_system( + sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m +): + module = "Canadian_Solar_CS5P_220M___2009_" module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=cec_inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=cec_inverter_parameters, + ) return system @pytest.fixture -def cec_dc_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, - sapm_temperature_cs5p_220m): +def cec_dc_snl_ac_system( + cec_module_cs5p_220m, cec_inverter_parameters, sapm_temperature_cs5p_220m +): module_parameters = cec_module_cs5p_220m.copy() - module_parameters['b'] = 0.05 - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["b"] = 0.05 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 temp_model_params = sapm_temperature_cs5p_220m.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module=module_parameters['Name'], - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=cec_inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module=module_parameters["Name"], + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=cec_inverter_parameters, + ) return system @pytest.fixture -def cec_dc_snl_ac_arrays(cec_module_cs5p_220m, cec_inverter_parameters, - sapm_temperature_cs5p_220m): +def cec_dc_snl_ac_arrays( + cec_module_cs5p_220m, cec_inverter_parameters, sapm_temperature_cs5p_220m +): module_parameters = cec_module_cs5p_220m.copy() - module_parameters['b'] = 0.05 - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["b"] = 0.05 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 temp_model_params = sapm_temperature_cs5p_220m.copy() array_one = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), - module=module_parameters['Name'], + module=module_parameters["Name"], module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) array_two = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), - module=module_parameters['Name'], + module=module_parameters["Name"], module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) system = PVSystem( arrays=[array_one, array_two], - inverter_parameters=cec_inverter_parameters + inverter_parameters=cec_inverter_parameters, ) return system @pytest.fixture -def cec_dc_native_snl_ac_system(cec_module_cs5p_220m, cec_inverter_parameters, - sapm_temperature_cs5p_220m): +def cec_dc_native_snl_ac_system( + cec_module_cs5p_220m, cec_inverter_parameters, sapm_temperature_cs5p_220m +): module_parameters = cec_module_cs5p_220m.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module=module_parameters['Name'], - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=cec_inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module=module_parameters["Name"], + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=cec_inverter_parameters, + ) return system @pytest.fixture -def pvsyst_dc_snl_ac_system(pvsyst_module_params, cec_inverter_parameters, - sapm_temperature_cs5p_220m): - module = 'PVsyst test module' +def pvsyst_dc_snl_ac_system( + pvsyst_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m +): + module = "PVsyst test module" module_parameters = pvsyst_module_params - module_parameters['b'] = 0.05 + module_parameters["b"] = 0.05 temp_model_params = sapm_temperature_cs5p_220m.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=cec_inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=cec_inverter_parameters, + ) return system @pytest.fixture -def pvsyst_dc_snl_ac_arrays(pvsyst_module_params, cec_inverter_parameters, - sapm_temperature_cs5p_220m): - module = 'PVsyst test module' +def pvsyst_dc_snl_ac_arrays( + pvsyst_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m +): + module = "PVsyst test module" module_parameters = pvsyst_module_params - module_parameters['b'] = 0.05 + module_parameters["b"] = 0.05 temp_model_params = sapm_temperature_cs5p_220m.copy() array_one = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module=module, module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) array_two = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), module=module, module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) system = PVSystem( arrays=[array_one, array_two], - inverter_parameters=cec_inverter_parameters + inverter_parameters=cec_inverter_parameters, ) return system @pytest.fixture -def cec_dc_adr_ac_system(sam_data, cec_module_cs5p_220m, - sapm_temperature_cs5p_220m): +def cec_dc_adr_ac_system( + sam_data, cec_module_cs5p_220m, sapm_temperature_cs5p_220m +): module_parameters = cec_module_cs5p_220m.copy() - module_parameters['b'] = 0.05 - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["b"] = 0.05 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 temp_model_params = sapm_temperature_cs5p_220m.copy() - inverters = sam_data['adrinverter'] - inverter = inverters['Zigor__Sunzet_3_TL_US_240V__CEC_2011_'].copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module=module_parameters['Name'], - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter) + inverters = sam_data["adrinverter"] + inverter = inverters["Zigor__Sunzet_3_TL_US_240V__CEC_2011_"].copy() + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module=module_parameters["Name"], + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter, + ) return system @pytest.fixture def pvwatts_dc_snl_ac_system(cec_inverter_parameters): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - inverter_parameters=cec_inverter_parameters) + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + inverter_parameters=cec_inverter_parameters, + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} temp_model_params = sapm_temperature_cs5p_220m.copy() - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_system_arrays(sapm_temperature_cs5p_220m): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} temp_model_params = sapm_temperature_cs5p_220m.copy() - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} array_one = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) array_two = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), module_parameters=module_parameters.copy(), - temperature_model_parameters=temp_model_params.copy() + temperature_model_parameters=temp_model_params.copy(), ) system = PVSystem( - arrays=[array_one, array_two], inverter_parameters=inverter_parameters) + arrays=[array_one, array_two], inverter_parameters=inverter_parameters + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_faiman_temp_system(): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} - temp_model_params = {'u0': 25.0, 'u1': 6.84} - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} + temp_model_params = {"u0": 25.0, "u1": 6.84} + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_pvsyst_temp_system(): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} - temp_model_params = {'u_c': 29.0, 'u_v': 0.0, 'module_efficiency': 0.1, - 'alpha_absorption': 0.9} - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} + temp_model_params = { + "u_c": 29.0, + "u_v": 0.0, + "module_efficiency": 0.1, + "alpha_absorption": 0.9, + } + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_fuentes_temp_system(): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} - temp_model_params = {'noct_installed': 45} - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} + temp_model_params = {"noct_installed": 45} + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture(scope="function") def pvwatts_dc_pvwatts_ac_noct_sam_temp_system(): - module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003} - temp_model_params = {'noct': 45, 'module_efficiency': 0.2} - inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 220, "gamma_pdc": -0.003} + temp_model_params = {"noct": 45, "module_efficiency": 0.2} + inverter_parameters = {"pdc0": 220, "eta_inv_nom": 0.95} + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture(scope="function") -def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m, - cec_inverter_parameters): +def system_no_aoi( + cec_module_cs5p_220m, sapm_temperature_cs5p_220m, cec_inverter_parameters +): module_parameters = cec_module_cs5p_220m.copy() - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 temp_model_params = sapm_temperature_cs5p_220m.copy() inverter_parameters = cec_inverter_parameters.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - inverter_parameters=inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture def system_no_temp(cec_module_cs5p_220m, cec_inverter_parameters): module_parameters = cec_module_cs5p_220m.copy() - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 inverter_parameters = cec_inverter_parameters.copy() - system = PVSystem(surface_tilt=32.2, surface_azimuth=180, - module_parameters=module_parameters, - inverter_parameters=inverter_parameters) + system = PVSystem( + surface_tilt=32.2, + surface_azimuth=180, + module_parameters=module_parameters, + inverter_parameters=inverter_parameters, + ) return system @@ -268,67 +317,84 @@ def location(): @pytest.fixture def weather(): - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - weather = pd.DataFrame({'ghi': [500, 0], 'dni': [800, 0], 'dhi': [100, 0]}, - index=times) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + weather = pd.DataFrame( + {"ghi": [500, 0], "dni": [800, 0], "dhi": [100, 0]}, index=times + ) return weather @pytest.fixture def total_irrad(weather): - return pd.DataFrame({'poa_global': [800., 500.], - 'poa_direct': [500., 300.], - 'poa_diffuse': [300., 200.]}, index=weather.index) + return pd.DataFrame( + { + "poa_global": [800.0, 500.0], + "poa_direct": [500.0, 300.0], + "poa_diffuse": [300.0, 200.0], + }, + index=weather.index, + ) -@pytest.fixture(scope='function') -def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, - sapm_temperature_cs5p_220m): - module = 'Canadian_Solar_CS5P_220M___2009_' +@pytest.fixture(scope="function") +def sapm_dc_snl_ac_system_Array( + sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m +): + module = "Canadian_Solar_CS5P_220M___2009_" module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32, - surface_azimuth=180), - albedo=0.2, module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - modules_per_string=1, - strings=1) - array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=15, - surface_azimuth=180), - albedo=0.2, module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - modules_per_string=1, - strings=1) - return PVSystem(arrays=[array_one, array_two], - inverter_parameters=cec_inverter_parameters) - - -@pytest.fixture(scope='function') -def sapm_dc_snl_ac_system_same_arrays(sapm_module_params, - cec_inverter_parameters, - sapm_temperature_cs5p_220m): + array_one = pvsystem.Array( + mount=pvsystem.FixedMount(surface_tilt=32, surface_azimuth=180), + albedo=0.2, + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1, + ) + array_two = pvsystem.Array( + mount=pvsystem.FixedMount(surface_tilt=15, surface_azimuth=180), + albedo=0.2, + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1, + ) + return PVSystem( + arrays=[array_one, array_two], + inverter_parameters=cec_inverter_parameters, + ) + + +@pytest.fixture(scope="function") +def sapm_dc_snl_ac_system_same_arrays( + sapm_module_params, cec_inverter_parameters, sapm_temperature_cs5p_220m +): """A system with two identical arrays.""" - module = 'Canadian_Solar_CS5P_220M___2009_' + module = "Canadian_Solar_CS5P_220M___2009_" module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2, - surface_azimuth=180), - module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - modules_per_string=1, - strings=1) - array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2, - surface_azimuth=180), - module=module, - module_parameters=module_parameters, - temperature_model_parameters=temp_model_params, - modules_per_string=1, - strings=1) - return PVSystem(arrays=[array_one, array_two], - inverter_parameters=cec_inverter_parameters) + array_one = pvsystem.Array( + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1, + ) + array_two = pvsystem.Array( + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), + module=module, + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + modules_per_string=1, + strings=1, + ) + return PVSystem( + arrays=[array_one, array_two], + inverter_parameters=cec_inverter_parameters, + ) def test_ModelChain_creation(sapm_dc_snl_ac_system, location): @@ -350,147 +416,144 @@ def test_with_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather): def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, - index=times) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + irradiance = pd.DataFrame( + {"dni": 900, "ghi": 600, "dhi": 150}, index=times + ) ac = mc.run_model(irradiance).results.ac - expected = pd.Series(np.array([187.80746494643176, -0.02]), - index=times) + expected = pd.Series(np.array([187.80746494643176, -0.02]), index=times) assert_series_equal(ac, expected) -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def multi_array_sapm_dc_snl_ac_system( - sapm_temperature_cs5p_220m, sapm_module_params, - cec_inverter_parameters): + sapm_temperature_cs5p_220m, sapm_module_params, cec_inverter_parameters +): module_parameters = sapm_module_params temp_model_parameters = sapm_temperature_cs5p_220m.copy() inverter_parameters = cec_inverter_parameters array_one = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module_parameters=module_parameters, - temperature_model_parameters=temp_model_parameters + temperature_model_parameters=temp_model_parameters, ) array_two = pvsystem.Array( mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=220), module_parameters=module_parameters, - temperature_model_parameters=temp_model_parameters + temperature_model_parameters=temp_model_parameters, ) two_array_system = PVSystem( - arrays=[array_one, array_two], - inverter_parameters=inverter_parameters + arrays=[array_one, array_two], inverter_parameters=inverter_parameters ) array_one_system = PVSystem( - arrays=[array_one], - inverter_parameters=inverter_parameters + arrays=[array_one], inverter_parameters=inverter_parameters ) array_two_system = PVSystem( - arrays=[array_two], - inverter_parameters=inverter_parameters + arrays=[array_two], inverter_parameters=inverter_parameters ) - return {'two_array_system': two_array_system, - 'array_one_system': array_one_system, - 'array_two_system': array_two_system} + return { + "two_array_system": two_array_system, + "array_one_system": array_one_system, + "array_two_system": array_two_system, + } def test_run_model_from_irradiance_arrays_no_loss( - multi_array_sapm_dc_snl_ac_system, location): + multi_array_sapm_dc_snl_ac_system, location +): mc_both = ModelChain( - multi_array_sapm_dc_snl_ac_system['two_array_system'], + multi_array_sapm_dc_snl_ac_system["two_array_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", ) mc_one = ModelChain( - multi_array_sapm_dc_snl_ac_system['array_one_system'], + multi_array_sapm_dc_snl_ac_system["array_one_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", ) mc_two = ModelChain( - multi_array_sapm_dc_snl_ac_system['array_two_system'], + multi_array_sapm_dc_snl_ac_system["array_two_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", + ) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + irradiance = pd.DataFrame( + {"dni": 900, "ghi": 600, "dhi": 150}, index=times ) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, - index=times) mc_one.run_model(irradiance) mc_two.run_model(irradiance) mc_both.run_model(irradiance) - assert_frame_equal( - mc_both.results.dc[0], - mc_one.results.dc - ) - assert_frame_equal( - mc_both.results.dc[1], - mc_two.results.dc - ) + assert_frame_equal(mc_both.results.dc[0], mc_one.results.dc) + assert_frame_equal(mc_both.results.dc[1], mc_two.results.dc) @pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_irradiance_arrays_no_loss_input_type( - multi_array_sapm_dc_snl_ac_system, location, input_type): + multi_array_sapm_dc_snl_ac_system, location, input_type +): mc_both = ModelChain( - multi_array_sapm_dc_snl_ac_system['two_array_system'], + multi_array_sapm_dc_snl_ac_system["two_array_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", ) mc_one = ModelChain( - multi_array_sapm_dc_snl_ac_system['array_one_system'], + multi_array_sapm_dc_snl_ac_system["array_one_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", ) mc_two = ModelChain( - multi_array_sapm_dc_snl_ac_system['array_two_system'], + multi_array_sapm_dc_snl_ac_system["array_two_system"], location, - aoi_model='no_loss', - spectral_model='no_loss', - losses_model='no_loss' + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", + ) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + irradiance = pd.DataFrame( + {"dni": 900, "ghi": 600, "dhi": 150}, index=times ) - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, - index=times) mc_one.run_model(irradiance) mc_two.run_model(irradiance) mc_both.run_model(input_type((irradiance, irradiance))) - assert_frame_equal( - mc_both.results.dc[0], mc_one.results.dc - ) - assert_frame_equal( - mc_both.results.dc[1], mc_two.results.dc - ) + assert_frame_equal(mc_both.results.dc[0], mc_one.results.dc) + assert_frame_equal(mc_both.results.dc[1], mc_two.results.dc) -@pytest.mark.parametrize('inverter', ['adr']) +@pytest.mark.parametrize("inverter", ["adr"]) def test_ModelChain_invalid_inverter_params_arrays( - inverter, sapm_dc_snl_ac_system_same_arrays, - location, adr_inverter_parameters): - inverter_params = {'adr': adr_inverter_parameters} - sapm_dc_snl_ac_system_same_arrays.inverter_parameters = \ - inverter_params[inverter] - with pytest.raises(ValueError, - match=r'adr inverter function cannot'): + inverter, + sapm_dc_snl_ac_system_same_arrays, + location, + adr_inverter_parameters, +): + inverter_params = {"adr": adr_inverter_parameters} + sapm_dc_snl_ac_system_same_arrays.inverter_parameters = inverter_params[ + inverter + ] + with pytest.raises(ValueError, match=r"adr inverter function cannot"): ModelChain(sapm_dc_snl_ac_system_same_arrays, location) @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_multi_weather( - sapm_dc_snl_ac_system_Array, location, input_type): - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6h') + sapm_dc_snl_ac_system_Array, location, input_type +): + times = pd.date_range( + start="20160101 1200-0700", end="20160101 1800-0700", freq="6h" + ) mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1}, - index=times) + weather = pd.DataFrame({"ghi": 1, "dhi": 1, "dni": 1}, index=times) mc.prepare_inputs(input_type((weather, weather))) num_arrays = sapm_dc_snl_ac_system_Array.num_arrays assert len(mc.results.total_irrad) == num_arrays @@ -500,12 +563,15 @@ def test_prepare_inputs_multi_weather( @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_albedo_in_weather( - sapm_dc_snl_ac_system_Array, location, input_type): - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6h') + sapm_dc_snl_ac_system_Array, location, input_type +): + times = pd.date_range( + start="20160101 1200-0700", end="20160101 1800-0700", freq="6h" + ) mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - weather = pd.DataFrame({'ghi': 1, 'dhi': 1, 'dni': 1, 'albedo': 0.5}, - index=times) + weather = pd.DataFrame( + {"ghi": 1, "dhi": 1, "dni": 1, "albedo": 0.5}, index=times + ) # weather as a single DataFrame mc.prepare_inputs(weather) num_arrays = sapm_dc_snl_ac_system_Array.num_arrays @@ -524,36 +590,36 @@ def test_prepare_inputs_no_irradiance(sapm_dc_snl_ac_system, location): def test_prepare_inputs_arrays_one_missing_irradiance( - sapm_dc_snl_ac_system_Array, location): + sapm_dc_snl_ac_system_Array, location +): """If any of the input DataFrames is missing a column then a ValueError is raised.""" mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - weather = pd.DataFrame( - {'ghi': [1], 'dhi': [1], 'dni': [1]} - ) - weather_incomplete = pd.DataFrame( - {'ghi': [1], 'dhi': [1]} - ) - with pytest.raises(ValueError, - match=r"Incomplete input data\. .*"): + weather = pd.DataFrame({"ghi": [1], "dhi": [1], "dni": [1]}) + weather_incomplete = pd.DataFrame({"ghi": [1], "dhi": [1]}) + with pytest.raises(ValueError, match=r"Incomplete input data\. .*"): mc.prepare_inputs((weather, weather_incomplete)) - with pytest.raises(ValueError, - match=r"Incomplete input data\. .*"): + with pytest.raises(ValueError, match=r"Incomplete input data\. .*"): mc.prepare_inputs((weather_incomplete, weather)) @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_weather_wrong_length( - sapm_dc_snl_ac_system_Array, location, input_type): + sapm_dc_snl_ac_system_Array, location, input_type +): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - weather = pd.DataFrame({'ghi': [1], 'dhi': [1], 'dni': [1]}) - with pytest.raises(ValueError, - match="Input must be same length as number of Arrays " - r"in system\. Expected 2, got 1\."): + weather = pd.DataFrame({"ghi": [1], "dhi": [1], "dni": [1]}) + with pytest.raises( + ValueError, + match="Input must be same length as number of Arrays " + r"in system\. Expected 2, got 1\.", + ): mc.prepare_inputs(input_type((weather,))) - with pytest.raises(ValueError, - match="Input must be same length as number of Arrays " - r"in system\. Expected 2, got 3\."): + with pytest.raises( + ValueError, + match="Input must be same length as number of Arrays " + r"in system\. Expected 2, got 3\.", + ): mc.prepare_inputs(input_type((weather, weather, weather))) @@ -563,19 +629,19 @@ def test_ModelChain_times_error_arrays(sapm_dc_snl_ac_system_Array, location): """ error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - irradiance = {'ghi': [1, 2], 'dhi': [1, 2], 'dni': [1, 2]} - times_one = pd.date_range(start='1/1/2020', freq='6h', periods=2) - times_two = pd.date_range(start='1/1/2020 00:15', freq='6h', periods=2) + irradiance = {"ghi": [1, 2], "dhi": [1, 2], "dni": [1, 2]} + times_one = pd.date_range(start="1/1/2020", freq="6h", periods=2) + times_two = pd.date_range(start="1/1/2020 00:15", freq="6h", periods=2) weather_one = pd.DataFrame(irradiance, index=times_one) weather_two = pd.DataFrame(irradiance, index=times_two) with pytest.raises(ValueError, match=error_str): mc.prepare_inputs((weather_one, weather_two)) # test with overlapping, but differently sized indices. - times_three = pd.date_range(start='1/1/2020', freq='6h', periods=3) + times_three = pd.date_range(start="1/1/2020", freq="6h", periods=3) irradiance_three = irradiance - irradiance_three['ghi'].append(3) - irradiance_three['dhi'].append(3) - irradiance_three['dni'].append(3) + irradiance_three["ghi"].append(3) + irradiance_three["dhi"].append(3) + irradiance_three["dni"].append(3) weather_three = pd.DataFrame(irradiance_three, index=times_three) with pytest.raises(ValueError, match=error_str): mc.prepare_inputs((weather_one, weather_three)) @@ -586,9 +652,9 @@ def test_ModelChain_times_arrays(sapm_dc_snl_ac_system_Array, location): DataFrames. """ mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - irradiance_one = {'ghi': [1, 2], 'dhi': [1, 2], 'dni': [1, 2]} - irradiance_two = {'ghi': [2, 1], 'dhi': [2, 1], 'dni': [2, 1]} - times = pd.date_range(start='1/1/2020', freq='6h', periods=2) + irradiance_one = {"ghi": [1, 2], "dhi": [1, 2], "dni": [1, 2]} + irradiance_two = {"ghi": [2, 1], "dhi": [2, 1], "dni": [2, 1]} + times = pd.date_range(start="1/1/2020", freq="6h", periods=2) weather_one = pd.DataFrame(irradiance_one, index=times) weather_two = pd.DataFrame(irradiance_two, index=times) mc.prepare_inputs((weather_one, weather_two)) @@ -598,179 +664,211 @@ def test_ModelChain_times_arrays(sapm_dc_snl_ac_system_Array, location): assert mc.results.times.equals(times) -@pytest.mark.parametrize("missing", ['dhi', 'ghi', 'dni']) +@pytest.mark.parametrize("missing", ["dhi", "ghi", "dni"]) def test_prepare_inputs_missing_irrad_component( - sapm_dc_snl_ac_system, location, missing): + sapm_dc_snl_ac_system, location, missing +): mc = ModelChain(sapm_dc_snl_ac_system, location) - weather = pd.DataFrame({'dhi': [1, 2], 'dni': [1, 2], 'ghi': [1, 2]}) + weather = pd.DataFrame({"dhi": [1, 2], "dni": [1, 2], "ghi": [1, 2]}) weather.drop(columns=missing, inplace=True) with pytest.raises(ValueError): mc.prepare_inputs(weather) -@pytest.mark.parametrize('ac_model', ['sandia', 'pvwatts']) +@pytest.mark.parametrize("ac_model", ["sandia", "pvwatts"]) @pytest.mark.parametrize("input_type", [tuple, list]) -def test_run_model_arrays_weather(sapm_dc_snl_ac_system_same_arrays, - pvwatts_dc_pvwatts_ac_system_arrays, - location, ac_model, input_type): - system = {'sandia': sapm_dc_snl_ac_system_same_arrays, - 'pvwatts': pvwatts_dc_pvwatts_ac_system_arrays} - mc = ModelChain(system[ac_model], location, aoi_model='no_loss', - spectral_model='no_loss') - times = pd.date_range('20200101 1200-0700', periods=2, freq='2h') - weather_one = pd.DataFrame({'dni': [900, 800], - 'ghi': [600, 500], - 'dhi': [150, 100]}, - index=times) - weather_two = pd.DataFrame({'dni': [500, 400], - 'ghi': [300, 200], - 'dhi': [75, 65]}, - index=times) +def test_run_model_arrays_weather( + sapm_dc_snl_ac_system_same_arrays, + pvwatts_dc_pvwatts_ac_system_arrays, + location, + ac_model, + input_type, +): + system = { + "sandia": sapm_dc_snl_ac_system_same_arrays, + "pvwatts": pvwatts_dc_pvwatts_ac_system_arrays, + } + mc = ModelChain( + system[ac_model], + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) + times = pd.date_range("20200101 1200-0700", periods=2, freq="2h") + weather_one = pd.DataFrame( + {"dni": [900, 800], "ghi": [600, 500], "dhi": [150, 100]}, index=times + ) + weather_two = pd.DataFrame( + {"dni": [500, 400], "ghi": [300, 200], "dhi": [75, 65]}, index=times + ) mc.run_model(input_type((weather_one, weather_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() assert not mc.results.ac.empty def test_run_model_perez(sapm_dc_snl_ac_system, location): - mc = ModelChain(sapm_dc_snl_ac_system, location, - transposition_model='perez') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, - index=times) + mc = ModelChain( + sapm_dc_snl_ac_system, location, transposition_model="perez" + ) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + irradiance = pd.DataFrame( + {"dni": 900, "ghi": 600, "dhi": 150}, index=times + ) ac = mc.run_model(irradiance).results.ac - expected = pd.Series(np.array([187.94295642, -2.00000000e-02]), - index=times) + expected = pd.Series( + np.array([187.94295642, -2.00000000e-02]), index=times + ) assert_series_equal(ac, expected) def test_run_model_gueymard_perez(sapm_dc_snl_ac_system, location): - mc = ModelChain(sapm_dc_snl_ac_system, location, - airmass_model='gueymard1993', - transposition_model='perez') - times = pd.date_range('20160101 1200-0700', periods=2, freq='6h') - irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150}, - index=times) + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + airmass_model="gueymard1993", + transposition_model="perez", + ) + times = pd.date_range("20160101 1200-0700", periods=2, freq="6h") + irradiance = pd.DataFrame( + {"dni": 900, "ghi": 600, "dhi": 150}, index=times + ) ac = mc.run_model(irradiance).results.ac - expected = pd.Series(np.array([187.94317405, -2.00000000e-02]), - index=times) + expected = pd.Series( + np.array([187.94317405, -2.00000000e-02]), index=times + ) assert_series_equal(ac, expected) -def test_run_model_with_weather_sapm_temp(sapm_dc_snl_ac_system, location, - weather, mocker): +def test_run_model_with_weather_sapm_temp( + sapm_dc_snl_ac_system, location, weather, mocker +): # test with sapm cell temperature model - weather['wind_speed'] = 5 - weather['temp_air'] = 10 + weather["wind_speed"] = 5 + weather["temp_air"] = 10 mc = ModelChain(sapm_dc_snl_ac_system, location) - mc.temperature_model = 'sapm' - m_sapm = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') + mc.temperature_model = "sapm" + m_sapm = mocker.spy(sapm_dc_snl_ac_system, "get_cell_temperature") mc.run_model(weather) assert m_sapm.call_count == 1 # assert_called_once_with cannot be used with series, so need to use # assert_series_equal on call_args - assert_series_equal(m_sapm.call_args[0][1], weather['temp_air']) # temp - assert_series_equal(m_sapm.call_args[0][2], weather['wind_speed']) # wind - assert m_sapm.call_args[1]['model'] == 'sapm' + assert_series_equal(m_sapm.call_args[0][1], weather["temp_air"]) # temp + assert_series_equal(m_sapm.call_args[0][2], weather["wind_speed"]) # wind + assert m_sapm.call_args[1]["model"] == "sapm" assert not mc.results.ac.empty -def test_run_model_with_weather_pvsyst_temp(sapm_dc_snl_ac_system, location, - weather, mocker): +def test_run_model_with_weather_pvsyst_temp( + sapm_dc_snl_ac_system, location, weather, mocker +): # test with pvsyst cell temperature model - weather['wind_speed'] = 5 - weather['temp_air'] = 10 - sapm_dc_snl_ac_system.arrays[0].racking_model = 'freestanding' - sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = \ - temperature._temperature_model_params('pvsyst', 'freestanding') + weather["wind_speed"] = 5 + weather["temp_air"] = 10 + sapm_dc_snl_ac_system.arrays[0].racking_model = "freestanding" + sapm_dc_snl_ac_system.arrays[ + 0 + ].temperature_model_parameters = temperature._temperature_model_params( + "pvsyst", "freestanding" + ) mc = ModelChain(sapm_dc_snl_ac_system, location) - mc.temperature_model = 'pvsyst' - m_pvsyst = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') + mc.temperature_model = "pvsyst" + m_pvsyst = mocker.spy(sapm_dc_snl_ac_system, "get_cell_temperature") mc.run_model(weather) assert m_pvsyst.call_count == 1 - assert_series_equal(m_pvsyst.call_args[0][1], weather['temp_air']) - assert_series_equal(m_pvsyst.call_args[0][2], weather['wind_speed']) - assert m_pvsyst.call_args[1]['model'] == 'pvsyst' + assert_series_equal(m_pvsyst.call_args[0][1], weather["temp_air"]) + assert_series_equal(m_pvsyst.call_args[0][2], weather["wind_speed"]) + assert m_pvsyst.call_args[1]["model"] == "pvsyst" assert not mc.results.ac.empty -def test_run_model_with_weather_faiman_temp(sapm_dc_snl_ac_system, location, - weather, mocker): +def test_run_model_with_weather_faiman_temp( + sapm_dc_snl_ac_system, location, weather, mocker +): # test with faiman cell temperature model - weather['wind_speed'] = 5 - weather['temp_air'] = 10 + weather["wind_speed"] = 5 + weather["temp_air"] = 10 sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = { - 'u0': 25.0, 'u1': 6.84 + "u0": 25.0, + "u1": 6.84, } mc = ModelChain(sapm_dc_snl_ac_system, location) - mc.temperature_model = 'faiman' - m_faiman = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') + mc.temperature_model = "faiman" + m_faiman = mocker.spy(sapm_dc_snl_ac_system, "get_cell_temperature") mc.run_model(weather) assert m_faiman.call_count == 1 - assert_series_equal(m_faiman.call_args[0][1], weather['temp_air']) - assert_series_equal(m_faiman.call_args[0][2], weather['wind_speed']) - assert m_faiman.call_args[1]['model'] == 'faiman' + assert_series_equal(m_faiman.call_args[0][1], weather["temp_air"]) + assert_series_equal(m_faiman.call_args[0][2], weather["wind_speed"]) + assert m_faiman.call_args[1]["model"] == "faiman" assert not mc.results.ac.empty -def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, - weather, mocker): - weather['wind_speed'] = 5 - weather['temp_air'] = 10 +def test_run_model_with_weather_fuentes_temp( + sapm_dc_snl_ac_system, location, weather, mocker +): + weather["wind_speed"] = 5 + weather["temp_air"] = 10 sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = { - 'noct_installed': 45, 'surface_tilt': 30, + "noct_installed": 45, + "surface_tilt": 30, } mc = ModelChain(sapm_dc_snl_ac_system, location) - mc.temperature_model = 'fuentes' - m_fuentes = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') + mc.temperature_model = "fuentes" + m_fuentes = mocker.spy(sapm_dc_snl_ac_system, "get_cell_temperature") mc.run_model(weather) assert m_fuentes.call_count == 1 - assert_series_equal(m_fuentes.call_args[0][1], weather['temp_air']) - assert_series_equal(m_fuentes.call_args[0][2], weather['wind_speed']) - assert m_fuentes.call_args[1]['model'] == 'fuentes' + assert_series_equal(m_fuentes.call_args[0][1], weather["temp_air"]) + assert_series_equal(m_fuentes.call_args[0][2], weather["wind_speed"]) + assert m_fuentes.call_args[1]["model"] == "fuentes" assert not mc.results.ac.empty -def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location, - weather, mocker): - weather['wind_speed'] = 5 - weather['temp_air'] = 10 +def test_run_model_with_weather_noct_sam_temp( + sapm_dc_snl_ac_system, location, weather, mocker +): + weather["wind_speed"] = 5 + weather["temp_air"] = 10 sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters = { - 'noct': 45, 'module_efficiency': 0.2 + "noct": 45, + "module_efficiency": 0.2, } mc = ModelChain(sapm_dc_snl_ac_system, location) - mc.temperature_model = 'noct_sam' - m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, 'get_cell_temperature') + mc.temperature_model = "noct_sam" + m_noct_sam = mocker.spy(sapm_dc_snl_ac_system, "get_cell_temperature") mc.run_model(weather) assert m_noct_sam.call_count == 1 - assert_series_equal(m_noct_sam.call_args[0][1], weather['temp_air']) - assert_series_equal(m_noct_sam.call_args[0][2], weather['wind_speed']) + assert_series_equal(m_noct_sam.call_args[0][1], weather["temp_air"]) + assert_series_equal(m_noct_sam.call_args[0][2], weather["wind_speed"]) # check that effective_irradiance was used assert m_noct_sam.call_args[1] == { - 'effective_irradiance': mc.results.effective_irradiance, - 'model': 'noct_sam'} + "effective_irradiance": mc.results.effective_irradiance, + "model": "noct_sam", + } -def test__assign_total_irrad(sapm_dc_snl_ac_system, location, weather, - total_irrad): +def test__assign_total_irrad( + sapm_dc_snl_ac_system, location, weather, total_irrad +): data = pd.concat([weather, total_irrad], axis=1) mc = ModelChain(sapm_dc_snl_ac_system, location) mc._assign_total_irrad(data) assert_frame_equal(mc.results.total_irrad, total_irrad) -def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, - weather, total_irrad): +def test_prepare_inputs_from_poa( + sapm_dc_snl_ac_system, location, weather, total_irrad +): data = pd.concat([weather, total_irrad], axis=1) mc = ModelChain(sapm_dc_snl_ac_system, location) mc.prepare_inputs_from_poa(data) weather_expected = weather.copy() - weather_expected['temp_air'] = 20 - weather_expected['wind_speed'] = 0 + weather_expected["temp_air"] = 20 + weather_expected["wind_speed"] = 0 # order as expected weather_expected = weather_expected[ - ['ghi', 'dhi', 'dni', 'wind_speed', 'temp_air']] + ["ghi", "dhi", "dni", "wind_speed", "temp_air"] + ] # weather attribute assert_frame_equal(mc.results.weather, weather_expected) # total_irrad attribute @@ -780,8 +878,8 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_from_poa_multi_data( - sapm_dc_snl_ac_system_Array, location, total_irrad, weather, - input_type): + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, input_type +): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) mc.prepare_inputs_from_poa(input_type((poa, poa))) @@ -791,10 +889,12 @@ def test_prepare_inputs_from_poa_multi_data( @pytest.mark.parametrize("input_type", [tuple, list]) def test_prepare_inputs_from_poa_wrong_number_arrays( - sapm_dc_snl_ac_system_Array, location, total_irrad, weather, - input_type): - len_error = r"Input must be same length as number of Arrays in system\. " \ - r"Expected 2, got [0-9]+\." + sapm_dc_snl_ac_system_Array, location, total_irrad, weather, input_type +): + len_error = ( + r"Input must be same length as number of Arrays in system\. " + r"Expected 2, got [0-9]+\." + ) type_error = r"Input must be a tuple of length 2, got .*\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) @@ -807,31 +907,41 @@ def test_prepare_inputs_from_poa_wrong_number_arrays( def test_prepare_inputs_from_poa_arrays_different_indices( - sapm_dc_snl_ac_system_Array, location, total_irrad, weather): + sapm_dc_snl_ac_system_Array, location, total_irrad, weather +): error_str = r"Input DataFrames must have same index\." mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) with pytest.raises(ValueError, match=error_str): - mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq='6h'))) + mc.prepare_inputs_from_poa((poa, poa.shift(periods=1, freq="6h"))) def test_prepare_inputs_from_poa_arrays_missing_column( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad +): mc = ModelChain(sapm_dc_snl_ac_system_Array, location) poa = pd.concat([weather, total_irrad], axis=1) - with pytest.raises(ValueError, match=r"Incomplete input data\. " - r"Data needs to contain .*\. " - r"Detected data in element 1 " - r"contains: .*"): - mc.prepare_inputs_from_poa((poa, poa.drop(columns='poa_global'))) - - -def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, - total_irrad): + with pytest.raises( + ValueError, + match=r"Incomplete input data\. " + r"Data needs to contain .*\. " + r"Detected data in element 1 " + r"contains: .*", + ): + mc.prepare_inputs_from_poa((poa, poa.drop(columns="poa_global"))) + + +def test__prepare_temperature( + sapm_dc_snl_ac_system, location, weather, total_irrad +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) # prepare_temperature expects mc.total_irrad and mc.results.weather # to be set mc._assign_weather(data) @@ -839,33 +949,45 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, mc._prepare_temperature(data) expected = pd.Series([48.928025, 38.080016], index=data.index) assert_series_equal(mc.results.cell_temperature, expected) - data['module_temperature'] = [40., 30.] + data["module_temperature"] = [40.0, 30.0] mc._prepare_temperature(data) expected = pd.Series([42.4, 31.5], index=data.index) assert_series_equal(mc.results.cell_temperature, expected) - data['cell_temperature'] = [50., 35.] + data["cell_temperature"] = [50.0, 35.0] mc._prepare_temperature(data) - assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) + assert_series_equal(mc.results.cell_temperature, data["cell_temperature"]) def test__prepare_temperature_len1_weather_tuple( - sapm_dc_snl_ac_system, location, weather, total_irrad): + sapm_dc_snl_ac_system, location, weather, total_irrad +): # GH 1192 - weather['module_temperature'] = [40., 30.] + weather["module_temperature"] = [40.0, 30.0] data = weather.copy() - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model([data]) expected = pd.Series([42.617244212941394, 30.0], index=data.index) assert_series_equal(mc.results.cell_temperature[0], expected) data = weather.copy().rename( columns={ - "ghi": "poa_global", "dhi": "poa_diffuse", "dni": "poa_direct"} + "ghi": "poa_global", + "dhi": "poa_diffuse", + "dni": "poa_direct", + } + ) + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", ) - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') mc.run_model_from_poa([data]) expected = pd.Series([41.5, 30.0], index=data.index) assert_series_equal(mc.results.cell_temperature[0], expected) @@ -873,21 +995,29 @@ def test__prepare_temperature_len1_weather_tuple( data = weather.copy()[["module_temperature", "ghi"]].rename( columns={"ghi": "effective_irradiance"} ) - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model_from_effective_irradiance([data]) expected = pd.Series([41.5, 30.0], index=data.index) assert_series_equal(mc.results.cell_temperature[0], expected) -def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, - location, weather, - total_irrad): +def test__prepare_temperature_arrays_weather( + sapm_dc_snl_ac_system_same_arrays, location, weather, total_irrad +): data = weather.copy() - data[['poa_global', 'poa_direct', 'poa_diffuse']] = total_irrad + data[["poa_global", "poa_direct", "poa_diffuse"]] = total_irrad data_two = data.copy() - mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, - aoi_model='no_loss', spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system_same_arrays, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) # prepare_temperature expects mc.results.total_irrad and mc.results.weather # to be set mc._assign_weather((data, data_two)) @@ -896,87 +1026,114 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, expected = pd.Series([48.928025, 38.080016], index=data.index) assert_series_equal(mc.results.cell_temperature[0], expected) assert_series_equal(mc.results.cell_temperature[1], expected) - data['module_temperature'] = [40., 30.] + data["module_temperature"] = [40.0, 30.0] mc._prepare_temperature((data, data_two)) expected = pd.Series([42.4, 31.5], index=data.index) assert (mc.results.cell_temperature[1] != expected).all() assert_series_equal(mc.results.cell_temperature[0], expected) - data['cell_temperature'] = [50., 35.] + data["cell_temperature"] = [50.0, 35.0] mc._prepare_temperature((data, data_two)) assert_series_equal( - mc.results.cell_temperature[0], data['cell_temperature']) - data_two['module_temperature'] = [40., 30.] + mc.results.cell_temperature[0], data["cell_temperature"] + ) + data_two["module_temperature"] = [40.0, 30.0] mc._prepare_temperature((data, data_two)) assert_series_equal(mc.results.cell_temperature[1], expected) assert_series_equal( - mc.results.cell_temperature[0], data['cell_temperature']) - data_two['cell_temperature'] = [10.0, 20.0] + mc.results.cell_temperature[0], data["cell_temperature"] + ) + data_two["cell_temperature"] = [10.0, 20.0] mc._prepare_temperature((data, data_two)) assert_series_equal( - mc.results.cell_temperature[1], data_two['cell_temperature']) + mc.results.cell_temperature[1], data_two["cell_temperature"] + ) assert_series_equal( - mc.results.cell_temperature[0], data['cell_temperature']) - - -@pytest.mark.parametrize('temp_params,temp_model', - [({'a': -3.47, 'b': -.0594, 'deltaT': 3}, - ModelChain.sapm_temp), - ({'u_c': 29.0, 'u_v': 0}, - ModelChain.pvsyst_temp), - ({'u0': 25.0, 'u1': 6.84}, - ModelChain.faiman_temp), - ({'noct_installed': 45}, - ModelChain.fuentes_temp), - ({'noct': 45, 'module_efficiency': 0.2}, - ModelChain.noct_sam_temp)]) + mc.results.cell_temperature[0], data["cell_temperature"] + ) + + +@pytest.mark.parametrize( + "temp_params,temp_model", + [ + ({"a": -3.47, "b": -0.0594, "deltaT": 3}, ModelChain.sapm_temp), + ({"u_c": 29.0, "u_v": 0}, ModelChain.pvsyst_temp), + ({"u0": 25.0, "u1": 6.84}, ModelChain.faiman_temp), + ({"noct_installed": 45}, ModelChain.fuentes_temp), + ({"noct": 45, "module_efficiency": 0.2}, ModelChain.noct_sam_temp), + ], +) def test_temperature_models_arrays_multi_weather( - temp_params, temp_model, - sapm_dc_snl_ac_system_same_arrays, - location, weather, total_irrad): + temp_params, + temp_model, + sapm_dc_snl_ac_system_same_arrays, + location, + weather, + total_irrad, +): for array in sapm_dc_snl_ac_system_same_arrays.arrays: array.temperature_model_parameters = temp_params # set air temp so it does not default to the same value for both arrays - weather['temp_air'] = 25 + weather["temp_air"] = 25 weather_one = weather weather_two = weather.copy() * 0.5 - mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, - aoi_model='no_loss', spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system_same_arrays, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.prepare_inputs((weather_one, weather_two)) temp_model(mc) - assert (mc.results.cell_temperature[0] - != mc.results.cell_temperature[1]).all() + assert ( + mc.results.cell_temperature[0] != mc.results.cell_temperature[1] + ).all() def test_run_model_solar_position_weather( - pvwatts_dc_pvwatts_ac_system, location, weather, mocker): - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, - aoi_model='no_loss', spectral_model='no_loss') - weather['pressure'] = 90000 - weather['temp_air'] = 25 - m = mocker.spy(location, 'get_solarposition') + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) + weather["pressure"] = 90000 + weather["temp_air"] = 25 + m = mocker.spy(location, "get_solarposition") mc.run_model(weather) # assert_called_once_with cannot be used with series, so need to use # assert_series_equal on call_args - assert_series_equal(m.call_args[1]['temperature'], weather['temp_air']) - assert_series_equal(m.call_args[1]['pressure'], weather['pressure']) + assert_series_equal(m.call_args[1]["temperature"], weather["temp_air"]) + assert_series_equal(m.call_args[1]["pressure"], weather["pressure"]) def test_run_model_from_poa(sapm_dc_snl_ac_system, location, total_irrad): - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_poa(total_irrad).results.ac - expected = pd.Series(np.array([149.280238, 96.678385]), - index=total_irrad.index) + expected = pd.Series( + np.array([149.280238, 96.678385]), index=total_irrad.index + ) assert_series_equal(ac, expected) @pytest.mark.parametrize("input_type", [tuple, list]) -def test_run_model_from_poa_arrays(sapm_dc_snl_ac_system_Array, location, - weather, total_irrad, input_type): +def test_run_model_from_poa_arrays( + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, input_type +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + mc = ModelChain( + sapm_dc_snl_ac_system_Array, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model_from_poa(input_type((data, data))) # arrays have different orientation, but should give same dc power # because we are the same passing POA irradiance and air @@ -985,46 +1142,59 @@ def test_run_model_from_poa_arrays(sapm_dc_snl_ac_system_Array, location, def test_run_model_from_poa_arrays_solar_position_weather( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad, mocker): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, mocker +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['pressure'] = 90000 - data['temp_air'] = 25 + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["pressure"] = 90000 + data["temp_air"] = 25 data2 = data.copy() - data2['pressure'] = 95000 - data2['temp_air'] = 30 - mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', - spectral_model='no_loss') - m = mocker.spy(location, 'get_solarposition') + data2["pressure"] = 95000 + data2["temp_air"] = 30 + mc = ModelChain( + sapm_dc_snl_ac_system_Array, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) + m = mocker.spy(location, "get_solarposition") mc.run_model_from_poa((data, data2)) # mc uses only the first weather data for solar position corrections - assert_series_equal(m.call_args[1]['temperature'], data['temp_air']) - assert_series_equal(m.call_args[1]['pressure'], data['pressure']) + assert_series_equal(m.call_args[1]["temperature"], data["temp_air"]) + assert_series_equal(m.call_args[1]["pressure"], data["pressure"]) @pytest.mark.parametrize("input_type", [lambda x: x[0], tuple, list]) -def test_run_model_from_effective_irradiance(sapm_dc_snl_ac_system, location, - weather, total_irrad, input_type): +def test_run_model_from_effective_irradiance( + sapm_dc_snl_ac_system, location, weather, total_irrad, input_type +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effective_irradiance'] = data['poa_global'] - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effective_irradiance"] = data["poa_global"] + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_effective_irradiance(input_type((data,))).results.ac - expected = pd.Series(np.array([149.280238, 96.678385]), - index=data.index) + expected = pd.Series(np.array([149.280238, 96.678385]), index=data.index) assert_series_equal(ac, expected) @pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_multi_array( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad, - input_type): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, input_type +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effective_irradiance'] = data['poa_global'] - mc = ModelChain(sapm_dc_snl_ac_system_Array, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effective_irradiance"] = data["poa_global"] + mc = ModelChain( + sapm_dc_snl_ac_system_Array, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model_from_effective_irradiance(input_type((data, data))) # arrays have different orientation, but should give same dc power # because we are the same passing POA irradiance and air @@ -1034,40 +1204,50 @@ def test_run_model_from_effective_irradiance_multi_array( @pytest.mark.parametrize("input_type", [lambda x: x[0], tuple, list]) def test_run_model_from_effective_irradiance_no_poa_global( - sapm_dc_snl_ac_system, location, weather, total_irrad, input_type): + sapm_dc_snl_ac_system, location, weather, total_irrad, input_type +): data = weather.copy() - data['effective_irradiance'] = total_irrad['poa_global'] - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + data["effective_irradiance"] = total_irrad["poa_global"] + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_effective_irradiance(input_type((data,))).results.ac - expected = pd.Series(np.array([149.280238, 96.678385]), - index=data.index) + expected = pd.Series(np.array([149.280238, 96.678385]), index=data.index) assert_series_equal(ac, expected) def test_run_model_from_effective_irradiance_poa_global_differs( - sapm_dc_snl_ac_system, location, weather, total_irrad): + sapm_dc_snl_ac_system, location, weather, total_irrad +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effective_irradiance'] = data['poa_global'] * 0.8 - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effective_irradiance"] = data["poa_global"] * 0.8 + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_effective_irradiance(data).results.ac - expected = pd.Series(np.array([118.302801, 76.099841]), - index=data.index) + expected = pd.Series(np.array([118.302801, 76.099841]), index=data.index) assert_series_equal(ac, expected) @pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays_error( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad, - input_type): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, input_type +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effetive_irradiance'] = data['poa_global'] + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effetive_irradiance"] = data["poa_global"] mc = ModelChain(sapm_dc_snl_ac_system_Array, location) - len_error = r"Input must be same length as number of Arrays in system\. " \ - r"Expected 2, got [0-9]+\." + len_error = ( + r"Input must be same length as number of Arrays in system\. " + r"Expected 2, got [0-9]+\." + ) type_error = r"Input must be a tuple of length 2, got DataFrame\." with pytest.raises(TypeError, match=type_error): mc.run_model_from_effective_irradiance(data) @@ -1075,21 +1255,22 @@ def test_run_model_from_effective_irradiance_arrays_error( mc.run_model_from_effective_irradiance(input_type((data,))) with pytest.raises(ValueError, match=len_error): mc.run_model_from_effective_irradiance(input_type((data, data, data))) - with pytest.raises(ValueError, - match=r"Input DataFrames must have same index\."): + with pytest.raises( + ValueError, match=r"Input DataFrames must have same index\." + ): mc.run_model_from_effective_irradiance( - (data, data.shift(periods=1, freq='6h')) + (data, data.shift(periods=1, freq="6h")) ) @pytest.mark.parametrize("input_type", [tuple, list]) def test_run_model_from_effective_irradiance_arrays( - sapm_dc_snl_ac_system_Array, location, weather, total_irrad, - input_type): + sapm_dc_snl_ac_system_Array, location, weather, total_irrad, input_type +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effective_irradiance'] = data['poa_global'] - data['cell_temperature'] = 40 + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effective_irradiance"] = data["poa_global"] + data["cell_temperature"] = 40 mc = ModelChain(sapm_dc_snl_ac_system_Array, location) mc.run_model_from_effective_irradiance(input_type((data, data))) # arrays have different orientation, but should give same dc power @@ -1098,21 +1279,25 @@ def test_run_model_from_effective_irradiance_arrays( assert_frame_equal(mc.results.dc[0], mc.results.dc[1]) # test that unequal inputs create unequal results data_two = data.copy() - data_two['effective_irradiance'] = data['poa_global'] * 0.5 + data_two["effective_irradiance"] = data["poa_global"] * 0.5 mc.run_model_from_effective_irradiance(input_type((data, data_two))) assert (mc.results.dc[0] != mc.results.dc[1]).all().all() def test_run_model_from_effective_irradiance_minimal_input( - sapm_dc_snl_ac_system, sapm_dc_snl_ac_system_Array, - location, total_irrad): - data = pd.DataFrame({'effective_irradiance': total_irrad['poa_global'], - 'cell_temperature': 40}, - index=total_irrad.index) + sapm_dc_snl_ac_system, sapm_dc_snl_ac_system_Array, location, total_irrad +): + data = pd.DataFrame( + { + "effective_irradiance": total_irrad["poa_global"], + "cell_temperature": 40, + }, + index=total_irrad.index, + ) mc = ModelChain(sapm_dc_snl_ac_system, location) mc.run_model_from_effective_irradiance(data) # make sure, for a single Array, the result is the correct type and value - assert_series_equal(mc.results.cell_temperature, data['cell_temperature']) + assert_series_equal(mc.results.cell_temperature, data["cell_temperature"]) assert not mc.results.dc.empty assert not mc.results.ac.empty # test with multiple arrays @@ -1122,10 +1307,15 @@ def test_run_model_from_effective_irradiance_minimal_input( assert not mc.results.ac.empty -def test_run_model_singleton_weather_single_array(cec_dc_snl_ac_system, - location, weather): - mc = ModelChain(cec_dc_snl_ac_system, location, - aoi_model="no_loss", spectral_model="no_loss") +def test_run_model_singleton_weather_single_array( + cec_dc_snl_ac_system, location, weather +): + mc = ModelChain( + cec_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model([weather]) assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.total_irrad, tuple) @@ -1140,12 +1330,18 @@ def test_run_model_singleton_weather_single_array(cec_dc_snl_ac_system, def test_run_model_from_poa_singleton_weather_single_array( - sapm_dc_snl_ac_system, location, total_irrad): - mc = ModelChain(sapm_dc_snl_ac_system, location, - aoi_model='no_loss', spectral_model='no_loss') + sapm_dc_snl_ac_system, location, total_irrad +): + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_poa([total_irrad]).results.ac - expected = pd.Series(np.array([149.280238, 96.678385]), - index=total_irrad.index) + expected = pd.Series( + np.array([149.280238, 96.678385]), index=total_irrad.index + ) assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.cell_temperature, tuple) assert len(mc.results.cell_temperature) == 1 @@ -1154,15 +1350,19 @@ def test_run_model_from_poa_singleton_weather_single_array( def test_run_model_from_effective_irradiance_weather_single_array( - sapm_dc_snl_ac_system, location, weather, total_irrad): + sapm_dc_snl_ac_system, location, weather, total_irrad +): data = weather.copy() - data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad - data['effective_irradiance'] = data['poa_global'] - mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', - spectral_model='no_loss') + data[["poa_global", "poa_diffuse", "poa_direct"]] = total_irrad + data["effective_irradiance"] = data["poa_global"] + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + ) ac = mc.run_model_from_effective_irradiance([data]).results.ac - expected = pd.Series(np.array([149.280238, 96.678385]), - index=data.index) + expected = pd.Series(np.array([149.280238, 96.678385]), index=data.index) assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.cell_temperature, tuple) assert len(mc.results.cell_temperature) == 1 @@ -1174,84 +1374,114 @@ def test_run_model_from_effective_irradiance_weather_single_array( def poadc(mc): - mc.results.dc = mc.results.total_irrad['poa_global'] * 0.2 + mc.results.dc = mc.results.total_irrad["poa_global"] * 0.2 mc.results.dc.name = None # assert_series_equal will fail without this -@pytest.mark.parametrize('dc_model', [ - 'sapm', 'cec', 'desoto', 'pvsyst', 'singlediode', 'pvwatts_dc']) -def test_infer_dc_model(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, - pvsyst_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_system, - location, dc_model, weather, mocker): - dc_systems = {'sapm': sapm_dc_snl_ac_system, - 'cec': cec_dc_snl_ac_system, - 'desoto': cec_dc_snl_ac_system, - 'pvsyst': pvsyst_dc_snl_ac_system, - 'singlediode': cec_dc_snl_ac_system, - 'pvwatts_dc': pvwatts_dc_pvwatts_ac_system} - dc_model_function = {'sapm': 'sapm', - 'cec': 'calcparams_cec', - 'desoto': 'calcparams_desoto', - 'pvsyst': 'calcparams_pvsyst', - 'singlediode': 'calcparams_desoto', - 'pvwatts_dc': 'pvwatts_dc'} - temp_model_function = {'sapm': 'sapm', - 'cec': 'sapm', - 'desoto': 'sapm', - 'pvsyst': 'pvsyst', - 'singlediode': 'sapm', - 'pvwatts_dc': 'sapm'} - temp_model_params = {'sapm': {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}, - 'pvsyst': {'u_c': 29.0, 'u_v': 0}} +@pytest.mark.parametrize( + "dc_model", + ["sapm", "cec", "desoto", "pvsyst", "singlediode", "pvwatts_dc"], +) +def test_infer_dc_model( + sapm_dc_snl_ac_system, + cec_dc_snl_ac_system, + pvsyst_dc_snl_ac_system, + pvwatts_dc_pvwatts_ac_system, + location, + dc_model, + weather, + mocker, +): + dc_systems = { + "sapm": sapm_dc_snl_ac_system, + "cec": cec_dc_snl_ac_system, + "desoto": cec_dc_snl_ac_system, + "pvsyst": pvsyst_dc_snl_ac_system, + "singlediode": cec_dc_snl_ac_system, + "pvwatts_dc": pvwatts_dc_pvwatts_ac_system, + } + dc_model_function = { + "sapm": "sapm", + "cec": "calcparams_cec", + "desoto": "calcparams_desoto", + "pvsyst": "calcparams_pvsyst", + "singlediode": "calcparams_desoto", + "pvwatts_dc": "pvwatts_dc", + } + temp_model_function = { + "sapm": "sapm", + "cec": "sapm", + "desoto": "sapm", + "pvsyst": "pvsyst", + "singlediode": "sapm", + "pvwatts_dc": "sapm", + } + temp_model_params = { + "sapm": {"a": -3.40641, "b": -0.0842075, "deltaT": 3}, + "pvsyst": {"u_c": 29.0, "u_v": 0}, + } system = dc_systems[dc_model] for array in system.arrays: array.temperature_model_parameters = temp_model_params[ - temp_model_function[dc_model]] + temp_model_function[dc_model] + ] # remove Adjust from model parameters for desoto, singlediode - if dc_model in ['desoto', 'singlediode']: + if dc_model in ["desoto", "singlediode"]: for array in system.arrays: - array.module_parameters.pop('Adjust') + array.module_parameters.pop("Adjust") m = mocker.spy(pvsystem, dc_model_function[dc_model]) - mc = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss', - temperature_model=temp_model_function[dc_model]) + mc = ModelChain( + system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + temperature_model=temp_model_function[dc_model], + ) mc.run_model(weather) assert m.call_count == 1 assert isinstance(mc.results.dc, (pd.Series, pd.DataFrame)) -def test_infer_dc_model_incomplete(multi_array_sapm_dc_snl_ac_system, - location): - match = 'Could not infer DC model from the module_parameters attributes ' - system = multi_array_sapm_dc_snl_ac_system['two_array_system'] - system.arrays[0].module_parameters.pop('A0') +def test_infer_dc_model_incomplete( + multi_array_sapm_dc_snl_ac_system, location +): + match = "Could not infer DC model from the module_parameters attributes " + system = multi_array_sapm_dc_snl_ac_system["two_array_system"] + system.arrays[0].module_parameters.pop("A0") with pytest.raises(ValueError, match=match): ModelChain(system, location) -@pytest.mark.parametrize('dc_model', ['cec', 'desoto', 'pvsyst']) -def test_singlediode_dc_arrays(location, dc_model, - cec_dc_snl_ac_arrays, - pvsyst_dc_snl_ac_arrays, - weather): - systems = {'cec': cec_dc_snl_ac_arrays, - 'pvsyst': pvsyst_dc_snl_ac_arrays, - 'desoto': cec_dc_snl_ac_arrays} - temp_sapm = {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3} - temp_pvsyst = {'u_c': 29.0, 'u_v': 0} - temp_model_params = {'cec': temp_sapm, - 'desoto': temp_sapm, - 'pvsyst': temp_pvsyst} - temp_model = {'cec': 'sapm', 'desoto': 'sapm', 'pvsyst': 'pvsyst'} +@pytest.mark.parametrize("dc_model", ["cec", "desoto", "pvsyst"]) +def test_singlediode_dc_arrays( + location, dc_model, cec_dc_snl_ac_arrays, pvsyst_dc_snl_ac_arrays, weather +): + systems = { + "cec": cec_dc_snl_ac_arrays, + "pvsyst": pvsyst_dc_snl_ac_arrays, + "desoto": cec_dc_snl_ac_arrays, + } + temp_sapm = {"a": -3.40641, "b": -0.0842075, "deltaT": 3} + temp_pvsyst = {"u_c": 29.0, "u_v": 0} + temp_model_params = { + "cec": temp_sapm, + "desoto": temp_sapm, + "pvsyst": temp_pvsyst, + } + temp_model = {"cec": "sapm", "desoto": "sapm", "pvsyst": "pvsyst"} system = systems[dc_model] for array in system.arrays: array.temperature_model_parameters = temp_model_params[dc_model] - if dc_model == 'desoto': + if dc_model == "desoto": for array in system.arrays: - array.module_parameters.pop('Adjust') - mc = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss', - temperature_model=temp_model[dc_model]) + array.module_parameters.pop("Adjust") + mc = ModelChain( + system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + temperature_model=temp_model[dc_model], + ) mc.run_model(weather) assert isinstance(mc.results.dc, tuple) assert len(mc.results.dc) == system.num_arrays @@ -1259,93 +1489,139 @@ def test_singlediode_dc_arrays(location, dc_model, assert isinstance(dc, (pd.Series, pd.DataFrame)) -@pytest.mark.parametrize('dc_model', ['sapm', 'cec', 'cec_native']) -def test_infer_spectral_model(location, sapm_dc_snl_ac_system, - cec_dc_snl_ac_system, - cec_dc_native_snl_ac_system, dc_model): - dc_systems = {'sapm': sapm_dc_snl_ac_system, - 'cec': cec_dc_snl_ac_system, - 'cec_native': cec_dc_native_snl_ac_system} +@pytest.mark.parametrize("dc_model", ["sapm", "cec", "cec_native"]) +def test_infer_spectral_model( + location, + sapm_dc_snl_ac_system, + cec_dc_snl_ac_system, + cec_dc_native_snl_ac_system, + dc_model, +): + dc_systems = { + "sapm": sapm_dc_snl_ac_system, + "cec": cec_dc_snl_ac_system, + "cec_native": cec_dc_native_snl_ac_system, + } system = dc_systems[dc_model] - mc = ModelChain(system, location, aoi_model='physical') + mc = ModelChain(system, location, aoi_model="physical") assert isinstance(mc, ModelChain) -@pytest.mark.parametrize('temp_model', [ - 'sapm_temp', 'faiman_temp', 'pvsyst_temp', 'fuentes_temp', - 'noct_sam_temp']) -def test_infer_temp_model(location, sapm_dc_snl_ac_system, - pvwatts_dc_pvwatts_ac_pvsyst_temp_system, - pvwatts_dc_pvwatts_ac_faiman_temp_system, - pvwatts_dc_pvwatts_ac_fuentes_temp_system, - pvwatts_dc_pvwatts_ac_noct_sam_temp_system, - temp_model): - dc_systems = {'sapm_temp': sapm_dc_snl_ac_system, - 'pvsyst_temp': pvwatts_dc_pvwatts_ac_pvsyst_temp_system, - 'faiman_temp': pvwatts_dc_pvwatts_ac_faiman_temp_system, - 'fuentes_temp': pvwatts_dc_pvwatts_ac_fuentes_temp_system, - 'noct_sam_temp': pvwatts_dc_pvwatts_ac_noct_sam_temp_system} +@pytest.mark.parametrize( + "temp_model", + [ + "sapm_temp", + "faiman_temp", + "pvsyst_temp", + "fuentes_temp", + "noct_sam_temp", + ], +) +def test_infer_temp_model( + location, + sapm_dc_snl_ac_system, + pvwatts_dc_pvwatts_ac_pvsyst_temp_system, + pvwatts_dc_pvwatts_ac_faiman_temp_system, + pvwatts_dc_pvwatts_ac_fuentes_temp_system, + pvwatts_dc_pvwatts_ac_noct_sam_temp_system, + temp_model, +): + dc_systems = { + "sapm_temp": sapm_dc_snl_ac_system, + "pvsyst_temp": pvwatts_dc_pvwatts_ac_pvsyst_temp_system, + "faiman_temp": pvwatts_dc_pvwatts_ac_faiman_temp_system, + "fuentes_temp": pvwatts_dc_pvwatts_ac_fuentes_temp_system, + "noct_sam_temp": pvwatts_dc_pvwatts_ac_noct_sam_temp_system, + } system = dc_systems[temp_model] - mc = ModelChain(system, location, aoi_model='physical', - spectral_model='no_loss') + mc = ModelChain( + system, location, aoi_model="physical", spectral_model="no_loss" + ) assert temp_model == mc.temperature_model.__name__ assert isinstance(mc, ModelChain) def test_infer_temp_model_invalid(location, sapm_dc_snl_ac_system): - sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters.pop('a') + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters.pop("a") with pytest.raises(ValueError): - ModelChain(sapm_dc_snl_ac_system, location, - aoi_model='physical', spectral_model='no_loss') + ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="physical", + spectral_model="no_loss", + ) def test_temperature_model_inconsistent(location, sapm_dc_snl_ac_system): with pytest.raises(ValueError): - ModelChain(sapm_dc_snl_ac_system, location, aoi_model='physical', - spectral_model='no_loss', temperature_model='pvsyst') + ModelChain( + sapm_dc_snl_ac_system, + location, + aoi_model="physical", + spectral_model="no_loss", + temperature_model="pvsyst", + ) def test_temperature_model_not_specified(): location = Location(latitude=32.2, longitude=-110.9) - arrays = [pvsystem.Array(pvsystem.FixedMount(), - module_parameters={'pdc0': 1, 'gamma_pdc': 0})] - system = pvsystem.PVSystem(arrays, - temperature_model_parameters={'u0': 1, 'u1': 1}, - inverter_parameters={'pdc0': 1}) - with pytest.raises(ValueError, - match='Could not infer temperature model from ' - 'ModelChain.system.'): - _ = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss') - - -def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, - mocker): - m = mocker.spy(sys.modules[__name__], 'poadc') - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model=poadc, - aoi_model='no_loss', spectral_model='no_loss') + arrays = [ + pvsystem.Array( + pvsystem.FixedMount(), + module_parameters={"pdc0": 1, "gamma_pdc": 0}, + ) + ] + system = pvsystem.PVSystem( + arrays, + temperature_model_parameters={"u0": 1, "u1": 1}, + inverter_parameters={"pdc0": 1}, + ) + with pytest.raises( + ValueError, + match="Could not infer temperature model from " "ModelChain.system.", + ): + _ = ModelChain( + system, location, aoi_model="no_loss", spectral_model="no_loss" + ) + + +def test_dc_model_user_func( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): + m = mocker.spy(sys.modules[__name__], "poadc") + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + dc_model=poadc, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model(weather) assert m.call_count == 1 assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) assert not mc.results.ac.empty -def test_pvwatts_dc_multiple_strings(pvwatts_dc_pvwatts_ac_system, location, - weather, mocker): +def test_pvwatts_dc_multiple_strings( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): system = pvwatts_dc_pvwatts_ac_system - m = mocker.spy(system, 'scale_voltage_current_power') - mc1 = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss') + m = mocker.spy(system, "scale_voltage_current_power") + mc1 = ModelChain( + system, location, aoi_model="no_loss", spectral_model="no_loss" + ) mc1.run_model(weather) assert m.call_count == 1 system.arrays[0].modules_per_string = 2 - mc2 = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss') + mc2 = ModelChain( + system, location, aoi_model="no_loss", spectral_model="no_loss" + ) mc2.run_model(weather) assert isinstance(mc2.results.ac, (pd.Series, pd.DataFrame)) assert not mc2.results.ac.empty - expected = pd.Series(data=[2., np.nan], index=mc2.results.dc.index, - name='p_mp') + expected = pd.Series( + data=[2.0, np.nan], index=mc2.results.dc.index, name="p_mp" + ) assert_series_equal(mc2.results.dc / mc1.results.dc, expected) @@ -1353,31 +1629,48 @@ def acdc(mc): mc.results.ac = mc.results.dc -@pytest.mark.parametrize('inverter_model', ['sandia', 'adr', - 'pvwatts', 'sandia_multi', - 'pvwatts_multi']) -def test_ac_models(sapm_dc_snl_ac_system, cec_dc_adr_ac_system, - pvwatts_dc_pvwatts_ac_system, cec_dc_snl_ac_arrays, - pvwatts_dc_pvwatts_ac_system_arrays, - location, inverter_model, weather, mocker): - ac_systems = {'sandia': sapm_dc_snl_ac_system, - 'sandia_multi': cec_dc_snl_ac_arrays, - 'adr': cec_dc_adr_ac_system, - 'pvwatts': pvwatts_dc_pvwatts_ac_system, - 'pvwatts_multi': pvwatts_dc_pvwatts_ac_system_arrays} +@pytest.mark.parametrize( + "inverter_model", + ["sandia", "adr", "pvwatts", "sandia_multi", "pvwatts_multi"], +) +def test_ac_models( + sapm_dc_snl_ac_system, + cec_dc_adr_ac_system, + pvwatts_dc_pvwatts_ac_system, + cec_dc_snl_ac_arrays, + pvwatts_dc_pvwatts_ac_system_arrays, + location, + inverter_model, + weather, + mocker, +): + ac_systems = { + "sandia": sapm_dc_snl_ac_system, + "sandia_multi": cec_dc_snl_ac_arrays, + "adr": cec_dc_adr_ac_system, + "pvwatts": pvwatts_dc_pvwatts_ac_system, + "pvwatts_multi": pvwatts_dc_pvwatts_ac_system_arrays, + } inverter_to_ac_model = { - 'sandia': 'sandia', - 'sandia_multi': 'sandia', - 'adr': 'adr', - 'pvwatts': 'pvwatts', - 'pvwatts_multi': 'pvwatts'} + "sandia": "sandia", + "sandia_multi": "sandia", + "adr": "adr", + "pvwatts": "pvwatts", + "pvwatts_multi": "pvwatts", + } ac_model = inverter_to_ac_model[inverter_model] system = ac_systems[inverter_model] - mc_inferred = ModelChain(system, location, - aoi_model='no_loss', spectral_model='no_loss') - mc = ModelChain(system, location, ac_model=ac_model, - aoi_model='no_loss', spectral_model='no_loss') + mc_inferred = ModelChain( + system, location, aoi_model="no_loss", spectral_model="no_loss" + ) + mc = ModelChain( + system, + location, + ac_model=ac_model, + aoi_model="no_loss", + spectral_model="no_loss", + ) # tests ModelChain.infer_ac_model assert mc_inferred.ac_model.__name__ == mc.ac_model.__name__ @@ -1390,11 +1683,17 @@ def test_ac_models(sapm_dc_snl_ac_system, cec_dc_adr_ac_system, assert mc.results.ac.iloc[1] < 1 -def test_ac_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, - mocker): - m = mocker.spy(sys.modules[__name__], 'acdc') - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, ac_model=acdc, - aoi_model='no_loss', spectral_model='no_loss') +def test_ac_model_user_func( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): + m = mocker.spy(sys.modules[__name__], "acdc") + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + ac_model=acdc, + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model(weather) assert m.call_count == 1 assert_series_equal(mc.results.ac, mc.results.dc) @@ -1402,24 +1701,30 @@ def test_ac_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather, def test_ac_model_not_a_model(pvwatts_dc_pvwatts_ac_system, location, weather): - exc_text = 'not a valid AC power model' + exc_text = "not a valid AC power model" with pytest.raises(ValueError, match=exc_text): - ModelChain(pvwatts_dc_pvwatts_ac_system, location, - ac_model='not_a_model', aoi_model='no_loss', - spectral_model='no_loss') + ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + ac_model="not_a_model", + aoi_model="no_loss", + spectral_model="no_loss", + ) def test_infer_ac_model_invalid_params(location): # only the keys are relevant here, using arbitrary values - module_parameters = {'pdc0': 1, 'gamma_pdc': 1} + module_parameters = {"pdc0": 1, "gamma_pdc": 1} system = pvsystem.PVSystem( - arrays=[pvsystem.Array( - mount=pvsystem.FixedMount(0, 180), - module_parameters=module_parameters - )], - inverter_parameters={'foo': 1, 'bar': 2} + arrays=[ + pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), + module_parameters=module_parameters, + ) + ], + inverter_parameters={"foo": 1, "bar": 2}, ) - with pytest.raises(ValueError, match='could not infer AC model'): + with pytest.raises(ValueError, match="could not infer AC model"): ModelChain(system, location) @@ -1427,14 +1732,20 @@ def constant_aoi_loss(mc): mc.results.aoi_modifier = 0.9 -@pytest.mark.parametrize('aoi_model', [ - 'sapm', 'ashrae', 'physical', 'martin_ruiz' -]) -def test_aoi_models(sapm_dc_snl_ac_system, location, aoi_model, - weather, mocker): - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model=aoi_model, spectral_model='no_loss') - m = mocker.spy(sapm_dc_snl_ac_system, 'get_iam') +@pytest.mark.parametrize( + "aoi_model", ["sapm", "ashrae", "physical", "martin_ruiz"] +) +def test_aoi_models( + sapm_dc_snl_ac_system, location, aoi_model, weather, mocker +): + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model=aoi_model, + spectral_model="no_loss", + ) + m = mocker.spy(sapm_dc_snl_ac_system, "get_iam") mc.run_model(weather=weather) assert m.call_count == 1 assert isinstance(mc.results.ac, pd.Series) @@ -1443,13 +1754,19 @@ def test_aoi_models(sapm_dc_snl_ac_system, location, aoi_model, assert mc.results.ac.iloc[1] < 1 -@pytest.mark.parametrize('aoi_model', [ - 'sapm', 'ashrae', 'physical', 'martin_ruiz' -]) +@pytest.mark.parametrize( + "aoi_model", ["sapm", "ashrae", "physical", "martin_ruiz"] +) def test_aoi_models_singleon_weather_single_array( - sapm_dc_snl_ac_system, location, aoi_model, weather): - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model=aoi_model, spectral_model='no_loss') + sapm_dc_snl_ac_system, location, aoi_model, weather +): + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model=aoi_model, + spectral_model="no_loss", + ) mc.run_model(weather=[weather]) assert isinstance(mc.results.aoi_modifier, tuple) assert len(mc.results.aoi_modifier) == 1 @@ -1460,8 +1777,13 @@ def test_aoi_models_singleon_weather_single_array( def test_aoi_model_no_loss(sapm_dc_snl_ac_system, location, weather): - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model='no_loss', spectral_model='no_loss') + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model="no_loss", + spectral_model="no_loss", + ) mc.run_model(weather) assert mc.results.aoi_modifier == 1.0 assert not mc.results.ac.empty @@ -1472,18 +1794,22 @@ def test_aoi_model_no_loss(sapm_dc_snl_ac_system, location, weather): def test_aoi_model_interp(sapm_dc_snl_ac_system, location, weather, mocker): # similar to test_aoi_models but requires arguments to work, so we # add 'interp' aoi losses model arguments to module - iam_ref = (1., 0.85) - theta_ref = (0., 80.) - sapm_dc_snl_ac_system.arrays[0].module_parameters['iam_ref'] = iam_ref - sapm_dc_snl_ac_system.arrays[0].module_parameters['theta_ref'] = theta_ref - mc = ModelChain(sapm_dc_snl_ac_system, location, - dc_model='sapm', aoi_model='interp', - spectral_model='no_loss') - m = mocker.spy(iam, 'interp') + iam_ref = (1.0, 0.85) + theta_ref = (0.0, 80.0) + sapm_dc_snl_ac_system.arrays[0].module_parameters["iam_ref"] = iam_ref + sapm_dc_snl_ac_system.arrays[0].module_parameters["theta_ref"] = theta_ref + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model="interp", + spectral_model="no_loss", + ) + m = mocker.spy(iam, "interp") mc.run_model(weather=weather) # only test kwargs - assert m.call_args[1]['iam_ref'] == iam_ref - assert m.call_args[1]['theta_ref'] == theta_ref + assert m.call_args[1]["iam_ref"] == iam_ref + assert m.call_args[1]["theta_ref"] == theta_ref assert isinstance(mc.results.ac, pd.Series) assert not mc.results.ac.empty assert mc.results.ac.iloc[0] > 150 and mc.results.ac.iloc[0] < 200 @@ -1491,9 +1817,14 @@ def test_aoi_model_interp(sapm_dc_snl_ac_system, location, weather, mocker): def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker): - m = mocker.spy(sys.modules[__name__], 'constant_aoi_loss') - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model=constant_aoi_loss, spectral_model='no_loss') + m = mocker.spy(sys.modules[__name__], "constant_aoi_loss") + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model=constant_aoi_loss, + spectral_model="no_loss", + ) mc.run_model(weather) assert m.call_count == 1 assert mc.results.aoi_modifier == 0.9 @@ -1502,30 +1833,47 @@ def test_aoi_model_user_func(sapm_dc_snl_ac_system, location, weather, mocker): assert mc.results.ac.iloc[1] < 1 -@pytest.mark.parametrize('aoi_model', [ - 'sapm', 'ashrae', 'physical', 'martin_ruiz', 'interp' -]) +@pytest.mark.parametrize( + "aoi_model", ["sapm", "ashrae", "physical", "martin_ruiz", "interp"] +) def test_infer_aoi_model(location, system_no_aoi, aoi_model): for k in iam._IAM_MODEL_PARAMS[aoi_model]: system_no_aoi.arrays[0].module_parameters.update({k: 1.0}) - mc = ModelChain(system_no_aoi, location, spectral_model='no_loss') + mc = ModelChain(system_no_aoi, location, spectral_model="no_loss") assert isinstance(mc, ModelChain) -@pytest.mark.parametrize('aoi_model,model_kwargs', [ - # model_kwargs has both required and optional kwargs; test all - ('physical', - {'n': 1.526, 'K': 4.0, 'L': 0.002, # required - 'n_ar': 1.8}), # extra - ('interp', - {'theta_ref': (0, 75, 85, 90), 'iam_ref': (1, 0.8, 0.42, 0), # required - 'method': 'cubic', 'normalize': False})]) # extra -def test_infer_aoi_model_with_extra_params(location, system_no_aoi, aoi_model, - model_kwargs, weather, mocker): +@pytest.mark.parametrize( + "aoi_model,model_kwargs", + [ + # model_kwargs has both required and optional kwargs; test all + ( + "physical", + { + "n": 1.526, + "K": 4.0, + "L": 0.002, # required + "n_ar": 1.8, + }, + ), # extra + ( + "interp", + { + "theta_ref": (0, 75, 85, 90), + "iam_ref": (1, 0.8, 0.42, 0), # required + "method": "cubic", + "normalize": False, + }, + ), + ], +) # extra +def test_infer_aoi_model_with_extra_params( + location, system_no_aoi, aoi_model, model_kwargs, weather, mocker +): # test extra parameters not defined at iam._IAM_MODEL_PARAMS are passed m = mocker.spy(iam, aoi_model) system_no_aoi.arrays[0].module_parameters.update(**model_kwargs) - mc = ModelChain(system_no_aoi, location, spectral_model='no_loss') + mc = ModelChain(system_no_aoi, location, spectral_model="no_loss") assert isinstance(mc, ModelChain) mc.run_model(weather=weather) _, call_kwargs = m.call_args @@ -1533,37 +1881,51 @@ def test_infer_aoi_model_with_extra_params(location, system_no_aoi, aoi_model, def test_infer_aoi_model_invalid(location, system_no_aoi): - exc_text = 'could not infer AOI model' + exc_text = "could not infer AOI model" with pytest.raises(ValueError, match=exc_text): - ModelChain(system_no_aoi, location, spectral_model='no_loss') + ModelChain(system_no_aoi, location, spectral_model="no_loss") def constant_spectral_loss(mc): mc.results.spectral_modifier = 0.9 -@pytest.mark.parametrize('spectral_model', [ - 'sapm', 'first_solar', 'no_loss', constant_spectral_loss -]) -def test_spectral_models(sapm_dc_snl_ac_system, location, spectral_model, - weather): +@pytest.mark.parametrize( + "spectral_model", + ["sapm", "first_solar", "no_loss", constant_spectral_loss], +) +def test_spectral_models( + sapm_dc_snl_ac_system, location, spectral_model, weather +): # add pw to weather dataframe - weather['precipitable_water'] = [0.3, 0.5] - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model='no_loss', spectral_model=spectral_model) + weather["precipitable_water"] = [0.3, 0.5] + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model="no_loss", + spectral_model=spectral_model, + ) spectral_modifier = mc.run_model(weather).results.spectral_modifier assert isinstance(spectral_modifier, (pd.Series, float, int)) -@pytest.mark.parametrize('spectral_model', [ - 'sapm', 'first_solar', 'no_loss', constant_spectral_loss -]) +@pytest.mark.parametrize( + "spectral_model", + ["sapm", "first_solar", "no_loss", constant_spectral_loss], +) def test_spectral_models_singleton_weather_single_array( - sapm_dc_snl_ac_system, location, spectral_model, weather): + sapm_dc_snl_ac_system, location, spectral_model, weather +): # add pw to weather dataframe - weather['precipitable_water'] = [0.3, 0.5] - mc = ModelChain(sapm_dc_snl_ac_system, location, dc_model='sapm', - aoi_model='no_loss', spectral_model=spectral_model) + weather["precipitable_water"] = [0.3, 0.5] + mc = ModelChain( + sapm_dc_snl_ac_system, + location, + dc_model="sapm", + aoi_model="no_loss", + spectral_model=spectral_model, + ) spectral_modifier = mc.run_model([weather]).results.spectral_modifier assert isinstance(spectral_modifier, tuple) assert len(spectral_modifier) == 1 @@ -1576,26 +1938,26 @@ def constant_losses(mc): def dc_constant_losses(mc): - mc.results.dc['p_mp'] *= 0.9 - + mc.results.dc["p_mp"] *= 0.9 -def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, - cec_dc_snl_ac_arrays, - location, - weather, - mocker): - m = mocker.spy(pvsystem, 'dc_ohms_from_percent') +def test_dc_ohmic_model_ohms_from_percent( + cec_dc_snl_ac_system, cec_dc_snl_ac_arrays, location, weather, mocker +): + m = mocker.spy(pvsystem, "dc_ohms_from_percent") system = cec_dc_snl_ac_system for array in system.arrays: array.array_losses_parameters = dict(dc_ohmic_percent=3) - mc = ModelChain(system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model='dc_ohms_from_percent') + mc = ModelChain( + system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + dc_ohmic_model="dc_ohms_from_percent", + ) mc.run_model(weather) assert m.call_count == 1 @@ -1607,10 +1969,13 @@ def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, for array in system.arrays: array.array_losses_parameters = dict(dc_ohmic_percent=3) - mc = ModelChain(system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model='dc_ohms_from_percent') + mc = ModelChain( + system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + dc_ohmic_model="dc_ohms_from_percent", + ) mc.run_model(weather) assert m.call_count == 3 @@ -1619,16 +1984,17 @@ def test_dc_ohmic_model_ohms_from_percent(cec_dc_snl_ac_system, assert isinstance(mc.results.dc_ohmic_losses, tuple) -def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, - location, - weather, - mocker): - - m = mocker.spy(modelchain.ModelChain, 'no_dc_ohmic_loss') - mc = ModelChain(cec_dc_snl_ac_system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model='no_loss') +def test_dc_ohmic_model_no_dc_ohmic_loss( + cec_dc_snl_ac_system, location, weather, mocker +): + m = mocker.spy(modelchain.ModelChain, "no_dc_ohmic_loss") + mc = ModelChain( + cec_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + dc_ohmic_model="no_loss", + ) mc.run_model(weather) assert mc.dc_ohmic_model == mc.no_dc_ohmic_loss @@ -1636,13 +2002,15 @@ def test_dc_ohmic_model_no_dc_ohmic_loss(cec_dc_snl_ac_system, assert mc.results.dc_ohmic_losses is None -def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, - weather, mocker): - m = mocker.spy(sys.modules[__name__], 'dc_constant_losses') - mc = ModelChain(cec_dc_snl_ac_system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model=dc_constant_losses) +def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, weather, mocker): + m = mocker.spy(sys.modules[__name__], "dc_constant_losses") + mc = ModelChain( + cec_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + dc_ohmic_model=dc_constant_losses, + ) mc.run_model(weather) assert m.call_count == 1 @@ -1650,24 +2018,32 @@ def test_dc_ohmic_ext_def(cec_dc_snl_ac_system, location, assert not mc.results.ac.empty -def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location, - weather, mocker): - exc_text = 'not_a_dc_model is not a valid losses model' +def test_dc_ohmic_not_a_model(cec_dc_snl_ac_system, location, weather, mocker): + exc_text = "not_a_dc_model is not a valid losses model" with pytest.raises(ValueError, match=exc_text): - ModelChain(cec_dc_snl_ac_system, location, - aoi_model='no_loss', - spectral_model='no_loss', - dc_ohmic_model='not_a_dc_model') + ModelChain( + cec_dc_snl_ac_system, + location, + aoi_model="no_loss", + spectral_model="no_loss", + dc_ohmic_model="not_a_dc_model", + ) -def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, - mocker): +def test_losses_models_pvwatts( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): age = 1 pvwatts_dc_pvwatts_ac_system.losses_parameters = dict(age=age) - m = mocker.spy(pvsystem, 'pvwatts_losses') - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts', - aoi_model='no_loss', spectral_model='no_loss', - losses_model='pvwatts') + m = mocker.spy(pvsystem, "pvwatts_losses") + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + dc_model="pvwatts", + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="pvwatts", + ) mc.run_model(weather) assert m.call_count == 1 m.assert_called_with(age=age) @@ -1676,38 +2052,58 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather, # check that we're applying correction to dc # GH 696 dc_with_loss = mc.results.dc - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts', - aoi_model='no_loss', spectral_model='no_loss', - losses_model='no_loss') + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + dc_model="pvwatts", + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", + ) mc.run_model(weather) assert not np.allclose(mc.results.dc, dc_with_loss, equal_nan=True) -def test_losses_models_pvwatts_arrays(multi_array_sapm_dc_snl_ac_system, - location, weather): +def test_losses_models_pvwatts_arrays( + multi_array_sapm_dc_snl_ac_system, location, weather +): age = 1 - system_both = multi_array_sapm_dc_snl_ac_system['two_array_system'] + system_both = multi_array_sapm_dc_snl_ac_system["two_array_system"] system_both.losses_parameters = dict(age=age) - mc = ModelChain(system_both, location, - aoi_model='no_loss', spectral_model='no_loss', - losses_model='pvwatts') + mc = ModelChain( + system_both, + location, + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="pvwatts", + ) mc.run_model(weather) dc_with_loss = mc.results.dc - mc = ModelChain(system_both, location, - aoi_model='no_loss', spectral_model='no_loss', - losses_model='no_loss') + mc = ModelChain( + system_both, + location, + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", + ) mc.run_model(weather) assert not np.allclose(mc.results.dc[0], dc_with_loss[0], equal_nan=True) assert not np.allclose(mc.results.dc[1], dc_with_loss[1], equal_nan=True) assert not mc.results.ac.empty -def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, - mocker): - m = mocker.spy(sys.modules[__name__], 'constant_losses') - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts', - aoi_model='no_loss', spectral_model='no_loss', - losses_model=constant_losses) +def test_losses_models_ext_def( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): + m = mocker.spy(sys.modules[__name__], "constant_losses") + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + dc_model="pvwatts", + aoi_model="no_loss", + spectral_model="no_loss", + losses_model=constant_losses, + ) mc.run_model(weather) assert m.call_count == 1 assert isinstance(mc.results.ac, (pd.Series, pd.DataFrame)) @@ -1715,67 +2111,96 @@ def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather, assert not mc.results.ac.empty -def test_losses_models_no_loss(pvwatts_dc_pvwatts_ac_system, location, weather, - mocker): - m = mocker.spy(pvsystem, 'pvwatts_losses') - mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts', - aoi_model='no_loss', spectral_model='no_loss', - losses_model='no_loss') +def test_losses_models_no_loss( + pvwatts_dc_pvwatts_ac_system, location, weather, mocker +): + m = mocker.spy(pvsystem, "pvwatts_losses") + mc = ModelChain( + pvwatts_dc_pvwatts_ac_system, + location, + dc_model="pvwatts", + aoi_model="no_loss", + spectral_model="no_loss", + losses_model="no_loss", + ) assert mc.losses_model == mc.no_extra_losses mc.run_model(weather) assert m.call_count == 0 assert mc.results.losses == 1 -def test_invalid_dc_model_params(sapm_dc_snl_ac_system, cec_dc_snl_ac_system, - pvwatts_dc_pvwatts_ac_system, location): - kwargs = {'dc_model': 'sapm', 'ac_model': 'sandia', - 'aoi_model': 'no_loss', 'spectral_model': 'no_loss', - 'temperature_model': 'sapm', 'losses_model': 'no_loss'} +def test_invalid_dc_model_params( + sapm_dc_snl_ac_system, + cec_dc_snl_ac_system, + pvwatts_dc_pvwatts_ac_system, + location, +): + kwargs = { + "dc_model": "sapm", + "ac_model": "sandia", + "aoi_model": "no_loss", + "spectral_model": "no_loss", + "temperature_model": "sapm", + "losses_model": "no_loss", + } for array in sapm_dc_snl_ac_system.arrays: - array.module_parameters.pop('A0') # remove a parameter + array.module_parameters.pop("A0") # remove a parameter with pytest.raises(ValueError): ModelChain(sapm_dc_snl_ac_system, location, **kwargs) - kwargs['dc_model'] = 'singlediode' + kwargs["dc_model"] = "singlediode" for array in cec_dc_snl_ac_system.arrays: - array.module_parameters.pop('a_ref') # remove a parameter + array.module_parameters.pop("a_ref") # remove a parameter with pytest.raises(ValueError): ModelChain(cec_dc_snl_ac_system, location, **kwargs) - kwargs['dc_model'] = 'pvwatts' - kwargs['ac_model'] = 'pvwatts' + kwargs["dc_model"] = "pvwatts" + kwargs["ac_model"] = "pvwatts" for array in pvwatts_dc_pvwatts_ac_system.arrays: - array.module_parameters.pop('pdc0') + array.module_parameters.pop("pdc0") - match = 'one or more Arrays are missing one or more required parameters' + match = "one or more Arrays are missing one or more required parameters" with pytest.raises(ValueError, match=match): ModelChain(pvwatts_dc_pvwatts_ac_system, location, **kwargs) -@pytest.mark.parametrize('model', [ - 'dc_model', 'ac_model', 'aoi_model', 'spectral_model', - 'temperature_model', 'losses_model' -]) +@pytest.mark.parametrize( + "model", + [ + "dc_model", + "ac_model", + "aoi_model", + "spectral_model", + "temperature_model", + "losses_model", + ], +) def test_invalid_models(model, sapm_dc_snl_ac_system, location): - kwargs = {'dc_model': 'pvwatts', 'ac_model': 'pvwatts', - 'aoi_model': 'no_loss', 'spectral_model': 'no_loss', - 'temperature_model': 'sapm', 'losses_model': 'no_loss'} - kwargs[model] = 'invalid' + kwargs = { + "dc_model": "pvwatts", + "ac_model": "pvwatts", + "aoi_model": "no_loss", + "spectral_model": "no_loss", + "temperature_model": "sapm", + "losses_model": "no_loss", + } + kwargs[model] = "invalid" with pytest.raises(ValueError): ModelChain(sapm_dc_snl_ac_system, location, **kwargs) def test_bad_get_orientation(): with pytest.raises(ValueError): - modelchain.get_orientation('bad value') + modelchain.get_orientation("bad value") # tests for PVSystem with multiple Arrays -def test_with_sapm_pvsystem_arrays(sapm_dc_snl_ac_system_Array, location, - weather): - mc = ModelChain.with_sapm(sapm_dc_snl_ac_system_Array, location, - ac_model='sandia') +def test_with_sapm_pvsystem_arrays( + sapm_dc_snl_ac_system_Array, location, weather +): + mc = ModelChain.with_sapm( + sapm_dc_snl_ac_system_Array, location, ac_model="sandia" + ) assert mc.dc_model == mc.sapm assert mc.ac_model == mc.sandia_inverter mc.run_model(weather) @@ -1784,103 +2209,130 @@ def test_with_sapm_pvsystem_arrays(sapm_dc_snl_ac_system_Array, location, def test_ModelChain_no_extra_kwargs(sapm_dc_snl_ac_system, location): with pytest.raises(TypeError, match="arbitrary_kwarg"): - ModelChain(sapm_dc_snl_ac_system, location, arbitrary_kwarg='value') + ModelChain(sapm_dc_snl_ac_system, location, arbitrary_kwarg="value") def test_complete_irradiance_clean_run(sapm_dc_snl_ac_system, location): """The DataFrame should not change if all columns are passed""" mc = ModelChain(sapm_dc_snl_ac_system, location) - times = pd.date_range('2010-07-05 9:00:00', periods=2, freq='h') + times = pd.date_range("2010-07-05 9:00:00", periods=2, freq="h") i = pd.DataFrame( - {'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times) + {"dni": [2, 3], "dhi": [4, 6], "ghi": [9, 5]}, index=times + ) mc.complete_irradiance(i) - assert_series_equal(mc.results.weather['dni'], - pd.Series([2, 3], index=times, name='dni')) - assert_series_equal(mc.results.weather['dhi'], - pd.Series([4, 6], index=times, name='dhi')) - assert_series_equal(mc.results.weather['ghi'], - pd.Series([9, 5], index=times, name='ghi')) + assert_series_equal( + mc.results.weather["dni"], pd.Series([2, 3], index=times, name="dni") + ) + assert_series_equal( + mc.results.weather["dhi"], pd.Series([4, 6], index=times, name="dhi") + ) + assert_series_equal( + mc.results.weather["ghi"], pd.Series([9, 5], index=times, name="ghi") + ) def test_complete_irradiance(sapm_dc_snl_ac_system, location, mocker): """Check calculations""" mc = ModelChain(sapm_dc_snl_ac_system, location) - times = pd.date_range('2010-07-05 7:00:00-0700', periods=2, freq='h') - i = pd.DataFrame({'dni': [49.756966, 62.153947], - 'ghi': [372.103976116, 497.087579068], - 'dhi': [356.543700, 465.44400]}, index=times) + times = pd.date_range("2010-07-05 7:00:00-0700", periods=2, freq="h") + i = pd.DataFrame( + { + "dni": [49.756966, 62.153947], + "ghi": [372.103976116, 497.087579068], + "dhi": [356.543700, 465.44400], + }, + index=times, + ) with pytest.warns(UserWarning): - mc.complete_irradiance(i[['ghi', 'dni']]) - assert_series_equal(mc.results.weather['dhi'], - pd.Series([356.543700, 465.44400], - index=times, name='dhi')) + mc.complete_irradiance(i[["ghi", "dni"]]) + assert_series_equal( + mc.results.weather["dhi"], + pd.Series([356.543700, 465.44400], index=times, name="dhi"), + ) with pytest.warns(UserWarning): - mc.complete_irradiance(i[['dhi', 'dni']]) - assert_series_equal(mc.results.weather['ghi'], - pd.Series([372.103976116, 497.087579068], - index=times, name='ghi')) + mc.complete_irradiance(i[["dhi", "dni"]]) + assert_series_equal( + mc.results.weather["ghi"], + pd.Series([372.103976116, 497.087579068], index=times, name="ghi"), + ) # check that clearsky_model is used correctly - m_ineichen = mocker.spy(location, 'get_clearsky') - mc.complete_irradiance(i[['dhi', 'ghi']]) + m_ineichen = mocker.spy(location, "get_clearsky") + mc.complete_irradiance(i[["dhi", "ghi"]]) assert m_ineichen.call_count == 1 - assert m_ineichen.call_args[1]['model'] == 'ineichen' - assert_series_equal(mc.results.weather['dni'], - pd.Series([49.756966, 62.153947], - index=times, name='dni')) + assert m_ineichen.call_args[1]["model"] == "ineichen" + assert_series_equal( + mc.results.weather["dni"], + pd.Series([49.756966, 62.153947], index=times, name="dni"), + ) @pytest.mark.filterwarnings("ignore:This function is not safe at the moment") @pytest.mark.parametrize("input_type", [tuple, list]) def test_complete_irradiance_arrays( - sapm_dc_snl_ac_system_same_arrays, location, input_type): + sapm_dc_snl_ac_system_same_arrays, location, input_type +): """ModelChain.complete_irradiance can accept a tuple of weather DataFrames.""" - times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='h') - weather = pd.DataFrame({'dni': [2, 3], - 'dhi': [4, 6], - 'ghi': [9, 5]}, index=times) + times = pd.date_range(start="2020-01-01 0700-0700", periods=2, freq="h") + weather = pd.DataFrame( + {"dni": [2, 3], "dhi": [4, 6], "ghi": [9, 5]}, index=times + ) mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - with pytest.raises(ValueError, - match=r"Input DataFrames must have same index\."): + with pytest.raises( + ValueError, match=r"Input DataFrames must have same index\." + ): mc.complete_irradiance(input_type((weather, weather[1:]))) mc.complete_irradiance(input_type((weather, weather))) for mc_weather in mc.results.weather: - assert_series_equal(mc_weather['dni'], - pd.Series([2, 3], index=times, name='dni')) - assert_series_equal(mc_weather['dhi'], - pd.Series([4, 6], index=times, name='dhi')) - assert_series_equal(mc_weather['ghi'], - pd.Series([9, 5], index=times, name='ghi')) + assert_series_equal( + mc_weather["dni"], pd.Series([2, 3], index=times, name="dni") + ) + assert_series_equal( + mc_weather["dhi"], pd.Series([4, 6], index=times, name="dhi") + ) + assert_series_equal( + mc_weather["ghi"], pd.Series([9, 5], index=times, name="ghi") + ) mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - mc.complete_irradiance(input_type((weather[['ghi', 'dhi']], - weather[['dhi', 'dni']]))) - assert 'dni' in mc.results.weather[0].columns - assert 'ghi' in mc.results.weather[1].columns - mc.complete_irradiance(input_type((weather, weather[['ghi', 'dni']]))) - assert_series_equal(mc.results.weather[0]['dhi'], - pd.Series([4, 6], index=times, name='dhi')) - assert_series_equal(mc.results.weather[0]['ghi'], - pd.Series([9, 5], index=times, name='ghi')) - assert_series_equal(mc.results.weather[0]['dni'], - pd.Series([2, 3], index=times, name='dni')) - assert 'dhi' in mc.results.weather[1].columns + mc.complete_irradiance( + input_type((weather[["ghi", "dhi"]], weather[["dhi", "dni"]])) + ) + assert "dni" in mc.results.weather[0].columns + assert "ghi" in mc.results.weather[1].columns + mc.complete_irradiance(input_type((weather, weather[["ghi", "dni"]]))) + assert_series_equal( + mc.results.weather[0]["dhi"], + pd.Series([4, 6], index=times, name="dhi"), + ) + assert_series_equal( + mc.results.weather[0]["ghi"], + pd.Series([9, 5], index=times, name="ghi"), + ) + assert_series_equal( + mc.results.weather[0]["dni"], + pd.Series([2, 3], index=times, name="dni"), + ) + assert "dhi" in mc.results.weather[1].columns @pytest.mark.parametrize("input_type", [tuple, list]) def test_complete_irradiance_arrays_wrong_length( - sapm_dc_snl_ac_system_same_arrays, location, input_type): + sapm_dc_snl_ac_system_same_arrays, location, input_type +): mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) - times = pd.date_range(start='2020-01-01 0700-0700', periods=2, freq='h') - weather = pd.DataFrame({'dni': [2, 3], - 'dhi': [4, 6], - 'ghi': [9, 5]}, index=times) - error_str = "Input must be same length as number " \ - r"of Arrays in system\. Expected 2, got [0-9]+\." + times = pd.date_range(start="2020-01-01 0700-0700", periods=2, freq="h") + weather = pd.DataFrame( + {"dni": [2, 3], "dhi": [4, 6], "ghi": [9, 5]}, index=times + ) + error_str = ( + "Input must be same length as number " + r"of Arrays in system\. Expected 2, got [0-9]+\." + ) with pytest.raises(ValueError, match=error_str): mc.complete_irradiance(input_type((weather,))) with pytest.raises(ValueError, match=error_str): @@ -1893,120 +2345,122 @@ def test_unknown_attribute(sapm_dc_snl_ac_system, location): mc.unknown_attribute -def test_inconsistent_array_params(location, - sapm_module_params, - cec_module_params): - module_error = ".* selected for the DC model but one or more Arrays are " \ - "missing one or more required parameters" - temperature_error = 'Could not infer temperature model from ' \ - 'ModelChain.system. ' +def test_inconsistent_array_params( + location, sapm_module_params, cec_module_params +): + module_error = ( + ".* selected for the DC model but one or more Arrays are " + "missing one or more required parameters" + ) + temperature_error = ( + "Could not infer temperature model from " "ModelChain.system. " + ) different_module_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( mount=pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params), + module_parameters=sapm_module_params, + ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), - module_parameters=cec_module_params), + module_parameters=cec_module_params, + ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), - module_parameters=cec_module_params)] + module_parameters=cec_module_params, + ), + ] ) with pytest.raises(ValueError, match=module_error): - ModelChain(different_module_system, location, dc_model='cec') + ModelChain(different_module_system, location, dc_model="cec") different_temp_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, - temperature_model_parameters={'a': 1, - 'b': 1, - 'deltaT': 1}), + temperature_model_parameters={"a": 1, "b": 1, "deltaT": 1}, + ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, - temperature_model_parameters={'a': 2, - 'b': 2, - 'deltaT': 2}), + temperature_model_parameters={"a": 2, "b": 2, "deltaT": 2}, + ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, - temperature_model_parameters={'b': 3, 'deltaT': 3})] + temperature_model_parameters={"b": 3, "deltaT": 3}, + ), + ] ) with pytest.raises(ValueError, match=temperature_error): - ModelChain(different_temp_system, location, - ac_model='sandia', - aoi_model='no_loss', spectral_model='no_loss', - temperature_model='sapm') + ModelChain( + different_temp_system, + location, + ac_model="sandia", + aoi_model="no_loss", + spectral_model="no_loss", + temperature_model="sapm", + ) def test_modelchain__common_keys(): - dictionary = {'a': 1, 'b': 1} + dictionary = {"a": 1, "b": 1} series = pd.Series(dictionary) - assert {'a', 'b'} == modelchain._common_keys( - {'a': 1, 'b': 1} - ) - assert {'a', 'b'} == modelchain._common_keys( - pd.Series({'a': 1, 'b': 1}) - ) - assert {'a', 'b'} == modelchain._common_keys( - (dictionary, series) - ) + assert {"a", "b"} == modelchain._common_keys({"a": 1, "b": 1}) + assert {"a", "b"} == modelchain._common_keys(pd.Series({"a": 1, "b": 1})) + assert {"a", "b"} == modelchain._common_keys((dictionary, series)) no_a = dictionary.copy() - del no_a['a'] - assert {'b'} == modelchain._common_keys( - (dictionary, no_a) - ) - assert {'b'} == modelchain._common_keys( - (series, pd.Series(no_a)) - ) - assert {'b'} == modelchain._common_keys( - (series, no_a) - ) + del no_a["a"] + assert {"b"} == modelchain._common_keys((dictionary, no_a)) + assert {"b"} == modelchain._common_keys((series, pd.Series(no_a))) + assert {"b"} == modelchain._common_keys((series, no_a)) def test__irrad_for_celltemp(): - total_irrad = pd.DataFrame(index=[0, 1], columns=['poa_global'], - data=[10., 20.]) - empty = total_irrad.drop('poa_global', axis=1) - effect_irrad = pd.Series(index=total_irrad.index, data=[5., 8.]) + total_irrad = pd.DataFrame( + index=[0, 1], columns=["poa_global"], data=[10.0, 20.0] + ) + empty = total_irrad.drop("poa_global", axis=1) + effect_irrad = pd.Series(index=total_irrad.index, data=[5.0, 8.0]) # test with single array inputs poa = modelchain._irrad_for_celltemp(total_irrad, effect_irrad) - assert_series_equal(poa, total_irrad['poa_global']) + assert_series_equal(poa, total_irrad["poa_global"]) poa = modelchain._irrad_for_celltemp(empty, effect_irrad) assert_series_equal(poa, effect_irrad) # test with tuples poa = modelchain._irrad_for_celltemp( - (total_irrad, total_irrad), (effect_irrad, effect_irrad)) + (total_irrad, total_irrad), (effect_irrad, effect_irrad) + ) assert len(poa) == 2 - assert_series_equal(poa[0], total_irrad['poa_global']) - assert_series_equal(poa[1], total_irrad['poa_global']) + assert_series_equal(poa[0], total_irrad["poa_global"]) + assert_series_equal(poa[1], total_irrad["poa_global"]) poa = modelchain._irrad_for_celltemp( - (empty, empty), (effect_irrad, effect_irrad)) + (empty, empty), (effect_irrad, effect_irrad) + ) assert len(poa) == 2 assert_series_equal(poa[0], effect_irrad) assert_series_equal(poa[1], effect_irrad) def test_ModelChain___repr__(sapm_dc_snl_ac_system, location): - - mc = ModelChain(sapm_dc_snl_ac_system, location, - name='my mc') - - expected = '\n'.join([ - 'ModelChain: ', - ' name: my mc', - ' clearsky_model: ineichen', - ' transposition_model: haydavies', - ' solar_position_method: nrel_numpy', - ' airmass_model: kastenyoung1989', - ' dc_model: sapm', - ' ac_model: sandia_inverter', - ' aoi_model: sapm_aoi_loss', - ' spectral_model: sapm_spectral_loss', - ' temperature_model: sapm_temp', - ' losses_model: no_extra_losses' - ]) + mc = ModelChain(sapm_dc_snl_ac_system, location, name="my mc") + + expected = "\n".join( + [ + "ModelChain: ", + " name: my mc", + " clearsky_model: ineichen", + " transposition_model: haydavies", + " solar_position_method: nrel_numpy", + " airmass_model: kastenyoung1989", + " dc_model: sapm", + " ac_model: sandia_inverter", + " aoi_model: sapm_aoi_loss", + " spectral_model: sapm_spectral_loss", + " temperature_model: sapm_temp", + " losses_model: no_extra_losses", + ] + ) assert mc.__repr__() == expected @@ -2016,5 +2470,5 @@ def test_ModelChainResult___repr__(sapm_dc_snl_ac_system, location, weather): mc.run_model(weather) mcres = mc.results.__repr__() mc_attrs = dir(mc.results) - mc_attrs = [a for a in mc_attrs if not a.startswith('_')] + mc_attrs = [a for a in mc_attrs if not a.startswith("_")] assert all(a in mcres for a in mc_attrs) diff --git a/pvlib/tests/test_numerical_precision.py b/pvlib/tests/test_numerical_precision.py index 53f535401a..f5ee7f5642 100644 --- a/pvlib/tests/test_numerical_precision.py +++ b/pvlib/tests/test_numerical_precision.py @@ -26,23 +26,26 @@ logging.basicConfig() LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) -TEST_DATA = 'bishop88_numerical_precision.csv' +TEST_DATA = "bishop88_numerical_precision.csv" DATA_PATH = DATA_DIR / TEST_DATA POA = 888 TCELL = 55 # module parameters from CEC module SunPower SPR-E20-327 SPR_E20_327 = { - 'alpha_sc': 0.004522, - 'a_ref': 2.6868, - 'I_L_ref': 6.468, - 'I_o_ref': 1.88e-10, - 'R_s': 0.37, - 'R_sh_ref': 298.13, + "alpha_sc": 0.004522, + "a_ref": 2.6868, + "I_L_ref": 6.468, + "I_o_ref": 1.88e-10, + "R_s": 0.37, + "R_sh_ref": 298.13, } # apply temp/irrad desoto corrections ARGS = pvsystem.calcparams_desoto( - effective_irradiance=POA, temp_cell=TCELL, - EgRef=1.121, dEgdT=-0.0002677, **SPR_E20_327, + effective_irradiance=POA, + temp_cell=TCELL, + EgRef=1.121, + dEgdT=-0.0002677, + **SPR_E20_327, ) IL, I0, RS, RSH, NNSVTH = ARGS IVCURVE_NPTS = 100 @@ -63,13 +66,13 @@ def generate_numerical_precision(): # pragma: no cover if symbols is NotImplemented: LOGGER.critical("SymPy is required to generate expected data.") raise ImportError("could not import sympy") - il, io, rs, rsh, nnsvt, vd = symbols('il, io, rs, rsh, nnsvt, vd') + il, io, rs, rsh, nnsvt, vd = symbols("il, io, rs, rsh, nnsvt, vd") a = sy_exp(vd / nnsvt) b = 1.0 / rsh i = il - io * (a - 1.0) - vd * b v = vd - i * rs c = io * a / nnsvt - grad_i = - c - b # di/dvd + grad_i = -c - b # di/dvd grad_v = 1.0 - grad_i * rs # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i grad = grad_i / grad_v # di/dv @@ -78,7 +81,9 @@ def generate_numerical_precision(): # pragma: no cover grad2i = -c / nnsvt grad2v = -grad2i * rs grad2p = ( - grad_v * grad + v * (grad2i/grad_v - grad_i*grad2v/grad_v**2) + grad_i + grad_v * grad + + v * (grad2i / grad_v - grad_i * grad2v / grad_v**2) + + grad_i ) # generate exact values data = dict(zip((il, io, rs, rsh, nnsvt), ARGS)) @@ -87,14 +92,14 @@ def generate_numerical_precision(): # pragma: no cover for test in vdtest: data[vd] = test test_data = { - 'i': np.float64(i.evalf(subs=data)), - 'v': np.float64(v.evalf(subs=data)), - 'p': np.float64(p.evalf(subs=data)), - 'grad_i': np.float64(grad_i.evalf(subs=data)), - 'grad_v': np.float64(grad_v.evalf(subs=data)), - 'grad': np.float64(grad.evalf(subs=data)), - 'grad_p': np.float64(grad_p.evalf(subs=data)), - 'grad2p': np.float64(grad2p.evalf(subs=data)) + "i": np.float64(i.evalf(subs=data)), + "v": np.float64(v.evalf(subs=data)), + "p": np.float64(p.evalf(subs=data)), + "grad_i": np.float64(grad_i.evalf(subs=data)), + "grad_v": np.float64(grad_v.evalf(subs=data)), + "grad": np.float64(grad.evalf(subs=data)), + "grad_p": np.float64(grad_p.evalf(subs=data)), + "grad2p": np.float64(grad2p.evalf(subs=data)), } LOGGER.debug(test_data) expected.append(test_data) @@ -108,17 +113,17 @@ def test_numerical_precision(): expected = pd.read_csv(DATA_PATH) vdtest = np.linspace(0, estimate_voc(IL, I0, NNSVTH), IVCURVE_NPTS) results = bishop88(vdtest, *ARGS, gradients=True) - assert np.allclose(expected['i'], results[0]) - assert np.allclose(expected['v'], results[1]) - assert np.allclose(expected['p'], results[2]) - assert np.allclose(expected['grad_i'], results[3]) - assert np.allclose(expected['grad_v'], results[4]) - assert np.allclose(expected['grad'], results[5]) - assert np.allclose(expected['grad_p'], results[6]) - assert np.allclose(expected['grad2p'], results[7]) + assert np.allclose(expected["i"], results[0]) + assert np.allclose(expected["v"], results[1]) + assert np.allclose(expected["p"], results[2]) + assert np.allclose(expected["grad_i"], results[3]) + assert np.allclose(expected["grad_v"], results[4]) + assert np.allclose(expected["grad"], results[5]) + assert np.allclose(expected["grad_p"], results[6]) + assert np.allclose(expected["grad2p"], results[7]) -if __name__ == '__main__': # pragma: no cover +if __name__ == "__main__": # pragma: no cover expected = generate_numerical_precision() expected.to_csv(DATA_PATH) test_numerical_precision() diff --git a/pvlib/tests/test_pvarray.py b/pvlib/tests/test_pvarray.py index 693ef78b2a..ac4ef3a188 100644 --- a/pvlib/tests/test_pvarray.py +++ b/pvlib/tests/test_pvarray.py @@ -36,7 +36,7 @@ def test_fit_pvefficiency_adr(): assert_allclose(result, params, rtol=1e-3) result = pvarray.fit_pvefficiency_adr(g, t, eta, dict_output=True) - assert 'k_a' in result + assert "k_a" in result def test_pvefficiency_adr_round_trip(): @@ -51,14 +51,21 @@ def test_pvefficiency_adr_round_trip(): def test_huld(): pdc0 = 100 - res = pvarray.huld(1000, 25, pdc0, cell_type='cSi') + res = pvarray.huld(1000, 25, pdc0, cell_type="cSi") assert np.isclose(res, pdc0) - exp_sum = np.exp(1) * (np.sum(pvarray._infer_k_huld('cSi', pdc0)) + pdc0) - res = pvarray.huld(1000*np.exp(1), 26, pdc0, cell_type='cSi') + exp_sum = np.exp(1) * (np.sum(pvarray._infer_k_huld("cSi", pdc0)) + pdc0) + res = pvarray.huld(1000 * np.exp(1), 26, pdc0, cell_type="cSi") assert np.isclose(res, exp_sum) res = pvarray.huld(100, 30, pdc0, k=(1, 1, 1, 1, 1, 1)) - exp_100 = 0.1 * (pdc0 + np.log(0.1) + np.log(0.1)**2 + 5 + 5*np.log(0.1) - + 5*np.log(0.1)**2 + 25) + exp_100 = 0.1 * ( + pdc0 + + np.log(0.1) + + np.log(0.1) ** 2 + + 5 + + 5 * np.log(0.1) + + 5 * np.log(0.1) ** 2 + + 25 + ) assert np.isclose(res, exp_100) # Series input, and irradiance = 0 eff_irr = pd.Series([1000, 100, 0]) @@ -66,6 +73,7 @@ def test_huld(): expected = pd.Series([pdc0, exp_100, 0]) res = pvarray.huld(eff_irr, tm, pdc0, k=(1, 1, 1, 1, 1, 1)) assert_series_equal(res, expected) - with pytest.raises(ValueError, - match='Either k or cell_type must be specified'): + with pytest.raises( + ValueError, match="Either k or cell_type must be specified" + ): res = pvarray.huld(1000, 25, 100) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index fd482c5127..5661e0af58 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -23,73 +23,82 @@ from pvlib.tests.test_singlediode import get_pvsyst_fs_495 -@pytest.mark.parametrize('iam_model,model_params', [ - ('ashrae', {'b': 0.05}), - ('physical', {'K': 4, 'L': 0.002, 'n': 1.526}), - ('martin_ruiz', {'a_r': 0.16}), -]) +@pytest.mark.parametrize( + "iam_model,model_params", + [ + ("ashrae", {"b": 0.05}), + ("physical", {"K": 4, "L": 0.002, "n": 1.526}), + ("martin_ruiz", {"a_r": 0.16}), + ], +) def test_PVSystem_get_iam(mocker, iam_model, model_params): m = mocker.spy(_iam, iam_model) system = pvsystem.PVSystem(module_parameters=model_params) thetas = 1 iam = system.get_iam(thetas, iam_model=iam_model) m.assert_called_with(thetas, **model_params) - assert iam < 1. + assert iam < 1.0 def test_PVSystem_multi_array_get_iam(): - model_params = {'b': 0.05} + model_params = {"b": 0.05} system = pvsystem.PVSystem( - arrays=[pvsystem.Array(mount=pvsystem.FixedMount(0, 180), - module_parameters=model_params), - pvsystem.Array(mount=pvsystem.FixedMount(0, 180), - module_parameters=model_params)] + arrays=[ + pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), + module_parameters=model_params, + ), + pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), + module_parameters=model_params, + ), + ] ) - iam = system.get_iam((1, 5), iam_model='ashrae') + iam = system.get_iam((1, 5), iam_model="ashrae") assert len(iam) == 2 assert iam[0] != iam[1] - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_iam((1,), iam_model='ashrae') + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_iam((1,), iam_model="ashrae") def test_PVSystem_get_iam_sapm(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) - mocker.spy(_iam, 'sapm') + mocker.spy(_iam, "sapm") aoi = 0 - out = system.get_iam(aoi, 'sapm') + out = system.get_iam(aoi, "sapm") _iam.sapm.assert_called_once_with(aoi, sapm_module_params) assert_allclose(out, 1.0, atol=0.01) def test_PVSystem_get_iam_interp(mocker): - interp_module_params = {'iam_ref': (1., 0.8), 'theta_ref': (0., 80.)} + interp_module_params = {"iam_ref": (1.0, 0.8), "theta_ref": (0.0, 80.0)} system = pvsystem.PVSystem(module_parameters=interp_module_params) - spy = mocker.spy(_iam, 'interp') - aoi = ((0., 40., 80.),) - expected = (1., 0.9, 0.8) - out = system.get_iam(aoi, iam_model='interp') + spy = mocker.spy(_iam, "interp") + aoi = ((0.0, 40.0, 80.0),) + expected = (1.0, 0.9, 0.8) + out = system.get_iam(aoi, iam_model="interp") assert_allclose(out, expected) spy.assert_called_once_with(aoi[0], **interp_module_params) def test__normalize_sam_product_names(): - - BAD_NAMES = [' -.()[]:+/",', 'Module[1]'] - NORM_NAMES = ['____________', 'Module_1_'] + BAD_NAMES = [' -.()[]:+/",', "Module[1]"] + NORM_NAMES = ["____________", "Module_1_"] norm_names = pvsystem._normalize_sam_product_names(BAD_NAMES) assert list(norm_names) == NORM_NAMES - BAD_NAMES = ['Module[1]', 'Module(1)'] - NORM_NAMES = ['Module_1_', 'Module_1_'] + BAD_NAMES = ["Module[1]", "Module(1)"] + NORM_NAMES = ["Module_1_", "Module_1_"] with pytest.warns(UserWarning): norm_names = pvsystem._normalize_sam_product_names(BAD_NAMES) assert list(norm_names) == NORM_NAMES - BAD_NAMES = ['Module[1]', 'Module[1]'] - NORM_NAMES = ['Module_1_', 'Module_1_'] + BAD_NAMES = ["Module[1]", "Module[1]"] + NORM_NAMES = ["Module_1_", "Module_1_"] with pytest.warns(UserWarning): norm_names = pvsystem._normalize_sam_product_names(BAD_NAMES) @@ -99,7 +108,7 @@ def test__normalize_sam_product_names(): def test_PVSystem_get_iam_invalid(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) with pytest.raises(ValueError): - system.get_iam(45, iam_model='not_a_model') + system.get_iam(45, iam_model="not_a_model") def test_retrieve_sam_raises_exceptions(): @@ -156,105 +165,150 @@ def test_retrieve_sam_databases(): def test_sapm(sapm_module_params): - - times = pd.date_range(start='2015-01-01', periods=5, freq='12h') - effective_irradiance = pd.Series([-1000, 500, 1100, np.nan, 1000], - index=times) + times = pd.date_range(start="2015-01-01", periods=5, freq="12h") + effective_irradiance = pd.Series( + [-1000, 500, 1100, np.nan, 1000], index=times + ) temp_cell = pd.Series([10, 25, 50, 25, np.nan], index=times) out = pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params) - expected = pd.DataFrame(np.array( - [[-5.0608322, -4.65037767, np.nan, np.nan, np.nan, - -4.91119927, -4.16721569], - [2.545575, 2.28773882, 56.86182059, 47.21121608, 108.00693168, - 2.48357383, 1.71782772], - [5.65584763, 5.01709903, 54.1943277, 42.51861718, 213.32011294, - 5.52987899, 3.46796463], - [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], - [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan]]), - columns=['i_sc', 'i_mp', 'v_oc', 'v_mp', 'p_mp', 'i_x', 'i_xx'], - index=times) + expected = pd.DataFrame( + np.array( + [ + [ + -5.0608322, + -4.65037767, + np.nan, + np.nan, + np.nan, + -4.91119927, + -4.16721569, + ], + [ + 2.545575, + 2.28773882, + 56.86182059, + 47.21121608, + 108.00693168, + 2.48357383, + 1.71782772, + ], + [ + 5.65584763, + 5.01709903, + 54.1943277, + 42.51861718, + 213.32011294, + 5.52987899, + 3.46796463, + ], + [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan], + ] + ), + columns=["i_sc", "i_mp", "v_oc", "v_mp", "p_mp", "i_x", "i_xx"], + index=times, + ) assert_frame_equal(out, expected, check_less_precise=4) out = pvsystem.sapm(1000, 25, sapm_module_params) expected = OrderedDict() - expected['i_sc'] = sapm_module_params['Isco'] - expected['i_mp'] = sapm_module_params['Impo'] - expected['v_oc'] = sapm_module_params['Voco'] - expected['v_mp'] = sapm_module_params['Vmpo'] - expected['p_mp'] = sapm_module_params['Impo'] * sapm_module_params['Vmpo'] - expected['i_x'] = sapm_module_params['IXO'] - expected['i_xx'] = sapm_module_params['IXXO'] + expected["i_sc"] = sapm_module_params["Isco"] + expected["i_mp"] = sapm_module_params["Impo"] + expected["v_oc"] = sapm_module_params["Voco"] + expected["v_mp"] = sapm_module_params["Vmpo"] + expected["p_mp"] = sapm_module_params["Impo"] * sapm_module_params["Vmpo"] + expected["i_x"] = sapm_module_params["IXO"] + expected["i_xx"] = sapm_module_params["IXXO"] for k, v in expected.items(): assert_allclose(out[k], v, atol=1e-4) # just make sure it works with Series input - pvsystem.sapm(effective_irradiance, temp_cell, - pd.Series(sapm_module_params)) + pvsystem.sapm( + effective_irradiance, temp_cell, pd.Series(sapm_module_params) + ) def test_PVSystem_sapm(sapm_module_params, mocker): - mocker.spy(pvsystem, 'sapm') + mocker.spy(pvsystem, "sapm") system = pvsystem.PVSystem(module_parameters=sapm_module_params) effective_irradiance = 500 temp_cell = 25 out = system.sapm(effective_irradiance, temp_cell) - pvsystem.sapm.assert_called_once_with(effective_irradiance, temp_cell, - sapm_module_params) - assert_allclose(out['p_mp'], 100, 10) + pvsystem.sapm.assert_called_once_with( + effective_irradiance, temp_cell, sapm_module_params + ) + assert_allclose(out["p_mp"], 100, 10) def test_PVSystem_multi_array_sapm(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params), - pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params)] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + ] ) effective_irradiance = (100, 500) temp_cell = (15, 25) sapm_one, sapm_two = system.sapm(effective_irradiance, temp_cell) - assert sapm_one['p_mp'] != sapm_two['p_mp'] + assert sapm_one["p_mp"] != sapm_two["p_mp"] sapm_one_flip, sapm_two_flip = system.sapm( (effective_irradiance[1], effective_irradiance[0]), - (temp_cell[1], temp_cell[0]) + (temp_cell[1], temp_cell[0]), ) - assert sapm_one_flip['p_mp'] == sapm_two['p_mp'] - assert sapm_two_flip['p_mp'] == sapm_one['p_mp'] - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + assert sapm_one_flip["p_mp"] == sapm_two["p_mp"] + assert sapm_two_flip["p_mp"] == sapm_one["p_mp"] + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): system.sapm(effective_irradiance, 10) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): system.sapm(500, temp_cell) def test_sapm_spectral_loss_deprecated(sapm_module_params): - with pytest.warns(pvlibDeprecationWarning, - match='Use pvlib.spectrum.spectral_factor_sapm'): + with pytest.warns( + pvlibDeprecationWarning, + match="Use pvlib.spectrum.spectral_factor_sapm", + ): pvsystem.sapm_spectral_loss(1, sapm_module_params) def test_PVSystem_sapm_spectral_loss(sapm_module_params, mocker): - mocker.spy(spectrum, 'spectral_factor_sapm') + mocker.spy(spectrum, "spectral_factor_sapm") system = pvsystem.PVSystem(module_parameters=sapm_module_params) airmass = 2 out = system.sapm_spectral_loss(airmass) - spectrum.spectral_factor_sapm.assert_called_once_with(airmass, - sapm_module_params) + spectrum.spectral_factor_sapm.assert_called_once_with( + airmass, sapm_module_params + ) assert_allclose(out, 1, atol=0.5) def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params), - pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params)] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + ] ) loss_one, loss_two = system.sapm_spectral_loss(2) assert loss_one == loss_two @@ -263,23 +317,38 @@ def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params): # this test could be improved to cover all cell types. # could remove the need for specifying spectral coefficients if we don't # care about the return value at all -@pytest.mark.parametrize('module_parameters,module_type,coefficients', [ - ({'Technology': 'mc-Si'}, 'multisi', None), - ({'Material': 'Multi-c-Si'}, 'multisi', None), - ({'first_solar_spectral_coefficients': ( - 0.84, -0.03, -0.008, 0.14, 0.04, -0.002)}, - None, - (0.84, -0.03, -0.008, 0.14, 0.04, -0.002)) - ]) -def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type, - coefficients, mocker): - mocker.spy(spectrum, 'spectral_factor_firstsolar') +@pytest.mark.parametrize( + "module_parameters,module_type,coefficients", + [ + ({"Technology": "mc-Si"}, "multisi", None), + ({"Material": "Multi-c-Si"}, "multisi", None), + ( + { + "first_solar_spectral_coefficients": ( + 0.84, + -0.03, + -0.008, + 0.14, + 0.04, + -0.002, + ) + }, + None, + (0.84, -0.03, -0.008, 0.14, 0.04, -0.002), + ), + ], +) +def test_PVSystem_first_solar_spectral_loss( + module_parameters, module_type, coefficients, mocker +): + mocker.spy(spectrum, "spectral_factor_firstsolar") system = pvsystem.PVSystem(module_parameters=module_parameters) pw = 3 airmass_absolute = 3 out = system.first_solar_spectral_loss(pw, airmass_absolute) spectrum.spectral_factor_firstsolar.assert_called_once_with( - pw, airmass_absolute, module_type, coefficients) + pw, airmass_absolute, module_type, coefficients + ) assert_allclose(out, 1, atol=0.5) @@ -288,31 +357,44 @@ def test_PVSystem_multi_array_first_solar_spectral_loss(): arrays=[ pvsystem.Array( mount=pvsystem.FixedMount(0, 180), - module_parameters={'Technology': 'mc-Si'}, - module_type='multisi' + module_parameters={"Technology": "mc-Si"}, + module_type="multisi", ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), - module_parameters={'Technology': 'mc-Si'}, - module_type='multisi' - ) + module_parameters={"Technology": "mc-Si"}, + module_type="multisi", + ), ] ) loss_one, loss_two = system.first_solar_spectral_loss(1, 3) assert loss_one == loss_two -@pytest.mark.parametrize('test_input,expected', [ - ([1000, 100, 5, 45], 1140.0510967821877), - ([np.array([np.nan, 1000, 1000]), - np.array([100, np.nan, 100]), - np.array([1.1, 1.1, 1.1]), - np.array([10, 10, 10])], - np.array([np.nan, np.nan, 1081.1574])), - ([pd.Series([1000]), pd.Series([100]), pd.Series([1.1]), - pd.Series([10])], - pd.Series([1081.1574])) -]) +@pytest.mark.parametrize( + "test_input,expected", + [ + ([1000, 100, 5, 45], 1140.0510967821877), + ( + [ + np.array([np.nan, 1000, 1000]), + np.array([100, np.nan, 100]), + np.array([1.1, 1.1, 1.1]), + np.array([10, 10, 10]), + ], + np.array([np.nan, np.nan, 1081.1574]), + ), + ( + [ + pd.Series([1000]), + pd.Series([100]), + pd.Series([1.1]), + pd.Series([10]), + ], + pd.Series([1081.1574]), + ), + ], +) def test_sapm_effective_irradiance(sapm_module_params, test_input, expected): test_input.append(sapm_module_params) out = pvsystem.sapm_effective_irradiance(*test_input) @@ -324,30 +406,42 @@ def test_sapm_effective_irradiance(sapm_module_params, test_input, expected): def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker): system = pvsystem.PVSystem(module_parameters=sapm_module_params) - mocker.spy(pvsystem, 'sapm_effective_irradiance') + mocker.spy(pvsystem, "sapm_effective_irradiance") poa_direct = 900 poa_diffuse = 100 airmass_absolute = 1.5 aoi = 0 - p = (sapm_module_params['A4'], sapm_module_params['A3'], - sapm_module_params['A2'], sapm_module_params['A1'], - sapm_module_params['A0']) + p = ( + sapm_module_params["A4"], + sapm_module_params["A3"], + sapm_module_params["A2"], + sapm_module_params["A1"], + sapm_module_params["A0"], + ) f1 = np.polyval(p, airmass_absolute) - expected = f1 * (poa_direct + sapm_module_params['FD'] * poa_diffuse) + expected = f1 * (poa_direct + sapm_module_params["FD"] * poa_diffuse) out = system.sapm_effective_irradiance( - poa_direct, poa_diffuse, airmass_absolute, aoi) + poa_direct, poa_diffuse, airmass_absolute, aoi + ) pvsystem.sapm_effective_irradiance.assert_called_once_with( - poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params) + poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params + ) assert_allclose(out, expected, atol=0.1) def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params), - pvsystem.Array(pvsystem.FixedMount(0, 180), - module_parameters=sapm_module_params)] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + pvsystem.Array( + pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params, + ), + ] ) poa_direct = (500, 900) poa_diffuse = (50, 100) @@ -365,42 +459,48 @@ def two_array_system(pvsyst_module_params, cec_module_params): Both arrays are identical. """ - temperature_model = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass' + temperature_model = temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_glass" ] # Need u_v to be non-zero so wind-speed changes cell temperature # under the pvsyst model. - temperature_model['u_v'] = 1.0 + temperature_model["u_v"] = 1.0 # parameter for fuentes temperature model - temperature_model['noct_installed'] = 45 + temperature_model["noct_installed"] = 45 # parameters for noct_sam temperature model - temperature_model['noct'] = 45. - temperature_model['module_efficiency'] = 0.2 + temperature_model["noct"] = 45.0 + temperature_model["module_efficiency"] = 0.2 module_params = {**pvsyst_module_params, **cec_module_params} return pvsystem.PVSystem( arrays=[ pvsystem.Array( mount=pvsystem.FixedMount(0, 180), temperature_model_parameters=temperature_model, - module_parameters=module_params + module_parameters=module_params, ), pvsystem.Array( mount=pvsystem.FixedMount(0, 180), temperature_model_parameters=temperature_model, - module_parameters=module_params - ) + module_parameters=module_params, + ), ] ) -@pytest.mark.parametrize("poa_direct, poa_diffuse, aoi", - [(20, (10, 10), (20, 20)), - ((20, 20), (10,), (20, 20)), - ((20, 20), (10, 10), 20)]) +@pytest.mark.parametrize( + "poa_direct, poa_diffuse, aoi", + [ + (20, (10, 10), (20, 20)), + ((20, 20), (10,), (20, 20)), + ((20, 20), (10, 10), 20), + ], +) def test_PVSystem_sapm_effective_irradiance_value_error( - poa_direct, poa_diffuse, aoi, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + poa_direct, poa_diffuse, aoi, two_array_system +): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): two_array_system.sapm_effective_irradiance( poa_direct, poa_diffuse, 10, aoi ) @@ -408,191 +508,240 @@ def test_PVSystem_sapm_effective_irradiance_value_error( def test_PVSystem_sapm_celltemp(mocker): a, b, deltaT = (-3.47, -0.0594, 3) # open_rack_glass_glass - temp_model_params = {'a': a, 'b': b, 'deltaT': deltaT} + temp_model_params = {"a": a, "b": b, "deltaT": deltaT} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'sapm_cell') + mocker.spy(temperature, "sapm_cell") temps = 25 irrads = 1000 winds = 1 - out = system.get_cell_temperature(irrads, temps, winds, model='sapm') - temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, a, b, - deltaT) + out = system.get_cell_temperature(irrads, temps, winds, model="sapm") + temperature.sapm_cell.assert_called_once_with( + irrads, temps, winds, a, b, deltaT + ) assert_allclose(out, 57, atol=1) def test_PVSystem_sapm_celltemp_kwargs(mocker): - temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass'] + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_glass" + ] system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'sapm_cell') + mocker.spy(temperature, "sapm_cell") temps = 25 irrads = 1000 winds = 1 - out = system.get_cell_temperature(irrads, temps, winds, model='sapm') - temperature.sapm_cell.assert_called_once_with(irrads, temps, winds, - temp_model_params['a'], - temp_model_params['b'], - temp_model_params['deltaT']) + out = system.get_cell_temperature(irrads, temps, winds, model="sapm") + temperature.sapm_cell.assert_called_once_with( + irrads, + temps, + winds, + temp_model_params["a"], + temp_model_params["b"], + temp_model_params["deltaT"], + ) assert_allclose(out, 57, atol=1) def test_PVSystem_multi_array_sapm_celltemp_different_arrays(): - temp_model_one = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass'] - temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'close_mount_glass_glass'] + temp_model_one = temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_glass" + ] + temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "close_mount_glass_glass" + ] system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), - temperature_model_parameters=temp_model_one), - pvsystem.Array(pvsystem.FixedMount(0, 180), - temperature_model_parameters=temp_model_two)] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(0, 180), + temperature_model_parameters=temp_model_one, + ), + pvsystem.Array( + pvsystem.FixedMount(0, 180), + temperature_model_parameters=temp_model_two, + ), + ] ) temp_one, temp_two = system.get_cell_temperature( - (1000, 1000), 25, 1, model='sapm' + (1000, 1000), 25, 1, model="sapm" ) assert temp_one != temp_two def test_PVSystem_pvsyst_celltemp(mocker): - parameter_set = 'insulated' - temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['pvsyst'][ - parameter_set] + parameter_set = "insulated" + temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS["pvsyst"][ + parameter_set + ] alpha_absorption = 0.85 module_efficiency = 0.17 - module_parameters = {'alpha_absorption': alpha_absorption, - 'module_efficiency': module_efficiency} - system = pvsystem.PVSystem(module_parameters=module_parameters, - temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'pvsyst_cell') + module_parameters = { + "alpha_absorption": alpha_absorption, + "module_efficiency": module_efficiency, + } + system = pvsystem.PVSystem( + module_parameters=module_parameters, + temperature_model_parameters=temp_model_params, + ) + mocker.spy(temperature, "pvsyst_cell") irrad = 800 temp = 45 wind = 0.5 - out = system.get_cell_temperature(irrad, temp, wind_speed=wind, - model='pvsyst') + out = system.get_cell_temperature( + irrad, temp, wind_speed=wind, model="pvsyst" + ) temperature.pvsyst_cell.assert_called_once_with( - irrad, temp, wind_speed=wind, u_c=temp_model_params['u_c'], - u_v=temp_model_params['u_v'], module_efficiency=module_efficiency, - alpha_absorption=alpha_absorption) + irrad, + temp, + wind_speed=wind, + u_c=temp_model_params["u_c"], + u_v=temp_model_params["u_v"], + module_efficiency=module_efficiency, + alpha_absorption=alpha_absorption, + ) assert (out < 90) and (out > 70) def test_PVSystem_faiman_celltemp(mocker): u0, u1 = 25.0, 6.84 # default values - temp_model_params = {'u0': u0, 'u1': u1} + temp_model_params = {"u0": u0, "u1": u1} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'faiman') + mocker.spy(temperature, "faiman") temps = 25 irrads = 1000 winds = 1 - out = system.get_cell_temperature(irrads, temps, winds, model='faiman') + out = system.get_cell_temperature(irrads, temps, winds, model="faiman") temperature.faiman.assert_called_once_with(irrads, temps, winds, u0, u1) assert_allclose(out, 56.4, atol=1e-1) def test_PVSystem_noct_celltemp(mocker): poa_global, temp_air, wind_speed, noct, module_efficiency = ( - 1000., 25., 1., 45., 0.2) + 1000.0, + 25.0, + 1.0, + 45.0, + 0.2, + ) expected = 55.230790492 - temp_model_params = {'noct': noct, 'module_efficiency': module_efficiency} + temp_model_params = {"noct": noct, "module_efficiency": module_efficiency} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - mocker.spy(temperature, 'noct_sam') - out = system.get_cell_temperature(poa_global, temp_air, wind_speed, - model='noct_sam') + mocker.spy(temperature, "noct_sam") + out = system.get_cell_temperature( + poa_global, temp_air, wind_speed, model="noct_sam" + ) temperature.noct_sam.assert_called_once_with( - poa_global, temp_air, wind_speed, noct, module_efficiency, - effective_irradiance=None) + poa_global, + temp_air, + wind_speed, + noct, + module_efficiency, + effective_irradiance=None, + ) assert_allclose(out, expected) # different types - out = system.get_cell_temperature(np.array(poa_global), np.array(temp_air), - np.array(wind_speed), model='noct_sam') + out = system.get_cell_temperature( + np.array(poa_global), + np.array(temp_air), + np.array(wind_speed), + model="noct_sam", + ) assert_allclose(out, expected) - dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00', - freq='1h') - out = system.get_cell_temperature(pd.Series(index=dr, data=poa_global), - pd.Series(index=dr, data=temp_air), - pd.Series(index=dr, data=wind_speed), - model='noct_sam') + dr = pd.date_range( + start="2020-01-01 12:00:00", end="2020-01-01 13:00:00", freq="1h" + ) + out = system.get_cell_temperature( + pd.Series(index=dr, data=poa_global), + pd.Series(index=dr, data=temp_air), + pd.Series(index=dr, data=wind_speed), + model="noct_sam", + ) assert_series_equal(out, pd.Series(index=dr, data=expected)) # now use optional arguments - temp_model_params.update({'transmittance_absorptance': 0.8, - 'array_height': 2, - 'mount_standoff': 2.0}) + temp_model_params.update( + { + "transmittance_absorptance": 0.8, + "array_height": 2, + "mount_standoff": 2.0, + } + ) expected = 60.477703576 system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - out = system.get_cell_temperature(poa_global, temp_air, wind_speed, - effective_irradiance=1100., - model='noct_sam') + out = system.get_cell_temperature( + poa_global, + temp_air, + wind_speed, + effective_irradiance=1100.0, + model="noct_sam", + ) assert_allclose(out, expected) def test_PVSystem_noct_celltemp_error(): - poa_global, temp_air, wind_speed, module_efficiency = (1000., 25., 1., 0.2) - temp_model_params = {'module_efficiency': module_efficiency} + poa_global, temp_air, wind_speed, module_efficiency = ( + 1000.0, + 25.0, + 1.0, + 0.2, + ) + temp_model_params = {"module_efficiency": module_efficiency} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) with pytest.raises(KeyError): - system.get_cell_temperature(poa_global, temp_air, wind_speed, - model='noct_sam') + system.get_cell_temperature( + poa_global, temp_air, wind_speed, model="noct_sam" + ) -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) def test_PVSystem_multi_array_celltemp_functions(model, two_array_system): - times = pd.date_range(start='2020-08-25 11:00', freq='h', periods=3) + times = pd.date_range(start="2020-08-25 11:00", freq="h", periods=3) irrad_one = pd.Series(1000, index=times) irrad_two = pd.Series(500, index=times) temp_air = pd.Series(25, index=times) wind_speed = pd.Series(1, index=times) temp_one, temp_two = two_array_system.get_cell_temperature( - (irrad_one, irrad_two), temp_air, wind_speed, model=model) + (irrad_one, irrad_two), temp_air, wind_speed, model=model + ) assert (temp_one != temp_two).all() -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) def test_PVSystem_multi_array_celltemp_multi_temp(model, two_array_system): - times = pd.date_range(start='2020-08-25 11:00', freq='h', periods=3) + times = pd.date_range(start="2020-08-25 11:00", freq="h", periods=3) irrad = pd.Series(1000, index=times) temp_air_one = pd.Series(25, index=times) temp_air_two = pd.Series(5, index=times) wind_speed = pd.Series(1, index=times) temp_one, temp_two = two_array_system.get_cell_temperature( - (irrad, irrad), - (temp_air_one, temp_air_two), - wind_speed, - model=model + (irrad, irrad), (temp_air_one, temp_air_two), wind_speed, model=model ) assert (temp_one != temp_two).all() temp_one_swtich, temp_two_switch = two_array_system.get_cell_temperature( - (irrad, irrad), - (temp_air_two, temp_air_one), - wind_speed, - model=model + (irrad, irrad), (temp_air_two, temp_air_one), wind_speed, model=model ) assert_series_equal(temp_one, temp_two_switch) assert_series_equal(temp_two, temp_one_swtich) -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) def test_PVSystem_multi_array_celltemp_multi_wind(model, two_array_system): - times = pd.date_range(start='2020-08-25 11:00', freq='h', periods=3) + times = pd.date_range(start="2020-08-25 11:00", freq="h", periods=3) irrad = pd.Series(1000, index=times) temp_air = pd.Series(25, index=times) wind_speed_one = pd.Series(1, index=times) wind_speed_two = pd.Series(5, index=times) temp_one, temp_two = two_array_system.get_cell_temperature( - (irrad, irrad), - temp_air, - (wind_speed_one, wind_speed_two), - model=model + (irrad, irrad), temp_air, (wind_speed_one, wind_speed_two), model=model ) assert (temp_one != temp_two).all() temp_one_swtich, temp_two_switch = two_array_system.get_cell_temperature( - (irrad, irrad), - temp_air, - (wind_speed_two, wind_speed_one), - model=model + (irrad, irrad), temp_air, (wind_speed_two, wind_speed_one), model=model ) assert_series_equal(temp_one, temp_two_switch) assert_series_equal(temp_two, temp_one_swtich) @@ -600,118 +749,137 @@ def test_PVSystem_multi_array_celltemp_multi_wind(model, two_array_system): def test_PVSystem_get_cell_temperature_invalid(): system = pvsystem.PVSystem() - with pytest.raises(ValueError, match='not a valid'): - system.get_cell_temperature(1000, 25, 1, 'not_a_model') - - -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) -def test_PVSystem_multi_array_celltemp_temp_too_short( - model, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - two_array_system.get_cell_temperature((1000, 1000), (1,), 1, - model=model) - - -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) -def test_PVSystem_multi_array_celltemp_temp_too_long( - model, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - two_array_system.get_cell_temperature((1000, 1000), (1, 1, 1), 1, - model=model) - - -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) -def test_PVSystem_multi_array_celltemp_wind_too_short( - model, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - two_array_system.get_cell_temperature((1000, 1000), 25, (1,), - model=model) - - -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) -def test_PVSystem_multi_array_celltemp_wind_too_long( - model, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - two_array_system.get_cell_temperature((1000, 1000), 25, (1, 1, 1), - model=model) - - -@pytest.mark.parametrize("model", - ['faiman', 'pvsyst', 'sapm', 'fuentes', 'noct_sam']) + with pytest.raises(ValueError, match="not a valid"): + system.get_cell_temperature(1000, 25, 1, "not_a_model") + + +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) +def test_PVSystem_multi_array_celltemp_temp_too_short(model, two_array_system): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + two_array_system.get_cell_temperature( + (1000, 1000), (1,), 1, model=model + ) + + +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) +def test_PVSystem_multi_array_celltemp_temp_too_long(model, two_array_system): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + two_array_system.get_cell_temperature( + (1000, 1000), (1, 1, 1), 1, model=model + ) + + +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) +def test_PVSystem_multi_array_celltemp_wind_too_short(model, two_array_system): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + two_array_system.get_cell_temperature( + (1000, 1000), 25, (1,), model=model + ) + + +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) +def test_PVSystem_multi_array_celltemp_wind_too_long(model, two_array_system): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + two_array_system.get_cell_temperature( + (1000, 1000), 25, (1, 1, 1), model=model + ) + + +@pytest.mark.parametrize( + "model", ["faiman", "pvsyst", "sapm", "fuentes", "noct_sam"] +) def test_PVSystem_multi_array_celltemp_poa_length_mismatch( - model, two_array_system): - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + model, two_array_system +): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): two_array_system.get_cell_temperature(1000, 25, 1, model=model) def test_PVSystem_fuentes_celltemp(mocker): noct_installed = 45 - temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 0} + temp_model_params = {"noct_installed": noct_installed, "surface_tilt": 0} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) - spy = mocker.spy(temperature, 'fuentes') - index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) + spy = mocker.spy(temperature, "fuentes") + index = pd.date_range("2019-01-01 11:00", freq="h", periods=3) temps = pd.Series(25, index) irrads = pd.Series(1000, index) winds = pd.Series(1, index) - out = system.get_cell_temperature(irrads, temps, winds, model='fuentes') + out = system.get_cell_temperature(irrads, temps, winds, model="fuentes") assert_series_equal(spy.call_args[0][0], irrads) assert_series_equal(spy.call_args[0][1], temps) assert_series_equal(spy.call_args[0][2], winds) assert spy.call_args[0][3] == noct_installed - assert_series_equal(out, pd.Series([52.85, 55.85, 55.85], index, - name='tmod')) + assert_series_equal( + out, pd.Series([52.85, 55.85, 55.85], index, name="tmod") + ) def test_PVSystem_fuentes_module_height(mocker): # check that fuentes picks up Array.mount.module_height correctly # (temperature.fuentes defaults to 5 for module_height) - array = pvsystem.Array(mount=FixedMount(module_height=3), - temperature_model_parameters={'noct_installed': 45}) - spy = mocker.spy(temperature, 'fuentes') - index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) + array = pvsystem.Array( + mount=FixedMount(module_height=3), + temperature_model_parameters={"noct_installed": 45}, + ) + spy = mocker.spy(temperature, "fuentes") + index = pd.date_range("2019-01-01 11:00", freq="h", periods=3) temps = pd.Series(25, index) irrads = pd.Series(1000, index) winds = pd.Series(1, index) - _ = array.get_cell_temperature(irrads, temps, winds, model='fuentes') - assert spy.call_args[1]['module_height'] == 3 + _ = array.get_cell_temperature(irrads, temps, winds, model="fuentes") + assert spy.call_args[1]["module_height"] == 3 def test_Array__infer_temperature_model_params(): - array = pvsystem.Array(mount=FixedMount(0, 180, - racking_model='open_rack'), - module_parameters={}, - module_type='glass_polymer') - expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ - 'sapm']['open_rack_glass_polymer'] + array = pvsystem.Array( + mount=FixedMount(0, 180, racking_model="open_rack"), + module_parameters={}, + module_type="glass_polymer", + ) + expected = temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_polymer" + ] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=FixedMount(0, 180, - racking_model='freestanding'), - module_parameters={}, - module_type='glass_polymer') - expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ - 'pvsyst']['freestanding'] + array = pvsystem.Array( + mount=FixedMount(0, 180, racking_model="freestanding"), + module_parameters={}, + module_type="glass_polymer", + ) + expected = temperature.TEMPERATURE_MODEL_PARAMETERS["pvsyst"][ + "freestanding" + ] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=FixedMount(0, 180, - racking_model='insulated'), - module_parameters={}, - module_type=None) - expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ - 'pvsyst']['insulated'] + array = pvsystem.Array( + mount=FixedMount(0, 180, racking_model="insulated"), + module_parameters={}, + module_type=None, + ) + expected = temperature.TEMPERATURE_MODEL_PARAMETERS["pvsyst"]["insulated"] assert expected == array._infer_temperature_model_params() def test_Array__infer_cell_type(): - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), - module_parameters={}) + array = pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters={} + ) assert array._infer_cell_type() is None @@ -725,15 +893,24 @@ def _calcparams_correct_Python_type_numeric_type_cases(): Returns a list of tuples of functions intended for transforming a Python scalar into a numeric type: scalar, np.ndarray, or pandas.Series """ - return list(itertools.product(*(2 * [[ - # scalars (e.g. Python floats) - lambda x: x, - # np.ndarrays (0d and 1d-arrays) - np.array, - lambda x: np.array([x]), - # pd.Series (1d-arrays) - pd.Series - ]]))) + return list( + itertools.product( + *( + 2 + * [ + [ + # scalars (e.g. Python floats) + lambda x: x, + # np.ndarrays (0d and 1d-arrays) + np.array, + lambda x: np.array([x]), + # pd.Series (1d-arrays) + pd.Series, + ] + ] + ) + ) + ) def _calcparams_correct_Python_type_check(out_value, numeric_args): @@ -763,93 +940,105 @@ def _calcparams_correct_Python_type_check(out_value, numeric_args): return np.isscalar(out_value) -@pytest.mark.parametrize('numeric_type_funcs', - _calcparams_correct_Python_type_numeric_type_cases()) -def test_calcparams_desoto_returns_correct_Python_type(numeric_type_funcs, - cec_module_params): +@pytest.mark.parametrize( + "numeric_type_funcs", _calcparams_correct_Python_type_numeric_type_cases() +) +def test_calcparams_desoto_returns_correct_Python_type( + numeric_type_funcs, cec_module_params +): numeric_args = dict( effective_irradiance=numeric_type_funcs[0](800.0), temp_cell=numeric_type_funcs[1](25), ) out = pvsystem.calcparams_desoto( **numeric_args, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) - assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) - for a in out) + assert all( + _calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out + ) -@pytest.mark.parametrize('numeric_type_funcs', - _calcparams_correct_Python_type_numeric_type_cases()) -def test_calcparams_cec_returns_correct_Python_type(numeric_type_funcs, - cec_module_params): +@pytest.mark.parametrize( + "numeric_type_funcs", _calcparams_correct_Python_type_numeric_type_cases() +) +def test_calcparams_cec_returns_correct_Python_type( + numeric_type_funcs, cec_module_params +): numeric_args = dict( effective_irradiance=numeric_type_funcs[0](800.0), temp_cell=numeric_type_funcs[1](25), ) out = pvsystem.calcparams_cec( **numeric_args, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - Adjust=cec_module_params['Adjust'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + Adjust=cec_module_params["Adjust"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) - assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) - for a in out) + assert all( + _calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out + ) -@pytest.mark.parametrize('numeric_type_funcs', - _calcparams_correct_Python_type_numeric_type_cases()) -def test_calcparams_pvsyst_returns_correct_Python_type(numeric_type_funcs, - pvsyst_module_params): +@pytest.mark.parametrize( + "numeric_type_funcs", _calcparams_correct_Python_type_numeric_type_cases() +) +def test_calcparams_pvsyst_returns_correct_Python_type( + numeric_type_funcs, pvsyst_module_params +): numeric_args = dict( effective_irradiance=numeric_type_funcs[0](800.0), temp_cell=numeric_type_funcs[1](25), ) out = pvsystem.calcparams_pvsyst( **numeric_args, - alpha_sc=pvsyst_module_params['alpha_sc'], - gamma_ref=pvsyst_module_params['gamma_ref'], - mu_gamma=pvsyst_module_params['mu_gamma'], - I_L_ref=pvsyst_module_params['I_L_ref'], - I_o_ref=pvsyst_module_params['I_o_ref'], - R_sh_ref=pvsyst_module_params['R_sh_ref'], - R_sh_0=pvsyst_module_params['R_sh_0'], - R_s=pvsyst_module_params['R_s'], - cells_in_series=pvsyst_module_params['cells_in_series'], - EgRef=pvsyst_module_params['EgRef'] + alpha_sc=pvsyst_module_params["alpha_sc"], + gamma_ref=pvsyst_module_params["gamma_ref"], + mu_gamma=pvsyst_module_params["mu_gamma"], + I_L_ref=pvsyst_module_params["I_L_ref"], + I_o_ref=pvsyst_module_params["I_o_ref"], + R_sh_ref=pvsyst_module_params["R_sh_ref"], + R_sh_0=pvsyst_module_params["R_sh_0"], + R_s=pvsyst_module_params["R_s"], + cells_in_series=pvsyst_module_params["cells_in_series"], + EgRef=pvsyst_module_params["EgRef"], ) - assert all(_calcparams_correct_Python_type_check(a, numeric_args.values()) - for a in out) + assert all( + _calcparams_correct_Python_type_check(a, numeric_args.values()) + for a in out + ) def test_calcparams_desoto_all_scalars(cec_module_params): IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( effective_irradiance=800.0, temp_cell=25, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) assert np.isclose(IL, 6.036, atol=1e-4, rtol=0) @@ -863,15 +1052,15 @@ def test_calcparams_cec_all_scalars(cec_module_params): IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_cec( effective_irradiance=800.0, temp_cell=25, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - Adjust=cec_module_params['Adjust'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + Adjust=cec_module_params["Adjust"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) assert np.isclose(IL, 6.036, atol=1e-4, rtol=0) @@ -885,16 +1074,17 @@ def test_calcparams_pvsyst_all_scalars(pvsyst_module_params): IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( effective_irradiance=800.0, temp_cell=50, - alpha_sc=pvsyst_module_params['alpha_sc'], - gamma_ref=pvsyst_module_params['gamma_ref'], - mu_gamma=pvsyst_module_params['mu_gamma'], - I_L_ref=pvsyst_module_params['I_L_ref'], - I_o_ref=pvsyst_module_params['I_o_ref'], - R_sh_ref=pvsyst_module_params['R_sh_ref'], - R_sh_0=pvsyst_module_params['R_sh_0'], - R_s=pvsyst_module_params['R_s'], - cells_in_series=pvsyst_module_params['cells_in_series'], - EgRef=pvsyst_module_params['EgRef']) + alpha_sc=pvsyst_module_params["alpha_sc"], + gamma_ref=pvsyst_module_params["gamma_ref"], + mu_gamma=pvsyst_module_params["mu_gamma"], + I_L_ref=pvsyst_module_params["I_L_ref"], + I_o_ref=pvsyst_module_params["I_o_ref"], + R_sh_ref=pvsyst_module_params["R_sh_ref"], + R_sh_0=pvsyst_module_params["R_sh_0"], + R_s=pvsyst_module_params["R_s"], + cells_in_series=pvsyst_module_params["cells_in_series"], + EgRef=pvsyst_module_params["EgRef"], + ) assert np.isclose(IL, 4.8200, atol=1e-4, rtol=0) assert np.isclose(I0, 1.47e-7, atol=1e-4, rtol=0) @@ -904,68 +1094,96 @@ def test_calcparams_pvsyst_all_scalars(pvsyst_module_params): def test_calcparams_desoto(cec_module_params): - times = pd.date_range(start='2015-01-01', periods=3, freq='12h') - df = pd.DataFrame({ - 'effective_irradiance': [0.0, 800.0, 800.0], - 'temp_cell': [25, 25, 50] - }, index=times) + times = pd.date_range(start="2015-01-01", periods=3, freq="12h") + df = pd.DataFrame( + { + "effective_irradiance": [0.0, 800.0, 800.0], + "temp_cell": [25, 25, 50], + }, + index=times, + ) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - df['effective_irradiance'], - df['temp_cell'], - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], + df["effective_irradiance"], + df["temp_cell"], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) - assert_series_equal(IL, pd.Series([0.0, 6.036, 6.096], index=times), - check_less_precise=3) - assert_series_equal(I0, pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), - check_less_precise=3) - assert_series_equal(Rs, pd.Series([0.094, 0.094, 0.094], index=times), - check_less_precise=3) - assert_series_equal(Rsh, pd.Series([np.inf, 19.65, 19.65], index=times), - check_less_precise=3) - assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), - check_less_precise=3) + assert_series_equal( + IL, pd.Series([0.0, 6.036, 6.096], index=times), check_less_precise=3 + ) + assert_series_equal( + I0, + pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), + check_less_precise=3, + ) + assert_series_equal( + Rs, pd.Series([0.094, 0.094, 0.094], index=times), check_less_precise=3 + ) + assert_series_equal( + Rsh, + pd.Series([np.inf, 19.65, 19.65], index=times), + check_less_precise=3, + ) + assert_series_equal( + nNsVth, + pd.Series([0.473, 0.473, 0.5127], index=times), + check_less_precise=3, + ) def test_calcparams_cec(cec_module_params): - times = pd.date_range(start='2015-01-01', periods=3, freq='12h') - df = pd.DataFrame({ - 'effective_irradiance': [0.0, 800.0, 800.0], - 'temp_cell': [25, 25, 50] - }, index=times) + times = pd.date_range(start="2015-01-01", periods=3, freq="12h") + df = pd.DataFrame( + { + "effective_irradiance": [0.0, 800.0, 800.0], + "temp_cell": [25, 25, 50], + }, + index=times, + ) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_cec( - df['effective_irradiance'], - df['temp_cell'], - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - Adjust=cec_module_params['Adjust'], + df["effective_irradiance"], + df["temp_cell"], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + Adjust=cec_module_params["Adjust"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) - assert_series_equal(IL, pd.Series([0.0, 6.036, 6.0896], index=times), - check_less_precise=3) - assert_series_equal(I0, pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), - check_less_precise=3) - assert_series_equal(Rs, pd.Series([0.094, 0.094, 0.094], index=times), - check_less_precise=3) - assert_series_equal(Rsh, pd.Series([np.inf, 19.65, 19.65], index=times), - check_less_precise=3) - assert_series_equal(nNsVth, pd.Series([0.473, 0.473, 0.5127], index=times), - check_less_precise=3) + assert_series_equal( + IL, pd.Series([0.0, 6.036, 6.0896], index=times), check_less_precise=3 + ) + assert_series_equal( + I0, + pd.Series([0.0, 1.94e-9, 7.419e-8], index=times), + check_less_precise=3, + ) + assert_series_equal( + Rs, pd.Series([0.094, 0.094, 0.094], index=times), check_less_precise=3 + ) + assert_series_equal( + Rsh, + pd.Series([np.inf, 19.65, 19.65], index=times), + check_less_precise=3, + ) + assert_series_equal( + nNsVth, + pd.Series([0.473, 0.473, 0.5127], index=times), + check_less_precise=3, + ) def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): @@ -979,7 +1197,7 @@ def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): checks that the latter is called with the expected parameters instead of some default values. """ - times = pd.date_range(start='2015-01-01', periods=3, freq='12h') + times = pd.date_range(start="2015-01-01", periods=3, freq="12h") effective_irradiance = pd.Series([0.0, 800.0, 800.0], index=times) temp_cell = pd.Series([25, 25, 50], index=times) extra_parameters = dict( @@ -988,17 +1206,17 @@ def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): irrad_ref=1100, temp_ref=23, ) - m = mocker.spy(pvsystem, 'calcparams_desoto') + m = mocker.spy(pvsystem, "calcparams_desoto") pvsystem.calcparams_cec( effective_irradiance=effective_irradiance, temp_cell=temp_cell, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - Adjust=cec_module_params['Adjust'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + Adjust=cec_module_params["Adjust"], **extra_parameters, ) assert m.call_count == 1 @@ -1006,59 +1224,66 @@ def test_calcparams_cec_extra_params_propagation(cec_module_params, mocker): def test_calcparams_pvsyst(pvsyst_module_params): - times = pd.date_range(start='2015-01-01', periods=2, freq='12h') - df = pd.DataFrame({ - 'effective_irradiance': [0.0, 800.0], - 'temp_cell': [25, 50] - }, index=times) + times = pd.date_range(start="2015-01-01", periods=2, freq="12h") + df = pd.DataFrame( + {"effective_irradiance": [0.0, 800.0], "temp_cell": [25, 50]}, + index=times, + ) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( - df['effective_irradiance'], - df['temp_cell'], - alpha_sc=pvsyst_module_params['alpha_sc'], - gamma_ref=pvsyst_module_params['gamma_ref'], - mu_gamma=pvsyst_module_params['mu_gamma'], - I_L_ref=pvsyst_module_params['I_L_ref'], - I_o_ref=pvsyst_module_params['I_o_ref'], - R_sh_ref=pvsyst_module_params['R_sh_ref'], - R_sh_0=pvsyst_module_params['R_sh_0'], - R_s=pvsyst_module_params['R_s'], - cells_in_series=pvsyst_module_params['cells_in_series'], - EgRef=pvsyst_module_params['EgRef']) + df["effective_irradiance"], + df["temp_cell"], + alpha_sc=pvsyst_module_params["alpha_sc"], + gamma_ref=pvsyst_module_params["gamma_ref"], + mu_gamma=pvsyst_module_params["mu_gamma"], + I_L_ref=pvsyst_module_params["I_L_ref"], + I_o_ref=pvsyst_module_params["I_o_ref"], + R_sh_ref=pvsyst_module_params["R_sh_ref"], + R_sh_0=pvsyst_module_params["R_sh_0"], + R_s=pvsyst_module_params["R_s"], + cells_in_series=pvsyst_module_params["cells_in_series"], + EgRef=pvsyst_module_params["EgRef"], + ) assert_series_equal( - IL.round(decimals=3), pd.Series([0.0, 4.8200], index=times)) + IL.round(decimals=3), pd.Series([0.0, 4.8200], index=times) + ) assert_series_equal( - I0.round(decimals=3), pd.Series([0.0, 1.47e-7], index=times)) + I0.round(decimals=3), pd.Series([0.0, 1.47e-7], index=times) + ) assert_series_equal( - Rs.round(decimals=3), pd.Series([0.500, 0.500], index=times)) + Rs.round(decimals=3), pd.Series([0.500, 0.500], index=times) + ) assert_series_equal( - Rsh.round(decimals=3), pd.Series([1000.0, 305.757], index=times)) + Rsh.round(decimals=3), pd.Series([1000.0, 305.757], index=times) + ) assert_series_equal( - nNsVth.round(decimals=4), pd.Series([1.6186, 1.7961], index=times)) + nNsVth.round(decimals=4), pd.Series([1.6186, 1.7961], index=times) + ) def test_PVSystem_calcparams_desoto(cec_module_params, mocker): - mocker.spy(pvsystem, 'calcparams_desoto') + mocker.spy(pvsystem, "calcparams_desoto") module_parameters = cec_module_params.copy() - module_parameters['EgRef'] = 1.121 - module_parameters['dEgdT'] = -0.0002677 + module_parameters["EgRef"] = 1.121 + module_parameters["dEgdT"] = -0.0002677 system = pvsystem.PVSystem(module_parameters=module_parameters) effective_irradiance = np.array([0, 800]) temp_cell = 25 - IL, I0, Rs, Rsh, nNsVth = system.calcparams_desoto(effective_irradiance, - temp_cell) + IL, I0, Rs, Rsh, nNsVth = system.calcparams_desoto( + effective_irradiance, temp_cell + ) pvsystem.calcparams_desoto.assert_called_once_with( effective_irradiance, temp_cell, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - EgRef=module_parameters['EgRef'], - dEgdT=module_parameters['dEgdT'] + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + EgRef=module_parameters["EgRef"], + dEgdT=module_parameters["dEgdT"], ) assert_allclose(IL, np.array([0.0, 6.036]), atol=1e-1) @@ -1069,27 +1294,28 @@ def test_PVSystem_calcparams_desoto(cec_module_params, mocker): def test_PVSystem_calcparams_pvsyst(pvsyst_module_params, mocker): - mocker.spy(pvsystem, 'calcparams_pvsyst') + mocker.spy(pvsystem, "calcparams_pvsyst") module_parameters = pvsyst_module_params.copy() system = pvsystem.PVSystem(module_parameters=module_parameters) effective_irradiance = np.array([0, 800]) temp_cell = np.array([25, 50]) - IL, I0, Rs, Rsh, nNsVth = system.calcparams_pvsyst(effective_irradiance, - temp_cell) + IL, I0, Rs, Rsh, nNsVth = system.calcparams_pvsyst( + effective_irradiance, temp_cell + ) pvsystem.calcparams_pvsyst.assert_called_once_with( effective_irradiance, temp_cell, - alpha_sc=pvsyst_module_params['alpha_sc'], - gamma_ref=pvsyst_module_params['gamma_ref'], - mu_gamma=pvsyst_module_params['mu_gamma'], - I_L_ref=pvsyst_module_params['I_L_ref'], - I_o_ref=pvsyst_module_params['I_o_ref'], - R_sh_ref=pvsyst_module_params['R_sh_ref'], - R_sh_0=pvsyst_module_params['R_sh_0'], - R_s=pvsyst_module_params['R_s'], - cells_in_series=pvsyst_module_params['cells_in_series'], - EgRef=pvsyst_module_params['EgRef'], - R_sh_exp=pvsyst_module_params['R_sh_exp'] + alpha_sc=pvsyst_module_params["alpha_sc"], + gamma_ref=pvsyst_module_params["gamma_ref"], + mu_gamma=pvsyst_module_params["mu_gamma"], + I_L_ref=pvsyst_module_params["I_L_ref"], + I_o_ref=pvsyst_module_params["I_o_ref"], + R_sh_ref=pvsyst_module_params["R_sh_ref"], + R_sh_0=pvsyst_module_params["R_sh_0"], + R_s=pvsyst_module_params["R_s"], + cells_in_series=pvsyst_module_params["cells_in_series"], + EgRef=pvsyst_module_params["EgRef"], + R_sh_exp=pvsyst_module_params["R_sh_exp"], ) assert_allclose(IL, np.array([0.0, 4.8200]), atol=1) @@ -1099,9 +1325,14 @@ def test_PVSystem_calcparams_pvsyst(pvsyst_module_params, mocker): assert_allclose(nNsVth, np.array([1.6186, 1.7961]), atol=0.1) -@pytest.mark.parametrize('calcparams', [pvsystem.PVSystem.calcparams_pvsyst, - pvsystem.PVSystem.calcparams_desoto, - pvsystem.PVSystem.calcparams_cec]) +@pytest.mark.parametrize( + "calcparams", + [ + pvsystem.PVSystem.calcparams_pvsyst, + pvsystem.PVSystem.calcparams_desoto, + pvsystem.PVSystem.calcparams_cec, + ], +) def test_PVSystem_multi_array_calcparams(calcparams, two_array_system): params_one, params_two = calcparams( two_array_system, (1000, 500), (30, 20) @@ -1109,148 +1340,176 @@ def test_PVSystem_multi_array_calcparams(calcparams, two_array_system): assert params_one != params_two -@pytest.mark.parametrize('calcparams, irrad, celltemp', - [ (f, irrad, celltemp) - for f in (pvsystem.PVSystem.calcparams_desoto, - pvsystem.PVSystem.calcparams_cec, - pvsystem.PVSystem.calcparams_pvsyst) - for irrad, celltemp in [(1, (1, 1)), ((1, 1), 1)]]) +@pytest.mark.parametrize( + "calcparams, irrad, celltemp", + [ + (f, irrad, celltemp) + for f in ( + pvsystem.PVSystem.calcparams_desoto, + pvsystem.PVSystem.calcparams_cec, + pvsystem.PVSystem.calcparams_pvsyst, + ) + for irrad, celltemp in [(1, (1, 1)), ((1, 1), 1)] + ], +) def test_PVSystem_multi_array_calcparams_value_error( - calcparams, irrad, celltemp, two_array_system): - with pytest.raises(ValueError, - match='Length mismatch for per-array parameter'): + calcparams, irrad, celltemp, two_array_system +): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): calcparams(two_array_system, irrad, celltemp) -@pytest.fixture(params=[ - { # Can handle all python scalar inputs - 'Rsh': 20., - 'Rs': 0.1, - 'nNsVth': 0.5, - 'I': 3., - 'I0': 6.e-7, - 'IL': 7., - 'V_expected': 7.5049875193450521 - }, - { # Can handle all rank-0 array inputs - 'Rsh': np.array(20.), - 'Rs': np.array(0.1), - 'nNsVth': np.array(0.5), - 'I': np.array(3.), - 'I0': np.array(6.e-7), - 'IL': np.array(7.), - 'V_expected': np.array(7.5049875193450521) - }, - { # Can handle all rank-1 singleton array inputs - 'Rsh': np.array([20.]), - 'Rs': np.array([0.1]), - 'nNsVth': np.array([0.5]), - 'I': np.array([3.]), - 'I0': np.array([6.e-7]), - 'IL': np.array([7.]), - 'V_expected': np.array([7.5049875193450521]) - }, - { # Can handle all rank-1 non-singleton array inputs with infinite shunt - # resistance, Rsh=inf gives V=Voc=nNsVth*(np.log(IL + I0) - np.log(I0) - # at I=0 - 'Rsh': np.array([np.inf, 20.]), - 'Rs': np.array([0.1, 0.1]), - 'nNsVth': np.array([0.5, 0.5]), - 'I': np.array([0., 3.]), - 'I0': np.array([6.e-7, 6.e-7]), - 'IL': np.array([7., 7.]), - 'V_expected': np.array([0.5*(np.log(7. + 6.e-7) - np.log(6.e-7)), - 7.5049875193450521]) - }, - { # Can handle mixed inputs with a rank-2 array with infinite shunt - # resistance, Rsh=inf gives V=Voc=nNsVth*(np.log(IL + I0) - np.log(I0) - # at I=0 - 'Rsh': np.array([[np.inf, np.inf], [np.inf, np.inf]]), - 'Rs': np.array([0.1]), - 'nNsVth': np.array(0.5), - 'I': 0., - 'I0': np.array([6.e-7]), - 'IL': np.array([7.]), - 'V_expected': 0.5*(np.log(7. + 6.e-7) - np.log(6.e-7))*np.ones((2, 2)) - }, - { # Can handle ideal series and shunt, Rsh=inf and Rs=0 give - # V = nNsVth*(np.log(IL - I + I0) - np.log(I0)) - 'Rsh': np.inf, - 'Rs': 0., - 'nNsVth': 0.5, - 'I': np.array([7., 7./2., 0.]), - 'I0': 6.e-7, - 'IL': 7., - 'V_expected': np.array([0., 0.5*(np.log(7. - 7./2. + 6.e-7) - - np.log(6.e-7)), 0.5*(np.log(7. + 6.e-7) - - np.log(6.e-7))]) - }, - { # Can handle only ideal series resistance, no closed form solution - 'Rsh': 20., - 'Rs': 0., - 'nNsVth': 0.5, - 'I': 3., - 'I0': 6.e-7, - 'IL': 7., - 'V_expected': 7.804987519345062 - }, - { # Can handle all python scalar inputs with big LambertW arg - 'Rsh': 500., - 'Rs': 10., - 'nNsVth': 4.06, - 'I': 0., - 'I0': 6.e-10, - 'IL': 1.2, - 'V_expected': 86.320000493521079 - }, - { # Can handle all python scalar inputs with bigger LambertW arg - # 1000 W/m^2 on a Canadian Solar 220M with 20 C ambient temp - # github issue 225 (this appears to be from PR 226 not issue 225) - 'Rsh': 190., - 'Rs': 1.065, - 'nNsVth': 2.89, - 'I': 0., - 'I0': 7.05196029e-08, - 'IL': 10.491262, - 'V_expected': 54.303958833791455 - }, - { # Can handle all python scalar inputs with bigger LambertW arg - # 1000 W/m^2 on a Canadian Solar 220M with 20 C ambient temp - # github issue 225 - 'Rsh': 381.68, - 'Rs': 1.065, - 'nNsVth': 2.681527737715915, - 'I': 0., - 'I0': 1.8739027472625636e-09, - 'IL': 5.1366949999999996, - 'V_expected': 58.19323124611128 - }, - { # Verify mixed solution type indexing logic - 'Rsh': np.array([np.inf, 190., 381.68]), - 'Rs': 1.065, - 'nNsVth': np.array([2.89, 2.89, 2.681527737715915]), - 'I': 0., - 'I0': np.array([7.05196029e-08, 7.05196029e-08, 1.8739027472625636e-09]), - 'IL': np.array([10.491262, 10.491262, 5.1366949999999996]), - 'V_expected': np.array([2.89*np.log1p(10.491262/7.05196029e-08), - 54.303958833791455, 58.19323124611128]) - }]) +@pytest.fixture( + params=[ + { # Can handle all python scalar inputs + "Rsh": 20.0, + "Rs": 0.1, + "nNsVth": 0.5, + "I": 3.0, + "I0": 6.0e-7, + "IL": 7.0, + "V_expected": 7.5049875193450521, + }, + { # Can handle all rank-0 array inputs + "Rsh": np.array(20.0), + "Rs": np.array(0.1), + "nNsVth": np.array(0.5), + "I": np.array(3.0), + "I0": np.array(6.0e-7), + "IL": np.array(7.0), + "V_expected": np.array(7.5049875193450521), + }, + { # Can handle all rank-1 singleton array inputs + "Rsh": np.array([20.0]), + "Rs": np.array([0.1]), + "nNsVth": np.array([0.5]), + "I": np.array([3.0]), + "I0": np.array([6.0e-7]), + "IL": np.array([7.0]), + "V_expected": np.array([7.5049875193450521]), + }, + { # Can handle all rank-1 non-singleton array inputs with infinite shunt + # resistance, Rsh=inf gives V=Voc=nNsVth*(np.log(IL + I0) - np.log(I0) + # at I=0 + "Rsh": np.array([np.inf, 20.0]), + "Rs": np.array([0.1, 0.1]), + "nNsVth": np.array([0.5, 0.5]), + "I": np.array([0.0, 3.0]), + "I0": np.array([6.0e-7, 6.0e-7]), + "IL": np.array([7.0, 7.0]), + "V_expected": np.array( + [ + 0.5 * (np.log(7.0 + 6.0e-7) - np.log(6.0e-7)), + 7.5049875193450521, + ] + ), + }, + { # Can handle mixed inputs with a rank-2 array with infinite shunt + # resistance, Rsh=inf gives V=Voc=nNsVth*(np.log(IL + I0) - np.log(I0) + # at I=0 + "Rsh": np.array([[np.inf, np.inf], [np.inf, np.inf]]), + "Rs": np.array([0.1]), + "nNsVth": np.array(0.5), + "I": 0.0, + "I0": np.array([6.0e-7]), + "IL": np.array([7.0]), + "V_expected": 0.5 + * (np.log(7.0 + 6.0e-7) - np.log(6.0e-7)) + * np.ones((2, 2)), + }, + { # Can handle ideal series and shunt, Rsh=inf and Rs=0 give + # V = nNsVth*(np.log(IL - I + I0) - np.log(I0)) + "Rsh": np.inf, + "Rs": 0.0, + "nNsVth": 0.5, + "I": np.array([7.0, 7.0 / 2.0, 0.0]), + "I0": 6.0e-7, + "IL": 7.0, + "V_expected": np.array( + [ + 0.0, + 0.5 * (np.log(7.0 - 7.0 / 2.0 + 6.0e-7) - np.log(6.0e-7)), + 0.5 * (np.log(7.0 + 6.0e-7) - np.log(6.0e-7)), + ] + ), + }, + { # Can handle only ideal series resistance, no closed form solution + "Rsh": 20.0, + "Rs": 0.0, + "nNsVth": 0.5, + "I": 3.0, + "I0": 6.0e-7, + "IL": 7.0, + "V_expected": 7.804987519345062, + }, + { # Can handle all python scalar inputs with big LambertW arg + "Rsh": 500.0, + "Rs": 10.0, + "nNsVth": 4.06, + "I": 0.0, + "I0": 6.0e-10, + "IL": 1.2, + "V_expected": 86.320000493521079, + }, + { # Can handle all python scalar inputs with bigger LambertW arg + # 1000 W/m^2 on a Canadian Solar 220M with 20 C ambient temp + # github issue 225 (this appears to be from PR 226 not issue 225) + "Rsh": 190.0, + "Rs": 1.065, + "nNsVth": 2.89, + "I": 0.0, + "I0": 7.05196029e-08, + "IL": 10.491262, + "V_expected": 54.303958833791455, + }, + { # Can handle all python scalar inputs with bigger LambertW arg + # 1000 W/m^2 on a Canadian Solar 220M with 20 C ambient temp + # github issue 225 + "Rsh": 381.68, + "Rs": 1.065, + "nNsVth": 2.681527737715915, + "I": 0.0, + "I0": 1.8739027472625636e-09, + "IL": 5.1366949999999996, + "V_expected": 58.19323124611128, + }, + { # Verify mixed solution type indexing logic + "Rsh": np.array([np.inf, 190.0, 381.68]), + "Rs": 1.065, + "nNsVth": np.array([2.89, 2.89, 2.681527737715915]), + "I": 0.0, + "I0": np.array( + [7.05196029e-08, 7.05196029e-08, 1.8739027472625636e-09] + ), + "IL": np.array([10.491262, 10.491262, 5.1366949999999996]), + "V_expected": np.array( + [ + 2.89 * np.log1p(10.491262 / 7.05196029e-08), + 54.303958833791455, + 58.19323124611128, + ] + ), + }, + ] +) def fixture_v_from_i(request): return request.param @pytest.mark.parametrize( - 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-8)] + "method, atol", [("lambertw", 1e-11), ("brentq", 1e-11), ("newton", 1e-8)] ) def test_v_from_i(fixture_v_from_i, method, atol): # Solution set loaded from fixture - Rsh = fixture_v_from_i['Rsh'] - Rs = fixture_v_from_i['Rs'] - nNsVth = fixture_v_from_i['nNsVth'] - I = fixture_v_from_i['I'] - I0 = fixture_v_from_i['I0'] - IL = fixture_v_from_i['IL'] - V_expected = fixture_v_from_i['V_expected'] + Rsh = fixture_v_from_i["Rsh"] + Rs = fixture_v_from_i["Rs"] + nNsVth = fixture_v_from_i["nNsVth"] + I = fixture_v_from_i["I"] + I0 = fixture_v_from_i["I0"] + IL = fixture_v_from_i["IL"] + V_expected = fixture_v_from_i["V_expected"] V = pvsystem.v_from_i(I, IL, I0, Rs, Rsh, nNsVth, method=method) @@ -1263,19 +1522,20 @@ def test_v_from_i(fixture_v_from_i, method, atol): def test_i_from_v_from_i(fixture_v_from_i): # Solution set loaded from fixture - Rsh = fixture_v_from_i['Rsh'] - Rs = fixture_v_from_i['Rs'] - nNsVth = fixture_v_from_i['nNsVth'] - current = fixture_v_from_i['I'] - I0 = fixture_v_from_i['I0'] - IL = fixture_v_from_i['IL'] - V = fixture_v_from_i['V_expected'] + Rsh = fixture_v_from_i["Rsh"] + Rs = fixture_v_from_i["Rs"] + nNsVth = fixture_v_from_i["nNsVth"] + current = fixture_v_from_i["I"] + I0 = fixture_v_from_i["I0"] + IL = fixture_v_from_i["IL"] + V = fixture_v_from_i["V_expected"] # Convergence criteria - atol = 1.e-11 + atol = 1.0e-11 - I_expected = pvsystem.i_from_v(V, IL, I0, Rs, Rsh, nNsVth, - method='lambertw') + I_expected = pvsystem.i_from_v( + V, IL, I0, Rs, Rsh, nNsVth, method="lambertw" + ) assert_allclose(current, I_expected, atol=atol) current = pvsystem.i_from_v(V, IL, I0, Rs, Rsh, nNsVth) @@ -1287,91 +1547,106 @@ def test_i_from_v_from_i(fixture_v_from_i): assert_allclose(current, I_expected, atol=atol) -@pytest.fixture(params=[ - { # Can handle all python scalar inputs - 'Rsh': 20., - 'Rs': 0.1, - 'nNsVth': 0.5, - 'V': 7.5049875193450521, - 'I0': 6.e-7, - 'IL': 7., - 'I_expected': 3. - }, - { # Can handle all rank-0 array inputs - 'Rsh': np.array(20.), - 'Rs': np.array(0.1), - 'nNsVth': np.array(0.5), - 'V': np.array(7.5049875193450521), - 'I0': np.array(6.e-7), - 'IL': np.array(7.), - 'I_expected': np.array(3.) - }, - { # Can handle all rank-1 singleton array inputs - 'Rsh': np.array([20.]), - 'Rs': np.array([0.1]), - 'nNsVth': np.array([0.5]), - 'V': np.array([7.5049875193450521]), - 'I0': np.array([6.e-7]), - 'IL': np.array([7.]), - 'I_expected': np.array([3.]) - }, - { # Can handle all rank-1 non-singleton array inputs with a zero - # series resistance, Rs=0 gives I=IL=Isc at V=0 - 'Rsh': np.array([20., 20.]), - 'Rs': np.array([0., 0.1]), - 'nNsVth': np.array([0.5, 0.5]), - 'V': np.array([0., 7.5049875193450521]), - 'I0': np.array([6.e-7, 6.e-7]), - 'IL': np.array([7., 7.]), - 'I_expected': np.array([7., 3.]) - }, - { # Can handle mixed inputs with a rank-2 array with zero series - # resistance, Rs=0 gives I=IL=Isc at V=0 - 'Rsh': np.array([20.]), - 'Rs': np.array([[0., 0.], [0., 0.]]), - 'nNsVth': np.array(0.5), - 'V': 0., - 'I0': np.array([6.e-7]), - 'IL': np.array([7.]), - 'I_expected': np.array([[7., 7.], [7., 7.]]) - }, - { # Can handle ideal series and shunt, Rsh=inf and Rs=0 give - # V_oc = nNsVth*(np.log(IL + I0) - np.log(I0)) - 'Rsh': np.inf, - 'Rs': 0., - 'nNsVth': 0.5, - 'V': np.array([0., 0.5*(np.log(7. + 6.e-7) - np.log(6.e-7))/2., - 0.5*(np.log(7. + 6.e-7) - np.log(6.e-7))]), - 'I0': 6.e-7, - 'IL': 7., - 'I_expected': np.array([7., 7. - 6.e-7*np.expm1((np.log(7. + 6.e-7) - - np.log(6.e-7))/2.), 0.]) - }, - { # Can handle only ideal shunt resistance, no closed form solution - 'Rsh': np.inf, - 'Rs': 0.1, - 'nNsVth': 0.5, - 'V': 7.5049875193450521, - 'I0': 6.e-7, - 'IL': 7., - 'I_expected': 3.2244873645510923 - }]) +@pytest.fixture( + params=[ + { # Can handle all python scalar inputs + "Rsh": 20.0, + "Rs": 0.1, + "nNsVth": 0.5, + "V": 7.5049875193450521, + "I0": 6.0e-7, + "IL": 7.0, + "I_expected": 3.0, + }, + { # Can handle all rank-0 array inputs + "Rsh": np.array(20.0), + "Rs": np.array(0.1), + "nNsVth": np.array(0.5), + "V": np.array(7.5049875193450521), + "I0": np.array(6.0e-7), + "IL": np.array(7.0), + "I_expected": np.array(3.0), + }, + { # Can handle all rank-1 singleton array inputs + "Rsh": np.array([20.0]), + "Rs": np.array([0.1]), + "nNsVth": np.array([0.5]), + "V": np.array([7.5049875193450521]), + "I0": np.array([6.0e-7]), + "IL": np.array([7.0]), + "I_expected": np.array([3.0]), + }, + { # Can handle all rank-1 non-singleton array inputs with a zero + # series resistance, Rs=0 gives I=IL=Isc at V=0 + "Rsh": np.array([20.0, 20.0]), + "Rs": np.array([0.0, 0.1]), + "nNsVth": np.array([0.5, 0.5]), + "V": np.array([0.0, 7.5049875193450521]), + "I0": np.array([6.0e-7, 6.0e-7]), + "IL": np.array([7.0, 7.0]), + "I_expected": np.array([7.0, 3.0]), + }, + { # Can handle mixed inputs with a rank-2 array with zero series + # resistance, Rs=0 gives I=IL=Isc at V=0 + "Rsh": np.array([20.0]), + "Rs": np.array([[0.0, 0.0], [0.0, 0.0]]), + "nNsVth": np.array(0.5), + "V": 0.0, + "I0": np.array([6.0e-7]), + "IL": np.array([7.0]), + "I_expected": np.array([[7.0, 7.0], [7.0, 7.0]]), + }, + { # Can handle ideal series and shunt, Rsh=inf and Rs=0 give + # V_oc = nNsVth*(np.log(IL + I0) - np.log(I0)) + "Rsh": np.inf, + "Rs": 0.0, + "nNsVth": 0.5, + "V": np.array( + [ + 0.0, + 0.5 * (np.log(7.0 + 6.0e-7) - np.log(6.0e-7)) / 2.0, + 0.5 * (np.log(7.0 + 6.0e-7) - np.log(6.0e-7)), + ] + ), + "I0": 6.0e-7, + "IL": 7.0, + "I_expected": np.array( + [ + 7.0, + 7.0 + - 6.0e-7 + * np.expm1((np.log(7.0 + 6.0e-7) - np.log(6.0e-7)) / 2.0), + 0.0, + ] + ), + }, + { # Can handle only ideal shunt resistance, no closed form solution + "Rsh": np.inf, + "Rs": 0.1, + "nNsVth": 0.5, + "V": 7.5049875193450521, + "I0": 6.0e-7, + "IL": 7.0, + "I_expected": 3.2244873645510923, + }, + ] +) def fixture_i_from_v(request): return request.param @pytest.mark.parametrize( - 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11)] + "method, atol", [("lambertw", 1e-11), ("brentq", 1e-11), ("newton", 1e-11)] ) def test_i_from_v(fixture_i_from_v, method, atol): # Solution set loaded from fixture - Rsh = fixture_i_from_v['Rsh'] - Rs = fixture_i_from_v['Rs'] - nNsVth = fixture_i_from_v['nNsVth'] - V = fixture_i_from_v['V'] - I0 = fixture_i_from_v['I0'] - IL = fixture_i_from_v['IL'] - I_expected = fixture_i_from_v['I_expected'] + Rsh = fixture_i_from_v["Rsh"] + Rs = fixture_i_from_v["Rs"] + nNsVth = fixture_i_from_v["nNsVth"] + V = fixture_i_from_v["V"] + I0 = fixture_i_from_v["I0"] + IL = fixture_i_from_v["IL"] + I_expected = fixture_i_from_v["I_expected"] current = pvsystem.i_from_v(V, IL, I0, Rs, Rsh, nNsVth, method=method) @@ -1384,7 +1659,7 @@ def test_i_from_v(fixture_i_from_v, method, atol): def test_PVSystem_i_from_v(mocker): system = pvsystem.PVSystem() - m = mocker.patch('pvlib.pvsystem.i_from_v', autospec=True) + m = mocker.patch("pvlib.pvsystem.i_from_v", autospec=True) args = (7.5049875193450521, 7, 6e-7, 0.1, 20, 0.5) system.i_from_v(*args) m.assert_called_once_with(*args) @@ -1392,37 +1667,55 @@ def test_PVSystem_i_from_v(mocker): def test_i_from_v_size(): with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) + pvsystem.i_from_v([7.5] * 3, 7.0, 6e-7, [0.1] * 2, 20, 0.5) with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='brentq') + pvsystem.i_from_v( + [7.5] * 3, 7.0, 6e-7, [0.1] * 2, 20, 0.5, method="brentq" + ) with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, np.array([7., 7.]), 6e-7, 0.1, 20, 0.5, - method='newton') + pvsystem.i_from_v( + [7.5] * 3, + np.array([7.0, 7.0]), + 6e-7, + 0.1, + 20, + 0.5, + method="newton", + ) def test_v_from_i_size(): with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) + pvsystem.v_from_i([3.0] * 3, 7.0, 6e-7, [0.1] * 2, 20, 0.5) with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='brentq') + pvsystem.v_from_i( + [3.0] * 3, 7.0, 6e-7, [0.1] * 2, 20, 0.5, method="brentq" + ) with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, np.array([7., 7.]), 6e-7, [0.1], 20, 0.5, - method='newton') + pvsystem.v_from_i( + [3.0] * 3, + np.array([7.0, 7.0]), + 6e-7, + [0.1], + 20, + 0.5, + method="newton", + ) def test_mpp_floats(): """test max_power_point""" - IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, .1, 20, .5) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') - expected = {'i_mp': 6.1362673597376753, # 6.1390251797935704, lambertw - 'v_mp': 6.2243393757884284, # 6.221535886625464, lambertw - 'p_mp': 38.194210547580511} # 38.194165464983037} lambertw + IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, 0.1, 20, 0.5) + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="brentq") + expected = { + "i_mp": 6.1362673597376753, # 6.1390251797935704, lambertw + "v_mp": 6.2243393757884284, # 6.221535886625464, lambertw + "p_mp": 38.194210547580511, + } # 38.194165464983037} lambertw assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="newton") for k, v in out.items(): assert np.isclose(v, expected[k]) @@ -1431,86 +1724,108 @@ def test_mpp_recombination(): """test max_power_point""" pvsyst_fs_495 = get_pvsyst_fs_495() IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( - effective_irradiance=pvsyst_fs_495['irrad_ref'], - temp_cell=pvsyst_fs_495['temp_ref'], - alpha_sc=pvsyst_fs_495['alpha_sc'], - gamma_ref=pvsyst_fs_495['gamma_ref'], - mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], - I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], - R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], - R_s=pvsyst_fs_495['R_s'], - cells_in_series=pvsyst_fs_495['cells_in_series'], - EgRef=pvsyst_fs_495['EgRef']) + effective_irradiance=pvsyst_fs_495["irrad_ref"], + temp_cell=pvsyst_fs_495["temp_ref"], + alpha_sc=pvsyst_fs_495["alpha_sc"], + gamma_ref=pvsyst_fs_495["gamma_ref"], + mu_gamma=pvsyst_fs_495["mu_gamma"], + I_L_ref=pvsyst_fs_495["I_L_ref"], + I_o_ref=pvsyst_fs_495["I_o_ref"], + R_sh_ref=pvsyst_fs_495["R_sh_ref"], + R_sh_0=pvsyst_fs_495["R_sh_0"], + R_sh_exp=pvsyst_fs_495["R_sh_exp"], + R_s=pvsyst_fs_495["R_s"], + cells_in_series=pvsyst_fs_495["cells_in_series"], + EgRef=pvsyst_fs_495["EgRef"], + ) out = pvsystem.max_power_point( - IL, I0, Rs, Rsh, nNsVth, - d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series'], - method='brentq') - expected_imp = pvsyst_fs_495['I_mp_ref'] - expected_vmp = pvsyst_fs_495['V_mp_ref'] - expected_pmp = expected_imp*expected_vmp - expected = {'i_mp': expected_imp, - 'v_mp': expected_vmp, - 'p_mp': expected_pmp} + IL, + I0, + Rs, + Rsh, + nNsVth, + d2mutau=pvsyst_fs_495["d2mutau"], + NsVbi=VOLTAGE_BUILTIN * pvsyst_fs_495["cells_in_series"], + method="brentq", + ) + expected_imp = pvsyst_fs_495["I_mp_ref"] + expected_vmp = pvsyst_fs_495["V_mp_ref"] + expected_pmp = expected_imp * expected_vmp + expected = { + "i_mp": expected_imp, + "v_mp": expected_vmp, + "p_mp": expected_pmp, + } assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k], 0.01) out = pvsystem.max_power_point( - IL, I0, Rs, Rsh, nNsVth, - d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series'], - method='newton') + IL, + I0, + Rs, + Rsh, + nNsVth, + d2mutau=pvsyst_fs_495["d2mutau"], + NsVbi=VOLTAGE_BUILTIN * pvsyst_fs_495["cells_in_series"], + method="newton", + ) for k, v in out.items(): assert np.isclose(v, expected[k], 0.01) def test_mpp_array(): """test max_power_point""" - IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') - expected = {'i_mp': [6.1362673597376753] * 2, - 'v_mp': [6.2243393757884284] * 2, - 'p_mp': [38.194210547580511] * 2} + IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, 0.1, 20, 0.5) + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="brentq") + expected = { + "i_mp": [6.1362673597376753] * 2, + "v_mp": [6.2243393757884284] * 2, + "p_mp": [38.194210547580511] * 2, + } assert isinstance(out, dict) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="newton") for k, v in out.items(): assert np.allclose(v, expected[k]) def test_mpp_series(): """test max_power_point""" - idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] - IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) + idx = ["2008-02-17T11:30:00-0800", "2008-02-17T12:30:00-0800"] + IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, 0.1, 20, 0.5) IL = pd.Series(IL, index=idx) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') - expected = pd.DataFrame({'i_mp': [6.1362673597376753] * 2, - 'v_mp': [6.2243393757884284] * 2, - 'p_mp': [38.194210547580511] * 2}, - index=idx) + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="brentq") + expected = pd.DataFrame( + { + "i_mp": [6.1362673597376753] * 2, + "v_mp": [6.2243393757884284] * 2, + "p_mp": [38.194210547580511] * 2, + }, + index=idx, + ) assert isinstance(out, pd.DataFrame) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method="newton") for k, v in out.items(): assert np.allclose(v, expected[k]) def test_singlediode_series(cec_module_params): - times = pd.date_range(start='2015-01-01', periods=2, freq='12h') + times = pd.date_range(start="2015-01-01", periods=2, freq="12h") effective_irradiance = pd.Series([0.0, 800.0], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( effective_irradiance, temp_cell=25, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], EgRef=1.121, - dEgdT=-0.0002677 + dEgdT=-0.0002677, ) out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth) assert isinstance(out, pd.DataFrame) @@ -1524,97 +1839,146 @@ def test_singlediode_array(): nNsVth = 0.473 saturation_current = 1.943e-09 - sd = pvsystem.singlediode(photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, - method='lambertw') + sd = pvsystem.singlediode( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + method="lambertw", + ) - expected_i = np.array([ - 0., 0.54614798740338, 1.435026463529, 2.3621366610078, 3.2953968319952, - 4.2303869378787, 5.1655276691892, 6.1000269648604, 7.0333996177802, - 7.9653036915959, 8.8954716265647]) - expected_v = np.array([ - 0., 7.0966259059555, 7.9961986643428, 8.2222496810656, 8.3255927555753, - 8.3766915453915, 8.3988872440242, 8.4027948807891, 8.3941399580559, - 8.3763655188855, 8.3517057522791]) + expected_i = np.array( + [ + 0.0, + 0.54614798740338, + 1.435026463529, + 2.3621366610078, + 3.2953968319952, + 4.2303869378787, + 5.1655276691892, + 6.1000269648604, + 7.0333996177802, + 7.9653036915959, + 8.8954716265647, + ] + ) + expected_v = np.array( + [ + 0.0, + 7.0966259059555, + 7.9961986643428, + 8.2222496810656, + 8.3255927555753, + 8.3766915453915, + 8.3988872440242, + 8.4027948807891, + 8.3941399580559, + 8.3763655188855, + 8.3517057522791, + ] + ) - assert_allclose(sd['i_mp'], expected_i, atol=1e-8) - assert_allclose(sd['v_mp'], expected_v, atol=1e-8) + assert_allclose(sd["i_mp"], expected_i, atol=1e-8) + assert_allclose(sd["v_mp"], expected_v, atol=1e-8) - sd = pvsystem.singlediode(photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth) - expected = pvsystem.i_from_v(sd['v_mp'], photocurrent, saturation_current, - resistance_series, resistance_shunt, nNsVth, - method='lambertw') - assert_allclose(sd['i_mp'], expected, atol=1e-8) + sd = pvsystem.singlediode( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + expected = pvsystem.i_from_v( + sd["v_mp"], + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + method="lambertw", + ) + assert_allclose(sd["i_mp"], expected, atol=1e-8) def test_singlediode_floats(): - out = pvsystem.singlediode(7., 6.e-7, .1, 20., .5, method='lambertw') - expected = {'i_xx': 4.264060478, - 'i_mp': 6.136267360, - 'v_oc': 8.106300147, - 'p_mp': 38.19421055, - 'i_x': 6.7558815684, - 'i_sc': 6.965172322, - 'v_mp': 6.224339375, - 'i': None, - 'v': None} + out = pvsystem.singlediode(7.0, 6.0e-7, 0.1, 20.0, 0.5, method="lambertw") + expected = { + "i_xx": 4.264060478, + "i_mp": 6.136267360, + "v_oc": 8.106300147, + "p_mp": 38.19421055, + "i_x": 6.7558815684, + "i_sc": 6.965172322, + "v_mp": 6.224339375, + "i": None, + "v": None, + } assert isinstance(out, dict) for k, v in out.items(): - if k in ['i', 'v']: + if k in ["i", "v"]: assert v is None else: assert_allclose(v, expected[k], atol=1e-6) def test_singlediode_floats_expected(): - out = pvsystem.singlediode(7., 6e-7, .1, 20., .5, method='lambertw') - expected = {'i_xx': 4.264060478, - 'i_mp': 6.136267360, - 'v_oc': 8.106300147, - 'p_mp': 38.19421055, - 'i_x': 6.7558815684, - 'i_sc': 6.965172322, - 'v_mp': 6.224339375} + out = pvsystem.singlediode(7.0, 6e-7, 0.1, 20.0, 0.5, method="lambertw") + expected = { + "i_xx": 4.264060478, + "i_mp": 6.136267360, + "v_oc": 8.106300147, + "p_mp": 38.19421055, + "i_x": 6.7558815684, + "i_sc": 6.965172322, + "v_mp": 6.224339375, + } assert isinstance(out, dict) for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-6) def test_singlediode_series_expected(cec_module_params): - times = pd.date_range(start='2015-06-01', periods=3, freq='6h') + times = pd.date_range(start="2015-06-01", periods=3, freq="6h") effective_irradiance = pd.Series([0.0, 400.0, 800.0], index=times) IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( - effective_irradiance, - temp_cell=25, - alpha_sc=cec_module_params['alpha_sc'], - a_ref=cec_module_params['a_ref'], - I_L_ref=cec_module_params['I_L_ref'], - I_o_ref=cec_module_params['I_o_ref'], - R_sh_ref=cec_module_params['R_sh_ref'], - R_s=cec_module_params['R_s'], - EgRef=1.121, - dEgdT=-0.0002677) - - out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, method='lambertw') - - expected = OrderedDict([('i_sc', array([0., 3.01079860, 6.00726296])), - ('v_oc', array([0., 9.96959733, 10.29603253])), - ('i_mp', array([0., 2.656285960, 5.290525645])), - ('v_mp', array([0., 8.321092255, 8.409413795])), - ('p_mp', array([0., 22.10320053, 44.49021934])), - ('i_x', array([0., 2.884132006, 5.746202281])), - ('i_xx', array([0., 2.052691562, 3.909673879]))]) + effective_irradiance, + temp_cell=25, + alpha_sc=cec_module_params["alpha_sc"], + a_ref=cec_module_params["a_ref"], + I_L_ref=cec_module_params["I_L_ref"], + I_o_ref=cec_module_params["I_o_ref"], + R_sh_ref=cec_module_params["R_sh_ref"], + R_s=cec_module_params["R_s"], + EgRef=1.121, + dEgdT=-0.0002677, + ) + + out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, method="lambertw") + + expected = OrderedDict( + [ + ("i_sc", array([0.0, 3.01079860, 6.00726296])), + ("v_oc", array([0.0, 9.96959733, 10.29603253])), + ("i_mp", array([0.0, 2.656285960, 5.290525645])), + ("v_mp", array([0.0, 8.321092255, 8.409413795])), + ("p_mp", array([0.0, 22.10320053, 44.49021934])), + ("i_x", array([0.0, 2.884132006, 5.746202281])), + ("i_xx", array([0.0, 2.052691562, 3.909673879])), + ] + ) for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-2) out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth) - expected['i_mp'] = pvsystem.i_from_v(out['v_mp'], IL, I0, Rs, Rsh, nNsVth, - method='lambertw') - expected['v_mp'] = pvsystem.v_from_i(out['i_mp'], IL, I0, Rs, Rsh, nNsVth, - method='lambertw') + expected["i_mp"] = pvsystem.i_from_v( + out["v_mp"], IL, I0, Rs, Rsh, nNsVth, method="lambertw" + ) + expected["v_mp"] = pvsystem.v_from_i( + out["i_mp"], IL, I0, Rs, Rsh, nNsVth, method="lambertw" + ) for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-6) @@ -1623,12 +1987,14 @@ def test_singlediode_series_expected(cec_module_params): def test_scale_voltage_current_power(): data = pd.DataFrame( np.array([[2, 1.5, 10, 8, 12, 0.5, 1.5]]), - columns=['i_sc', 'i_mp', 'v_oc', 'v_mp', 'p_mp', 'i_x', 'i_xx'], - index=[0]) + columns=["i_sc", "i_mp", "v_oc", "v_mp", "p_mp", "i_x", "i_xx"], + index=[0], + ) expected = pd.DataFrame( np.array([[6, 4.5, 20, 16, 72, 1.5, 4.5]]), - columns=['i_sc', 'i_mp', 'v_oc', 'v_mp', 'p_mp', 'i_x', 'i_xx'], - index=[0]) + columns=["i_sc", "i_mp", "v_oc", "v_mp", "p_mp", "i_x", "i_xx"], + index=[0], + ) out = pvsystem.scale_voltage_current_power(data, voltage=2, current=3) assert_frame_equal(out, expected, check_less_precise=5) @@ -1637,7 +2003,8 @@ def test_PVSystem_scale_voltage_current_power(mocker): data = None system = pvsystem.PVSystem(modules_per_string=2, strings_per_inverter=3) m = mocker.patch( - 'pvlib.pvsystem.scale_voltage_current_power', autospec=True) + "pvlib.pvsystem.scale_voltage_current_power", autospec=True + ) system.scale_voltage_current_power(data) m.assert_called_once_with(data, voltage=2, current=3) @@ -1645,167 +2012,199 @@ def test_PVSystem_scale_voltage_current_power(mocker): def test_PVSystem_multi_scale_voltage_current_power(mocker): data = (1, 2) system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), - modules_per_string=2, strings=3), - pvsystem.Array(pvsystem.FixedMount(0, 180), - modules_per_string=3, strings=5)] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(0, 180), modules_per_string=2, strings=3 + ), + pvsystem.Array( + pvsystem.FixedMount(0, 180), modules_per_string=3, strings=5 + ), + ] ) m = mocker.patch( - 'pvlib.pvsystem.scale_voltage_current_power', autospec=True + "pvlib.pvsystem.scale_voltage_current_power", autospec=True ) system.scale_voltage_current_power(data) m.assert_has_calls( - [mock.call(1, voltage=2, current=3), - mock.call(2, voltage=3, current=5)], - any_order=True + [ + mock.call(1, voltage=2, current=3), + mock.call(2, voltage=3, current=5), + ], + any_order=True, ) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): system.scale_voltage_current_power(None) def test_PVSystem_get_ac_sandia(cec_inverter_parameters, mocker): - inv_fun = mocker.spy(inverter, 'sandia') + inv_fun = mocker.spy(inverter, "sandia") system = pvsystem.PVSystem( - inverter=cec_inverter_parameters['Name'], + inverter=cec_inverter_parameters["Name"], inverter_parameters=cec_inverter_parameters, ) vdcs = pd.Series(np.linspace(0, 50, 3)) idcs = pd.Series(np.linspace(0, 11, 3)) pdcs = idcs * vdcs - pacs = system.get_ac('sandia', pdcs, v_dc=vdcs) + pacs = system.get_ac("sandia", pdcs, v_dc=vdcs) inv_fun.assert_called_once() assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) def test_PVSystem_get_ac_sandia_multi(cec_inverter_parameters, mocker): - inv_fun = mocker.spy(inverter, 'sandia_multi') + inv_fun = mocker.spy(inverter, "sandia_multi") system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180))], - inverter=cec_inverter_parameters['Name'], + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + ], + inverter=cec_inverter_parameters["Name"], inverter_parameters=cec_inverter_parameters, ) vdcs = pd.Series(np.linspace(0, 50, 3)) idcs = pd.Series(np.linspace(0, 11, 3)) / 2 pdcs = idcs * vdcs - pacs = system.get_ac('sandia', (pdcs, pdcs), v_dc=(vdcs, vdcs)) + pacs = system.get_ac("sandia", (pdcs, pdcs), v_dc=(vdcs, vdcs)) inv_fun.assert_called_once() assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('sandia', vdcs, (pdcs, pdcs)) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('sandia', vdcs, (pdcs,)) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('sandia', (vdcs, vdcs), (pdcs, pdcs, pdcs)) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("sandia", vdcs, (pdcs, pdcs)) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("sandia", vdcs, (pdcs,)) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("sandia", (vdcs, vdcs), (pdcs, pdcs, pdcs)) def test_PVSystem_get_ac_pvwatts(pvwatts_system_defaults, mocker): - mocker.spy(inverter, 'pvwatts') + mocker.spy(inverter, "pvwatts") pdc = 50 - out = pvwatts_system_defaults.get_ac('pvwatts', pdc) + out = pvwatts_system_defaults.get_ac("pvwatts", pdc) inverter.pvwatts.assert_called_once_with( - pdc, **pvwatts_system_defaults.inverter_parameters) + pdc, **pvwatts_system_defaults.inverter_parameters + ) assert out < pdc def test_PVSystem_get_ac_pvwatts_kwargs(pvwatts_system_kwargs, mocker): - mocker.spy(inverter, 'pvwatts') + mocker.spy(inverter, "pvwatts") pdc = 50 - out = pvwatts_system_kwargs.get_ac('pvwatts', pdc) + out = pvwatts_system_kwargs.get_ac("pvwatts", pdc) inverter.pvwatts.assert_called_once_with( - pdc, **pvwatts_system_kwargs.inverter_parameters) + pdc, **pvwatts_system_kwargs.inverter_parameters + ) assert out < pdc def test_PVSystem_get_ac_pvwatts_multi( - pvwatts_system_defaults, pvwatts_system_kwargs, mocker): - mocker.spy(inverter, 'pvwatts_multi') - expected = [pd.Series([0.0, 48.123524, 86.400000]), - pd.Series([0.0, 45.893550, 85.500000])] + pvwatts_system_defaults, pvwatts_system_kwargs, mocker +): + mocker.spy(inverter, "pvwatts_multi") + expected = [ + pd.Series([0.0, 48.123524, 86.400000]), + pd.Series([0.0, 45.893550, 85.500000]), + ] systems = [pvwatts_system_defaults, pvwatts_system_kwargs] for base_sys, exp in zip(systems, expected): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180),)], + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array( + pvsystem.FixedMount(0, 180), + ), + ], inverter_parameters=base_sys.inverter_parameters, ) - pdcs = pd.Series([0., 25., 50.]) - pacs = system.get_ac('pvwatts', (pdcs, pdcs)) + pdcs = pd.Series([0.0, 25.0, 50.0]) + pacs = system.get_ac("pvwatts", (pdcs, pdcs)) assert_series_equal(pacs, exp) assert inverter.pvwatts_multi.call_count == 2 - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('pvwatts', (pdcs,)) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('pvwatts', pdcs) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): - system.get_ac('pvwatts', (pdcs, pdcs, pdcs)) - - -@pytest.mark.parametrize('model', ['sandia', 'adr', 'pvwatts']) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("pvwatts", (pdcs,)) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("pvwatts", pdcs) + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): + system.get_ac("pvwatts", (pdcs, pdcs, pdcs)) + + +@pytest.mark.parametrize("model", ["sandia", "adr", "pvwatts"]) def test_PVSystem_get_ac_single_array_tuple_input( - model, - pvwatts_system_defaults, - cec_inverter_parameters, - adr_inverter_parameters): + model, + pvwatts_system_defaults, + cec_inverter_parameters, + adr_inverter_parameters, +): vdcs = { - 'sandia': pd.Series(np.linspace(0, 50, 3)), - 'pvwatts': None, - 'adr': pd.Series([135, 154, 390, 420, 551]) + "sandia": pd.Series(np.linspace(0, 50, 3)), + "pvwatts": None, + "adr": pd.Series([135, 154, 390, 420, 551]), + } + pdcs = { + "adr": pd.Series([135, 1232, 1170, 420, 551]), + "sandia": pd.Series(np.linspace(0, 11, 3)) * vdcs["sandia"], + "pvwatts": 50, } - pdcs = {'adr': pd.Series([135, 1232, 1170, 420, 551]), - 'sandia': pd.Series(np.linspace(0, 11, 3)) * vdcs['sandia'], - 'pvwatts': 50} inverter_parameters = { - 'sandia': cec_inverter_parameters, - 'adr': adr_inverter_parameters, - 'pvwatts': pvwatts_system_defaults.inverter_parameters + "sandia": cec_inverter_parameters, + "adr": adr_inverter_parameters, + "pvwatts": pvwatts_system_defaults.inverter_parameters, } expected = { - 'adr': pd.Series([np.nan, 1161.5745, 1116.4459, 382.6679, np.nan]), - 'sandia': pd.Series([-0.020000, 132.004308, 250.000000]) + "adr": pd.Series([np.nan, 1161.5745, 1116.4459, 382.6679, np.nan]), + "sandia": pd.Series([-0.020000, 132.004308, 250.000000]), } system = pvsystem.PVSystem( arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180))], - inverter_parameters=inverter_parameters[model] + inverter_parameters=inverter_parameters[model], ) ac = system.get_ac(p_dc=(pdcs[model],), v_dc=(vdcs[model],), model=model) - if model == 'pvwatts': - assert ac < pdcs['pvwatts'] + if model == "pvwatts": + assert ac < pdcs["pvwatts"] else: assert_series_equal(ac, expected[model]) def test_PVSystem_get_ac_adr(adr_inverter_parameters, mocker): - mocker.spy(inverter, 'adr') + mocker.spy(inverter, "adr") system = pvsystem.PVSystem( inverter_parameters=adr_inverter_parameters, ) vdcs = pd.Series([135, 154, 390, 420, 551]) pdcs = pd.Series([135, 1232, 1170, 420, 551]) - pacs = system.get_ac('adr', pdcs, vdcs) - assert_series_equal(pacs, pd.Series([np.nan, 1161.5745, 1116.4459, - 382.6679, np.nan])) - inverter.adr.assert_called_once_with(vdcs, pdcs, - system.inverter_parameters) + pacs = system.get_ac("adr", pdcs, vdcs) + assert_series_equal( + pacs, pd.Series([np.nan, 1161.5745, 1116.4459, 382.6679, np.nan]) + ) + inverter.adr.assert_called_once_with( + vdcs, pdcs, system.inverter_parameters + ) def test_PVSystem_get_ac_adr_multi(adr_inverter_parameters): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180))], + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + ], inverter_parameters=adr_inverter_parameters, ) pdcs = pd.Series([135, 1232, 1170, 420, 551]) - with pytest.raises(ValueError, - match="The adr inverter function cannot be used"): - system.get_ac(model='adr', p_dc=pdcs) + with pytest.raises( + ValueError, match="The adr inverter function cannot be used" + ): + system.get_ac(model="adr", p_dc=pdcs) def test_PVSystem_get_ac_invalid(cec_inverter_parameters): @@ -1814,22 +2213,23 @@ def test_PVSystem_get_ac_invalid(cec_inverter_parameters): ) pdcs = pd.Series(np.linspace(0, 50, 3)) with pytest.raises(ValueError, match="is not a valid AC power model"): - system.get_ac(model='not_a_model', p_dc=pdcs) + system.get_ac(model="not_a_model", p_dc=pdcs) def test_PVSystem_creation(): - pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') + pv_system = pvsystem.PVSystem(module="blah", inverter="blarg") # ensure that parameter attributes are dict-like. GH 294 - pv_system.inverter_parameters['Paco'] = 1 + pv_system.inverter_parameters["Paco"] = 1 def test_PVSystem_multiple_array_creation(): array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32)) - array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=15), - module_parameters={'pdc0': 1}) + array_two = pvsystem.Array( + pvsystem.FixedMount(surface_tilt=15), module_parameters={"pdc0": 1} + ) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) assert pv_system.arrays[0].module_parameters == {} - assert pv_system.arrays[1].module_parameters == {'pdc0': 1} + assert pv_system.arrays[1].module_parameters == {"pdc0": 1} assert pv_system.arrays == (array_one, array_two) @@ -1841,10 +2241,14 @@ def test_PVSystem_get_aoi(): def test_PVSystem_multiple_array_get_aoi(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=15, - surface_azimuth=135)), - pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, - surface_azimuth=135))] + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(surface_tilt=15, surface_azimuth=135) + ), + pvsystem.Array( + pvsystem.FixedMount(surface_tilt=32, surface_azimuth=135) + ), + ] ) aoi_one, aoi_two = system.get_aoi(30, 225) assert np.round(aoi_two, 4) == 42.7408 @@ -1854,114 +2258,176 @@ def test_PVSystem_multiple_array_get_aoi(): @pytest.fixture def solar_pos(): - times = pd.date_range(start='20160101 1200-0700', - end='20160101 1800-0700', freq='6h') + times = pd.date_range( + start="20160101 1200-0700", end="20160101 1800-0700", freq="6h" + ) location = Location(latitude=32, longitude=-111) return location.get_solarposition(times) def test_PVSystem_get_irradiance(solar_pos): system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) - irrads = pd.DataFrame({'dni':[900,0], 'ghi':[600,0], 'dhi':[100,0]}, - index=solar_pos.index) - - irradiance = system.get_irradiance(solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], - irrads['ghi'], - irrads['dhi']) - expected = pd.DataFrame(data=np.array( - [[883.65494055, 745.86141676, 137.79352379, 126.397131, 11.39639279], - [0., -0., 0., 0., 0.]]), - columns=['poa_global', 'poa_direct', - 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'], - index=solar_pos.index) + irrads = pd.DataFrame( + {"dni": [900, 0], "ghi": [600, 0], "dhi": [100, 0]}, + index=solar_pos.index, + ) + + irradiance = system.get_irradiance( + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], + ) + expected = pd.DataFrame( + data=np.array( + [ + [ + 883.65494055, + 745.86141676, + 137.79352379, + 126.397131, + 11.39639279, + ], + [0.0, -0.0, 0.0, 0.0, 0.0], + ] + ), + columns=[ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ], + index=solar_pos.index, + ) assert_frame_equal(irradiance, expected, check_less_precise=2) def test_PVSystem_get_irradiance_float(): system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) - irrads = {'dni': 900., 'ghi': 600., 'dhi': 100.} + irrads = {"dni": 900.0, "ghi": 600.0, "dhi": 100.0} zenith = 55.366831 azimuth = 172.320038 - irradiance = system.get_irradiance(zenith, - azimuth, - irrads['dni'], - irrads['ghi'], - irrads['dhi']) - expected = {'poa_global': 884.80903423, 'poa_direct': 745.84258835, - 'poa_diffuse': 138.96644588, 'poa_sky_diffuse': 127.57005309, - 'poa_ground_diffuse': 11.39639279} + irradiance = system.get_irradiance( + zenith, azimuth, irrads["dni"], irrads["ghi"], irrads["dhi"] + ) + expected = { + "poa_global": 884.80903423, + "poa_direct": 745.84258835, + "poa_diffuse": 138.96644588, + "poa_sky_diffuse": 127.57005309, + "poa_ground_diffuse": 11.39639279, + } for k, v in irradiance.items(): assert np.isclose(v, expected[k], rtol=1e-6) def test_PVSystem_get_irradiance_albedo(solar_pos): system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) - irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0], - 'albedo': [0.5, 0.5]}, - index=solar_pos.index) + irrads = pd.DataFrame( + { + "dni": [900, 0], + "ghi": [600, 0], + "dhi": [100, 0], + "albedo": [0.5, 0.5], + }, + index=solar_pos.index, + ) # albedo as a Series - irradiance = system.get_irradiance(solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], - irrads['ghi'], - irrads['dhi'], - albedo=irrads['albedo']) - expected = pd.DataFrame(data=np.array( - [[895.05134334, 745.86141676, 149.18992658, 126.397131, 22.79279558], - [0., -0., 0., 0., 0.]]), - columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'], - index=solar_pos.index) + irradiance = system.get_irradiance( + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], + albedo=irrads["albedo"], + ) + expected = pd.DataFrame( + data=np.array( + [ + [ + 895.05134334, + 745.86141676, + 149.18992658, + 126.397131, + 22.79279558, + ], + [0.0, -0.0, 0.0, 0.0, 0.0], + ] + ), + columns=[ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ], + index=solar_pos.index, + ) assert_frame_equal(irradiance, expected, check_less_precise=2) def test_PVSystem_get_irradiance_model(mocker, solar_pos): - spy_perez = mocker.spy(irradiance, 'perez') - spy_haydavies = mocker.spy(irradiance, 'haydavies') + spy_perez = mocker.spy(irradiance, "perez") + spy_haydavies = mocker.spy(irradiance, "haydavies") system = pvsystem.PVSystem(surface_tilt=32, surface_azimuth=135) - irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=solar_pos.index) - system.get_irradiance(solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], - irrads['ghi'], - irrads['dhi']) + irrads = pd.DataFrame( + {"dni": [900, 0], "ghi": [600, 0], "dhi": [100, 0]}, + index=solar_pos.index, + ) + system.get_irradiance( + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], + ) spy_haydavies.assert_called_once() - system.get_irradiance(solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], - irrads['ghi'], - irrads['dhi'], - model='perez') + system.get_irradiance( + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], + model="perez", + ) spy_perez.assert_called_once() def test_PVSystem_multi_array_get_irradiance(solar_pos): - array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, - surface_azimuth=135)) - array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=5, - surface_azimuth=150)) + array_one = pvsystem.Array( + pvsystem.FixedMount(surface_tilt=32, surface_azimuth=135) + ) + array_two = pvsystem.Array( + pvsystem.FixedMount(surface_tilt=5, surface_azimuth=150) + ) system = pvsystem.PVSystem(arrays=[array_one, array_two]) - irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=solar_pos.index) + irrads = pd.DataFrame( + {"dni": [900, 0], "ghi": [600, 0], "dhi": [100, 0]}, + index=solar_pos.index, + ) array_one_expected = array_one.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], irrads['ghi'], irrads['dhi'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], ) array_two_expected = array_two.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], irrads['ghi'], irrads['dhi'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], ) array_one_irrad, array_two_irrad = system.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], irrads['ghi'], irrads['dhi'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], ) assert_frame_equal( array_one_irrad, array_one_expected, check_less_precise=2 @@ -1983,102 +2449,138 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(solar_pos): array_two = pvsystem.Array(pvsystem.FixedMount(0, 180)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) - irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=solar_pos.index) + irrads = pd.DataFrame( + {"dni": [900, 0], "ghi": [600, 0], "dhi": [100, 0]}, + index=solar_pos.index, + ) irrads_two = pd.DataFrame( - {'dni': [0, 900], 'ghi': [0, 600], 'dhi': [0, 100]}, - index=solar_pos.index + {"dni": [0, 900], "ghi": [0, 600], "dhi": [0, 100]}, + index=solar_pos.index, ) array_irrad = system.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - (irrads['dhi'], irrads['dhi']), - (irrads['ghi'], irrads['ghi']), - (irrads['dni'], irrads['dni']) + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + (irrads["dhi"], irrads["dhi"]), + (irrads["ghi"], irrads["ghi"]), + (irrads["dni"], irrads["dni"]), ) assert_frame_equal(array_irrad[0], array_irrad[1]) array_irrad = system.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - (irrads['dhi'], irrads_two['dhi']), - (irrads['ghi'], irrads_two['ghi']), - (irrads['dni'], irrads_two['dni']) + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + (irrads["dhi"], irrads_two["dhi"]), + (irrads["ghi"], irrads_two["ghi"]), + (irrads["dni"], irrads_two["dni"]), ) array_one_expected = array_one.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dhi'], irrads['ghi'], irrads['dni'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dhi"], + irrads["ghi"], + irrads["dni"], ) array_two_expected = array_two.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads_two['dhi'], irrads_two['ghi'], irrads_two['dni'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads_two["dhi"], + irrads_two["ghi"], + irrads_two["dni"], ) assert not array_irrad[0].equals(array_irrad[1]) assert_frame_equal(array_irrad[0], array_one_expected) assert_frame_equal(array_irrad[1], array_two_expected) - with pytest.raises(ValueError, - match="Length mismatch for per-array parameter"): + with pytest.raises( + ValueError, match="Length mismatch for per-array parameter" + ): system.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - (irrads['dhi'], irrads_two['dhi'], irrads['dhi']), - (irrads['ghi'], irrads_two['ghi']), - irrads['dni'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + (irrads["dhi"], irrads_two["dhi"], irrads["dhi"]), + (irrads["ghi"], irrads_two["ghi"]), + irrads["dni"], ) array_irrad = system.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - (irrads['dhi'], irrads_two['dhi']), - irrads['ghi'], - irrads['dni'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + (irrads["dhi"], irrads_two["dhi"]), + irrads["ghi"], + irrads["dni"], ) assert_frame_equal(array_irrad[0], array_one_expected) assert not array_irrad[0].equals(array_irrad[1]) def test_Array_get_irradiance(solar_pos): - array = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, - surface_azimuth=135)) - irrads = pd.DataFrame({'dni': [900, 0], 'ghi': [600, 0], 'dhi': [100, 0]}, - index=solar_pos.index) + array = pvsystem.Array( + pvsystem.FixedMount(surface_tilt=32, surface_azimuth=135) + ) + irrads = pd.DataFrame( + {"dni": [900, 0], "ghi": [600, 0], "dhi": [100, 0]}, + index=solar_pos.index, + ) # defaults for kwargs modeled = array.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], irrads['ghi'], irrads['dhi'] + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], ) expected = pd.DataFrame( data=np.array( - [[883.65494055, 745.86141676, 137.79352379, 126.397131, - 11.39639279], - [0., -0., 0., 0., 0.]]), - columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', - 'poa_ground_diffuse'], - index=solar_pos.index + [ + [ + 883.65494055, + 745.86141676, + 137.79352379, + 126.397131, + 11.39639279, + ], + [0.0, -0.0, 0.0, 0.0, 0.0], + ] + ), + columns=[ + "poa_global", + "poa_direct", + "poa_diffuse", + "poa_sky_diffuse", + "poa_ground_diffuse", + ], + index=solar_pos.index, ) assert_frame_equal(modeled, expected, check_less_precise=5) # with specified kwargs, use isotropic sky diffuse because it's easier modeled = array.get_irradiance( - solar_pos['apparent_zenith'], - solar_pos['azimuth'], - irrads['dni'], irrads['ghi'], irrads['dhi'], - albedo=0.5, model='isotropic' + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + irrads["dni"], + irrads["ghi"], + irrads["dhi"], + albedo=0.5, + model="isotropic", ) - sky_diffuse = irradiance.isotropic(array.mount.surface_tilt, irrads['dhi']) + sky_diffuse = irradiance.isotropic(array.mount.surface_tilt, irrads["dhi"]) ground_diff = irradiance.get_ground_diffuse( - array.mount.surface_tilt, irrads['ghi'], 0.5, surface_type=None) - aoi = irradiance.aoi(array.mount.surface_tilt, array.mount.surface_azimuth, - solar_pos['apparent_zenith'], solar_pos['azimuth']) - direct = irrads['dni'] * cosd(aoi) + array.mount.surface_tilt, irrads["ghi"], 0.5, surface_type=None + ) + aoi = irradiance.aoi( + array.mount.surface_tilt, + array.mount.surface_azimuth, + solar_pos["apparent_zenith"], + solar_pos["azimuth"], + ) + direct = irrads["dni"] * cosd(aoi) expected = sky_diffuse + ground_diff + direct assert_series_equal(expected, expected, check_less_precise=5) def test_PVSystem___repr__(): system = pvsystem.PVSystem( - module='blah', inverter='blarg', name='pv ftw', - temperature_model_parameters={'a': -3.56}) + module="blah", + inverter="blarg", + name="pv ftw", + temperature_model_parameters={"a": -3.56}, + ) expected = """PVSystem: name: pv ftw @@ -2097,12 +2599,16 @@ def test_PVSystem___repr__(): def test_PVSystem_multi_array___repr__(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, - surface_azimuth=100)), - pvsystem.Array(pvsystem.FixedMount(surface_tilt=20, - surface_azimuth=220), - name='foo')], - inverter='blarg', + arrays=[ + pvsystem.Array( + pvsystem.FixedMount(surface_tilt=30, surface_azimuth=100) + ), + pvsystem.Array( + pvsystem.FixedMount(surface_tilt=20, surface_azimuth=220), + name="foo", + ), + ], + inverter="blarg", ) expected = """PVSystem: name: None @@ -2130,14 +2636,17 @@ def test_PVSystem_multi_array___repr__(): def test_Array___repr__(): array = pvsystem.Array( - mount=pvsystem.FixedMount(surface_tilt=10, surface_azimuth=100, - racking_model='close_mount'), - albedo=0.15, module_type='glass_glass', - temperature_model_parameters={'a': -3.56}, - module_parameters={'foo': 'bar'}, + mount=pvsystem.FixedMount( + surface_tilt=10, surface_azimuth=100, racking_model="close_mount" + ), + albedo=0.15, + module_type="glass_glass", + temperature_model_parameters={"a": -3.56}, + module_parameters={"foo": "bar"}, modules_per_string=100, - strings=10, module='baz', - name='biz' + strings=10, + module="baz", + name="biz", ) expected = """Array: name: biz @@ -2161,9 +2670,9 @@ def test_pvwatts_dc_arrays(): irrad_trans = np.array([np.nan, 900, 900]) temp_cell = np.array([30, np.nan, 30]) irrad_trans, temp_cell = np.meshgrid(irrad_trans, temp_cell) - expected = np.array([[nan, 88.65, 88.65], - [nan, nan, nan], - [nan, 88.65, 88.65]]) + expected = np.array( + [[nan, 88.65, 88.65], [nan, nan, nan], [nan, 88.65, 88.65]] + ) out = pvsystem.pvwatts_dc(irrad_trans, temp_cell, 100, -0.003) assert_allclose(out, expected, equal_nan=True) @@ -2171,7 +2680,7 @@ def test_pvwatts_dc_arrays(): def test_pvwatts_dc_series(): irrad_trans = pd.Series([np.nan, 900, 900]) temp_cell = pd.Series([30, np.nan, 30]) - expected = pd.Series(np.array([ nan, nan, 88.65])) + expected = pd.Series(np.array([nan, nan, 88.65])) out = pvsystem.pvwatts_dc(irrad_trans, temp_cell, 100, -0.003) assert_series_equal(expected, out) @@ -2198,82 +2707,96 @@ def test_pvwatts_losses_series(): @pytest.fixture def pvwatts_system_defaults(): - module_parameters = {'pdc0': 100, 'gamma_pdc': -0.003} - inverter_parameters = {'pdc0': 90} - system = pvsystem.PVSystem(module_parameters=module_parameters, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 100, "gamma_pdc": -0.003} + inverter_parameters = {"pdc0": 90} + system = pvsystem.PVSystem( + module_parameters=module_parameters, + inverter_parameters=inverter_parameters, + ) return system @pytest.fixture def pvwatts_system_kwargs(): - module_parameters = {'pdc0': 100, 'gamma_pdc': -0.003, 'temp_ref': 20} - inverter_parameters = {'pdc0': 90, 'eta_inv_nom': 0.95, 'eta_inv_ref': 1.0} - system = pvsystem.PVSystem(module_parameters=module_parameters, - inverter_parameters=inverter_parameters) + module_parameters = {"pdc0": 100, "gamma_pdc": -0.003, "temp_ref": 20} + inverter_parameters = {"pdc0": 90, "eta_inv_nom": 0.95, "eta_inv_ref": 1.0} + system = pvsystem.PVSystem( + module_parameters=module_parameters, + inverter_parameters=inverter_parameters, + ) return system def test_PVSystem_pvwatts_dc(pvwatts_system_defaults, mocker): - mocker.spy(pvsystem, 'pvwatts_dc') + mocker.spy(pvsystem, "pvwatts_dc") irrad = 900 temp_cell = 30 expected = 90 out = pvwatts_system_defaults.pvwatts_dc(irrad, temp_cell) pvsystem.pvwatts_dc.assert_called_once_with( - irrad, temp_cell, - **pvwatts_system_defaults.arrays[0].module_parameters) + irrad, temp_cell, **pvwatts_system_defaults.arrays[0].module_parameters + ) assert_allclose(expected, out, atol=10) def test_PVSystem_pvwatts_dc_kwargs(pvwatts_system_kwargs, mocker): - mocker.spy(pvsystem, 'pvwatts_dc') + mocker.spy(pvsystem, "pvwatts_dc") irrad = 900 temp_cell = 30 expected = 90 out = pvwatts_system_kwargs.pvwatts_dc(irrad, temp_cell) pvsystem.pvwatts_dc.assert_called_once_with( - irrad, temp_cell, **pvwatts_system_kwargs.arrays[0].module_parameters) + irrad, temp_cell, **pvwatts_system_kwargs.arrays[0].module_parameters + ) assert_allclose(expected, out, atol=10) def test_PVSystem_multiple_array_pvwatts_dc(): array_one_module_parameters = { - 'pdc0': 100, 'gamma_pdc': -0.003, 'temp_ref': 20 + "pdc0": 100, + "gamma_pdc": -0.003, + "temp_ref": 20, } array_one = pvsystem.Array( pvsystem.FixedMount(0, 180), - module_parameters=array_one_module_parameters + module_parameters=array_one_module_parameters, ) array_two_module_parameters = { - 'pdc0': 150, 'gamma_pdc': -0.002, 'temp_ref': 25 + "pdc0": 150, + "gamma_pdc": -0.002, + "temp_ref": 25, } array_two = pvsystem.Array( pvsystem.FixedMount(0, 180), - module_parameters=array_two_module_parameters + module_parameters=array_two_module_parameters, ) system = pvsystem.PVSystem(arrays=[array_one, array_two]) irrad_one = 900 irrad_two = 500 temp_cell_one = 30 temp_cell_two = 20 - expected_one = pvsystem.pvwatts_dc(irrad_one, temp_cell_one, - **array_one_module_parameters) - expected_two = pvsystem.pvwatts_dc(irrad_two, temp_cell_two, - **array_two_module_parameters) - dc_one, dc_two = system.pvwatts_dc((irrad_one, irrad_two), - (temp_cell_one, temp_cell_two)) + expected_one = pvsystem.pvwatts_dc( + irrad_one, temp_cell_one, **array_one_module_parameters + ) + expected_two = pvsystem.pvwatts_dc( + irrad_two, temp_cell_two, **array_two_module_parameters + ) + dc_one, dc_two = system.pvwatts_dc( + (irrad_one, irrad_two), (temp_cell_one, temp_cell_two) + ) assert dc_one == expected_one assert dc_two == expected_two def test_PVSystem_multiple_array_pvwatts_dc_value_error(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180))] + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + ] ) - error_message = 'Length mismatch for per-array parameter' + error_message = "Length mismatch for per-array parameter" with pytest.raises(ValueError, match=error_message): system.pvwatts_dc(10, (1, 1, 1)) with pytest.raises(ValueError, match=error_message): @@ -2296,7 +2819,7 @@ def test_PVSystem_multiple_array_pvwatts_dc_value_error(): def test_PVSystem_pvwatts_losses(pvwatts_system_defaults, mocker): - mocker.spy(pvsystem, 'pvwatts_losses') + mocker.spy(pvsystem, "pvwatts_losses") age = 1 pvwatts_system_defaults.losses_parameters = dict(age=age) expected = 15 @@ -2307,16 +2830,20 @@ def test_PVSystem_pvwatts_losses(pvwatts_system_defaults, mocker): def test_PVSystem_num_arrays(): system_one = pvsystem.PVSystem() - system_two = pvsystem.PVSystem(arrays=[ - pvsystem.Array(pvsystem.FixedMount(0, 180)), - pvsystem.Array(pvsystem.FixedMount(0, 180))]) + system_two = pvsystem.PVSystem( + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + ] + ) assert system_one.num_arrays == 1 assert system_two.num_arrays == 2 def test_PVSystem_at_least_one_array(): - with pytest.raises(ValueError, - match="PVSystem must have at least one Array"): + with pytest.raises( + ValueError, match="PVSystem must have at least one Array" + ): pvsystem.PVSystem(arrays=[]) @@ -2329,20 +2856,25 @@ def test_PVSystem_single_array(): def test_combine_loss_factors(): - test_index = pd.date_range(start='1990/01/01T12:00', periods=365, freq='d') - loss_1 = pd.Series(.10, index=test_index) - loss_2 = pd.Series(.05, index=pd.date_range(start='1990/01/01T12:00', - periods=365*2, freq='d')) - loss_3 = pd.Series(.02, index=pd.date_range(start='1990/01/01', - periods=12, freq='MS')) - expected = pd.Series(.1621, index=test_index) + test_index = pd.date_range(start="1990/01/01T12:00", periods=365, freq="d") + loss_1 = pd.Series(0.10, index=test_index) + loss_2 = pd.Series( + 0.05, + index=pd.date_range( + start="1990/01/01T12:00", periods=365 * 2, freq="d" + ), + ) + loss_3 = pd.Series( + 0.02, index=pd.date_range(start="1990/01/01", periods=12, freq="MS") + ) + expected = pd.Series(0.1621, index=test_index) out = pvsystem.combine_loss_factors(test_index, loss_1, loss_2, loss_3) assert_series_equal(expected, out) def test_no_extra_kwargs(): with pytest.raises(TypeError, match="arbitrary_kwarg"): - pvsystem.PVSystem(arbitrary_kwarg='value') + pvsystem.PVSystem(arbitrary_kwarg="value") def test_AbstractMount_constructor(): @@ -2358,9 +2890,14 @@ def fixed_mount(): @pytest.fixture def single_axis_tracker_mount(): - return pvsystem.SingleAxisTrackerMount(axis_tilt=10, axis_azimuth=170, - max_angle=45, backtrack=False, - gcr=0.4, cross_axis_tilt=-5) + return pvsystem.SingleAxisTrackerMount( + axis_tilt=10, + axis_azimuth=170, + max_angle=45, + backtrack=False, + gcr=0.4, + cross_axis_tilt=-5, + ) def test_FixedMount_constructor(fixed_mount): @@ -2369,19 +2906,25 @@ def test_FixedMount_constructor(fixed_mount): def test_FixedMount_get_orientation(fixed_mount): - expected = {'surface_tilt': 20, 'surface_azimuth': 180} + expected = {"surface_tilt": 20, "surface_azimuth": 180} assert fixed_mount.get_orientation(45, 130) == expected def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount): - expected = dict(axis_tilt=10, axis_azimuth=170, max_angle=45, - backtrack=False, gcr=0.4, cross_axis_tilt=-5) + expected = dict( + axis_tilt=10, + axis_azimuth=170, + max_angle=45, + backtrack=False, + gcr=0.4, + cross_axis_tilt=-5, + ) for attr_name, expected_value in expected.items(): assert getattr(single_axis_tracker_mount, attr_name) == expected_value def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): - expected = {'surface_tilt': 19.29835284, 'surface_azimuth': 229.7643755} + expected = {"surface_tilt": 19.29835284, "surface_azimuth": 229.7643755} actual = single_axis_tracker_mount.get_orientation(45, 190) for key, expected_value in expected.items(): err_msg = f"{key} value incorrect" @@ -2390,7 +2933,7 @@ def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): def test_SingleAxisTrackerMount_get_orientation_asymmetric_max(): mount = pvsystem.SingleAxisTrackerMount(max_angle=(-30, 45)) - expected = {'surface_tilt': [45, 30], 'surface_azimuth': [90, 270]} + expected = {"surface_tilt": [45, 30], "surface_azimuth": [90, 270]} actual = mount.get_orientation([60, 60], [90, 270]) for key, expected_value in expected.items(): err_msg = f"{key} value incorrect" @@ -2398,18 +2941,19 @@ def test_SingleAxisTrackerMount_get_orientation_asymmetric_max(): def test_dc_ohms_from_percent(): - expected = .1425 + expected = 0.1425 out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1) assert_allclose(out, expected) def test_PVSystem_dc_ohms_from_percent(mocker): - mocker.spy(pvsystem, 'dc_ohms_from_percent') + mocker.spy(pvsystem, "dc_ohms_from_percent") - expected = .1425 - system = pvsystem.PVSystem(losses_parameters={'dc_ohmic_percent': 3}, - module_parameters={'I_mp_ref': 8, - 'V_mp_ref': 38}) + expected = 0.1425 + system = pvsystem.PVSystem( + losses_parameters={"dc_ohmic_percent": 3}, + module_parameters={"I_mp_ref": 8, "V_mp_ref": 38}, + ) out = system.dc_ohms_from_percent() pvsystem.dc_ohms_from_percent.assert_called_once_with( @@ -2417,7 +2961,7 @@ def test_PVSystem_dc_ohms_from_percent(mocker): vmp_ref=38, imp_ref=8, modules_per_string=1, - strings=1 + strings=1, ) assert_allclose(out, expected) @@ -2425,47 +2969,50 @@ def test_PVSystem_dc_ohms_from_percent(mocker): def test_dc_ohmic_losses(): expected = 9.12 - out = pvsystem.dc_ohmic_losses(.1425, 8) + out = pvsystem.dc_ohmic_losses(0.1425, 8) assert_allclose(out, expected) def test_Array_dc_ohms_from_percent(mocker): - mocker.spy(pvsystem, 'dc_ohms_from_percent') + mocker.spy(pvsystem, "dc_ohms_from_percent") - expected = .1425 + expected = 0.1425 - array = pvsystem.Array(pvsystem.FixedMount(0, 180), - array_losses_parameters={'dc_ohmic_percent': 3}, - module_parameters={'I_mp_ref': 8, - 'V_mp_ref': 38}) + array = pvsystem.Array( + pvsystem.FixedMount(0, 180), + array_losses_parameters={"dc_ohmic_percent": 3}, + module_parameters={"I_mp_ref": 8, "V_mp_ref": 38}, + ) out = array.dc_ohms_from_percent() pvsystem.dc_ohms_from_percent.assert_called_with( dc_ohmic_percent=3, vmp_ref=38, imp_ref=8, modules_per_string=1, - strings=1 + strings=1, ) assert_allclose(out, expected) - array = pvsystem.Array(pvsystem.FixedMount(0, 180), - array_losses_parameters={'dc_ohmic_percent': 3}, - module_parameters={'Impo': 8, - 'Vmpo': 38}) + array = pvsystem.Array( + pvsystem.FixedMount(0, 180), + array_losses_parameters={"dc_ohmic_percent": 3}, + module_parameters={"Impo": 8, "Vmpo": 38}, + ) out = array.dc_ohms_from_percent() pvsystem.dc_ohms_from_percent.assert_called_with( dc_ohmic_percent=3, vmp_ref=38, imp_ref=8, modules_per_string=1, - strings=1 + strings=1, ) assert_allclose(out, expected) - array = pvsystem.Array(pvsystem.FixedMount(0, 180), - array_losses_parameters={'dc_ohmic_percent': 3}, - module_parameters={'Impp': 8, - 'Vmpp': 38}) + array = pvsystem.Array( + pvsystem.FixedMount(0, 180), + array_losses_parameters={"dc_ohmic_percent": 3}, + module_parameters={"Impp": 8, "Vmpp": 38}, + ) out = array.dc_ohms_from_percent() pvsystem.dc_ohms_from_percent.assert_called_with( @@ -2473,31 +3020,40 @@ def test_Array_dc_ohms_from_percent(mocker): vmp_ref=38, imp_ref=8, modules_per_string=1, - strings=1 + strings=1, ) assert_allclose(out, expected) - with pytest.raises(ValueError, - match=('Parameters for Vmp and Imp could not be found ' - 'in the array module parameters. Module ' - 'parameters must include one set of ' - '{"V_mp_ref", "I_mp_Ref"}, ' - '{"Vmpo", "Impo"}, or ' - '{"Vmpp", "Impp"}.')): - array = pvsystem.Array(pvsystem.FixedMount(0, 180), - array_losses_parameters={'dc_ohmic_percent': 3}) + with pytest.raises( + ValueError, + match=( + "Parameters for Vmp and Imp could not be found " + "in the array module parameters. Module " + "parameters must include one set of " + '{"V_mp_ref", "I_mp_Ref"}, ' + '{"Vmpo", "Impo"}, or ' + '{"Vmpp", "Impp"}.' + ), + ): + array = pvsystem.Array( + pvsystem.FixedMount(0, 180), + array_losses_parameters={"dc_ohmic_percent": 3}, + ) out = array.dc_ohms_from_percent() -@pytest.mark.parametrize('model,keys', [ - ('sapm', ('a', 'b', 'deltaT')), - ('fuentes', ('noct_installed',)), - ('noct_sam', ('noct', 'module_efficiency')) -]) +@pytest.mark.parametrize( + "model,keys", + [ + ("sapm", ("a", "b", "deltaT")), + ("fuentes", ("noct_installed",)), + ("noct_sam", ("noct", "module_efficiency")), + ], +) def test_Array_temperature_missing_parameters(model, keys): # test that a nice error is raised when required temp params are missing array = pvsystem.Array(pvsystem.FixedMount(0, 180)) - index = pd.date_range('2019-01-01', freq='h', periods=5) + index = pd.date_range("2019-01-01", freq="h", periods=5) temps = pd.Series(25, index) irrads = pd.Series(1000, index) winds = pd.Series(1, index) diff --git a/pvlib/tests/test_scaling.py b/pvlib/tests/test_scaling.py index 344e2209b5..96253fd76b 100644 --- a/pvlib/tests/test_scaling.py +++ b/pvlib/tests/test_scaling.py @@ -40,13 +40,13 @@ def time(clear_sky_index): @pytest.fixture def time_60s(clear_sky_index): # Sample time vector 60s resolution - return np.arange(0, len(clear_sky_index))*60 + return np.arange(0, len(clear_sky_index)) * 60 @pytest.fixture def time_500ms(clear_sky_index): # Sample time vector 0.5s resolution - return np.arange(0, len(clear_sky_index))*0.5 + return np.arange(0, len(clear_sky_index)) * 0.5 @pytest.fixture @@ -80,8 +80,8 @@ def expect_wavelet(): # Expected wavelet for indices 5000:5004 for clear_sky_index above (Matlab) e = np.zeros([13, 5]) e[0, :] = np.array([0, -0.05, 0.1, -0.05, 0]) - e[1, :] = np.array([-0.025, 0.05, 0., -0.05, 0.025]) - e[2, :] = np.array([0.025, 0., 0., 0., -0.025]) + e[1, :] = np.array([-0.025, 0.05, 0.0, -0.05, 0.025]) + e[2, :] = np.array([0.025, 0.0, 0.0, 0.0, -0.025]) e[-1, :] = np.array([1, 1, 1, 1, 1]) return e @@ -89,14 +89,29 @@ def expect_wavelet(): @pytest.fixture def expect_cs_smooth(): # Expected smoothed clear sky index for indices 5000:5004 (Matlab) - return np.array([1., 1., 1.05774, 0.94226, 1.]) + return np.array([1.0, 1.0, 1.05774, 0.94226, 1.0]) @pytest.fixture def expect_vr(): # Expected VR for expecttmscale - return np.array([3., 3., 3., 3., 3., 3., 2.9997844, 2.9708118, 2.6806291, - 2.0726611, 1.5653324, 1.2812714, 1.1389995]) + return np.array( + [ + 3.0, + 3.0, + 3.0, + 3.0, + 3.0, + 3.0, + 2.9997844, + 2.9708118, + 2.6806291, + 2.0726611, + 1.5653324, + 1.2812714, + 1.1389995, + ] + ) def test_latlon_to_xy_zero(): @@ -123,44 +138,50 @@ def test_latlon_to_xy_list(coordinates, positions): assert_almost_equal(pos, positions, decimal=1) -def test_compute_wavelet_series(clear_sky_index, time, - expect_tmscale, expect_wavelet): +def test_compute_wavelet_series( + clear_sky_index, time, expect_tmscale, expect_wavelet +): csi_series = pd.Series(clear_sky_index, index=time) wavelet, tmscale = scaling._compute_wavelet(csi_series) assert_almost_equal(tmscale, expect_tmscale) assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet) -def test_compute_wavelet_series_numindex(clear_sky_index, time, - expect_tmscale, expect_wavelet): - dtindex = pd.to_datetime(time, unit='s') +def test_compute_wavelet_series_numindex( + clear_sky_index, time, expect_tmscale, expect_wavelet +): + dtindex = pd.to_datetime(time, unit="s") csi_series = pd.Series(clear_sky_index, index=dtindex) wavelet, tmscale = scaling._compute_wavelet(csi_series) assert_almost_equal(tmscale, expect_tmscale) assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet) -def test_compute_wavelet_series_highres(clear_sky_index, time_500ms, - expect_tmscale_500ms, expect_wavelet): - dtindex = pd.to_datetime(time_500ms, unit='s') +def test_compute_wavelet_series_highres( + clear_sky_index, time_500ms, expect_tmscale_500ms, expect_wavelet +): + dtindex = pd.to_datetime(time_500ms, unit="s") csi_series = pd.Series(clear_sky_index, index=dtindex) wavelet, tmscale = scaling._compute_wavelet(csi_series) assert_almost_equal(tmscale, expect_tmscale_500ms) assert_almost_equal(wavelet[:, 5000:5005].shape, (14, 5)) -def test_compute_wavelet_series_minuteres(clear_sky_index, time_60s, - expect_tmscale_1min, expect_wavelet): - dtindex = pd.to_datetime(time_60s, unit='s') +def test_compute_wavelet_series_minuteres( + clear_sky_index, time_60s, expect_tmscale_1min, expect_wavelet +): + dtindex = pd.to_datetime(time_60s, unit="s") csi_series = pd.Series(clear_sky_index, index=dtindex) wavelet, tmscale = scaling._compute_wavelet(csi_series) assert_almost_equal(tmscale, expect_tmscale_1min) - assert_almost_equal(wavelet[:, 5000:5005].shape, - expect_wavelet[0:len(tmscale), :].shape) + assert_almost_equal( + wavelet[:, 5000:5005].shape, expect_wavelet[0 : len(tmscale), :].shape + ) -def test_compute_wavelet_array(clear_sky_index, - expect_tmscale, expect_wavelet): +def test_compute_wavelet_array( + clear_sky_index, expect_tmscale, expect_wavelet +): wavelet, tmscale = scaling._compute_wavelet(clear_sky_index, dt) assert_almost_equal(tmscale, expect_tmscale) assert_almost_equal(wavelet[:, 5000:5005], expect_wavelet) @@ -171,8 +192,9 @@ def test_compute_wavelet_array_invalid(clear_sky_index): scaling._compute_wavelet(clear_sky_index) -def test_compute_wavelet_dwttheory(clear_sky_index, time, - expect_tmscale, expect_wavelet): +def test_compute_wavelet_dwttheory( + clear_sky_index, time, expect_tmscale, expect_wavelet +): # Confirm detail coeffs sum to original signal csi_series = pd.Series(clear_sky_index, index=time) wavelet, tmscale = scaling._compute_wavelet(csi_series) @@ -195,8 +217,9 @@ def test_wvm_array(clear_sky_index, positions, expect_cs_smooth): assert_almost_equal(cs_sm[5000:5005], expect_cs_smooth, decimal=4) -def test_wvm_series_xyaslist(clear_sky_index, time, positions, - expect_cs_smooth): +def test_wvm_series_xyaslist( + clear_sky_index, time, positions, expect_cs_smooth +): csi_series = pd.Series(clear_sky_index, index=time) cs_sm, _, _ = scaling.wvm(csi_series, positions.tolist(), cloud_speed) assert_almost_equal(cs_sm[5000:5005], expect_cs_smooth, decimal=4) diff --git a/pvlib/tests/test_shading.py b/pvlib/tests/test_shading.py index f83f2db47b..54bb451c80 100644 --- a/pvlib/tests/test_shading.py +++ b/pvlib/tests/test_shading.py @@ -144,29 +144,34 @@ def projected_solar_zenith_angle_edge_cases(): premises_and_result_matrix = pd.DataFrame( data=[ # s_zen | s_azm | ax_tilt | ax_azm | psza - [ 0, 0, 0, 0, 0], - [ 0, 180, 0, 0, 0], - [ 0, 0, 0, 180, 0], - [ 0, 180, 0, 180, 0], - [ 45, 0, 0, 180, 0], - [ 45, 90, 0, 180, -45], - [ 45, 270, 0, 180, 45], - [ 45, 90, 90, 180, -90], - [ 45, 270, 90, 180, 90], - [ 45, 90, 90, 0, 90], - [ 45, 270, 90, 0, -90], - [ 45, 45, 90, 180, -135], - [ 45, 315, 90, 180, 135], + [0, 0, 0, 0, 0], + [0, 180, 0, 0, 0], + [0, 0, 0, 180, 0], + [0, 180, 0, 180, 0], + [45, 0, 0, 180, 0], + [45, 90, 0, 180, -45], + [45, 270, 0, 180, 45], + [45, 90, 90, 180, -90], + [45, 270, 90, 180, 90], + [45, 90, 90, 0, 90], + [45, 270, 90, 0, -90], + [45, 45, 90, 180, -135], + [45, 315, 90, 180, 135], + ], + columns=[ + "solar_zenith", + "solar_azimuth", + "axis_tilt", + "axis_azimuth", + "psza", ], - columns=["solar_zenith", "solar_azimuth", "axis_tilt", "axis_azimuth", - "psza"], ) return premises_and_result_matrix def test_projected_solar_zenith_angle_numeric( true_tracking_angle_and_inputs_NREL, - projected_solar_zenith_angle_edge_cases + projected_solar_zenith_angle_edge_cases, ): psza_func = shading.projected_solar_zenith_angle axis_tilt, axis_azimuth, timedata = true_tracking_angle_and_inputs_NREL diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 0271e53e90..1a48930ef2 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -6,8 +6,14 @@ import pandas as pd import scipy from pvlib import pvsystem -from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN, - bishop88, bishop88_i_from_v, bishop88_v_from_i) +from pvlib.singlediode import ( + bishop88_mpp, + estimate_voc, + VOLTAGE_BUILTIN, + bishop88, + bishop88_i_from_v, + bishop88_v_from_i, +) import pytest from numpy.testing import assert_array_equal from .conftest import DATA_DIR @@ -16,48 +22,60 @@ TCELL = 55 -@pytest.mark.parametrize('method', ['brentq', 'newton']) +@pytest.mark.parametrize("method", ["brentq", "newton"]) def test_method_spr_e20_327(method, cec_module_spr_e20_327): """test pvsystem.singlediode with different methods on SPR-E20-327""" spr_e20_327 = cec_module_spr_e20_327 x = pvsystem.calcparams_desoto( - effective_irradiance=POA, temp_cell=TCELL, - alpha_sc=spr_e20_327['alpha_sc'], a_ref=spr_e20_327['a_ref'], - I_L_ref=spr_e20_327['I_L_ref'], I_o_ref=spr_e20_327['I_o_ref'], - R_sh_ref=spr_e20_327['R_sh_ref'], R_s=spr_e20_327['R_s'], - EgRef=1.121, dEgdT=-0.0002677) - pvs = pvsystem.singlediode(*x, method='lambertw') + effective_irradiance=POA, + temp_cell=TCELL, + alpha_sc=spr_e20_327["alpha_sc"], + a_ref=spr_e20_327["a_ref"], + I_L_ref=spr_e20_327["I_L_ref"], + I_o_ref=spr_e20_327["I_o_ref"], + R_sh_ref=spr_e20_327["R_sh_ref"], + R_s=spr_e20_327["R_s"], + EgRef=1.121, + dEgdT=-0.0002677, + ) + pvs = pvsystem.singlediode(*x, method="lambertw") out = pvsystem.singlediode(*x, method=method) - assert np.isclose(pvs['i_sc'], out['i_sc']) - assert np.isclose(pvs['v_oc'], out['v_oc']) - assert np.isclose(pvs['i_mp'], out['i_mp']) - assert np.isclose(pvs['v_mp'], out['v_mp']) - assert np.isclose(pvs['p_mp'], out['p_mp']) - assert np.isclose(pvs['i_x'], out['i_x']) - assert np.isclose(pvs['i_xx'], out['i_xx']) + assert np.isclose(pvs["i_sc"], out["i_sc"]) + assert np.isclose(pvs["v_oc"], out["v_oc"]) + assert np.isclose(pvs["i_mp"], out["i_mp"]) + assert np.isclose(pvs["v_mp"], out["v_mp"]) + assert np.isclose(pvs["p_mp"], out["p_mp"]) + assert np.isclose(pvs["i_x"], out["i_x"]) + assert np.isclose(pvs["i_xx"], out["i_xx"]) -@pytest.mark.parametrize('method', ['brentq', 'newton']) +@pytest.mark.parametrize("method", ["brentq", "newton"]) def test_newton_fs_495(method, cec_module_fs_495): """test pvsystem.singlediode with different methods on FS495""" fs_495 = cec_module_fs_495 x = pvsystem.calcparams_desoto( - effective_irradiance=POA, temp_cell=TCELL, - alpha_sc=fs_495['alpha_sc'], a_ref=fs_495['a_ref'], - I_L_ref=fs_495['I_L_ref'], I_o_ref=fs_495['I_o_ref'], - R_sh_ref=fs_495['R_sh_ref'], R_s=fs_495['R_s'], - EgRef=1.475, dEgdT=-0.0003) - pvs = pvsystem.singlediode(*x, method='lambertw') + effective_irradiance=POA, + temp_cell=TCELL, + alpha_sc=fs_495["alpha_sc"], + a_ref=fs_495["a_ref"], + I_L_ref=fs_495["I_L_ref"], + I_o_ref=fs_495["I_o_ref"], + R_sh_ref=fs_495["R_sh_ref"], + R_s=fs_495["R_s"], + EgRef=1.475, + dEgdT=-0.0003, + ) + pvs = pvsystem.singlediode(*x, method="lambertw") out = pvsystem.singlediode(*x, method=method) - assert np.isclose(pvs['i_sc'], out['i_sc']) - assert np.isclose(pvs['v_oc'], out['v_oc']) - assert np.isclose(pvs['i_mp'], out['i_mp']) - assert np.isclose(pvs['v_mp'], out['v_mp']) - assert np.isclose(pvs['p_mp'], out['p_mp']) - assert np.isclose(pvs['i_x'], out['i_x']) - assert np.isclose(pvs['i_xx'], out['i_xx']) + assert np.isclose(pvs["i_sc"], out["i_sc"]) + assert np.isclose(pvs["v_oc"], out["v_oc"]) + assert np.isclose(pvs["i_mp"], out["i_mp"]) + assert np.isclose(pvs["v_mp"], out["v_mp"]) + assert np.isclose(pvs["p_mp"], out["p_mp"]) + assert np.isclose(pvs["i_x"], out["i_x"]) + assert np.isclose(pvs["i_xx"], out["i_xx"]) def build_precise_iv_curve_dataframe(file_csv, file_json): @@ -102,51 +120,73 @@ def build_precise_iv_curve_dataframe(file_csv, file_json): """ params = pd.read_csv(file_csv) curves_metadata = pd.read_json(file_json) - curves = pd.DataFrame(curves_metadata['IV Curves'].values.tolist()) - curves['cells_in_series'] = curves_metadata['cells_in_series'] - joined = params.merge(curves, on='Index', how='inner', - suffixes=(None, '_drop'), validate='one_to_one') - joined = joined[(c for c in joined.columns if not c.endswith('_drop'))] + curves = pd.DataFrame(curves_metadata["IV Curves"].values.tolist()) + curves["cells_in_series"] = curves_metadata["cells_in_series"] + joined = params.merge( + curves, + on="Index", + how="inner", + suffixes=(None, "_drop"), + validate="one_to_one", + ) + joined = joined[(c for c in joined.columns if not c.endswith("_drop"))] # parse strings to np.float64 - is_array = ['Currents', 'Voltages', 'diode_voltage'] + is_array = ["Currents", "Voltages", "diode_voltage"] for col in is_array: joined[col] = [np.asarray(a, dtype=np.float64) for a in joined[col]] - is_number = ['v_oc', 'i_sc', 'v_mp', 'i_mp', 'p_mp', 'i_x', 'i_xx', - 'Temperature'] + is_number = [ + "v_oc", + "i_sc", + "v_mp", + "i_mp", + "p_mp", + "i_x", + "i_xx", + "Temperature", + ] joined[is_number] = joined[is_number].astype(np.float64) - joined['Boltzmann'] = scipy.constants.Boltzmann - joined['Elementary Charge'] = scipy.constants.elementary_charge - joined['Vth'] = ( - joined['Boltzmann'] * joined['Temperature'] - / joined['Elementary Charge'] + joined["Boltzmann"] = scipy.constants.Boltzmann + joined["Elementary Charge"] = scipy.constants.elementary_charge + joined["Vth"] = ( + joined["Boltzmann"] + * joined["Temperature"] + / joined["Elementary Charge"] ) return joined -@pytest.fixture(scope='function', params=[ - { - 'csv': f'{DATA_DIR}/precise_iv_curves_parameter_sets1.csv', - 'json': f'{DATA_DIR}/precise_iv_curves1.json' - }, - { - 'csv': f'{DATA_DIR}/precise_iv_curves_parameter_sets2.csv', - 'json': f'{DATA_DIR}/precise_iv_curves2.json' - } -], ids=[1, 2]) +@pytest.fixture( + scope="function", + params=[ + { + "csv": f"{DATA_DIR}/precise_iv_curves_parameter_sets1.csv", + "json": f"{DATA_DIR}/precise_iv_curves1.json", + }, + { + "csv": f"{DATA_DIR}/precise_iv_curves_parameter_sets2.csv", + "json": f"{DATA_DIR}/precise_iv_curves2.json", + }, + ], + ids=[1, 2], +) def precise_iv_curves(request): - file_csv, file_json = request.param['csv'], request.param['json'] + file_csv, file_json = request.param["csv"], request.param["json"] pc = build_precise_iv_curve_dataframe(file_csv, file_json) - params = ['photocurrent', 'saturation_current', 'resistance_series', - 'resistance_shunt'] + params = [ + "photocurrent", + "saturation_current", + "resistance_series", + "resistance_shunt", + ] singlediode_params = pc.loc[:, params] - singlediode_params['nNsVth'] = pc['n'] * pc['cells_in_series'] * pc['Vth'] + singlediode_params["nNsVth"] = pc["n"] * pc["cells_in_series"] * pc["Vth"] return singlediode_params, pc -@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton']) +@pytest.mark.parametrize("method", ["lambertw", "brentq", "newton"]) def test_singlediode_precision(method, precise_iv_curves): """ Tests the accuracy of singlediode. ivcurve_pnts is not tested. @@ -154,17 +194,17 @@ def test_singlediode_precision(method, precise_iv_curves): x, pc = precise_iv_curves outs = pvsystem.singlediode(method=method, **x) - assert np.allclose(pc['i_sc'], outs['i_sc'], atol=1e-10, rtol=0) - assert np.allclose(pc['v_oc'], outs['v_oc'], atol=1e-10, rtol=0) - assert np.allclose(pc['i_mp'], outs['i_mp'], atol=7e-8, rtol=0) - assert np.allclose(pc['v_mp'], outs['v_mp'], atol=1e-6, rtol=0) - assert np.allclose(pc['p_mp'], outs['p_mp'], atol=1e-10, rtol=0) - assert np.allclose(pc['i_x'], outs['i_x'], atol=1e-10, rtol=0) + assert np.allclose(pc["i_sc"], outs["i_sc"], atol=1e-10, rtol=0) + assert np.allclose(pc["v_oc"], outs["v_oc"], atol=1e-10, rtol=0) + assert np.allclose(pc["i_mp"], outs["i_mp"], atol=7e-8, rtol=0) + assert np.allclose(pc["v_mp"], outs["v_mp"], atol=1e-6, rtol=0) + assert np.allclose(pc["p_mp"], outs["p_mp"], atol=1e-10, rtol=0) + assert np.allclose(pc["i_x"], outs["i_x"], atol=1e-10, rtol=0) # This test should pass with atol=9e-8 on MacOS and Windows. # The atol was lowered to pass on Linux when the vectorized umath module # introduced in NumPy 1.22.0 is used. - assert np.allclose(pc['i_xx'], outs['i_xx'], atol=1e-6, rtol=0) + assert np.allclose(pc["i_xx"], outs["i_xx"], atol=1e-6, rtol=0) def test_singlediode_lambert_negative_voc(mocker): @@ -187,13 +227,13 @@ def test_singlediode_lambert_negative_voc(mocker): assert_array_equal(outs["v_oc"], [0, 0]) -@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton']) +@pytest.mark.parametrize("method", ["lambertw", "brentq", "newton"]) def test_v_from_i_i_from_v_precision(method, precise_iv_curves): """ Tests the accuracy of pvsystem.v_from_i and pvsystem.i_from_v. """ x, pc = precise_iv_curves - pc_i, pc_v = pc['Currents'], pc['Voltages'] + pc_i, pc_v = pc["Currents"], pc["Voltages"] for i, v, (_, x_one_curve) in zip(pc_i, pc_v, x.iterrows()): out_i = pvsystem.i_from_v(voltage=v, method=method, **x_one_curve) out_v = pvsystem.v_from_i(current=i, method=method, **x_one_curve) @@ -218,96 +258,121 @@ def get_pvsyst_fs_495(): """ return { - 'd2mutau': 1.31, 'alpha_sc': 0.00039, 'gamma_ref': 1.48, - 'mu_gamma': 0.001, 'I_o_ref': 9.62e-10, 'R_sh_ref': 5000, - 'R_sh_0': 12500, 'R_sh_exp': 3.1, 'R_s': 4.6, 'beta_oc': -0.2116, - 'EgRef': 1.5, 'cells_in_series': 108, 'cells_in_parallel': 2, - 'I_sc_ref': 1.55, 'V_oc_ref': 86.5, 'I_mp_ref': 1.4, 'V_mp_ref': 67.85, - 'temp_ref': 25, 'irrad_ref': 1000, 'I_L_ref': 1.5743233463848496 + "d2mutau": 1.31, + "alpha_sc": 0.00039, + "gamma_ref": 1.48, + "mu_gamma": 0.001, + "I_o_ref": 9.62e-10, + "R_sh_ref": 5000, + "R_sh_0": 12500, + "R_sh_exp": 3.1, + "R_s": 4.6, + "beta_oc": -0.2116, + "EgRef": 1.5, + "cells_in_series": 108, + "cells_in_parallel": 2, + "I_sc_ref": 1.55, + "V_oc_ref": 86.5, + "I_mp_ref": 1.4, + "V_mp_ref": 67.85, + "temp_ref": 25, + "irrad_ref": 1000, + "I_L_ref": 1.5743233463848496, } + # DeSoto @(888[W/m**2], 55[degC]) = {Pmp: 72.71, Isc: 1.402, Voc: 75.42) @pytest.mark.parametrize( - 'poa, temp_cell, expected, tol', [ + "poa, temp_cell, expected, tol", + [ # reference conditions ( - get_pvsyst_fs_495()['irrad_ref'], - get_pvsyst_fs_495()['temp_ref'], + get_pvsyst_fs_495()["irrad_ref"], + get_pvsyst_fs_495()["temp_ref"], { - 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * - get_pvsyst_fs_495()['V_mp_ref']), - 'isc': get_pvsyst_fs_495()['I_sc_ref'], - 'voc': get_pvsyst_fs_495()['V_oc_ref'] + "pmp": ( + get_pvsyst_fs_495()["I_mp_ref"] + * get_pvsyst_fs_495()["V_mp_ref"] + ), + "isc": get_pvsyst_fs_495()["I_sc_ref"], + "voc": get_pvsyst_fs_495()["V_oc_ref"], }, - (5e-4, 0.04) + (5e-4, 0.04), ), # other conditions ( POA, TCELL, - { - 'pmp': 76.262, - 'isc': 1.3868, - 'voc': 79.292 - }, - (1e-4, 1e-4) - ) - ] + {"pmp": 76.262, "isc": 1.3868, "voc": 79.292}, + (1e-4, 1e-4), + ), + ], ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize("method", ["newton", "brentq"]) def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): """test PVSst recombination loss""" pvsyst_fs_495 = get_pvsyst_fs_495() # first evaluate PVSyst model with thin-film recombination loss current # at reference conditions x = pvsystem.calcparams_pvsyst( - effective_irradiance=poa, temp_cell=temp_cell, - alpha_sc=pvsyst_fs_495['alpha_sc'], - gamma_ref=pvsyst_fs_495['gamma_ref'], - mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], - I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], - R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], - R_s=pvsyst_fs_495['R_s'], - cells_in_series=pvsyst_fs_495['cells_in_series'], - EgRef=pvsyst_fs_495['EgRef'] + effective_irradiance=poa, + temp_cell=temp_cell, + alpha_sc=pvsyst_fs_495["alpha_sc"], + gamma_ref=pvsyst_fs_495["gamma_ref"], + mu_gamma=pvsyst_fs_495["mu_gamma"], + I_L_ref=pvsyst_fs_495["I_L_ref"], + I_o_ref=pvsyst_fs_495["I_o_ref"], + R_sh_ref=pvsyst_fs_495["R_sh_ref"], + R_sh_0=pvsyst_fs_495["R_sh_0"], + R_sh_exp=pvsyst_fs_495["R_sh_exp"], + R_s=pvsyst_fs_495["R_s"], + cells_in_series=pvsyst_fs_495["cells_in_series"], + EgRef=pvsyst_fs_495["EgRef"], ) il_pvsyst, io_pvsyst, rs_pvsyst, rsh_pvsyst, nnsvt_pvsyst = x - voc_est_pvsyst = estimate_voc(photocurrent=il_pvsyst, - saturation_current=io_pvsyst, - nNsVth=nnsvt_pvsyst) + voc_est_pvsyst = estimate_voc( + photocurrent=il_pvsyst, + saturation_current=io_pvsyst, + nNsVth=nnsvt_pvsyst, + ) vd_pvsyst = np.linspace(0, voc_est_pvsyst, 1000) pvsyst = bishop88( - diode_voltage=vd_pvsyst, photocurrent=il_pvsyst, - saturation_current=io_pvsyst, resistance_series=rs_pvsyst, - resistance_shunt=rsh_pvsyst, nNsVth=nnsvt_pvsyst, - d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series'] + diode_voltage=vd_pvsyst, + photocurrent=il_pvsyst, + saturation_current=io_pvsyst, + resistance_series=rs_pvsyst, + resistance_shunt=rsh_pvsyst, + nNsVth=nnsvt_pvsyst, + d2mutau=pvsyst_fs_495["d2mutau"], + NsVbi=VOLTAGE_BUILTIN * pvsyst_fs_495["cells_in_series"], ) # test max power - assert np.isclose(max(pvsyst[2]), expected['pmp'], *tol) + assert np.isclose(max(pvsyst[2]), expected["pmp"], *tol) # test short circuit current isc_pvsyst = np.interp(0, pvsyst[1], pvsyst[0]) - assert np.isclose(isc_pvsyst, expected['isc'], *tol) + assert np.isclose(isc_pvsyst, expected["isc"], *tol) # test open circuit voltage voc_pvsyst = np.interp(0, pvsyst[0][::-1], pvsyst[1][::-1]) - assert np.isclose(voc_pvsyst, expected['voc'], *tol) + assert np.isclose(voc_pvsyst, expected["voc"], *tol) # repeat tests as above with specialized bishop88 functions - y = dict(d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series']) + y = dict( + d2mutau=pvsyst_fs_495["d2mutau"], + NsVbi=VOLTAGE_BUILTIN * pvsyst_fs_495["cells_in_series"], + ) mpp_88 = bishop88_mpp(*x, **y, method=method) - assert np.isclose(mpp_88[2], expected['pmp'], *tol) + assert np.isclose(mpp_88[2], expected["pmp"], *tol) isc_88 = bishop88_i_from_v(0, *x, **y, method=method) - assert np.isclose(isc_88, expected['isc'], *tol) + assert np.isclose(isc_88, expected["isc"], *tol) voc_88 = bishop88_v_from_i(0, *x, **y, method=method) - assert np.isclose(voc_88, expected['voc'], *tol) + assert np.isclose(voc_88, expected["voc"], *tol) ioc_88 = bishop88_i_from_v(voc_88, *x, **y, method=method) assert np.isclose(ioc_88, 0.0, *tol) @@ -317,96 +382,113 @@ def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): @pytest.mark.parametrize( - 'brk_params, recomb_params, poa, temp_cell, expected, tol', [ + "brk_params, recomb_params, poa, temp_cell, expected, tol", + [ # reference conditions without breakdown model ( - (0., -5.5, 3.28), - (get_pvsyst_fs_495()['d2mutau'], - VOLTAGE_BUILTIN * get_pvsyst_fs_495()['cells_in_series']), - get_pvsyst_fs_495()['irrad_ref'], - get_pvsyst_fs_495()['temp_ref'], + (0.0, -5.5, 3.28), + ( + get_pvsyst_fs_495()["d2mutau"], + VOLTAGE_BUILTIN * get_pvsyst_fs_495()["cells_in_series"], + ), + get_pvsyst_fs_495()["irrad_ref"], + get_pvsyst_fs_495()["temp_ref"], { - 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * # noqa: W504 - get_pvsyst_fs_495()['V_mp_ref']), - 'isc': get_pvsyst_fs_495()['I_sc_ref'], - 'voc': get_pvsyst_fs_495()['V_oc_ref'] + "pmp": ( + get_pvsyst_fs_495()["I_mp_ref"] # noqa: W504 + * get_pvsyst_fs_495()["V_mp_ref"] + ), + "isc": get_pvsyst_fs_495()["I_sc_ref"], + "voc": get_pvsyst_fs_495()["V_oc_ref"], }, - (5e-4, 0.04) + (5e-4, 0.04), ), # other conditions with breakdown model on and recombination model off ( - (1.e-4, -5.5, 3.28), - (0., np.inf), + (1.0e-4, -5.5, 3.28), + (0.0, np.inf), POA, TCELL, - { - 'pmp': 79.723, - 'isc': 1.4071, - 'voc': 79.646 - }, - (1e-4, 1e-4) - ) - ] + {"pmp": 79.723, "isc": 1.4071, "voc": 79.646}, + (1e-4, 1e-4), + ), + ], ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) -def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, - expected, tol): +@pytest.mark.parametrize("method", ["newton", "brentq"]) +def test_pvsyst_breakdown( + method, brk_params, recomb_params, poa, temp_cell, expected, tol +): """test PVSyst recombination loss""" pvsyst_fs_495 = get_pvsyst_fs_495() # first evaluate PVSyst model with thin-film recombination loss current # at reference conditions x = pvsystem.calcparams_pvsyst( - effective_irradiance=poa, temp_cell=temp_cell, - alpha_sc=pvsyst_fs_495['alpha_sc'], - gamma_ref=pvsyst_fs_495['gamma_ref'], - mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], - I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], - R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], - R_s=pvsyst_fs_495['R_s'], - cells_in_series=pvsyst_fs_495['cells_in_series'], - EgRef=pvsyst_fs_495['EgRef'] + effective_irradiance=poa, + temp_cell=temp_cell, + alpha_sc=pvsyst_fs_495["alpha_sc"], + gamma_ref=pvsyst_fs_495["gamma_ref"], + mu_gamma=pvsyst_fs_495["mu_gamma"], + I_L_ref=pvsyst_fs_495["I_L_ref"], + I_o_ref=pvsyst_fs_495["I_o_ref"], + R_sh_ref=pvsyst_fs_495["R_sh_ref"], + R_sh_0=pvsyst_fs_495["R_sh_0"], + R_sh_exp=pvsyst_fs_495["R_sh_exp"], + R_s=pvsyst_fs_495["R_s"], + cells_in_series=pvsyst_fs_495["cells_in_series"], + EgRef=pvsyst_fs_495["EgRef"], ) il_pvsyst, io_pvsyst, rs_pvsyst, rsh_pvsyst, nnsvt_pvsyst = x d2mutau, NsVbi = recomb_params breakdown_factor, breakdown_voltage, breakdown_exp = brk_params - voc_est_pvsyst = estimate_voc(photocurrent=il_pvsyst, - saturation_current=io_pvsyst, - nNsVth=nnsvt_pvsyst) + voc_est_pvsyst = estimate_voc( + photocurrent=il_pvsyst, + saturation_current=io_pvsyst, + nNsVth=nnsvt_pvsyst, + ) vd_pvsyst = np.linspace(0, voc_est_pvsyst, 1000) pvsyst = bishop88( - diode_voltage=vd_pvsyst, photocurrent=il_pvsyst, - saturation_current=io_pvsyst, resistance_series=rs_pvsyst, - resistance_shunt=rsh_pvsyst, nNsVth=nnsvt_pvsyst, - d2mutau=d2mutau, NsVbi=NsVbi, - breakdown_factor=breakdown_factor, breakdown_voltage=breakdown_voltage, - breakdown_exp=breakdown_exp + diode_voltage=vd_pvsyst, + photocurrent=il_pvsyst, + saturation_current=io_pvsyst, + resistance_series=rs_pvsyst, + resistance_shunt=rsh_pvsyst, + nNsVth=nnsvt_pvsyst, + d2mutau=d2mutau, + NsVbi=NsVbi, + breakdown_factor=breakdown_factor, + breakdown_voltage=breakdown_voltage, + breakdown_exp=breakdown_exp, ) # test max power - assert np.isclose(max(pvsyst[2]), expected['pmp'], *tol) + assert np.isclose(max(pvsyst[2]), expected["pmp"], *tol) # test short circuit current isc_pvsyst = np.interp(0, pvsyst[1], pvsyst[0]) - assert np.isclose(isc_pvsyst, expected['isc'], *tol) + assert np.isclose(isc_pvsyst, expected["isc"], *tol) # test open circuit voltage voc_pvsyst = np.interp(0, pvsyst[0][::-1], pvsyst[1][::-1]) - assert np.isclose(voc_pvsyst, expected['voc'], *tol) + assert np.isclose(voc_pvsyst, expected["voc"], *tol) # repeat tests as above with specialized bishop88 functions - y = {'d2mutau': recomb_params[0], 'NsVbi': recomb_params[1], - 'breakdown_factor': brk_params[0], 'breakdown_voltage': brk_params[1], - 'breakdown_exp': brk_params[2]} + y = { + "d2mutau": recomb_params[0], + "NsVbi": recomb_params[1], + "breakdown_factor": brk_params[0], + "breakdown_voltage": brk_params[1], + "breakdown_exp": brk_params[2], + } mpp_88 = bishop88_mpp(*x, **y, method=method) - assert np.isclose(mpp_88[2], expected['pmp'], *tol) + assert np.isclose(mpp_88[2], expected["pmp"], *tol) isc_88 = bishop88_i_from_v(0, *x, **y, method=method) - assert np.isclose(isc_88, expected['isc'], *tol) + assert np.isclose(isc_88, expected["isc"], *tol) voc_88 = bishop88_v_from_i(0, *x, **y, method=method) - assert np.isclose(voc_88, expected['voc'], *tol) + assert np.isclose(voc_88, expected["voc"], *tol) ioc_88 = bishop88_i_from_v(voc_88, *x, **y, method=method) assert np.isclose(ioc_88, 0.0, *tol) @@ -421,105 +503,147 @@ def bishop88_arguments(): # evaluate PVSyst model with thin-film recombination loss current # at reference conditions x = pvsystem.calcparams_pvsyst( - effective_irradiance=pvsyst_fs_495['irrad_ref'], - temp_cell=pvsyst_fs_495['temp_ref'], - alpha_sc=pvsyst_fs_495['alpha_sc'], - gamma_ref=pvsyst_fs_495['gamma_ref'], - mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], - I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], - R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], - R_s=pvsyst_fs_495['R_s'], - cells_in_series=pvsyst_fs_495['cells_in_series'], - EgRef=pvsyst_fs_495['EgRef'] + effective_irradiance=pvsyst_fs_495["irrad_ref"], + temp_cell=pvsyst_fs_495["temp_ref"], + alpha_sc=pvsyst_fs_495["alpha_sc"], + gamma_ref=pvsyst_fs_495["gamma_ref"], + mu_gamma=pvsyst_fs_495["mu_gamma"], + I_L_ref=pvsyst_fs_495["I_L_ref"], + I_o_ref=pvsyst_fs_495["I_o_ref"], + R_sh_ref=pvsyst_fs_495["R_sh_ref"], + R_sh_0=pvsyst_fs_495["R_sh_0"], + R_sh_exp=pvsyst_fs_495["R_sh_exp"], + R_s=pvsyst_fs_495["R_s"], + cells_in_series=pvsyst_fs_495["cells_in_series"], + EgRef=pvsyst_fs_495["EgRef"], + ) + y = dict( + d2mutau=pvsyst_fs_495["d2mutau"], + NsVbi=VOLTAGE_BUILTIN * pvsyst_fs_495["cells_in_series"], ) - y = dict(d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series']) # Convert (*x, **y) in a bishop88_.* call to dict of arguments args_dict = { - 'photocurrent': x[0], - 'saturation_current': x[1], - 'resistance_series': x[2], - 'resistance_shunt': x[3], - 'nNsVth': x[4], + "photocurrent": x[0], + "saturation_current": x[1], + "resistance_series": x[2], + "resistance_shunt": x[3], + "nNsVth": x[4], } args_dict.update(y) return args_dict -@pytest.mark.parametrize('method, method_kwargs', [ - ('newton', { - 'tol': 1e-8, - 'rtol': 1e-8, - 'maxiter': 30, - }), - ('brentq', { - 'xtol': 1e-8, - 'rtol': 1e-8, - 'maxiter': 30, - }) -]) -def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, - bishop88_arguments): +@pytest.mark.parametrize( + "method, method_kwargs", + [ + ( + "newton", + { + "tol": 1e-8, + "rtol": 1e-8, + "maxiter": 30, + }, + ), + ( + "brentq", + { + "xtol": 1e-8, + "rtol": 1e-8, + "maxiter": 30, + }, + ), + ], +) +def test_bishop88_kwargs_transfer( + method, method_kwargs, mocker, bishop88_arguments +): """test method_kwargs modifying optimizer does not break anything""" # patch method namespace at singlediode module namespace - optimizer_mock = mocker.patch('pvlib.singlediode.' + method) + optimizer_mock = mocker.patch("pvlib.singlediode." + method) # check kwargs passed to bishop_.* are a subset of the call args # since they are called with more keyword arguments - bishop88_i_from_v(0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + bishop88_i_from_v( + 0, **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() - bishop88_v_from_i(0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + bishop88_v_from_i( + 0, **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() - bishop88_mpp(**bishop88_arguments, method=method, - method_kwargs=method_kwargs) + bishop88_mpp( + **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() -@pytest.mark.parametrize('method, method_kwargs', [ - ('newton', { - 'tol': 1e-4, - 'rtol': 1e-4, - 'maxiter': 20, - '_inexistent_param': "0.01" - }), - ('brentq', { - 'xtol': 1e-4, - 'rtol': 1e-4, - 'maxiter': 20, - '_inexistent_param': "0.01" - }) -]) +@pytest.mark.parametrize( + "method, method_kwargs", + [ + ( + "newton", + { + "tol": 1e-4, + "rtol": 1e-4, + "maxiter": 20, + "_inexistent_param": "0.01", + }, + ), + ( + "brentq", + { + "xtol": 1e-4, + "rtol": 1e-4, + "maxiter": 20, + "_inexistent_param": "0.01", + }, + ), + ], +) def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" - pytest.raises(TypeError, bishop88_i_from_v, - 0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + pytest.raises( + TypeError, + bishop88_i_from_v, + 0, + **bishop88_arguments, + method=method, + method_kwargs=method_kwargs, + ) - pytest.raises(TypeError, bishop88_v_from_i, - 0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + pytest.raises( + TypeError, + bishop88_v_from_i, + 0, + **bishop88_arguments, + method=method, + method_kwargs=method_kwargs, + ) - pytest.raises(TypeError, bishop88_mpp, - **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + pytest.raises( + TypeError, + bishop88_mpp, + **bishop88_arguments, + method=method, + method_kwargs=method_kwargs, + ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize("method", ["newton", "brentq"]) def test_bishop88_full_output_kwarg(method, bishop88_arguments): """test call to bishop88_.* with full_output=True return values are ok""" - method_kwargs = {'full_output': True} + method_kwargs = {"full_output": True} - ret_val = bishop88_i_from_v(0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + ret_val = bishop88_i_from_v( + 0, **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], float) # first one has bishop88 result @@ -527,8 +651,9 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): # any root finder returns at least 2 elements with full_output=True assert len(ret_val[1]) >= 2 - ret_val = bishop88_v_from_i(0, **bishop88_arguments, method=method, - method_kwargs=method_kwargs) + ret_val = bishop88_v_from_i( + 0, **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], float) # first one has bishop88 result @@ -536,8 +661,9 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): # any root finder returns at least 2 elements with full_output=True assert len(ret_val[1]) >= 2 - ret_val = bishop88_mpp(**bishop88_arguments, method=method, - method_kwargs=method_kwargs) + ret_val = bishop88_mpp( + **bishop88_arguments, method=method, method_kwargs=method_kwargs + ) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], tuple) # first one has bishop88 result @@ -547,7 +673,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert len(ret_val[1]) >= 2 -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize("method", ["newton", "brentq"]) def test_bishop88_pdSeries_len_one(method, bishop88_arguments): for k, v in bishop88_arguments.items(): bishop88_arguments[k] = pd.Series([v]) @@ -558,25 +684,29 @@ def test_bishop88_pdSeries_len_one(method, bishop88_arguments): bishop88_mpp(**bishop88_arguments, method=method) -def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0., NsVbi=np.inf): +def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0.0, NsVbi=np.inf): vd = v + rs * i - return il - io*np.expm1(vd/a) - vd/rsh - il*d2mutau/(NsVbi - vd) - i + return ( + il - io * np.expm1(vd / a) - vd / rsh - il * d2mutau / (NsVbi - vd) - i + ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize("method", ["newton", "brentq"]) def test_bishop88_init_cond(method): # GH 2013 - p = {'alpha_sc': 0.0012256, - 'gamma_ref': 1.2916241612804187, - 'mu_gamma': 0.00047308959960937403, - 'I_L_ref': 3.068717040806731, - 'I_o_ref': 2.2691248021217617e-11, - 'R_sh_ref': 7000, - 'R_sh_0': 7000, - 'R_s': 4.602, - 'cells_in_series': 268, - 'R_sh_exp': 5.5, - 'EgRef': 1.5} + p = { + "alpha_sc": 0.0012256, + "gamma_ref": 1.2916241612804187, + "mu_gamma": 0.00047308959960937403, + "I_L_ref": 3.068717040806731, + "I_o_ref": 2.2691248021217617e-11, + "R_sh_ref": 7000, + "R_sh_0": 7000, + "R_s": 4.602, + "cells_in_series": 268, + "R_sh_exp": 5.5, + "EgRef": 1.5, + } NsVbi = 268 * 0.9 d2mutau = 1.4 irrad = np.arange(20, 1100, 20) @@ -589,20 +719,36 @@ def test_bishop88_init_cond(method): # test _mpp result = bishop88_mpp(*sde_params, d2mutau=d2mutau, NsVbi=NsVbi) imp, vmp, pmp = result - err = np.abs(_sde_check_solution( - imp, vmp, sde_params[0], sde_params[1], sde_params[2], sde_params[3], - sde_params[4], d2mutau=d2mutau, NsVbi=NsVbi)) + err = np.abs( + _sde_check_solution( + imp, + vmp, + sde_params[0], + sde_params[1], + sde_params[2], + sde_params[3], + sde_params[4], + d2mutau=d2mutau, + NsVbi=NsVbi, + ) + ) bad_results = np.isnan(pmp) | (pmp < 0) | (err > 0.00001) # 0.01mA error assert not bad_results.any() # test v_from_i vmp2 = bishop88_v_from_i(imp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) - err = np.abs(_sde_check_solution(imp, vmp2, *sde_params, d2mutau=d2mutau, - NsVbi=NsVbi)) + err = np.abs( + _sde_check_solution( + imp, vmp2, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi + ) + ) bad_results = np.isnan(vmp2) | (vmp2 < 0) | (err > 0.00001) assert not bad_results.any() # test v_from_i imp2 = bishop88_i_from_v(vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) - err = np.abs(_sde_check_solution(imp2, vmp, *sde_params, d2mutau=d2mutau, - NsVbi=NsVbi)) + err = np.abs( + _sde_check_solution( + imp2, vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi + ) + ) bad_results = np.isnan(imp2) | (imp2 < 0) | (err > 0.00001) assert not bad_results.any() diff --git a/pvlib/tests/test_snow.py b/pvlib/tests/test_snow.py index 19e79b5179..9b1b7158bc 100644 --- a/pvlib/tests/test_snow.py +++ b/pvlib/tests/test_snow.py @@ -10,11 +10,13 @@ def test_fully_covered_nrel(): - dt = pd.date_range(start="2019-1-1 12:00:00", end="2019-1-1 18:00:00", - freq='1h') - snowfall_data = pd.Series([1, 5, .6, 4, .23, -5, 19], index=dt) - expected = pd.Series([False, True, False, True, False, False, True], - index=dt) + dt = pd.date_range( + start="2019-1-1 12:00:00", end="2019-1-1 18:00:00", freq="1h" + ) + snowfall_data = pd.Series([1, 5, 0.6, 4, 0.23, -5, 19], index=dt) + expected = pd.Series( + [False, True, False, True, False, False, True], index=dt + ) fully_covered = snow.fully_covered_nrel(snowfall_data) assert_series_equal(expected, fully_covered) @@ -22,15 +24,21 @@ def test_fully_covered_nrel(): def test_coverage_nrel_hourly(): surface_tilt = 45 slide_amount_coefficient = 0.197 - dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", - freq='1h') - poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], - index=dt) + dt = pd.date_range( + start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", freq="1h" + ) + poa_irradiance = pd.Series( + [400, 200, 100, 1234, 134, 982, 100, 100], index=dt + ) temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10], index=dt) - snowfall_data = pd.Series([1, .5, .6, .4, .23, -5, .1, .1], index=dt) + snowfall_data = pd.Series([1, 0.5, 0.6, 0.4, 0.23, -5, 0.1, 0.1], index=dt) snow_coverage = snow.coverage_nrel( - snowfall_data, poa_irradiance, temp_air, surface_tilt, - threshold_snowfall=0.6) + snowfall_data, + poa_irradiance, + temp_air, + surface_tilt, + threshold_snowfall=0.6, + ) slide_amt = slide_amount_coefficient * sind(surface_tilt) covered = 1.0 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) @@ -41,32 +49,47 @@ def test_coverage_nrel_hourly(): def test_coverage_nrel_subhourly(): surface_tilt = 45 slide_amount_coefficient = 0.197 - dt = pd.date_range(start="2019-1-1 11:00:00", end="2019-1-1 14:00:00", - freq='15min') - poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100, 100, - 100, 100, 100, 0], - index=dt) - temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10, 10, 10, -10, -10, - 10], index=dt) - snowfall_data = pd.Series([1, .5, .6, .4, .23, -5, .1, .1, 0., 1., 0., 0., - 0.], index=dt) + dt = pd.date_range( + start="2019-1-1 11:00:00", end="2019-1-1 14:00:00", freq="15min" + ) + poa_irradiance = pd.Series( + [400, 200, 100, 1234, 134, 982, 100, 100, 100, 100, 100, 100, 0], + index=dt, + ) + temp_air = pd.Series( + [10, 2, 10, 1234, 34, 982, 10, 10, 10, 10, -10, -10, 10], index=dt + ) + snowfall_data = pd.Series( + [1, 0.5, 0.6, 0.4, 0.23, -5, 0.1, 0.1, 0.0, 1.0, 0.0, 0.0, 0.0], + index=dt, + ) snow_coverage = snow.coverage_nrel( - snowfall_data, poa_irradiance, temp_air, surface_tilt) + snowfall_data, poa_irradiance, temp_air, surface_tilt + ) slide_amt = slide_amount_coefficient * sind(surface_tilt) * 0.25 - covered = np.append(np.array([1., 1., 1., 1.]), - 1.0 - slide_amt * np.array([1, 2, 3, 4, 5])) - covered = np.append(covered, np.array([1., 1., 1., 1. - slide_amt])) + covered = np.append( + np.array([1.0, 1.0, 1.0, 1.0]), + 1.0 - slide_amt * np.array([1, 2, 3, 4, 5]), + ) + covered = np.append(covered, np.array([1.0, 1.0, 1.0, 1.0 - slide_amt])) expected = pd.Series(covered, index=dt) assert_series_equal(expected, snow_coverage) def test_fully_covered_nrel_irregular(): # test when frequency is not specified and can't be inferred - dt = pd.DatetimeIndex(["2019-1-1 11:00:00", "2019-1-1 14:30:00", - "2019-1-1 15:07:00", "2019-1-1 14:00:00"]) - snowfall_data = pd.Series([1, .5, .6, .4], index=dt) - snow_coverage = snow.fully_covered_nrel(snowfall_data, - threshold_snowfall=0.5) + dt = pd.DatetimeIndex( + [ + "2019-1-1 11:00:00", + "2019-1-1 14:30:00", + "2019-1-1 15:07:00", + "2019-1-1 14:00:00", + ] + ) + snowfall_data = pd.Series([1, 0.5, 0.6, 0.4], index=dt) + snow_coverage = snow.fully_covered_nrel( + snowfall_data, threshold_snowfall=0.5 + ) covered = np.array([False, False, True, False]) expected = pd.Series(covered, index=dt) assert_series_equal(expected, snow_coverage) @@ -75,138 +98,292 @@ def test_fully_covered_nrel_irregular(): def test_coverage_nrel_initial(): surface_tilt = 45 slide_amount_coefficient = 0.197 - dt = pd.date_range(start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", - freq='1h') - poa_irradiance = pd.Series([400, 200, 100, 1234, 134, 982, 100, 100], - index=dt) + dt = pd.date_range( + start="2019-1-1 10:00:00", end="2019-1-1 17:00:00", freq="1h" + ) + poa_irradiance = pd.Series( + [400, 200, 100, 1234, 134, 982, 100, 100], index=dt + ) temp_air = pd.Series([10, 2, 10, 1234, 34, 982, 10, 10], index=dt) - snowfall_data = pd.Series([0, .5, .6, .4, .23, -5, .1, .1], index=dt) + snowfall_data = pd.Series([0, 0.5, 0.6, 0.4, 0.23, -5, 0.1, 0.1], index=dt) snow_coverage = snow.coverage_nrel( - snowfall_data, poa_irradiance, temp_air, surface_tilt, - initial_coverage=0.5, threshold_snowfall=1.) + snowfall_data, + poa_irradiance, + temp_air, + surface_tilt, + initial_coverage=0.5, + threshold_snowfall=1.0, + ) slide_amt = slide_amount_coefficient * sind(surface_tilt) covered = 0.5 - slide_amt * np.array([0, 1, 2, 3, 4, 5, 6, 7]) - covered = np.where(covered < 0, 0., covered) + covered = np.where(covered < 0, 0.0, covered) expected = pd.Series(covered, index=dt) assert_series_equal(expected, snow_coverage) def test_dc_loss_nrel(): num_strings = 8 - snow_coverage = pd.Series([1, 1, .5, .6, .2, .4, 0]) - expected = pd.Series([1, 1, .5, .625, .25, .5, 0]) + snow_coverage = pd.Series([1, 1, 0.5, 0.6, 0.2, 0.4, 0]) + expected = pd.Series([1, 1, 0.5, 0.625, 0.25, 0.5, 0]) actual = snow.dc_loss_nrel(snow_coverage, num_strings) assert_series_equal(expected, actual) def test__townsend_effective_snow(): - snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7, - 25.4]) + snow_total = np.array( + [25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7, 25.4] + ) snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3]) - expected = np.array([19.05, 19.05, 12.7, 0, 0, 0, 0, 0, 0, 0, 9.525, - 254 / 15]) + expected = np.array( + [19.05, 19.05, 12.7, 0, 0, 0, 0, 0, 0, 0, 9.525, 254 / 15] + ) actual = snow._townsend_effective_snow(snow_total, snow_events) np.testing.assert_allclose(expected, actual, rtol=1e-07) def test_loss_townsend(): # hand-calculated solution - snow_total = np.array([25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7, - 25.4]) + snow_total = np.array( + [25.4, 25.4, 12.7, 2.54, 0, 0, 0, 0, 0, 0, 12.7, 25.4] + ) snow_events = np.array([2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 2, 3]) surface_tilt = 20 - relative_humidity = np.array([80, 80, 80, 80, 80, 80, 80, 80, 80, 80, - 80, 80]) + relative_humidity = np.array( + [80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80] + ) temp_air = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - poa_global = np.array([350000, 350000, 350000, 350000, 350000, 350000, - 350000, 350000, 350000, 350000, 350000, 350000]) + poa_global = np.array( + [ + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + 350000, + ] + ) angle_of_repose = 40 string_factor = 1.0 slant_height = 2.54 lower_edge_height = 0.254 - expected = np.array([0.07696253, 0.07992262, 0.06216201, 0.01715392, 0, 0, - 0, 0, 0, 0, 0.02643821, 0.06068194]) - actual = snow.loss_townsend(snow_total, snow_events, surface_tilt, - relative_humidity, temp_air, - poa_global, slant_height, - lower_edge_height, string_factor, - angle_of_repose) + expected = np.array( + [ + 0.07696253, + 0.07992262, + 0.06216201, + 0.01715392, + 0, + 0, + 0, + 0, + 0, + 0, + 0.02643821, + 0.06068194, + ] + ) + actual = snow.loss_townsend( + snow_total, + snow_events, + surface_tilt, + relative_humidity, + temp_air, + poa_global, + slant_height, + lower_edge_height, + string_factor, + angle_of_repose, + ) np.testing.assert_allclose(expected, actual, rtol=1e-05) @pytest.mark.parametrize( - 'poa_global,surface_tilt,slant_height,lower_edge_height,string_factor,expected', # noQA: E501 + "poa_global,surface_tilt,slant_height,lower_edge_height,string_factor,expected", # noQA: E501 [ - (np.asarray( - [60., 80., 100., 125., 175., 225., 225., 210., 175., 125., 90., - 60.], dtype=float) * 1000., - 2., - 79. / 39.37, - 3. / 39.37, - 1.0, - np.asarray( - [44, 34, 20, 9, 3, 1, 0, 0, 0, 2, 6, 25], dtype=float) - ), - (np.asarray( - [60., 80., 100., 125., 175., 225., 225., 210., 175., 125., 90., - 60.], dtype=float) * 1000., - 5., - 316 / 39.37, - 120. / 39.37, - 0.75, - np.asarray( - [22, 16, 9, 4, 1, 0, 0, 0, 0, 1, 2, 12], dtype=float) - ), - (np.asarray( - [60., 80., 100., 125., 175., 225., 225., 210., 175., 125., 90., - 60.], dtype=float) * 1000., - 23., - 158 / 39.27, - 12 / 39.37, - 0.75, - np.asarray( - [28, 21, 13, 6, 2, 0, 0, 0, 0, 1, 4, 16], dtype=float) - ), - (np.asarray( - [80., 100., 125., 150., 225., 300., 300., 275., 225., 150., 115., - 80.], dtype=float) * 1000., - 52., - 39.5 / 39.37, - 34. / 39.37, - 0.75, - np.asarray( - [7, 5, 3, 1, 0, 0, 0, 0, 0, 0, 1, 4], dtype=float) - ), - (np.asarray( - [80., 100., 125., 150., 225., 300., 300., 275., 225., 150., 115., - 80.], dtype=float) * 1000., - 60., - 39.5 / 39.37, - 25. / 39.37, - 1., - np.asarray( - [7, 5, 3, 1, 0, 0, 0, 0, 0, 0, 1, 3], dtype=float) - ) - ] + ( + np.asarray( + [ + 60.0, + 80.0, + 100.0, + 125.0, + 175.0, + 225.0, + 225.0, + 210.0, + 175.0, + 125.0, + 90.0, + 60.0, + ], + dtype=float, + ) + * 1000.0, + 2.0, + 79.0 / 39.37, + 3.0 / 39.37, + 1.0, + np.asarray([44, 34, 20, 9, 3, 1, 0, 0, 0, 2, 6, 25], dtype=float), + ), + ( + np.asarray( + [ + 60.0, + 80.0, + 100.0, + 125.0, + 175.0, + 225.0, + 225.0, + 210.0, + 175.0, + 125.0, + 90.0, + 60.0, + ], + dtype=float, + ) + * 1000.0, + 5.0, + 316 / 39.37, + 120.0 / 39.37, + 0.75, + np.asarray([22, 16, 9, 4, 1, 0, 0, 0, 0, 1, 2, 12], dtype=float), + ), + ( + np.asarray( + [ + 60.0, + 80.0, + 100.0, + 125.0, + 175.0, + 225.0, + 225.0, + 210.0, + 175.0, + 125.0, + 90.0, + 60.0, + ], + dtype=float, + ) + * 1000.0, + 23.0, + 158 / 39.27, + 12 / 39.37, + 0.75, + np.asarray([28, 21, 13, 6, 2, 0, 0, 0, 0, 1, 4, 16], dtype=float), + ), + ( + np.asarray( + [ + 80.0, + 100.0, + 125.0, + 150.0, + 225.0, + 300.0, + 300.0, + 275.0, + 225.0, + 150.0, + 115.0, + 80.0, + ], + dtype=float, + ) + * 1000.0, + 52.0, + 39.5 / 39.37, + 34.0 / 39.37, + 0.75, + np.asarray([7, 5, 3, 1, 0, 0, 0, 0, 0, 0, 1, 4], dtype=float), + ), + ( + np.asarray( + [ + 80.0, + 100.0, + 125.0, + 150.0, + 225.0, + 300.0, + 300.0, + 275.0, + 225.0, + 150.0, + 115.0, + 80.0, + ], + dtype=float, + ) + * 1000.0, + 60.0, + 39.5 / 39.37, + 25.0 / 39.37, + 1.0, + np.asarray([7, 5, 3, 1, 0, 0, 0, 0, 0, 0, 1, 3], dtype=float), + ), + ], ) -def test_loss_townsend_cases(poa_global, surface_tilt, slant_height, - lower_edge_height, string_factor, expected): +def test_loss_townsend_cases( + poa_global, + surface_tilt, + slant_height, + lower_edge_height, + string_factor, + expected, +): # test cases from Townsend, 1/27/2023, addeed by cwh # snow_total in inches, convert to cm for pvlib - snow_total = np.asarray( - [20, 15, 10, 4, 1.5, 0, 0, 0, 0, 1.5, 4, 15], dtype=float) * 2.54 + snow_total = ( + np.asarray([20, 15, 10, 4, 1.5, 0, 0, 0, 0, 1.5, 4, 15], dtype=float) + * 2.54 + ) # snow events are an average for each month snow_events = np.asarray( - [5, 4.2, 2.8, 1.3, 0.8, 0, 0, 0, 0, 0.5, 1.5, 4.5], dtype=float) + [5, 4.2, 2.8, 1.3, 0.8, 0, 0, 0, 0, 0.5, 1.5, 4.5], dtype=float + ) # air temperature in C temp_air = np.asarray( - [-6., -2., 1., 4., 7., 10., 13., 16., 14., 12., 7., -3.], dtype=float) + [-6.0, -2.0, 1.0, 4.0, 7.0, 10.0, 13.0, 16.0, 14.0, 12.0, 7.0, -3.0], + dtype=float, + ) # relative humidity in % relative_humidity = np.asarray( - [78., 80., 75., 65., 60., 55., 55., 55., 50., 55., 60., 70.], - dtype=float) + [ + 78.0, + 80.0, + 75.0, + 65.0, + 60.0, + 55.0, + 55.0, + 55.0, + 50.0, + 55.0, + 60.0, + 70.0, + ], + dtype=float, + ) actual = snow.loss_townsend( - snow_total, snow_events, surface_tilt, relative_humidity, temp_air, - poa_global, slant_height, lower_edge_height, string_factor) + snow_total, + snow_events, + surface_tilt, + relative_humidity, + temp_air, + poa_global, + slant_height, + lower_edge_height, + string_factor, + ) actual = np.around(actual * 100) assert np.allclose(expected, actual) diff --git a/pvlib/tests/test_soiling.py b/pvlib/tests/test_soiling.py index e774e62963..5b5876a1cc 100644 --- a/pvlib/tests/test_soiling.py +++ b/pvlib/tests/test_soiling.py @@ -13,72 +13,224 @@ @pytest.fixture def expected_output(): # Sample output (calculated manually) - dt = pd.date_range(start=pd.Timestamp(2019, 1, 1, 0, 0, 0), - end=pd.Timestamp(2019, 1, 1, 23, 59, 0), freq='1h') + dt = pd.date_range( + start=pd.Timestamp(2019, 1, 1, 0, 0, 0), + end=pd.Timestamp(2019, 1, 1, 23, 59, 0), + freq="1h", + ) expected_no_cleaning = pd.Series( - data=[0.96998483, 0.94623958, 0.92468139, 0.90465654, 0.88589707, - 0.86826366, 0.85167258, 0.83606715, 0.82140458, 0.80764919, - 0.79476875, 0.78273241, 0.77150951, 0.76106905, 0.75137932, - 0.74240789, 0.73412165, 0.72648695, 0.71946981, 0.7130361, - 0.70715176, 0.70178307, 0.69689677, 0.69246034], - index=dt) + data=[ + 0.96998483, + 0.94623958, + 0.92468139, + 0.90465654, + 0.88589707, + 0.86826366, + 0.85167258, + 0.83606715, + 0.82140458, + 0.80764919, + 0.79476875, + 0.78273241, + 0.77150951, + 0.76106905, + 0.75137932, + 0.74240789, + 0.73412165, + 0.72648695, + 0.71946981, + 0.7130361, + 0.70715176, + 0.70178307, + 0.69689677, + 0.69246034, + ], + index=dt, + ) return expected_no_cleaning @pytest.fixture def expected_output_1(): - dt = pd.date_range(start=pd.Timestamp(2019, 1, 1, 0, 0, 0), - end=pd.Timestamp(2019, 1, 1, 23, 59, 0), freq='1h') + dt = pd.date_range( + start=pd.Timestamp(2019, 1, 1, 0, 0, 0), + end=pd.Timestamp(2019, 1, 1, 23, 59, 0), + freq="1h", + ) expected_output_1 = pd.Series( - data=[0.98484972, 0.97277367, 0.96167471, 0.95119603, 1., - 0.98484972, 0.97277367, 0.96167471, 1., 1., - 0.98484972, 0.97277367, 0.96167471, 0.95119603, 0.94118234, - 0.93154854, 0.922242, 0.91322759, 0.90448058, 0.89598283, - 0.88772062, 0.87968325, 0.8718622, 0.86425049], - index=dt) + data=[ + 0.98484972, + 0.97277367, + 0.96167471, + 0.95119603, + 1.0, + 0.98484972, + 0.97277367, + 0.96167471, + 1.0, + 1.0, + 0.98484972, + 0.97277367, + 0.96167471, + 0.95119603, + 0.94118234, + 0.93154854, + 0.922242, + 0.91322759, + 0.90448058, + 0.89598283, + 0.88772062, + 0.87968325, + 0.8718622, + 0.86425049, + ], + index=dt, + ) return expected_output_1 @pytest.fixture def expected_output_2(): - dt = pd.date_range(start=pd.Timestamp(2019, 1, 1, 0, 0, 0), - end=pd.Timestamp(2019, 1, 1, 23, 59, 0), freq='1h') + dt = pd.date_range( + start=pd.Timestamp(2019, 1, 1, 0, 0, 0), + end=pd.Timestamp(2019, 1, 1, 23, 59, 0), + freq="1h", + ) expected_output_2 = pd.Series( - data=[0.95036261, 0.91178179, 0.87774818, 0.84732079, 1., - 1., 1., 0.95036261, 1., 1., - 1., 1., 0.95036261, 0.91178179, 0.87774818, - 0.84732079, 0.8201171, 1., 1., 1., - 1., 0.95036261, 0.91178179, 0.87774818], - index=dt) + data=[ + 0.95036261, + 0.91178179, + 0.87774818, + 0.84732079, + 1.0, + 1.0, + 1.0, + 0.95036261, + 1.0, + 1.0, + 1.0, + 1.0, + 0.95036261, + 0.91178179, + 0.87774818, + 0.84732079, + 0.8201171, + 1.0, + 1.0, + 1.0, + 1.0, + 0.95036261, + 0.91178179, + 0.87774818, + ], + index=dt, + ) return expected_output_2 @pytest.fixture def expected_output_3(): - dt = pd.date_range(start=pd.Timestamp(2019, 1, 1, 0, 0, 0), - end=pd.Timestamp(2019, 1, 1, 23, 59, 0), freq='1h') - timedelta = [0, 0, 0, 0, 0, 30, 0, 30, 0, 30, 0, -30, - -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - dt_new = dt + pd.to_timedelta(timedelta, 'm') + dt = pd.date_range( + start=pd.Timestamp(2019, 1, 1, 0, 0, 0), + end=pd.Timestamp(2019, 1, 1, 23, 59, 0), + freq="1h", + ) + timedelta = [ + 0, + 0, + 0, + 0, + 0, + 30, + 0, + 30, + 0, + 30, + 0, + -30, + -30, + -30, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + dt_new = dt + pd.to_timedelta(timedelta, "m") expected_output_3 = pd.Series( - data=[0.96576705, 0.9387675, 0.91437615, 0.89186852, 1., - 1., 0.98093819, 0.9387675, 1., 1., - 1., 1., 0.96576705, 0.9387675, 0.90291005, - 0.88122293, 0.86104089, 1., 1., 1., - 0.96576705, 0.9387675, 0.91437615, 0.89186852], - index=dt_new) + data=[ + 0.96576705, + 0.9387675, + 0.91437615, + 0.89186852, + 1.0, + 1.0, + 0.98093819, + 0.9387675, + 1.0, + 1.0, + 1.0, + 1.0, + 0.96576705, + 0.9387675, + 0.90291005, + 0.88122293, + 0.86104089, + 1.0, + 1.0, + 1.0, + 0.96576705, + 0.9387675, + 0.91437615, + 0.89186852, + ], + index=dt_new, + ) return expected_output_3 @pytest.fixture def rainfall_input(): - - dt = pd.date_range(start=pd.Timestamp(2019, 1, 1, 0, 0, 0), - end=pd.Timestamp(2019, 1, 1, 23, 59, 0), freq='1h') + dt = pd.date_range( + start=pd.Timestamp(2019, 1, 1, 0, 0, 0), + end=pd.Timestamp(2019, 1, 1, 23, 59, 0), + freq="1h", + ) rainfall = pd.Series( - data=[0., 0., 0., 0., 1., 0., 0., 0., 0.5, 0.5, 0., 0., 0., 0., 0., - 0., 0.3, 0.3, 0.3, 0.3, 0., 0., 0., 0.], index=dt) + data=[ + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3, + 0.3, + 0.3, + 0.3, + 0.0, + 0.0, + 0.0, + 0.0, + ], + index=dt, + ) return rainfall @@ -88,13 +240,19 @@ def test_hsu_no_cleaning(rainfall_input, expected_output): rainfall = rainfall_input pm2_5 = 1.0 pm10 = 2.0 - depo_veloc = {'2_5': 1.0e-5, '10': 1.0e-4} - tilt = 0. + depo_veloc = {"2_5": 1.0e-5, "10": 1.0e-4} + tilt = 0.0 expected_no_cleaning = expected_output - result = hsu(rainfall=rainfall, cleaning_threshold=10., surface_tilt=tilt, - pm2_5=pm2_5, pm10=pm10, depo_veloc=depo_veloc, - rain_accum_period=pd.Timedelta('1h')) + result = hsu( + rainfall=rainfall, + cleaning_threshold=10.0, + surface_tilt=tilt, + pm2_5=pm2_5, + pm10=pm10, + depo_veloc=depo_veloc, + rain_accum_period=pd.Timedelta("1h"), + ) assert_series_equal(result, expected_no_cleaning) @@ -104,13 +262,19 @@ def test_hsu(rainfall_input, expected_output_2): rainfall = rainfall_input pm2_5 = 1.0 pm10 = 2.0 - depo_veloc = {'2_5': 1.0e-4, '10': 1.0e-4} - tilt = 0. + depo_veloc = {"2_5": 1.0e-4, "10": 1.0e-4} + tilt = 0.0 # three cleaning events at 4:00-6:00, 8:00-11:00, and 17:00-20:00 - result = hsu(rainfall=rainfall, cleaning_threshold=0.5, surface_tilt=tilt, - pm2_5=pm2_5, pm10=pm10, depo_veloc=depo_veloc, - rain_accum_period=pd.Timedelta('3h')) + result = hsu( + rainfall=rainfall, + cleaning_threshold=0.5, + surface_tilt=tilt, + pm2_5=pm2_5, + pm10=pm10, + depo_veloc=depo_veloc, + rain_accum_period=pd.Timedelta("3h"), + ) assert_series_equal(result, expected_output_2) @@ -120,8 +284,13 @@ def test_hsu_defaults(rainfall_input, expected_output_1): Test Soiling HSU function with default deposition velocity and default rain accumulation period. """ - result = hsu(rainfall=rainfall_input, cleaning_threshold=0.5, - surface_tilt=0.0, pm2_5=1.0e-2, pm10=2.0e-2) + result = hsu( + rainfall=rainfall_input, + cleaning_threshold=0.5, + surface_tilt=0.0, + pm2_5=1.0e-2, + pm10=2.0e-2, + ) assert np.allclose(result.values, expected_output_1) @@ -129,34 +298,66 @@ def test_hsu_variable_time_intervals(rainfall_input, expected_output_3): """ Test Soiling HSU function with variable time intervals. """ - depo_veloc = {'2_5': 1.0e-4, '10': 1.0e-4} + depo_veloc = {"2_5": 1.0e-4, "10": 1.0e-4} rain = pd.DataFrame(data=rainfall_input) # define time deltas in minutes - timedelta = [0, 0, 0, 0, 0, 30, 0, 30, 0, 30, 0, -30, - -30, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - rain['mins_added'] = pd.to_timedelta(timedelta, 'm') - rain['new_time'] = rain.index + rain['mins_added'] - rain_var_times = rain.set_index('new_time').iloc[:, 0] + timedelta = [ + 0, + 0, + 0, + 0, + 0, + 30, + 0, + 30, + 0, + 30, + 0, + -30, + -30, + -30, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + rain["mins_added"] = pd.to_timedelta(timedelta, "m") + rain["new_time"] = rain.index + rain["mins_added"] + rain_var_times = rain.set_index("new_time").iloc[:, 0] result = hsu( - rainfall=rain_var_times, cleaning_threshold=0.5, surface_tilt=50.0, - pm2_5=1, pm10=2, depo_veloc=depo_veloc, - rain_accum_period=pd.Timedelta('2h')) + rainfall=rain_var_times, + cleaning_threshold=0.5, + surface_tilt=50.0, + pm2_5=1, + pm10=2, + depo_veloc=depo_veloc, + rain_accum_period=pd.Timedelta("2h"), + ) assert np.allclose(result, expected_output_3) @pytest.fixture def greensboro_rain(): # get TMY3 data with rain - greensboro, _ = read_tmy3(DATA_DIR / '723170TYA.CSV', coerce_year=1990, - map_variables=True) - return greensboro['Lprecip depth (mm)'] + greensboro, _ = read_tmy3( + DATA_DIR / "723170TYA.CSV", coerce_year=1990, map_variables=True + ) + return greensboro["Lprecip depth (mm)"] @pytest.fixture def expected_kimber_nowash(): return pd.read_csv( - DATA_DIR / 'greensboro_kimber_soil_nowash.dat', - parse_dates=True, index_col='timestamp') + DATA_DIR / "greensboro_kimber_soil_nowash.dat", + parse_dates=True, + index_col="timestamp", + ) def test_kimber_nowash(greensboro_rain, expected_kimber_nowash): @@ -166,26 +367,30 @@ def test_kimber_nowash(greensboro_rain, expected_kimber_nowash): # calculate soiling with no wash dates nowash = kimber(greensboro_rain) # test no washes - assert np.allclose(nowash.values, expected_kimber_nowash['soiling'].values) + assert np.allclose(nowash.values, expected_kimber_nowash["soiling"].values) @pytest.fixture def expected_kimber_manwash(): return pd.read_csv( - DATA_DIR / 'greensboro_kimber_soil_manwash.dat', - parse_dates=True, index_col='timestamp') + DATA_DIR / "greensboro_kimber_soil_manwash.dat", + parse_dates=True, + index_col="timestamp", + ) def test_kimber_manwash(greensboro_rain, expected_kimber_manwash): """Test Kimber soiling model with a manual wash""" # a manual wash date - manwash = [datetime.date(1990, 2, 15), ] + manwash = [ + datetime.date(1990, 2, 15), + ] # calculate soiling with manual wash manwash = kimber(greensboro_rain, manual_wash_dates=manwash) # test manual wash assert np.allclose( - manwash.values, - expected_kimber_manwash['soiling'].values) + manwash.values, expected_kimber_manwash["soiling"].values + ) @pytest.fixture @@ -193,7 +398,7 @@ def expected_kimber_norain(): # expected soiling reaches maximum soiling_loss_rate = 0.0015 max_loss_rate = 0.3 - norain = np.ones(8760) * soiling_loss_rate/24 + norain = np.ones(8760) * soiling_loss_rate / 24 norain[0] = 0.0 norain = np.cumsum(norain) return np.where(norain > max_loss_rate, max_loss_rate, norain) @@ -214,7 +419,7 @@ def expected_kimber_initial_soil(): # expected soiling reaches maximum soiling_loss_rate = 0.0015 max_loss_rate = 0.3 - norain = np.ones(8760) * soiling_loss_rate/24 + norain = np.ones(8760) * soiling_loss_rate / 24 norain[0] = 0.1 norain = np.cumsum(norain) return np.where(norain > max_loss_rate, max_loss_rate, norain) diff --git a/pvlib/tests/test_solarposition.py b/pvlib/tests/test_solarposition.py index 88093e05f9..52aa797aa6 100644 --- a/pvlib/tests/test_solarposition.py +++ b/pvlib/tests/test_solarposition.py @@ -14,14 +14,20 @@ from pvlib import solarposition, spa from .conftest import ( - requires_ephem, requires_spa_c, requires_numba, requires_pandas_2_0 + requires_ephem, + requires_spa_c, + requires_numba, + requires_pandas_2_0, ) # setup times and locations to be tested. -times = pd.date_range(start=datetime.datetime(2014, 6, 24), - end=datetime.datetime(2014, 6, 26), freq='15min') +times = pd.date_range( + start=datetime.datetime(2014, 6, 24), + end=datetime.datetime(2014, 6, 26), + freq="15min", +) -tus = Location(32.2, -111, 'US/Arizona', 700) # no DST issues possible +tus = Location(32.2, -111, "US/Arizona", 700) # no DST issues possible times_localized = times.tz_localize(tus.tz) tol = 5 @@ -29,158 +35,263 @@ @pytest.fixture() def expected_solpos_multi(): - return pd.DataFrame({'elevation': [39.872046, 39.505196], - 'apparent_zenith': [50.111622, 50.478260], - 'azimuth': [194.340241, 194.311132], - 'apparent_elevation': [39.888378, 39.521740]}, - index=['2003-10-17T12:30:30Z', '2003-10-18T12:30:30Z']) + return pd.DataFrame( + { + "elevation": [39.872046, 39.505196], + "apparent_zenith": [50.111622, 50.478260], + "azimuth": [194.340241, 194.311132], + "apparent_elevation": [39.888378, 39.521740], + }, + index=["2003-10-17T12:30:30Z", "2003-10-18T12:30:30Z"], + ) @pytest.fixture() def expected_rise_set_spa(): # for Golden, CO, from NREL SPA website - times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2), - datetime.datetime(2015, 8, 2), - ]).tz_localize('MST') - sunrise = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 7, 21, 55), - datetime.datetime(2015, 8, 2, 5, 0, 27) - ]).tz_localize('MST').tolist() - sunset = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 16, 47, 43), - datetime.datetime(2015, 8, 2, 19, 13, 58) - ]).tz_localize('MST').tolist() - transit = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 12, 4, 45), - datetime.datetime(2015, 8, 2, 12, 6, 58) - ]).tz_localize('MST').tolist() - return pd.DataFrame({'sunrise': sunrise, - 'sunset': sunset, - 'transit': transit}, - index=times) + times = pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2), + datetime.datetime(2015, 8, 2), + ] + ).tz_localize("MST") + sunrise = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2, 7, 21, 55), + datetime.datetime(2015, 8, 2, 5, 0, 27), + ] + ) + .tz_localize("MST") + .tolist() + ) + sunset = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2, 16, 47, 43), + datetime.datetime(2015, 8, 2, 19, 13, 58), + ] + ) + .tz_localize("MST") + .tolist() + ) + transit = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2, 12, 4, 45), + datetime.datetime(2015, 8, 2, 12, 6, 58), + ] + ) + .tz_localize("MST") + .tolist() + ) + return pd.DataFrame( + {"sunrise": sunrise, "sunset": sunset, "transit": transit}, index=times + ) @pytest.fixture() def expected_rise_set_ephem(): # for Golden, CO, from USNO websites - times = pd.DatetimeIndex([datetime.datetime(2015, 1, 1), - datetime.datetime(2015, 1, 2), - datetime.datetime(2015, 1, 3), - datetime.datetime(2015, 8, 2), - ]).tz_localize('MST') - sunrise = pd.DatetimeIndex([datetime.datetime(2015, 1, 1, 7, 22, 0), - datetime.datetime(2015, 1, 2, 7, 22, 0), - datetime.datetime(2015, 1, 3, 7, 22, 0), - datetime.datetime(2015, 8, 2, 5, 0, 0) - ]).tz_localize('MST').tolist() - sunset = pd.DatetimeIndex([datetime.datetime(2015, 1, 1, 16, 47, 0), - datetime.datetime(2015, 1, 2, 16, 48, 0), - datetime.datetime(2015, 1, 3, 16, 49, 0), - datetime.datetime(2015, 8, 2, 19, 13, 0) - ]).tz_localize('MST').tolist() - transit = pd.DatetimeIndex([datetime.datetime(2015, 1, 1, 12, 4, 0), - datetime.datetime(2015, 1, 2, 12, 5, 0), - datetime.datetime(2015, 1, 3, 12, 5, 0), - datetime.datetime(2015, 8, 2, 12, 7, 0) - ]).tz_localize('MST').tolist() - return pd.DataFrame({'sunrise': sunrise, - 'sunset': sunset, - 'transit': transit}, - index=times) + times = pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 1), + datetime.datetime(2015, 1, 2), + datetime.datetime(2015, 1, 3), + datetime.datetime(2015, 8, 2), + ] + ).tz_localize("MST") + sunrise = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 1, 7, 22, 0), + datetime.datetime(2015, 1, 2, 7, 22, 0), + datetime.datetime(2015, 1, 3, 7, 22, 0), + datetime.datetime(2015, 8, 2, 5, 0, 0), + ] + ) + .tz_localize("MST") + .tolist() + ) + sunset = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 1, 16, 47, 0), + datetime.datetime(2015, 1, 2, 16, 48, 0), + datetime.datetime(2015, 1, 3, 16, 49, 0), + datetime.datetime(2015, 8, 2, 19, 13, 0), + ] + ) + .tz_localize("MST") + .tolist() + ) + transit = ( + pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 1, 12, 4, 0), + datetime.datetime(2015, 1, 2, 12, 5, 0), + datetime.datetime(2015, 1, 3, 12, 5, 0), + datetime.datetime(2015, 8, 2, 12, 7, 0), + ] + ) + .tz_localize("MST") + .tolist() + ) + return pd.DataFrame( + {"sunrise": sunrise, "sunset": sunset, "transit": transit}, index=times + ) # the physical tests are run at the same time as the NREL SPA test. # pyephem reproduces the NREL result to 2 decimal places. # this doesn't mean that one code is better than the other. + @requires_spa_c def test_spa_c_physical(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.spa_c(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) + ephem_data = solarposition.spa_c( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) @requires_spa_c def test_spa_c_physical_dst(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.spa_c(times, golden.latitude, - golden.longitude, - pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.spa_c( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) def test_spa_python_numpy_physical(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numpy') + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) + ephem_data = solarposition.spa_python( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numpy", + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) def test_spa_python_numpy_physical_dst(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, - pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numpy') + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.spa_python( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numpy", + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -@pytest.mark.parametrize('delta_t', [65.0, None, np.array([65, 65])]) +@pytest.mark.parametrize("delta_t", [65.0, None, np.array([65, 65])]) def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t): # solution from NREL SAP web calculator - south = Location(-35.0, 0.0, tz='UTC') - times = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 0), - datetime.datetime(2004, 12, 4, 0)] - ).tz_localize('UTC') - sunrise = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 7, 8, 15), - datetime.datetime(2004, 12, 4, 4, 38, 57)] - ).tz_localize('UTC').tolist() - sunset = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 17, 1, 4), - datetime.datetime(2004, 12, 4, 19, 2, 3)] - ).tz_localize('UTC').tolist() - transit = pd.DatetimeIndex([datetime.datetime(1996, 7, 5, 12, 4, 36), - datetime.datetime(2004, 12, 4, 11, 50, 22)] - ).tz_localize('UTC').tolist() - frame = pd.DataFrame({'sunrise': sunrise, - 'sunset': sunset, - 'transit': transit}, index=times) - - result = solarposition.sun_rise_set_transit_spa(times, south.latitude, - south.longitude, - delta_t=delta_t) + south = Location(-35.0, 0.0, tz="UTC") + times = pd.DatetimeIndex( + [datetime.datetime(1996, 7, 5, 0), datetime.datetime(2004, 12, 4, 0)] + ).tz_localize("UTC") + sunrise = ( + pd.DatetimeIndex( + [ + datetime.datetime(1996, 7, 5, 7, 8, 15), + datetime.datetime(2004, 12, 4, 4, 38, 57), + ] + ) + .tz_localize("UTC") + .tolist() + ) + sunset = ( + pd.DatetimeIndex( + [ + datetime.datetime(1996, 7, 5, 17, 1, 4), + datetime.datetime(2004, 12, 4, 19, 2, 3), + ] + ) + .tz_localize("UTC") + .tolist() + ) + transit = ( + pd.DatetimeIndex( + [ + datetime.datetime(1996, 7, 5, 12, 4, 36), + datetime.datetime(2004, 12, 4, 11, 50, 22), + ] + ) + .tz_localize("UTC") + .tolist() + ) + frame = pd.DataFrame( + {"sunrise": sunrise, "sunset": sunset, "transit": transit}, index=times + ) + + result = solarposition.sun_rise_set_transit_spa( + times, south.latitude, south.longitude, delta_t=delta_t + ) result_rounded = pd.DataFrame(index=result.index) # need to iterate because to_datetime does not accept 2D data # the rounding fails on pandas < 0.17 for col, data in result.items(): - result_rounded[col] = data.dt.round('1s') + result_rounded[col] = data.dt.round("1s") assert_frame_equal(frame, result_rounded) # test for Golden, CO compare to NREL SPA result = solarposition.sun_rise_set_transit_spa( - expected_rise_set_spa.index, golden.latitude, golden.longitude, - delta_t=delta_t) + expected_rise_set_spa.index, + golden.latitude, + golden.longitude, + delta_t=delta_t, + ) # round to nearest minute result_rounded = pd.DataFrame(index=result.index) # need to iterate because to_datetime does not accept 2D data for col, data in result.items(): - result_rounded[col] = data.dt.round('s').tz_convert('MST') + result_rounded[col] = data.dt.round("s").tz_convert("MST") assert_frame_equal(expected_rise_set_spa, result_rounded) @@ -189,157 +300,222 @@ def test_sun_rise_set_transit_spa(expected_rise_set_spa, golden, delta_t): def test_sun_rise_set_transit_ephem(expected_rise_set_ephem, golden): # test for Golden, CO compare to USNO, using local midnight result = solarposition.sun_rise_set_transit_ephem( - expected_rise_set_ephem.index, golden.latitude, golden.longitude, - next_or_previous='next', altitude=golden.altitude, pressure=0, - temperature=11, horizon='-0:34') + expected_rise_set_ephem.index, + golden.latitude, + golden.longitude, + next_or_previous="next", + altitude=golden.altitude, + pressure=0, + temperature=11, + horizon="-0:34", + ) # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): - result_rounded[col] = data.dt.round('min').tz_convert('MST') + result_rounded[col] = data.dt.round("min").tz_convert("MST") assert_frame_equal(expected_rise_set_ephem, result_rounded) # test next sunrise/sunset with times - times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 3, 0, 0), - datetime.datetime(2015, 1, 2, 10, 15, 0), - datetime.datetime(2015, 1, 2, 15, 3, 0), - datetime.datetime(2015, 1, 2, 21, 6, 7) - ]).tz_localize('MST') - expected = pd.DataFrame(index=times, - columns=['sunrise', 'sunset'], - dtype='datetime64[ns]') - idx_sunrise = pd.to_datetime(['2015-01-02', '2015-01-03', '2015-01-03', - '2015-01-03']).tz_localize('MST') - expected['sunrise'] = \ - expected_rise_set_ephem.loc[idx_sunrise, 'sunrise'].tolist() - idx_sunset = pd.to_datetime(['2015-01-02', '2015-01-02', '2015-01-02', - '2015-01-03']).tz_localize('MST') - expected['sunset'] = \ - expected_rise_set_ephem.loc[idx_sunset, 'sunset'].tolist() - idx_transit = pd.to_datetime(['2015-01-02', '2015-01-02', '2015-01-03', - '2015-01-03']).tz_localize('MST') - expected['transit'] = \ - expected_rise_set_ephem.loc[idx_transit, 'transit'].tolist() - - result = solarposition.sun_rise_set_transit_ephem(times, - golden.latitude, - golden.longitude, - next_or_previous='next', - altitude=golden.altitude, - pressure=0, - temperature=11, - horizon='-0:34') + times = pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2, 3, 0, 0), + datetime.datetime(2015, 1, 2, 10, 15, 0), + datetime.datetime(2015, 1, 2, 15, 3, 0), + datetime.datetime(2015, 1, 2, 21, 6, 7), + ] + ).tz_localize("MST") + expected = pd.DataFrame( + index=times, columns=["sunrise", "sunset"], dtype="datetime64[ns]" + ) + idx_sunrise = pd.to_datetime( + ["2015-01-02", "2015-01-03", "2015-01-03", "2015-01-03"] + ).tz_localize("MST") + expected["sunrise"] = expected_rise_set_ephem.loc[ + idx_sunrise, "sunrise" + ].tolist() + idx_sunset = pd.to_datetime( + ["2015-01-02", "2015-01-02", "2015-01-02", "2015-01-03"] + ).tz_localize("MST") + expected["sunset"] = expected_rise_set_ephem.loc[ + idx_sunset, "sunset" + ].tolist() + idx_transit = pd.to_datetime( + ["2015-01-02", "2015-01-02", "2015-01-03", "2015-01-03"] + ).tz_localize("MST") + expected["transit"] = expected_rise_set_ephem.loc[ + idx_transit, "transit" + ].tolist() + + result = solarposition.sun_rise_set_transit_ephem( + times, + golden.latitude, + golden.longitude, + next_or_previous="next", + altitude=golden.altitude, + pressure=0, + temperature=11, + horizon="-0:34", + ) # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): - result_rounded[col] = data.dt.round('min').tz_convert('MST') + result_rounded[col] = data.dt.round("min").tz_convert("MST") assert_frame_equal(expected, result_rounded) # test previous sunrise/sunset with times - times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 3, 0, 0), - datetime.datetime(2015, 1, 2, 10, 15, 0), - datetime.datetime(2015, 1, 3, 3, 0, 0), - datetime.datetime(2015, 1, 3, 13, 6, 7) - ]).tz_localize('MST') - expected = pd.DataFrame(index=times, - columns=['sunrise', 'sunset'], - dtype='datetime64[ns]') - idx_sunrise = pd.to_datetime(['2015-01-01', '2015-01-02', '2015-01-02', - '2015-01-03']).tz_localize('MST') - expected['sunrise'] = \ - expected_rise_set_ephem.loc[idx_sunrise, 'sunrise'].tolist() - idx_sunset = pd.to_datetime(['2015-01-01', '2015-01-01', '2015-01-02', - '2015-01-02']).tz_localize('MST') - expected['sunset'] = \ - expected_rise_set_ephem.loc[idx_sunset, 'sunset'].tolist() - idx_transit = pd.to_datetime(['2015-01-01', '2015-01-01', '2015-01-02', - '2015-01-03']).tz_localize('MST') - expected['transit'] = \ - expected_rise_set_ephem.loc[idx_transit, 'transit'].tolist() + times = pd.DatetimeIndex( + [ + datetime.datetime(2015, 1, 2, 3, 0, 0), + datetime.datetime(2015, 1, 2, 10, 15, 0), + datetime.datetime(2015, 1, 3, 3, 0, 0), + datetime.datetime(2015, 1, 3, 13, 6, 7), + ] + ).tz_localize("MST") + expected = pd.DataFrame( + index=times, columns=["sunrise", "sunset"], dtype="datetime64[ns]" + ) + idx_sunrise = pd.to_datetime( + ["2015-01-01", "2015-01-02", "2015-01-02", "2015-01-03"] + ).tz_localize("MST") + expected["sunrise"] = expected_rise_set_ephem.loc[ + idx_sunrise, "sunrise" + ].tolist() + idx_sunset = pd.to_datetime( + ["2015-01-01", "2015-01-01", "2015-01-02", "2015-01-02"] + ).tz_localize("MST") + expected["sunset"] = expected_rise_set_ephem.loc[ + idx_sunset, "sunset" + ].tolist() + idx_transit = pd.to_datetime( + ["2015-01-01", "2015-01-01", "2015-01-02", "2015-01-03"] + ).tz_localize("MST") + expected["transit"] = expected_rise_set_ephem.loc[ + idx_transit, "transit" + ].tolist() result = solarposition.sun_rise_set_transit_ephem( times, - golden.latitude, golden.longitude, next_or_previous='previous', - altitude=golden.altitude, pressure=0, temperature=11, horizon='-0:34') + golden.latitude, + golden.longitude, + next_or_previous="previous", + altitude=golden.altitude, + pressure=0, + temperature=11, + horizon="-0:34", + ) # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): - result_rounded[col] = data.dt.round('min').tz_convert('MST') + result_rounded[col] = data.dt.round("min").tz_convert("MST") assert_frame_equal(expected, result_rounded) # test with different timezone - times = times.tz_convert('UTC') - expected = expected.tz_convert('UTC') # resuse result from previous + times = times.tz_convert("UTC") + expected = expected.tz_convert("UTC") # resuse result from previous for col, data in expected.items(): - expected[col] = data.dt.tz_convert('UTC') + expected[col] = data.dt.tz_convert("UTC") result = solarposition.sun_rise_set_transit_ephem( times, - golden.latitude, golden.longitude, next_or_previous='previous', - altitude=golden.altitude, pressure=0, temperature=11, horizon='-0:34') + golden.latitude, + golden.longitude, + next_or_previous="previous", + altitude=golden.altitude, + pressure=0, + temperature=11, + horizon="-0:34", + ) # round to nearest minute result_rounded = pd.DataFrame(index=result.index) for col, data in result.items(): - result_rounded[col] = data.dt.round('min').tz_convert(times.tz) + result_rounded[col] = data.dt.round("min").tz_convert(times.tz) assert_frame_equal(expected, result_rounded) @requires_ephem def test_sun_rise_set_transit_ephem_error(expected_rise_set_ephem, golden): with pytest.raises(ValueError): - solarposition.sun_rise_set_transit_ephem(expected_rise_set_ephem.index, - golden.latitude, - golden.longitude, - next_or_previous='other') + solarposition.sun_rise_set_transit_ephem( + expected_rise_set_ephem.index, + golden.latitude, + golden.longitude, + next_or_previous="other", + ) tz_naive = pd.DatetimeIndex([datetime.datetime(2015, 1, 2, 3, 0, 0)]) with pytest.raises(ValueError): - solarposition.sun_rise_set_transit_ephem(tz_naive, - golden.latitude, - golden.longitude, - next_or_previous='next') + solarposition.sun_rise_set_transit_ephem( + tz_naive, + golden.latitude, + golden.longitude, + next_or_previous="next", + ) @requires_ephem def test_sun_rise_set_transit_ephem_horizon(golden): - times = pd.DatetimeIndex([datetime.datetime(2016, 1, 3, 0, 0, 0) - ]).tz_localize('MST') + times = pd.DatetimeIndex( + [datetime.datetime(2016, 1, 3, 0, 0, 0)] + ).tz_localize("MST") # center of sun disk center = solarposition.sun_rise_set_transit_ephem( - times, - latitude=golden.latitude, longitude=golden.longitude) + times, latitude=golden.latitude, longitude=golden.longitude + ) edge = solarposition.sun_rise_set_transit_ephem( times, - latitude=golden.latitude, longitude=golden.longitude, horizon='-0:34') - result_rounded = (edge['sunrise'] - center['sunrise']).dt.round('min') - - sunrise_delta = datetime.datetime(2016, 1, 3, 7, 17, 11) - \ - datetime.datetime(2016, 1, 3, 7, 21, 33) - expected = pd.Series(index=times, - data=[sunrise_delta], - name='sunrise').dt.round('min') + latitude=golden.latitude, + longitude=golden.longitude, + horizon="-0:34", + ) + result_rounded = (edge["sunrise"] - center["sunrise"]).dt.round("min") + + sunrise_delta = datetime.datetime( + 2016, 1, 3, 7, 17, 11 + ) - datetime.datetime(2016, 1, 3, 7, 21, 33) + expected = pd.Series( + index=times, data=[sunrise_delta], name="sunrise" + ).dt.round("min") assert_series_equal(expected, result_rounded) @requires_ephem def test_pyephem_physical(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.pyephem(times, golden_mst.latitude, - golden_mst.longitude, pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) + ephem_data = solarposition.pyephem( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times - assert_frame_equal(expected_solpos.round(2), - ephem_data[expected_solpos.columns].round(2)) + assert_frame_equal( + expected_solpos.round(2), ephem_data[expected_solpos.columns].round(2) + ) @requires_ephem def test_pyephem_physical_dst(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.pyephem(times, golden.latitude, - golden.longitude, pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.pyephem( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times - assert_frame_equal(expected_solpos.round(2), - ephem_data[expected_solpos.columns].round(2)) + assert_frame_equal( + expected_solpos.round(2), ephem_data[expected_solpos.columns].round(2) + ) @requires_ephem @@ -354,36 +530,51 @@ def test_calc_time(): loc = tus loc.pressure = 0 actual_time = pytz.timezone(loc.tz).localize( - datetime.datetime(2014, 10, 10, 8, 30)) + datetime.datetime(2014, 10, 10, 8, 30) + ) lb = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, tol)) ub = pytz.timezone(loc.tz).localize(datetime.datetime(2014, 10, 10, 10)) - alt = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, - 'alt', math.radians(24.7)) - az = solarposition.calc_time(lb, ub, loc.latitude, loc.longitude, - 'az', math.radians(116.3)) + alt = solarposition.calc_time( + lb, ub, loc.latitude, loc.longitude, "alt", math.radians(24.7) + ) + az = solarposition.calc_time( + lb, ub, loc.latitude, loc.longitude, "az", math.radians(116.3) + ) actual_timestamp = (actual_time - epoch_dt).total_seconds() - assert_allclose((alt.replace(second=0, microsecond=0) - - epoch_dt).total_seconds(), actual_timestamp) - assert_allclose((az.replace(second=0, microsecond=0) - - epoch_dt).total_seconds(), actual_timestamp) + assert_allclose( + (alt.replace(second=0, microsecond=0) - epoch_dt).total_seconds(), + actual_timestamp, + ) + assert_allclose( + (az.replace(second=0, microsecond=0) - epoch_dt).total_seconds(), + actual_timestamp, + ) @requires_ephem def test_earthsun_distance(): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D') + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), periods=1, freq="D" + ) distance = solarposition.pyephem_earthsun_distance(times).values[0] assert_allclose(1, distance, atol=0.1) def test_ephemeris_physical(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) - ephem_data = solarposition.ephemeris(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) + ephem_data = solarposition.ephemeris( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) @@ -391,11 +582,19 @@ def test_ephemeris_physical(expected_solpos, golden_mst): def test_ephemeris_physical_dst(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.ephemeris(times, golden.latitude, - golden.longitude, pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.ephemeris( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) @@ -403,12 +602,16 @@ def test_ephemeris_physical_dst(expected_solpos, golden): def test_ephemeris_physical_no_tz(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 19, 30, 30), - periods=1, freq='D') - ephem_data = solarposition.ephemeris(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 19, 30, 30), periods=1, freq="D" + ) + ephem_data = solarposition.ephemeris( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + ) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) @@ -416,34 +619,72 @@ def test_ephemeris_physical_no_tz(expected_solpos, golden_mst): def test_get_solarposition_error(golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) with pytest.raises(ValueError): - solarposition.get_solarposition(times, golden.latitude, - golden.longitude, - pressure=82000, - temperature=11, - method='error this') - - -@pytest.mark.parametrize("pressure, expected", [ - (82000, 'expected_solpos'), - (90000, pd.DataFrame( - np.array([[39.88997, 50.11003, 194.34024, 39.87205, 14.64151, - 50.12795]]), - columns=['apparent_elevation', 'apparent_zenith', 'azimuth', - 'elevation', 'equation_of_time', 'zenith'], - index=['2003-10-17T12:30:30Z'])) - ]) + solarposition.get_solarposition( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + method="error this", + ) + + +@pytest.mark.parametrize( + "pressure, expected", + [ + (82000, "expected_solpos"), + ( + 90000, + pd.DataFrame( + np.array( + [ + [ + 39.88997, + 50.11003, + 194.34024, + 39.87205, + 14.64151, + 50.12795, + ] + ] + ), + columns=[ + "apparent_elevation", + "apparent_zenith", + "azimuth", + "elevation", + "equation_of_time", + "zenith", + ], + index=["2003-10-17T12:30:30Z"], + ), + ), + ], +) def test_get_solarposition_pressure( - pressure, expected, golden, expected_solpos): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, - pressure=pressure, - temperature=11) - if isinstance(expected, str) and expected == 'expected_solpos': + pressure, expected, golden, expected_solpos +): + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.get_solarposition( + times, + golden.latitude, + golden.longitude, + pressure=pressure, + temperature=11, + ) + if isinstance(expected, str) and expected == "expected_solpos": expected = expected_solpos this_expected = expected.copy() this_expected.index = times @@ -452,24 +693,55 @@ def test_get_solarposition_pressure( assert_frame_equal(this_expected, ephem_data[this_expected.columns]) -@pytest.mark.parametrize("altitude, expected", [ - (1830.14, 'expected_solpos'), - (2000, pd.DataFrame( - np.array([[39.88788, 50.11212, 194.34024, 39.87205, 14.64151, - 50.12795]]), - columns=['apparent_elevation', 'apparent_zenith', 'azimuth', - 'elevation', 'equation_of_time', 'zenith'], - index=['2003-10-17T12:30:30Z'])) - ]) +@pytest.mark.parametrize( + "altitude, expected", + [ + (1830.14, "expected_solpos"), + ( + 2000, + pd.DataFrame( + np.array( + [ + [ + 39.88788, + 50.11212, + 194.34024, + 39.87205, + 14.64151, + 50.12795, + ] + ] + ), + columns=[ + "apparent_elevation", + "apparent_zenith", + "azimuth", + "elevation", + "equation_of_time", + "zenith", + ], + index=["2003-10-17T12:30:30Z"], + ), + ), + ], +) def test_get_solarposition_altitude( - altitude, expected, golden, expected_solpos): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, - altitude=altitude, - temperature=11) - if isinstance(expected, str) and expected == 'expected_solpos': + altitude, expected, golden, expected_solpos +): + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.get_solarposition( + times, + golden.latitude, + golden.longitude, + altitude=altitude, + temperature=11, + ) + if isinstance(expected, str) and expected == "expected_solpos": expected = expected_solpos this_expected = expected.copy() this_expected.index = times @@ -478,28 +750,39 @@ def test_get_solarposition_altitude( assert_frame_equal(this_expected, ephem_data[this_expected.columns]) -@pytest.mark.parametrize("delta_t, method", [ - (None, 'nrel_numba'), - (67.0, 'nrel_numba'), - (np.array([67.0, 67.0]), 'nrel_numba'), - # minimize reloads, with numpy being last - (None, 'nrel_numpy'), - (67.0, 'nrel_numpy'), - (np.array([67.0, 67.0]), 'nrel_numpy'), -]) -def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi, - golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=2, freq='D', tz=golden.tz) +@pytest.mark.parametrize( + "delta_t, method", + [ + (None, "nrel_numba"), + (67.0, "nrel_numba"), + (np.array([67.0, 67.0]), "nrel_numba"), + # minimize reloads, with numpy being last + (None, "nrel_numpy"), + (67.0, "nrel_numpy"), + (np.array([67.0, 67.0]), "nrel_numpy"), + ], +) +def test_get_solarposition_deltat( + delta_t, method, expected_solpos_multi, golden +): + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=2, + freq="D", + tz=golden.tz, + ) with warnings.catch_warnings(): # don't warn on method reload warnings.simplefilter("ignore") - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, - pressure=82000, - delta_t=delta_t, - temperature=11, - method=method) + ephem_data = solarposition.get_solarposition( + times, + golden.latitude, + golden.longitude, + pressure=82000, + delta_t=delta_t, + temperature=11, + method=method, + ) this_expected = expected_solpos_multi this_expected.index = times this_expected = np.round(this_expected, 5) @@ -507,7 +790,7 @@ def test_get_solarposition_deltat(delta_t, method, expected_solpos_multi, assert_frame_equal(this_expected, ephem_data[this_expected.columns]) -@pytest.mark.parametrize("method", ['nrel_numba', 'nrel_numpy']) +@pytest.mark.parametrize("method", ["nrel_numba", "nrel_numpy"]) def test_spa_array_delta_t(method): # make sure that time-varying delta_t produces different answers times = pd.to_datetime(["2019-01-01", "2019-01-01"]).tz_localize("UTC") @@ -515,18 +798,23 @@ def test_spa_array_delta_t(method): with warnings.catch_warnings(): # don't warn on method reload warnings.simplefilter("ignore") - ephem_data = solarposition.get_solarposition(times, 40, -80, - delta_t=np.array([67, 0]), - method=method) + ephem_data = solarposition.get_solarposition( + times, 40, -80, delta_t=np.array([67, 0]), method=method + ) - assert_series_equal(ephem_data['azimuth'], expected, check_names=False) + assert_series_equal(ephem_data["azimuth"], expected, check_names=False) def test_get_solarposition_no_kwargs(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.get_solarposition( + times, golden.latitude, golden.longitude + ) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) @@ -535,40 +823,52 @@ def test_get_solarposition_no_kwargs(expected_solpos, golden): @requires_ephem def test_get_solarposition_method_pyephem(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) - ephem_data = solarposition.get_solarposition(times, golden.latitude, - golden.longitude, - method='pyephem') + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) + ephem_data = solarposition.get_solarposition( + times, golden.latitude, golden.longitude, method="pyephem" + ) expected_solpos.index = times expected_solpos = np.round(expected_solpos, 2) ephem_data = np.round(ephem_data, 2) assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) -@pytest.mark.parametrize('delta_t', [64.0, None, np.array([64, 64])]) +@pytest.mark.parametrize("delta_t", [64.0, None, np.array([64, 64])]) def test_nrel_earthsun_distance(delta_t): - times = pd.DatetimeIndex([datetime.datetime(2015, 1, 2), - datetime.datetime(2015, 8, 2)] - ).tz_localize('MST') + times = pd.DatetimeIndex( + [datetime.datetime(2015, 1, 2), datetime.datetime(2015, 8, 2)] + ).tz_localize("MST") result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t) - expected = pd.Series(np.array([0.983289204601, 1.01486146446]), - index=times) + expected = pd.Series( + np.array([0.983289204601, 1.01486146446]), index=times + ) assert_series_equal(expected, result) if np.size(delta_t) == 1: # skip the array delta_t times = datetime.datetime(2015, 1, 2) result = solarposition.nrel_earthsun_distance(times, delta_t=delta_t) - expected = pd.Series(np.array([0.983289204601]), - index=pd.DatetimeIndex([times, ])) + expected = pd.Series( + np.array([0.983289204601]), + index=pd.DatetimeIndex( + [ + times, + ] + ), + ) assert_series_equal(expected, result) def test_equation_of_time(): - times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00", - freq="h") + times = pd.date_range( + start="1/1/2015 0:00", end="12/31/2015 23:00", freq="h" + ) output = solarposition.spa_python(times, 37.8, -122.25, 100) - eot = output['equation_of_time'] + eot = output["equation_of_time"] eot_rng = eot.max() - eot.min() # range of values, around 30 minutes eot_1 = solarposition.equation_of_time_spencer71(times.dayofyear) eot_2 = solarposition.equation_of_time_pvcdrom(times.dayofyear) @@ -577,14 +877,23 @@ def test_equation_of_time(): def test_declination(): - times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00", - freq="h") + times = pd.date_range( + start="1/1/2015 0:00", end="12/31/2015 23:00", freq="h" + ) atmos_refract = 0.5667 delta_t = spa.calculate_deltat(times.year, times.month) unixtime = np.array([calendar.timegm(t.timetuple()) for t in times]) - _, _, declination = spa.solar_position(unixtime, 37.8, -122.25, 100, - 1013.25, 25, delta_t, atmos_refract, - sst=True) + _, _, declination = spa.solar_position( + unixtime, + 37.8, + -122.25, + 100, + 1013.25, + 25, + delta_t, + atmos_refract, + sst=True, + ) declination = np.deg2rad(declination) declination_rng = declination.max() - declination.min() declination_1 = solarposition.declination_cooper69(times.dayofyear) @@ -596,13 +905,14 @@ def test_declination(): def test_analytical_zenith(): - times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00", - freq="h").tz_localize('Etc/GMT+8') - times_utc = times.tz_convert('UTC') + times = pd.date_range( + start="1/1/2015 0:00", end="12/31/2015 23:00", freq="h" + ).tz_localize("Etc/GMT+8") + times_utc = times.tz_convert("UTC") lat, lon = 37.8, -122.25 lat_rad = np.deg2rad(lat) output = solarposition.spa_python(times, lat, lon, 100) - solar_zenith = np.deg2rad(output['zenith']) # spa + solar_zenith = np.deg2rad(output["zenith"]) # spa # spencer eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) @@ -618,54 +928,63 @@ def test_analytical_zenith(): def test_analytical_azimuth(): - times = pd.date_range(start="1/1/2015 0:00", end="12/31/2015 23:00", - freq="h").tz_localize('Etc/GMT+8') - times_utc = times.tz_convert('UTC') + times = pd.date_range( + start="1/1/2015 0:00", end="12/31/2015 23:00", freq="h" + ).tz_localize("Etc/GMT+8") + times_utc = times.tz_convert("UTC") lat, lon = 37.8, -122.25 lat_rad = np.deg2rad(lat) output = solarposition.spa_python(times, lat, lon, 100) - solar_azimuth = np.deg2rad(output['azimuth']) # spa - solar_zenith = np.deg2rad(output['zenith']) + solar_azimuth = np.deg2rad(output["azimuth"]) # spa + solar_zenith = np.deg2rad(output["zenith"]) # spencer eot = solarposition.equation_of_time_spencer71(times_utc.dayofyear) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) decl = solarposition.declination_spencer71(times_utc.dayofyear) zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) - azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, - decl, zenith) + azimuth_1 = solarposition.solar_azimuth_analytical( + lat_rad, hour_angle, decl, zenith + ) # pvcdrom and cooper eot = solarposition.equation_of_time_pvcdrom(times_utc.dayofyear) hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot)) decl = solarposition.declination_cooper69(times_utc.dayofyear) zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) - azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, - decl, zenith) + azimuth_2 = solarposition.solar_azimuth_analytical( + lat_rad, hour_angle, decl, zenith + ) - idx = np.where(solar_zenith < np.pi/2) + idx = np.where(solar_zenith < np.pi / 2) assert np.allclose(azimuth_1[idx], solar_azimuth.values[idx], atol=0.01) assert np.allclose(azimuth_2[idx], solar_azimuth.values[idx], atol=0.017) # test for NaN values at boundary conditions (PR #431) - test_angles = np.radians(np.array( - [[ 0., -180., -20.], - [ 0., 0., -5.], - [ 0., 0., 0.], - [ 0., 0., 15.], - [ 0., 180., 20.], - [ 30., 0., -20.], - [ 30., 0., -5.], - [ 30., 0., 0.], - [ 30., 180., 5.], - [ 30., 0., 10.], - [ -30., 0., -20.], - [ -30., 0., -15.], - [ -30., 0., 0.], - [ -30., -180., 5.], - [ -30., 180., 10.]])) + test_angles = np.radians( + np.array( + [ + [0.0, -180.0, -20.0], + [0.0, 0.0, -5.0], + [0.0, 0.0, 0.0], + [0.0, 0.0, 15.0], + [0.0, 180.0, 20.0], + [30.0, 0.0, -20.0], + [30.0, 0.0, -5.0], + [30.0, 0.0, 0.0], + [30.0, 180.0, 5.0], + [30.0, 0.0, 10.0], + [-30.0, 0.0, -20.0], + [-30.0, 0.0, -15.0], + [-30.0, 0.0, 0.0], + [-30.0, -180.0, 5.0], + [-30.0, 180.0, 10.0], + ] + ) + ) zeniths = solarposition.solar_zenith_analytical(*test_angles.T) - azimuths = solarposition.solar_azimuth_analytical(*test_angles.T, - zenith=zeniths) + azimuths = solarposition.solar_azimuth_analytical( + *test_angles.T, zenith=zeniths + ) assert not np.isnan(azimuths).any() @@ -680,11 +999,13 @@ def test_hour_angle(): 1/2/2015,12:04:45,-4.026295,-70.699400,70.512721 """ longitude = -105.1786 # degrees - times = pd.DatetimeIndex([ - '2015-01-02 07:21:55.2132', - '2015-01-02 16:47:42.9828', - '2015-01-02 12:04:44.6340' - ]).tz_localize('Etc/GMT+7') + times = pd.DatetimeIndex( + [ + "2015-01-02 07:21:55.2132", + "2015-01-02 16:47:42.9828", + "2015-01-02 12:04:44.6340", + ] + ).tz_localize("Etc/GMT+7") eot = np.array([-3.935172, -4.117227, -4.026295]) hourangle = solarposition.hour_angle(times, longitude, eot) expected = (-70.682338, 70.72118825000001, 0.000801250) @@ -694,7 +1015,8 @@ def test_hour_angle(): assert np.allclose(hourangle, expected) hours = solarposition._hour_angle_to_hours( - times, hourangle, longitude, eot) + times, hourangle, longitude, eot + ) result = solarposition._times_to_hours_after_local_midnight(times) assert np.allclose(result, hours) @@ -719,12 +1041,14 @@ def test_hour_angle_with_tricky_timezones(): eot = np.array([-3.935172, -4.117227, -4.026295, -4.026295]) longitude = 70.6693 - times = pd.DatetimeIndex([ - '2014-09-06 23:00:00', - '2014-09-07 00:00:00', - '2014-09-07 01:00:00', - '2014-09-07 02:00:00', - ]).tz_localize('America/Santiago', nonexistent='shift_forward') + times = pd.DatetimeIndex( + [ + "2014-09-06 23:00:00", + "2014-09-07 00:00:00", + "2014-09-07 01:00:00", + "2014-09-07 02:00:00", + ] + ).tz_localize("America/Santiago", nonexistent="shift_forward") with pytest.raises(pytz.exceptions.NonExistentTimeError): times.normalize() @@ -733,12 +1057,14 @@ def test_hour_angle_with_tricky_timezones(): solarposition.hour_angle(times, longitude, eot) longitude = 82.3666 - times = pd.DatetimeIndex([ - '2014-11-01 23:00:00', - '2014-11-02 00:00:00', - '2014-11-02 01:00:00', - '2014-11-02 02:00:00', - ]).tz_localize('America/Havana', ambiguous=[True, True, False, False]) + times = pd.DatetimeIndex( + [ + "2014-11-01 23:00:00", + "2014-11-02 00:00:00", + "2014-11-02 01:00:00", + "2014-11-02 02:00:00", + ] + ).tz_localize("America/Havana", ambiguous=[True, True, False, False]) with pytest.raises(pytz.exceptions.AmbiguousTimeError): solarposition.hour_angle(times, longitude, eot) @@ -747,19 +1073,28 @@ def test_hour_angle_with_tricky_timezones(): def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): """Test geometric calculations for sunrise, sunset, and transit times""" times = expected_rise_set_spa.index - times_utc = times.tz_convert('UTC') + times_utc = times.tz_convert("UTC") latitude = golden_mst.latitude longitude = golden_mst.longitude eot = solarposition.equation_of_time_spencer71( - times_utc.dayofyear) # minutes + times_utc.dayofyear + ) # minutes decl = solarposition.declination_spencer71(times_utc.dayofyear) # radians with pytest.raises(ValueError): solarposition.sun_rise_set_transit_geometric( - times.tz_convert(None), latitude=latitude, longitude=longitude, - declination=decl, equation_of_time=eot) + times.tz_convert(None), + latitude=latitude, + longitude=longitude, + declination=decl, + equation_of_time=eot, + ) sr, ss, st = solarposition.sun_rise_set_transit_geometric( - times, latitude=latitude, longitude=longitude, declination=decl, - equation_of_time=eot) + times, + latitude=latitude, + longitude=longitude, + declination=decl, + equation_of_time=eot, + ) # sunrise: 2015-01-02 07:26:39.763224487, 2015-08-02 05:04:35.688533801 # sunset: 2015-01-02 16:41:29.951096777, 2015-08-02 19:09:46.597355085 # transit: 2015-01-02 12:04:04.857160632, 2015-08-02 12:07:11.142944443 @@ -767,73 +1102,92 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): test_sunset = solarposition._times_to_hours_after_local_midnight(ss) test_transit = solarposition._times_to_hours_after_local_midnight(st) # convert expected SPA sunrise, sunset, transit to local datetime indices - expected_sunrise = pd.DatetimeIndex(expected_rise_set_spa.sunrise.values, - tz='UTC').tz_convert(golden_mst.tz) - expected_sunset = pd.DatetimeIndex(expected_rise_set_spa.sunset.values, - tz='UTC').tz_convert(golden_mst.tz) - expected_transit = pd.DatetimeIndex(expected_rise_set_spa.transit.values, - tz='UTC').tz_convert(golden_mst.tz) + expected_sunrise = pd.DatetimeIndex( + expected_rise_set_spa.sunrise.values, tz="UTC" + ).tz_convert(golden_mst.tz) + expected_sunset = pd.DatetimeIndex( + expected_rise_set_spa.sunset.values, tz="UTC" + ).tz_convert(golden_mst.tz) + expected_transit = pd.DatetimeIndex( + expected_rise_set_spa.transit.values, tz="UTC" + ).tz_convert(golden_mst.tz) # convert expected times to hours since midnight as arrays of floats expected_sunrise = solarposition._times_to_hours_after_local_midnight( - expected_sunrise) + expected_sunrise + ) expected_sunset = solarposition._times_to_hours_after_local_midnight( - expected_sunset) + expected_sunset + ) expected_transit = solarposition._times_to_hours_after_local_midnight( - expected_transit) + expected_transit + ) # geometric time has about 4-6 minute error compared to SPA sunset/sunrise expected_sunrise_error = np.array( - [0.07910089555555544, 0.06908014805555496]) # 4.8[min], 4.2[min] + [0.07910089555555544, 0.06908014805555496] + ) # 4.8[min], 4.2[min] expected_sunset_error = np.array( - [-0.1036246955555562, -0.06983406805555603]) # -6.2[min], -4.2[min] + [-0.1036246955555562, -0.06983406805555603] + ) # -6.2[min], -4.2[min] expected_transit_error = np.array( - [-0.011150788888889096, 0.0036508177777765383]) # -40[sec], 13.3[sec] - assert np.allclose(test_sunrise, expected_sunrise, - atol=np.abs(expected_sunrise_error).max()) - assert np.allclose(test_sunset, expected_sunset, - atol=np.abs(expected_sunset_error).max()) - assert np.allclose(test_transit, expected_transit, - atol=np.abs(expected_transit_error).max()) - - -@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern']) + [-0.011150788888889096, 0.0036508177777765383] + ) # -40[sec], 13.3[sec] + assert np.allclose( + test_sunrise, + expected_sunrise, + atol=np.abs(expected_sunrise_error).max(), + ) + assert np.allclose( + test_sunset, expected_sunset, atol=np.abs(expected_sunset_error).max() + ) + assert np.allclose( + test_transit, + expected_transit, + atol=np.abs(expected_transit_error).max(), + ) + + +@pytest.mark.parametrize("tz", [None, "utc", "US/Eastern"]) def test__datetime_to_unixtime(tz): # for pandas < 2.0 where "unit" doesn't exist in pd.date_range. note that # unit of ns is the only option in pandas<2, and the default in pandas 2.x - times = pd.date_range(start='2019-01-01', freq='h', periods=3, tz=tz) - expected = times.view(np.int64)/10**9 + times = pd.date_range(start="2019-01-01", freq="h", periods=3, tz=tz) + expected = times.view(np.int64) / 10**9 actual = solarposition._datetime_to_unixtime(times) np.testing.assert_equal(expected, actual) @requires_pandas_2_0 -@pytest.mark.parametrize('unit', ['ns', 'us', 's']) -@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern']) +@pytest.mark.parametrize("unit", ["ns", "us", "s"]) +@pytest.mark.parametrize("tz", [None, "utc", "US/Eastern"]) def test__datetime_to_unixtime_units(unit, tz): - kwargs = dict(start='2019-01-01', freq='h', periods=3) - times = pd.date_range(**kwargs, unit='ns', tz='UTC') - expected = times.view(np.int64)/10**9 + kwargs = dict(start="2019-01-01", freq="h", periods=3) + times = pd.date_range(**kwargs, unit="ns", tz="UTC") + expected = times.view(np.int64) / 10**9 - times = pd.date_range(**kwargs, unit=unit, tz='UTC').tz_convert(tz) + times = pd.date_range(**kwargs, unit=unit, tz="UTC").tz_convert(tz) actual = solarposition._datetime_to_unixtime(times) np.testing.assert_equal(expected, actual) @requires_pandas_2_0 -@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern']) -@pytest.mark.parametrize('method', [ - 'nrel_numpy', - 'ephemeris', - pytest.param('pyephem', marks=requires_ephem), - pytest.param('nrel_numba', marks=requires_numba), - pytest.param('nrel_c', marks=requires_spa_c), -]) +@pytest.mark.parametrize("tz", [None, "utc", "US/Eastern"]) +@pytest.mark.parametrize( + "method", + [ + "nrel_numpy", + "ephemeris", + pytest.param("pyephem", marks=requires_ephem), + pytest.param("nrel_numba", marks=requires_numba), + pytest.param("nrel_c", marks=requires_spa_c), + ], +) def test_get_solarposition_microsecond_index(method, tz): # https://github.com/pvlib/pvlib-python/issues/1932 - kwargs = dict(start='2019-01-01', freq='h', periods=24, tz=tz) + kwargs = dict(start="2019-01-01", freq="h", periods=24, tz=tz) - index_ns = pd.date_range(unit='ns', **kwargs) - index_us = pd.date_range(unit='us', **kwargs) + index_ns = pd.date_range(unit="ns", **kwargs) + index_us = pd.date_range(unit="us", **kwargs) with warnings.catch_warnings(): # don't warn on method reload @@ -846,14 +1200,14 @@ def test_get_solarposition_microsecond_index(method, tz): @requires_pandas_2_0 -@pytest.mark.parametrize('tz', [None, 'utc', 'US/Eastern']) +@pytest.mark.parametrize("tz", [None, "utc", "US/Eastern"]) def test_nrel_earthsun_distance_microsecond_index(tz): # https://github.com/pvlib/pvlib-python/issues/1932 - kwargs = dict(start='2019-01-01', freq='h', periods=24, tz=tz) + kwargs = dict(start="2019-01-01", freq="h", periods=24, tz=tz) - index_ns = pd.date_range(unit='ns', **kwargs) - index_us = pd.date_range(unit='us', **kwargs) + index_ns = pd.date_range(unit="ns", **kwargs) + index_us = pd.date_range(unit="us", **kwargs) esd_ns = solarposition.nrel_earthsun_distance(index_ns) esd_us = solarposition.nrel_earthsun_distance(index_us) @@ -862,14 +1216,14 @@ def test_nrel_earthsun_distance_microsecond_index(tz): @requires_pandas_2_0 -@pytest.mark.parametrize('tz', ['utc', 'US/Eastern']) +@pytest.mark.parametrize("tz", ["utc", "US/Eastern"]) def test_hour_angle_microsecond_index(tz): # https://github.com/pvlib/pvlib-python/issues/1932 - kwargs = dict(start='2019-01-01', freq='h', periods=24, tz=tz) + kwargs = dict(start="2019-01-01", freq="h", periods=24, tz=tz) - index_ns = pd.date_range(unit='ns', **kwargs) - index_us = pd.date_range(unit='us', **kwargs) + index_ns = pd.date_range(unit="ns", **kwargs) + index_us = pd.date_range(unit="us", **kwargs) ha_ns = solarposition.hour_angle(index_ns, -80, 0) ha_us = solarposition.hour_angle(index_us, -80, 0) @@ -878,14 +1232,14 @@ def test_hour_angle_microsecond_index(tz): @requires_pandas_2_0 -@pytest.mark.parametrize('tz', ['utc', 'US/Eastern']) +@pytest.mark.parametrize("tz", ["utc", "US/Eastern"]) def test_rise_set_transit_spa_microsecond_index(tz): # https://github.com/pvlib/pvlib-python/issues/1932 - kwargs = dict(start='2019-01-01', freq='h', periods=24, tz=tz) + kwargs = dict(start="2019-01-01", freq="h", periods=24, tz=tz) - index_ns = pd.date_range(unit='ns', **kwargs) - index_us = pd.date_range(unit='us', **kwargs) + index_ns = pd.date_range(unit="ns", **kwargs) + index_us = pd.date_range(unit="us", **kwargs) rst_ns = solarposition.sun_rise_set_transit_spa(index_ns, 40, -80) rst_us = solarposition.sun_rise_set_transit_spa(index_us, 40, -80) @@ -894,14 +1248,14 @@ def test_rise_set_transit_spa_microsecond_index(tz): @requires_pandas_2_0 -@pytest.mark.parametrize('tz', ['utc', 'US/Eastern']) +@pytest.mark.parametrize("tz", ["utc", "US/Eastern"]) def test_rise_set_transit_geometric_microsecond_index(tz): # https://github.com/pvlib/pvlib-python/issues/1932 - kwargs = dict(start='2019-01-01', freq='h', periods=24, tz=tz) + kwargs = dict(start="2019-01-01", freq="h", periods=24, tz=tz) - index_ns = pd.date_range(unit='ns', **kwargs) - index_us = pd.date_range(unit='us', **kwargs) + index_ns = pd.date_range(unit="ns", **kwargs) + index_us = pd.date_range(unit="us", **kwargs) args = (40, -80, 0, 0) rst_ns = solarposition.sun_rise_set_transit_geometric(index_ns, *args) @@ -914,53 +1268,83 @@ def test_rise_set_transit_geometric_microsecond_index(tz): # put numba tests at end of file to minimize reloading + @requires_numba def test_spa_python_numba_physical(expected_solpos, golden_mst): - times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), - periods=1, freq='D', tz=golden_mst.tz) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 12, 30, 30), + periods=1, + freq="D", + tz=golden_mst.tz, + ) with warnings.catch_warnings(): # don't warn on method reload # ensure that numpy is the most recently used method so that # we can use the warns filter below warnings.simplefilter("ignore") - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numpy', numthreads=1) + ephem_data = solarposition.spa_python( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numpy", + numthreads=1, + ) with pytest.warns(UserWarning): - ephem_data = solarposition.spa_python(times, golden_mst.latitude, - golden_mst.longitude, - pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numba', numthreads=1) + ephem_data = solarposition.spa_python( + times, + golden_mst.latitude, + golden_mst.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numba", + numthreads=1, + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) @requires_numba def test_spa_python_numba_physical_dst(expected_solpos, golden): - times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), - periods=1, freq='D', tz=golden.tz) + times = pd.date_range( + datetime.datetime(2003, 10, 17, 13, 30, 30), + periods=1, + freq="D", + tz=golden.tz, + ) with warnings.catch_warnings(): # don't warn on method reload warnings.simplefilter("ignore") - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numba', numthreads=1) + ephem_data = solarposition.spa_python( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numba", + numthreads=1, + ) expected_solpos.index = times assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) with pytest.warns(UserWarning): # test that we get a warning when reloading to use numpy only - ephem_data = solarposition.spa_python(times, golden.latitude, - golden.longitude, - pressure=82000, - temperature=11, delta_t=67, - atmos_refract=0.5667, - how='numpy', numthreads=1) + ephem_data = solarposition.spa_python( + times, + golden.latitude, + golden.longitude, + pressure=82000, + temperature=11, + delta_t=67, + atmos_refract=0.5667, + how="numpy", + numthreads=1, + ) diff --git a/pvlib/tests/test_spa.py b/pvlib/tests/test_spa.py index 67cab4cbdb..388d543b49 100644 --- a/pvlib/tests/test_spa.py +++ b/pvlib/tests/test_spa.py @@ -20,9 +20,10 @@ from .conftest import requires_numba -times = (pd.date_range('2003-10-17 12:30:30', periods=1, freq='D') - .tz_localize('MST')) -unixtimes = np.array(times.tz_convert('UTC').view(np.int64)*1.0/10**9) +times = pd.date_range("2003-10-17 12:30:30", periods=1, freq="D").tz_localize( + "MST" +) +unixtimes = np.array(times.tz_convert("UTC").view(np.int64) * 1.0 / 10**9) lat = 39.742476 lon = -105.1786 @@ -72,17 +73,48 @@ Phi = 194.340241 year = 1985 month = 2 -year_array = np.array([-499, 500, 1000, 1500, 1800, 1860, 1900, 1950, - 1970, 1985, 1990, 2000, 2005, 2050, 2150]) +year_array = np.array( + [ + -499, + 500, + 1000, + 1500, + 1800, + 1860, + 1900, + 1950, + 1970, + 1985, + 1990, + 2000, + 2005, + 2050, + 2150, + ] +) # `month_array` is used with `year_array` in `test_calculate_deltat`. # Both arrays need to have the same length for the test, hence the duplicates. month_array = np.array([1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12, 12, 12]) dt_actual = 54.413442486 -dt_actual_array = np.array([1.7184831e+04, 5.7088051e+03, 1.5730419e+03, - 1.9801820e+02, 1.3596506e+01, 7.8316585e+00, - -2.1171894e+00, 2.9289261e+01, 4.0824887e+01, - 5.4724581e+01, 5.7426651e+01, 6.4108015e+01, - 6.5038015e+01, 9.4952955e+01, 3.3050693e+02]) +dt_actual_array = np.array( + [ + 1.7184831e04, + 5.7088051e03, + 1.5730419e03, + 1.9801820e02, + 1.3596506e01, + 7.8316585e00, + -2.1171894e00, + 2.9289261e01, + 4.0824887e01, + 5.4724581e01, + 5.7426651e01, + 6.4108015e01, + 6.5038015e01, + 9.4952955e01, + 3.3050693e02, + ] +) mix_year_array = np.full((10), year) mix_month_array = np.full((10), month) mix_year_actual = np.full((10), dt_actual) @@ -91,9 +123,10 @@ class SpaBase: """Test functions common to numpy and numba spa""" + def test_julian_day_dt(self): # add 1us manually to the test timestamp (GH #940) - dt = times.tz_convert('UTC')[0] + pd.Timedelta(1, unit='us') + dt = times.tz_convert("UTC")[0] + pd.Timedelta(1, unit="us") year = dt.year month = dt.month day = dt.day @@ -101,10 +134,13 @@ def test_julian_day_dt(self): minute = dt.minute second = dt.second microsecond = dt.microsecond - assert_almost_equal(JD + 1e-6 / (3600*24), # modify expected JD by 1us - self.spa.julian_day_dt( - year, month, day, hour, - minute, second, microsecond), 6) + assert_almost_equal( + JD + 1e-6 / (3600 * 24), # modify expected JD by 1us + self.spa.julian_day_dt( + year, month, day, hour, minute, second, microsecond + ), + 6, + ) def test_julian_ephemeris_day(self): assert_almost_equal(JDE, self.spa.julian_ephemeris_day(JD, delta_t), 5) @@ -159,30 +195,37 @@ def test_mean_ecliptic_obliquity(self): assert_almost_equal(epsilon0, self.spa.mean_ecliptic_obliquity(JME), 6) def test_true_ecliptic_obliquity(self): - assert_almost_equal(epsilon, self.spa.true_ecliptic_obliquity( - epsilon0, dEpsilon), 6) + assert_almost_equal( + epsilon, self.spa.true_ecliptic_obliquity(epsilon0, dEpsilon), 6 + ) def test_aberration_correction(self): assert_almost_equal(dTau, self.spa.aberration_correction(R), 6) def test_apparent_sun_longitude(self): - assert_almost_equal(lamd, self.spa.apparent_sun_longitude( - Theta, dPsi, dTau), 6) + assert_almost_equal( + lamd, self.spa.apparent_sun_longitude(Theta, dPsi, dTau), 6 + ) def test_mean_sidereal_time(self): assert_almost_equal(v0, self.spa.mean_sidereal_time(JD, JC), 3) def test_apparent_sidereal_time(self): - assert_almost_equal(v, self.spa.apparent_sidereal_time( - v0, dPsi, epsilon), 5) + assert_almost_equal( + v, self.spa.apparent_sidereal_time(v0, dPsi, epsilon), 5 + ) def test_geocentric_sun_right_ascension(self): - assert_almost_equal(alpha, self.spa.geocentric_sun_right_ascension( - lamd, epsilon, beta), 6) + assert_almost_equal( + alpha, + self.spa.geocentric_sun_right_ascension(lamd, epsilon, beta), + 6, + ) def test_geocentric_sun_declination(self): - assert_almost_equal(delta, self.spa.geocentric_sun_declination( - lamd, epsilon, beta), 6) + assert_almost_equal( + delta, self.spa.geocentric_sun_declination(lamd, epsilon, beta), 6 + ) def test_local_hour_angle(self): assert_almost_equal(H, self.spa.local_hour_angle(v, lon, alpha), 4) @@ -193,33 +236,49 @@ def test_equatorial_horizontal_parallax(self): def test_parallax_sun_right_ascension(self): u = self.spa.uterm(lat) x = self.spa.xterm(u, lat, elev) - assert_almost_equal(dAlpha, self.spa.parallax_sun_right_ascension( - x, xi, H, delta), 4) + assert_almost_equal( + dAlpha, self.spa.parallax_sun_right_ascension(x, xi, H, delta), 4 + ) def test_topocentric_sun_right_ascension(self): - assert_almost_equal(alpha_prime, - self.spa.topocentric_sun_right_ascension( - alpha, dAlpha), 5) + assert_almost_equal( + alpha_prime, + self.spa.topocentric_sun_right_ascension(alpha, dAlpha), + 5, + ) def test_topocentric_sun_declination(self): u = self.spa.uterm(lat) x = self.spa.xterm(u, lat, elev) y = self.spa.yterm(u, lat, elev) - assert_almost_equal(delta_prime, self.spa.topocentric_sun_declination( - delta, x, y, xi, dAlpha, H), 5) + assert_almost_equal( + delta_prime, + self.spa.topocentric_sun_declination(delta, x, y, xi, dAlpha, H), + 5, + ) def test_topocentric_local_hour_angle(self): - assert_almost_equal(H_prime, self.spa.topocentric_local_hour_angle( - H, dAlpha), 5) + assert_almost_equal( + H_prime, self.spa.topocentric_local_hour_angle(H, dAlpha), 5 + ) def test_topocentric_elevation_angle_without_atmosphere(self): assert_almost_equal( - e0, self.spa.topocentric_elevation_angle_without_atmosphere( - lat, delta_prime, H_prime), 6) + e0, + self.spa.topocentric_elevation_angle_without_atmosphere( + lat, delta_prime, H_prime + ), + 6, + ) def test_atmospheric_refraction_correction(self): - assert_almost_equal(de, self.spa.atmospheric_refraction_correction( - pressure, temp, e0, atmos_refract), 6) + assert_almost_equal( + de, + self.spa.atmospheric_refraction_correction( + pressure, temp, e0, atmos_refract + ), + 6, + ) def test_topocentric_elevation_angle(self): assert_almost_equal(e, self.spa.topocentric_elevation_angle(e0, de), 6) @@ -228,8 +287,13 @@ def test_topocentric_zenith_angle(self): assert_almost_equal(theta, self.spa.topocentric_zenith_angle(e), 5) def test_topocentric_astronomers_azimuth(self): - assert_almost_equal(Gamma, self.spa.topocentric_astronomers_azimuth( - H_prime, delta_prime, lat), 5) + assert_almost_equal( + Gamma, + self.spa.topocentric_astronomers_azimuth( + H_prime, delta_prime, lat + ), + 5, + ) def test_topocentric_azimuth_angle(self): assert_almost_equal(Phi, self.spa.topocentric_azimuth_angle(Gamma), 5) @@ -239,116 +303,228 @@ def test_solar_position(self): # don't warn on method reload warnings.simplefilter("ignore") spa_out_0 = self.spa.solar_position( - unixtimes, lat, lon, elev, pressure, temp, delta_t, - atmos_refract)[:-1] + unixtimes, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + )[:-1] spa_out_1 = self.spa.solar_position( - unixtimes, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, sst=True)[:3] - assert_almost_equal(np.array([[theta, theta0, e, e0, Phi]]).T, - spa_out_0, 5) + unixtimes, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + sst=True, + )[:3] + assert_almost_equal( + np.array([[theta, theta0, e, e0, Phi]]).T, spa_out_0, 5 + ) assert_almost_equal(np.array([[v, alpha, delta]]).T, spa_out_1, 5) def test_equation_of_time(self): eot = 14.64 M = self.spa.sun_mean_longitude(JME) - assert_almost_equal(eot, self.spa.equation_of_time( - M, alpha, dPsi, epsilon), 2) + assert_almost_equal( + eot, self.spa.equation_of_time(M, alpha, dPsi, epsilon), 2 + ) def test_transit_sunrise_sunset(self): # tests at greenwich - times = pd.DatetimeIndex([dt.datetime(1996, 7, 5, 0), - dt.datetime(2004, 12, 4, 0)] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunrise = pd.DatetimeIndex([dt.datetime(1996, 7, 5, 7, 8, 15), - dt.datetime(2004, 12, 4, 4, 38, 57)] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunset = pd.DatetimeIndex([dt.datetime(1996, 7, 5, 17, 1, 4), - dt.datetime(2004, 12, 4, 19, 2, 2)] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 + times = ( + pd.DatetimeIndex( + [dt.datetime(1996, 7, 5, 0), dt.datetime(2004, 12, 4, 0)] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunrise = ( + pd.DatetimeIndex( + [ + dt.datetime(1996, 7, 5, 7, 8, 15), + dt.datetime(2004, 12, 4, 4, 38, 57), + ] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunset = ( + pd.DatetimeIndex( + [ + dt.datetime(1996, 7, 5, 17, 1, 4), + dt.datetime(2004, 12, 4, 19, 2, 2), + ] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) times = np.array(times) sunrise = np.array(sunrise) sunset = np.array(sunset) result = self.spa.transit_sunrise_sunset(times, -35.0, 0.0, 64.0, 1) - assert_almost_equal(sunrise/1e3, result[1]/1e3, 3) - assert_almost_equal(sunset/1e3, result[2]/1e3, 3) - - times = pd.DatetimeIndex([dt.datetime(1994, 1, 2), ] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunset = pd.DatetimeIndex([dt.datetime(1994, 1, 2, 16, 59, 55), ] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunrise = pd.DatetimeIndex([dt.datetime(1994, 1, 2, 7, 8, 12), ] - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 + assert_almost_equal(sunrise / 1e3, result[1] / 1e3, 3) + assert_almost_equal(sunset / 1e3, result[2] / 1e3, 3) + + times = ( + pd.DatetimeIndex( + [ + dt.datetime(1994, 1, 2), + ] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunset = ( + pd.DatetimeIndex( + [ + dt.datetime(1994, 1, 2, 16, 59, 55), + ] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunrise = ( + pd.DatetimeIndex( + [ + dt.datetime(1994, 1, 2, 7, 8, 12), + ] + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) times = np.array(times) sunrise = np.array(sunrise) sunset = np.array(sunset) result = self.spa.transit_sunrise_sunset(times, 35.0, 0.0, 64.0, 1) - assert_almost_equal(sunrise/1e3, result[1]/1e3, 3) - assert_almost_equal(sunset/1e3, result[2]/1e3, 3) + assert_almost_equal(sunrise / 1e3, result[1] / 1e3, 3) + assert_almost_equal(sunset / 1e3, result[2] / 1e3, 3) # tests from USNO # Golden - times = pd.DatetimeIndex([dt.datetime(2015, 1, 2), - dt.datetime(2015, 4, 2), - dt.datetime(2015, 8, 2), - dt.datetime(2015, 12, 2)], - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunrise = pd.DatetimeIndex([dt.datetime(2015, 1, 2, 7, 19), - dt.datetime(2015, 4, 2, 5, 43), - dt.datetime(2015, 8, 2, 5, 1), - dt.datetime(2015, 12, 2, 7, 1)], - ).tz_localize( - 'MST').view(np.int64)*1.0/10**9 - sunset = pd.DatetimeIndex([dt.datetime(2015, 1, 2, 16, 49), - dt.datetime(2015, 4, 2, 18, 24), - dt.datetime(2015, 8, 2, 19, 10), - dt.datetime(2015, 12, 2, 16, 38)], - ).tz_localize( - 'MST').view(np.int64)*1.0/10**9 + times = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2), + dt.datetime(2015, 4, 2), + dt.datetime(2015, 8, 2), + dt.datetime(2015, 12, 2), + ], + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunrise = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2, 7, 19), + dt.datetime(2015, 4, 2, 5, 43), + dt.datetime(2015, 8, 2, 5, 1), + dt.datetime(2015, 12, 2, 7, 1), + ], + ) + .tz_localize("MST") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunset = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2, 16, 49), + dt.datetime(2015, 4, 2, 18, 24), + dt.datetime(2015, 8, 2, 19, 10), + dt.datetime(2015, 12, 2, 16, 38), + ], + ) + .tz_localize("MST") + .view(np.int64) + * 1.0 + / 10**9 + ) times = np.array(times) sunrise = np.array(sunrise) sunset = np.array(sunset) result = self.spa.transit_sunrise_sunset(times, 39.0, -105.0, 64.0, 1) - assert_almost_equal(sunrise/1e3, result[1]/1e3, 1) - assert_almost_equal(sunset/1e3, result[2]/1e3, 1) + assert_almost_equal(sunrise / 1e3, result[1] / 1e3, 1) + assert_almost_equal(sunset / 1e3, result[2] / 1e3, 1) # Beijing - times = pd.DatetimeIndex([dt.datetime(2015, 1, 2), - dt.datetime(2015, 4, 2), - dt.datetime(2015, 8, 2), - dt.datetime(2015, 12, 2)], - ).tz_localize( - 'UTC').view(np.int64)*1.0/10**9 - sunrise = pd.DatetimeIndex([dt.datetime(2015, 1, 2, 7, 36), - dt.datetime(2015, 4, 2, 5, 58), - dt.datetime(2015, 8, 2, 5, 13), - dt.datetime(2015, 12, 2, 7, 17)], - ).tz_localize('Asia/Shanghai').view( - np.int64)*1.0/10**9 - sunset = pd.DatetimeIndex([dt.datetime(2015, 1, 2, 17, 0), - dt.datetime(2015, 4, 2, 18, 39), - dt.datetime(2015, 8, 2, 19, 28), - dt.datetime(2015, 12, 2, 16, 50)], - ).tz_localize('Asia/Shanghai').view( - np.int64)*1.0/10**9 + times = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2), + dt.datetime(2015, 4, 2), + dt.datetime(2015, 8, 2), + dt.datetime(2015, 12, 2), + ], + ) + .tz_localize("UTC") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunrise = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2, 7, 36), + dt.datetime(2015, 4, 2, 5, 58), + dt.datetime(2015, 8, 2, 5, 13), + dt.datetime(2015, 12, 2, 7, 17), + ], + ) + .tz_localize("Asia/Shanghai") + .view(np.int64) + * 1.0 + / 10**9 + ) + sunset = ( + pd.DatetimeIndex( + [ + dt.datetime(2015, 1, 2, 17, 0), + dt.datetime(2015, 4, 2, 18, 39), + dt.datetime(2015, 8, 2, 19, 28), + dt.datetime(2015, 12, 2, 16, 50), + ], + ) + .tz_localize("Asia/Shanghai") + .view(np.int64) + * 1.0 + / 10**9 + ) times = np.array(times) sunrise = np.array(sunrise) sunset = np.array(sunset) result = self.spa.transit_sunrise_sunset( - times, 39.917, 116.383, 64.0, 1) - assert_almost_equal(sunrise/1e3, result[1]/1e3, 1) - assert_almost_equal(sunset/1e3, result[2]/1e3, 1) + times, 39.917, 116.383, 64.0, 1 + ) + assert_almost_equal(sunrise / 1e3, result[1] / 1e3, 1) + assert_almost_equal(sunset / 1e3, result[2] / 1e3, 1) def test_earthsun_distance(self): - times = (pd.date_range('2003-10-17 12:30:30', periods=1, freq='D') - .tz_localize('MST')) - unixtimes = times.tz_convert('UTC').view(np.int64)*1.0/10**9 + times = pd.date_range( + "2003-10-17 12:30:30", periods=1, freq="D" + ).tz_localize("MST") + unixtimes = times.tz_convert("UTC").view(np.int64) * 1.0 / 10**9 unixtimes = np.array(unixtimes) result = self.spa.earthsun_distance(unixtimes, 64.0, 1) assert_almost_equal(R, result, 6) @@ -369,16 +545,18 @@ def test_calculate_deltat(self): class NumpySpaTest(unittest.TestCase, SpaBase): """Import spa without compiling to numba then run tests""" + @classmethod def setUpClass(self): - os.environ['PVLIB_USE_NUMBA'] = '0' + os.environ["PVLIB_USE_NUMBA"] = "0" import pvlib.spa as spa + spa = reload(spa) self.spa = spa @classmethod def tearDownClass(self): - del os.environ['PVLIB_USE_NUMBA'] + del os.environ["PVLIB_USE_NUMBA"] def test_julian_day(self): assert_almost_equal(JD, self.spa.julian_day(unixtimes)[0], 6) @@ -387,44 +565,92 @@ def test_julian_day(self): @requires_numba class NumbaSpaTest(unittest.TestCase, SpaBase): """Import spa, compiling to numba, and run tests""" + @classmethod def setUpClass(self): - os.environ['PVLIB_USE_NUMBA'] = '1' + os.environ["PVLIB_USE_NUMBA"] = "1" import pvlib.spa as spa + spa = reload(spa) self.spa = spa @classmethod def tearDownClass(self): - del os.environ['PVLIB_USE_NUMBA'] + del os.environ["PVLIB_USE_NUMBA"] def test_julian_day(self): assert_almost_equal(JD, self.spa.julian_day(unixtimes[0]), 6) def test_solar_position_singlethreaded(self): assert_almost_equal( - np.array([[theta, theta0, e, e0, Phi]]).T, self.spa.solar_position( - unixtimes, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads=1)[:-1], 5) + np.array([[theta, theta0, e, e0, Phi]]).T, + self.spa.solar_position( + unixtimes, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads=1, + )[:-1], + 5, + ) assert_almost_equal( - np.array([[v, alpha, delta]]).T, self.spa.solar_position( - unixtimes, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads=1, sst=True)[:3], 5) + np.array([[v, alpha, delta]]).T, + self.spa.solar_position( + unixtimes, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads=1, + sst=True, + )[:3], + 5, + ) def test_solar_position_multithreaded(self): result = np.array([theta, theta0, e, e0, Phi]) nresult = np.array([result, result, result]).T times = np.array([unixtimes[0], unixtimes[0], unixtimes[0]]) assert_almost_equal( - nresult, self.spa.solar_position( - times, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads=3)[:-1], 5) + nresult, + self.spa.solar_position( + times, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads=3, + )[:-1], + 5, + ) result = np.array([v, alpha, delta]) nresult = np.array([result, result, result]).T assert_almost_equal( - nresult, self.spa.solar_position( - times, lat, lon, elev, pressure, temp, delta_t, - atmos_refract, numthreads=3, sst=True)[:3], 5) + nresult, + self.spa.solar_position( + times, + lat, + lon, + elev, + pressure, + temp, + delta_t, + atmos_refract, + numthreads=3, + sst=True, + )[:3], + 5, + ) # Define extra test cases for issue #2077 diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 1cc32a732a..dedc715d87 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -6,64 +6,82 @@ from numpy.testing import assert_allclose from pvlib import temperature, tools -from pvlib._deprecation import pvlibDeprecationWarning import re @pytest.fixture def sapm_default(): - return temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass'] + return temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_glass" + ] def test_sapm_cell(sapm_default): - default = temperature.sapm_cell(900, 20, 5, sapm_default['a'], - sapm_default['b'], sapm_default['deltaT']) + default = temperature.sapm_cell( + 900, + 20, + 5, + sapm_default["a"], + sapm_default["b"], + sapm_default["deltaT"], + ) assert_allclose(default, 43.509, 1e-3) def test_sapm_module(sapm_default): - default = temperature.sapm_module(900, 20, 5, sapm_default['a'], - sapm_default['b']) + default = temperature.sapm_module( + 900, 20, 5, sapm_default["a"], sapm_default["b"] + ) assert_allclose(default, 40.809, 1e-3) def test_sapm_cell_from_module(sapm_default): - default = temperature.sapm_cell_from_module(50, 900, - sapm_default['deltaT']) - assert_allclose(default, 50 + 900 / 1000 * sapm_default['deltaT']) + default = temperature.sapm_cell_from_module( + 50, 900, sapm_default["deltaT"] + ) + assert_allclose(default, 50 + 900 / 1000 * sapm_default["deltaT"]) def test_sapm_ndarray(sapm_default): temps = np.array([0, 10, 5]) irrads = np.array([0, 500, 0]) winds = np.array([10, 5, 0]) - cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'], - sapm_default['b'], - sapm_default['deltaT']) - module_temps = temperature.sapm_module(irrads, temps, winds, - sapm_default['a'], - sapm_default['b']) - expected_cell = np.array([0., 23.06066166, 5.]) - expected_module = np.array([0., 21.56066166, 5.]) + cell_temps = temperature.sapm_cell( + irrads, + temps, + winds, + sapm_default["a"], + sapm_default["b"], + sapm_default["deltaT"], + ) + module_temps = temperature.sapm_module( + irrads, temps, winds, sapm_default["a"], sapm_default["b"] + ) + expected_cell = np.array([0.0, 23.06066166, 5.0]) + expected_module = np.array([0.0, 21.56066166, 5.0]) assert_allclose(expected_cell, cell_temps, 1e-3) assert_allclose(expected_module, module_temps, 1e-3) def test_sapm_series(sapm_default): - times = pd.date_range(start='2015-01-01', end='2015-01-02', freq='12h') + times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12h") temps = pd.Series([0, 10, 5], index=times) irrads = pd.Series([0, 500, 0], index=times) winds = pd.Series([10, 5, 0], index=times) - cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'], - sapm_default['b'], - sapm_default['deltaT']) - module_temps = temperature.sapm_module(irrads, temps, winds, - sapm_default['a'], - sapm_default['b']) - expected_cell = pd.Series([0., 23.06066166, 5.], index=times) - expected_module = pd.Series([0., 21.56066166, 5.], index=times) + cell_temps = temperature.sapm_cell( + irrads, + temps, + winds, + sapm_default["a"], + sapm_default["b"], + sapm_default["deltaT"], + ) + module_temps = temperature.sapm_module( + irrads, temps, winds, sapm_default["a"], sapm_default["b"] + ) + expected_cell = pd.Series([0.0, 23.06066166, 5.0], index=times) + expected_module = pd.Series([0.0, 21.56066166, 5.0], index=times) assert_series_equal(expected_cell, cell_temps) assert_series_equal(expected_module, module_temps) @@ -74,8 +92,9 @@ def test_pvsyst_cell_default(): def test_pvsyst_cell_kwargs(): - result = temperature.pvsyst_cell(900, 20, wind_speed=5.0, u_c=23.5, - u_v=6.25, module_efficiency=0.1) + result = temperature.pvsyst_cell( + 900, 20, wind_speed=5.0, u_c=23.5, u_v=6.25, module_efficiency=0.1 + ) assert_allclose(result, 33.315, 0.001) @@ -105,7 +124,7 @@ def test_faiman_default(): def test_faiman_kwargs(): - result = temperature.faiman(900, 20, wind_speed=5.0, u0=22.0, u1=6.) + result = temperature.faiman(900, 20, wind_speed=5.0, u0=22.0, u1=6.0) assert_allclose(result, 37.308, atol=0.001) @@ -141,22 +160,24 @@ def test_faiman_rad_ir(): sky_view = np.array([1.0, 0.5, 0.0]) expected = [-4.071, -2.036, 0.000] - result = temperature.faiman_rad(0, 0, 0, ir_down=200, - sky_view=sky_view) + result = temperature.faiman_rad(0, 0, 0, ir_down=200, sky_view=sky_view) assert_allclose(result, expected, atol=0.001) emissivity = np.array([1.0, 0.88, 0.5, 0.0]) expected = [-4.626, -4.071, -2.313, 0.000] - result = temperature.faiman_rad(0, 0, 0, ir_down=200, - emissivity=emissivity) + result = temperature.faiman_rad( + 0, 0, 0, ir_down=200, emissivity=emissivity + ) assert_allclose(result, expected, atol=0.001) def test_ross(): - result = temperature.ross(np.array([1000., 600., 1000.]), - np.array([20., 40., 60.]), - np.array([40., 100., 20.])) - expected = np.array([45., 100., 60.]) + result = temperature.ross( + np.array([1000.0, 600.0, 1000.0]), + np.array([20.0, 40.0, 60.0]), + np.array([40.0, 100.0, 20.0]), + ) + expected = np.array([45.0, 100.0, 60.0]) assert_allclose(expected, result) @@ -172,53 +193,64 @@ def test_faiman_series(): def test__temperature_model_params(): - params = temperature._temperature_model_params('sapm', - 'open_rack_glass_glass') - assert params == temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass'] + params = temperature._temperature_model_params( + "sapm", "open_rack_glass_glass" + ) + assert ( + params + == temperature.TEMPERATURE_MODEL_PARAMETERS["sapm"][ + "open_rack_glass_glass" + ] + ) with pytest.raises(KeyError): - temperature._temperature_model_params('sapm', 'not_a_parameter_set') + temperature._temperature_model_params("sapm", "not_a_parameter_set") def _read_pvwatts_8760(filename): - df = pd.read_csv(filename, - skiprows=17, # ignore location/simulation metadata - skipfooter=1, # ignore "Totals" row - engine='python') - df['Year'] = 2019 - df.index = pd.to_datetime(df[['Year', 'Month', 'Day', 'Hour']]) + df = pd.read_csv( + filename, + skiprows=17, # ignore location/simulation metadata + skipfooter=1, # ignore "Totals" row + engine="python", + ) + df["Year"] = 2019 + df.index = pd.to_datetime(df[["Year", "Month", "Day", "Hour"]]) return df -@pytest.mark.parametrize('filename,inoct', [ - ('pvwatts_8760_rackmount.csv', 45), - ('pvwatts_8760_roofmount.csv', 49), -]) +@pytest.mark.parametrize( + "filename,inoct", + [ + ("pvwatts_8760_rackmount.csv", 45), + ("pvwatts_8760_roofmount.csv", 49), + ], +) def test_fuentes(filename, inoct): # Test against data exported from pvwatts.nrel.gov data = _read_pvwatts_8760(DATA_DIR / filename) - data = data.iloc[:24*7, :] # just use one week + data = data.iloc[: 24 * 7, :] # just use one week inputs = { - 'poa_global': data['Plane of Array Irradiance (W/m^2)'], - 'temp_air': data['Ambient Temperature (C)'], - 'wind_speed': data['Wind Speed (m/s)'], - 'noct_installed': inoct, + "poa_global": data["Plane of Array Irradiance (W/m^2)"], + "temp_air": data["Ambient Temperature (C)"], + "wind_speed": data["Wind Speed (m/s)"], + "noct_installed": inoct, } - expected_tcell = data['Cell Temperature (C)'] - expected_tcell.name = 'tmod' + expected_tcell = data["Cell Temperature (C)"] + expected_tcell.name = "tmod" actual_tcell = temperature.fuentes(**inputs) # the SSC implementation of PVWatts diverges from the Fuentes model at # at night by setting Tcell=Tamb when POA=0. This not only means that # nighttime values are slightly different (Fuentes models cooling to sky # at night), but because of the thermal inertia, there is a transient # error after dawn as well. Test each case separately: - is_night = inputs['poa_global'] == 0 + is_night = inputs["poa_global"] == 0 is_dawn = is_night.shift(1) & ~is_night - is_daytime = (inputs['poa_global'] > 0) & ~is_dawn + is_daytime = (inputs["poa_global"] > 0) & ~is_dawn # the accuracy is probably higher than 3 digits here, but the PVWatts # export data has low precision so can only test up to 3 digits - assert_series_equal(expected_tcell[is_daytime].round(3), - actual_tcell[is_daytime].round(3)) + assert_series_equal( + expected_tcell[is_daytime].round(3), actual_tcell[is_daytime].round(3) + ) # use lower precision for dawn times to accommodate the dawn transient error = actual_tcell[is_dawn] - expected_tcell[is_dawn] assert (error.abs() < 0.1).all() @@ -228,39 +260,55 @@ def test_fuentes(filename, inoct): assert night_difference.min() > 0 -@pytest.mark.parametrize('tz', [None, 'Etc/GMT+5']) +@pytest.mark.parametrize("tz", [None, "Etc/GMT+5"]) def test_fuentes_timezone(tz): - index = pd.date_range('2019-01-01', freq='h', periods=3, tz=tz) + index = pd.date_range("2019-01-01", freq="h", periods=3, tz=tz) - df = pd.DataFrame({'poa_global': 1000, 'temp_air': 20, 'wind_speed': 1}, - index) + df = pd.DataFrame( + {"poa_global": 1000, "temp_air": 20, "wind_speed": 1}, index + ) - out = temperature.fuentes(df['poa_global'], df['temp_air'], - df['wind_speed'], noct_installed=45) + out = temperature.fuentes( + df["poa_global"], df["temp_air"], df["wind_speed"], noct_installed=45 + ) - assert_series_equal(out, pd.Series([47.85, 50.85, 50.85], index=index, - name='tmod')) + assert_series_equal( + out, pd.Series([47.85, 50.85, 50.85], index=index, name="tmod") + ) def test_noct_sam(): poa_global, temp_air, wind_speed, noct, module_efficiency = ( - 1000., 25., 1., 45., 0.2) + 1000.0, + 25.0, + 1.0, + 45.0, + 0.2, + ) expected = 55.230790492 - result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, - module_efficiency) + result = temperature.noct_sam( + poa_global, temp_air, wind_speed, noct, module_efficiency + ) assert_allclose(result, expected) # test with different types - result = temperature.noct_sam(np.array(poa_global), np.array(temp_air), - np.array(wind_speed), np.array(noct), - np.array(module_efficiency)) + result = temperature.noct_sam( + np.array(poa_global), + np.array(temp_air), + np.array(wind_speed), + np.array(noct), + np.array(module_efficiency), + ) assert_allclose(result, expected) - dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00', - freq='1h') - result = temperature.noct_sam(pd.Series(index=dr, data=poa_global), - pd.Series(index=dr, data=temp_air), - pd.Series(index=dr, data=wind_speed), - pd.Series(index=dr, data=noct), - module_efficiency) + dr = pd.date_range( + start="2020-01-01 12:00:00", end="2020-01-01 13:00:00", freq="1h" + ) + result = temperature.noct_sam( + pd.Series(index=dr, data=poa_global), + pd.Series(index=dr, data=temp_air), + pd.Series(index=dr, data=wind_speed), + pd.Series(index=dr, data=noct), + module_efficiency, + ) assert_series_equal(result, pd.Series(index=dr, data=expected)) @@ -271,7 +319,12 @@ def test_noct_sam_against_sam(): # loss is set to 0. Weather input is TMY3 for Phoenix AZ. # Values are taken from the Jan 1 12:00:00 timestamp. poa_total, temp_air, wind_speed, noct, module_efficiency = ( - 860.673, 25, 3, 46.4, 0.20551) + 860.673, + 25, + 3, + 46.4, + 0.20551, + ) poa_total_after_refl = 851.458 # from SAM output # compute effective irradiance # spectral loss coefficients fixed in lib_cec6par.cpp @@ -279,17 +332,24 @@ def test_noct_sam_against_sam(): # reproduce SAM air mass calculation zen = 56.4284 elev = 358 - air_mass = 1. / (tools.cosd(zen) + 0.5057 * (96.080 - zen)**-1.634) + air_mass = 1.0 / (tools.cosd(zen) + 0.5057 * (96.080 - zen) ** -1.634) air_mass *= np.exp(-0.0001184 * elev) f1 = np.polyval(a, air_mass) effective_irradiance = f1 * poa_total_after_refl transmittance_absorptance = 0.9 array_height = 1 mount_standoff = 4.0 - result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct, - module_efficiency, effective_irradiance, - transmittance_absorptance, array_height, - mount_standoff) + result = temperature.noct_sam( + poa_total, + temp_air, + wind_speed, + noct, + module_efficiency, + effective_irradiance, + transmittance_absorptance, + array_height, + mount_standoff, + ) expected = 43.0655 # rtol from limited SAM output precision assert_allclose(result, expected, rtol=1e-5) @@ -297,50 +357,96 @@ def test_noct_sam_against_sam(): def test_noct_sam_options(): poa_global, temp_air, wind_speed, noct, module_efficiency = ( - 1000., 25., 1., 45., 0.2) - effective_irradiance = 1100. + 1000.0, + 25.0, + 1.0, + 45.0, + 0.2, + ) + effective_irradiance = 1100.0 transmittance_absorptance = 0.8 array_height = 2 mount_standoff = 2.0 - result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct, - module_efficiency, effective_irradiance, - transmittance_absorptance, array_height, - mount_standoff) + result = temperature.noct_sam( + poa_global, + temp_air, + wind_speed, + noct, + module_efficiency, + effective_irradiance, + transmittance_absorptance, + array_height, + mount_standoff, + ) expected = 60.477703576 assert_allclose(result, expected) def test_noct_sam_errors(): with pytest.raises(ValueError): - temperature.noct_sam(1000., 25., 1., 34., 0.2, array_height=3) + temperature.noct_sam(1000.0, 25.0, 1.0, 34.0, 0.2, array_height=3) def test_prilliman(): # test against values calculated using pvl_MAmodel_2, see pvlib #1081 - times = pd.date_range('2019-01-01', freq='5min', periods=8) + times = pd.date_range("2019-01-01", freq="5min", periods=8) cell_temperature = pd.Series([0, 1, 3, 6, 10, 15, 21, 27], index=times) wind_speed = pd.Series([0, 1, 2, 3, 2, 1, 2, 3]) # default coeffs - expected = pd.Series([0, 0, 0.7047457, 2.21176412, 4.45584299, 7.63635512, - 12.26808265, 18.00305776], index=times) + expected = pd.Series( + [ + 0, + 0, + 0.7047457, + 2.21176412, + 4.45584299, + 7.63635512, + 12.26808265, + 18.00305776, + ], + index=times, + ) actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=10) assert_series_equal(expected, actual) # custom coeffs coefficients = [0.0046, 4.5537e-4, -2.2586e-4, -1.5661e-5] - expected = pd.Series([0, 0, 0.70716941, 2.2199537, 4.47537694, 7.6676931, - 12.30423167, 18.04215198], index=times) - actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=10, - coefficients=coefficients) + expected = pd.Series( + [ + 0, + 0, + 0.70716941, + 2.2199537, + 4.47537694, + 7.6676931, + 12.30423167, + 18.04215198, + ], + index=times, + ) + actual = temperature.prilliman( + cell_temperature, wind_speed, unit_mass=10, coefficients=coefficients + ) assert_series_equal(expected, actual) # even very short inputs < 20 minutes total still work - times = pd.date_range('2019-01-01', freq='1min', periods=8) + times = pd.date_range("2019-01-01", freq="1min", periods=8) cell_temperature = pd.Series([0, 1, 3, 6, 10, 15, 21, 27], index=times) wind_speed = pd.Series([0, 1, 2, 3, 2, 1, 2, 3]) - expected = pd.Series([0, 0, 0.53557976, 1.49270094, 2.85940173, - 4.63914366, 7.09641845, 10.24899272], index=times) + expected = pd.Series( + [ + 0, + 0, + 0.53557976, + 1.49270094, + 2.85940173, + 4.63914366, + 7.09641845, + 10.24899272, + ], + index=times, + ) actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=12) assert_series_equal(expected, actual) @@ -348,13 +454,15 @@ def test_prilliman(): def test_prilliman_coarse(): # if the input series time step is >= 20 min, input is returned unchanged, # and a warning is emitted - times = pd.date_range('2019-01-01', freq='30min', periods=3) + times = pd.date_range("2019-01-01", freq="30min", periods=3) cell_temperature = pd.Series([0, 1, 3], index=times) wind_speed = pd.Series([0, 1, 2]) - msg = re.escape("temperature.prilliman only applies smoothing when the " - "sampling interval is shorter than 20 minutes (input " - "sampling interval: 30.0 minutes); returning " - "input temperature series unchanged") + msg = re.escape( + "temperature.prilliman only applies smoothing when the " + "sampling interval is shorter than 20 minutes (input " + "sampling interval: 30.0 minutes); returning " + "input temperature series unchanged" + ) with pytest.warns(UserWarning, match=msg): actual = temperature.prilliman(cell_temperature, wind_speed) assert_series_equal(cell_temperature, actual) @@ -363,12 +471,13 @@ def test_prilliman_coarse(): def test_prilliman_nans(): # nans in inputs are handled appropriately; nans in input tcell # are ignored but nans in wind speed cause nan in output - times = pd.date_range('2019-01-01', freq='1min', periods=8) + times = pd.date_range("2019-01-01", freq="1min", periods=8) cell_temperature = pd.Series([0, 1, 3, 6, 10, np.nan, 21, 27], index=times) wind_speed = pd.Series([0, 1, 2, 3, 2, 1, np.nan, 3]) actual = temperature.prilliman(cell_temperature, wind_speed) - expected = pd.Series([True, True, True, True, True, True, False, True], - index=times) + expected = pd.Series( + [True, True, True, True, True, True, False, True], index=times + ) assert_series_equal(actual.notnull(), expected) # check that nan temperatures do not mess up the weighted average; @@ -379,52 +488,58 @@ def test_prilliman_nans(): wind_speed = pd.Series(1, index=times) actual = temperature.prilliman(cell_temperature, wind_speed) # original implementation would return some values < 1 here - expected = pd.Series(1., index=times) + expected = pd.Series(1.0, index=times) assert_series_equal(actual, expected) def test_glm_conversions(): # it is easiest and sufficient to test conversion from & to the same model - glm = temperature.GenericLinearModel(module_efficiency=0.1, - absorptance=0.9) + glm = temperature.GenericLinearModel( + module_efficiency=0.1, absorptance=0.9 + ) - inp = {'u0': 25.0, 'u1': 6.84} + inp = {"u0": 25.0, "u1": 6.84} glm.use_faiman(**inp) out = glm.to_faiman() for k, v in inp.items(): assert np.isclose(out[k], v) - inp = {'u_c': 25, 'u_v': 4} + inp = {"u_c": 25, "u_v": 4} glm.use_pvsyst(**inp) out = glm.to_pvsyst() for k, v in inp.items(): assert np.isclose(out[k], v) # test with optional parameters - inp = {'u_c': 25, 'u_v': 4, - 'module_efficiency': 0.15, - 'alpha_absorption': 0.95} + inp = { + "u_c": 25, + "u_v": 4, + "module_efficiency": 0.15, + "alpha_absorption": 0.95, + } glm.use_pvsyst(**inp) out = glm.to_pvsyst() for k, v in inp.items(): assert np.isclose(out[k], v) - inp = {'noct': 47} + inp = {"noct": 47} glm.use_noct_sam(**inp) out = glm.to_noct_sam() for k, v in inp.items(): assert np.isclose(out[k], v) # test with optional parameters - inp = {'noct': 47, - 'module_efficiency': 0.15, - 'transmittance_absorptance': 0.95} + inp = { + "noct": 47, + "module_efficiency": 0.15, + "transmittance_absorptance": 0.95, + } glm.use_noct_sam(**inp) out = glm.to_noct_sam() for k, v in inp.items(): assert np.isclose(out[k], v) - inp = {'a': -3.5, 'b': -0.1} + inp = {"a": -3.5, "b": -0.1} glm.use_sapm(**inp) out = glm.to_sapm() for k, v in inp.items(): @@ -432,13 +547,13 @@ def test_glm_conversions(): def test_glm_simulations(): - - glm = temperature.GenericLinearModel(module_efficiency=0.1, - absorptance=0.9) - wind = np.array([1.4, 1/.51, 5.4]) + glm = temperature.GenericLinearModel( + module_efficiency=0.1, absorptance=0.9 + ) + wind = np.array([1.4, 1 / 0.51, 5.4]) weather = (800, 20, wind) - inp = {'u0': 20.0, 'u1': 5.0} + inp = {"u0": 20.0, "u1": 5.0} glm.use_faiman(**inp) out = glm(*weather) expected = temperature.faiman(*weather, **inp) @@ -456,15 +571,17 @@ def test_glm_simulations(): def test_glm_repr(): - - glm = temperature.GenericLinearModel(module_efficiency=0.1, - absorptance=0.9) - inp = {'u0': 20.0, 'u1': 5.0} + glm = temperature.GenericLinearModel( + module_efficiency=0.1, absorptance=0.9 + ) + inp = {"u0": 20.0, "u1": 5.0} glm.use_faiman(**inp) - expected = ("GenericLinearModel: {" - "'u_const': 16.0, " - "'du_wind': 4.0, " - "'eta': 0.1, " - "'alpha': 0.9}") + expected = ( + "GenericLinearModel: {" + "'u_const': 16.0, " + "'du_wind': 4.0, " + "'eta': 0.1, " + "'alpha': 0.9}" + ) assert glm.__repr__() == expected diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index eb9e65c895..1c960b4ff3 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -6,89 +6,117 @@ from numpy.testing import assert_allclose -@pytest.mark.parametrize('keys, input_dict, expected', [ - (['a', 'b'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}), - (['a', 'b', 'd'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}), - (['a'], {}, {}), - (['a'], {'b': 2}, {}) -]) +@pytest.mark.parametrize( + "keys, input_dict, expected", + [ + (["a", "b"], {"a": 1, "b": 2, "c": 3}, {"a": 1, "b": 2}), + (["a", "b", "d"], {"a": 1, "b": 2, "c": 3}, {"a": 1, "b": 2}), + (["a"], {}, {}), + (["a"], {"b": 2}, {}), + ], +) def test_build_kwargs(keys, input_dict, expected): kwargs = tools._build_kwargs(keys, input_dict) assert kwargs == expected def _obj_test_golden_sect(params, loc): - return params[loc] * (1. - params['c'] * params[loc]**params['n']) - - -@pytest.mark.parametrize('params, lb, ub, expected, func', [ - ({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect), - ({'c': 1e6, 'n': 6.}, 0., 1., 0.07230200263994839, _obj_test_golden_sect), - ({'c': 0.2, 'n': 0.3}, 0., 100., 89.14332727531685, _obj_test_golden_sect) -]) + return params[loc] * (1.0 - params["c"] * params[loc] ** params["n"]) + + +@pytest.mark.parametrize( + "params, lb, ub, expected, func", + [ + ({"c": 1.0, "n": 1.0}, 0.0, 1.0, 0.5, _obj_test_golden_sect), + ( + {"c": 1e6, "n": 6.0}, + 0.0, + 1.0, + 0.07230200263994839, + _obj_test_golden_sect, + ), + ( + {"c": 0.2, "n": 0.3}, + 0.0, + 100.0, + 89.14332727531685, + _obj_test_golden_sect, + ), + ], +) def test__golden_sect_DataFrame(params, lb, ub, expected, func): v, x = tools._golden_sect_DataFrame(params, lb, ub, func) assert np.isclose(x, expected, atol=1e-8) def test__golden_sect_DataFrame_atol(): - params = {'c': 0.2, 'n': 0.3} + params = {"c": 0.2, "n": 0.3} expected = 89.14332727531685 v, x = tools._golden_sect_DataFrame( - params, 0., 100., _obj_test_golden_sect, atol=1e-12) + params, 0.0, 100.0, _obj_test_golden_sect, atol=1e-12 + ) assert np.isclose(x, expected, atol=1e-12) def test__golden_sect_DataFrame_vector(): - params = {'c': np.array([1., 2.]), 'n': np.array([1., 1.])} - lower = np.array([0., 0.001]) + params = {"c": np.array([1.0, 2.0]), "n": np.array([1.0, 1.0])} + lower = np.array([0.0, 0.001]) upper = np.array([1.1, 1.2]) expected = np.array([0.5, 0.25]) - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8) # some upper and lower bounds equal - params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])} - lower = np.array([0., 0.001, 1.]) - upper = np.array([1., 1.2, 1.]) + params = {"c": np.array([1.0, 2.0, 1.0]), "n": np.array([1.0, 1.0, 1.0])} + lower = np.array([0.0, 0.001, 1.0]) + upper = np.array([1.0, 1.2, 1.0]) expected = np.array([0.5, 0.25, 1.0]) # x values for maxima - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8) # all upper and lower bounds equal, arrays of length 1 - params = {'c': np.array([1.]), 'n': np.array([1.])} - lower = np.array([1.]) - upper = np.array([1.]) - expected = np.array([1.]) # x values for maxima - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + params = {"c": np.array([1.0]), "n": np.array([1.0])} + lower = np.array([1.0]) + upper = np.array([1.0]) + expected = np.array([1.0]) # x values for maxima + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8) def test__golden_sect_DataFrame_nans(): # nan in bounds - params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])} - lower = np.array([0., 0.001, np.nan]) - upper = np.array([1.1, 1.2, 1.]) + params = {"c": np.array([1.0, 2.0, 1.0]), "n": np.array([1.0, 1.0, 1.0])} + lower = np.array([0.0, 0.001, np.nan]) + upper = np.array([1.1, 1.2, 1.0]) expected = np.array([0.5, 0.25, np.nan]) - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8, equal_nan=True) # nan in function values - params = {'c': np.array([1., 2., np.nan]), 'n': np.array([1., 1., 1.])} - lower = np.array([0., 0.001, 0.]) - upper = np.array([1.1, 1.2, 1.]) + params = { + "c": np.array([1.0, 2.0, np.nan]), + "n": np.array([1.0, 1.0, 1.0]), + } + lower = np.array([0.0, 0.001, 0.0]) + upper = np.array([1.1, 1.2, 1.0]) expected = np.array([0.5, 0.25, np.nan]) - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8, equal_nan=True) # all nan in bounds - params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])} + params = {"c": np.array([1.0, 2.0, 1.0]), "n": np.array([1.0, 1.0, 1.0])} lower = np.array([np.nan, np.nan, np.nan]) - upper = np.array([1.1, 1.2, 1.]) + upper = np.array([1.1, 1.2, 1.0]) expected = np.array([np.nan, np.nan, np.nan]) - v, x = tools._golden_sect_DataFrame(params, lower, upper, - _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame( + params, lower, upper, _obj_test_golden_sect + ) assert np.allclose(x, expected, atol=1e-8, equal_nan=True) @@ -96,24 +124,53 @@ def test_degrees_to_index_1(): """Test that _degrees_to_index raises an error when something other than 'latitude' or 'longitude' is passed.""" with pytest.raises(IndexError): # invalid value for coordinate argument - tools._degrees_to_index(degrees=22.0, coordinate='width') - - -@pytest.mark.parametrize('args, args_idx', [ - # no pandas.Series or pandas.DataFrame args - ((1,), None), - (([1],), None), - ((np.array(1),), None), - ((np.array([1]),), None), - # has pandas.Series or pandas.DataFrame args - ((pd.DataFrame([1], index=[1]),), 0), - ((pd.Series([1], index=[1]),), 0), - ((1, pd.Series([1], index=[1]),), 1), - ((1, pd.DataFrame([1], index=[1]),), 1), - # first pandas.Series or pandas.DataFrame is used - ((1, pd.Series([1], index=[1]), pd.DataFrame([2], index=[2]),), 1), - ((1, pd.DataFrame([1], index=[1]), pd.Series([2], index=[2]),), 1), -]) + tools._degrees_to_index(degrees=22.0, coordinate="width") + + +@pytest.mark.parametrize( + "args, args_idx", + [ + # no pandas.Series or pandas.DataFrame args + ((1,), None), + (([1],), None), + ((np.array(1),), None), + ((np.array([1]),), None), + # has pandas.Series or pandas.DataFrame args + ((pd.DataFrame([1], index=[1]),), 0), + ((pd.Series([1], index=[1]),), 0), + ( + ( + 1, + pd.Series([1], index=[1]), + ), + 1, + ), + ( + ( + 1, + pd.DataFrame([1], index=[1]), + ), + 1, + ), + # first pandas.Series or pandas.DataFrame is used + ( + ( + 1, + pd.Series([1], index=[1]), + pd.DataFrame([2], index=[2]), + ), + 1, + ), + ( + ( + 1, + pd.DataFrame([1], index=[1]), + pd.Series([2], index=[2]), + ), + 1, + ), + ], +) def test_get_pandas_index(args, args_idx): index = tools.get_pandas_index(*args) @@ -123,24 +180,29 @@ def test_get_pandas_index(args, args_idx): pd.testing.assert_index_equal(args[args_idx].index, index) -@pytest.mark.parametrize('data_in,expected', [ - (np.array([1, 2, 3, 4, 5]), - np.array([0.2, 0.4, 0.6, 0.8, 1])), - (np.array([[0, 1, 2], [0, 3, 6]]), - np.array([[0, 0.5, 1], [0, 0.5, 1]])), - (pd.Series([1, 2, 3, 4, 5]), - pd.Series([0.2, 0.4, 0.6, 0.8, 1])), - (pd.DataFrame({"a": [0, 1, 2], "b": [0, 2, 8]}), - pd.DataFrame({"a": [0, 0.5, 1], "b": [0, 0.25, 1]})), - # test with NaN and all zeroes - (pd.DataFrame({"a": [0, np.nan, 1], "b": [0, 0, 0]}), - pd.DataFrame({"a": [0, np.nan, 1], "b": [np.nan]*3})), - # test with negative values - (np.array([1, 2, -3, 4, -5]), - np.array([0.2, 0.4, -0.6, 0.8, -1])), - (pd.Series([-2, np.nan, 1]), - pd.Series([-1, np.nan, 0.5])), -]) +@pytest.mark.parametrize( + "data_in,expected", + [ + (np.array([1, 2, 3, 4, 5]), np.array([0.2, 0.4, 0.6, 0.8, 1])), + ( + np.array([[0, 1, 2], [0, 3, 6]]), + np.array([[0, 0.5, 1], [0, 0.5, 1]]), + ), + (pd.Series([1, 2, 3, 4, 5]), pd.Series([0.2, 0.4, 0.6, 0.8, 1])), + ( + pd.DataFrame({"a": [0, 1, 2], "b": [0, 2, 8]}), + pd.DataFrame({"a": [0, 0.5, 1], "b": [0, 0.25, 1]}), + ), + # test with NaN and all zeroes + ( + pd.DataFrame({"a": [0, np.nan, 1], "b": [0, 0, 0]}), + pd.DataFrame({"a": [0, np.nan, 1], "b": [np.nan] * 3}), + ), + # test with negative values + (np.array([1, 2, -3, 4, -5]), np.array([0.2, 0.4, -0.6, 0.8, -1])), + (pd.Series([-2, np.nan, 1]), pd.Series([-1, np.nan, 0.5])), + ], +) def test_normalize_max2one(data_in, expected): result = tools.normalize_max2one(data_in) assert_allclose(result, expected) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 03536762b5..a94ed3c167 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -8,24 +8,39 @@ import pvlib from pvlib import tracking from .conftest import DATA_DIR, assert_frame_equal, assert_series_equal -from pvlib._deprecation import pvlibDeprecationWarning -SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', - 'surface_azimuth', 'surface_tilt'] +SINGLEAXIS_COL_ORDER = [ + "tracker_theta", + "aoi", + "surface_azimuth", + "surface_tilt", +] def test_solar_noon(): - index = pd.date_range(start='20180701T1200', freq='1s', periods=1) + index = pd.date_range(start="20180701T1200", freq="1s", periods=1) apparent_zenith = pd.Series([10], index=index) apparent_azimuth = pd.Series([180], index=index) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'tracker_theta': 0, 'aoi': 10, - 'surface_azimuth': 90, 'surface_tilt': 0}, - index=index, dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "tracker_theta": 0, + "aoi": 10, + "surface_azimuth": 90, + "surface_tilt": 0, + }, + index=index, + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -34,13 +49,22 @@ def test_solar_noon(): def test_scalars(): apparent_zenith = 10 apparent_azimuth = 180 - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) assert isinstance(tracker_data, dict) - expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90, - 'surface_tilt': 0} + expect = { + "tracker_theta": 0, + "aoi": 10, + "surface_azimuth": 90, + "surface_tilt": 0, + } for k, v in expect.items(): assert np.isclose(tracker_data[k], v) @@ -48,13 +72,22 @@ def test_scalars(): def test_arrays(): apparent_zenith = np.array([10]) apparent_azimuth = np.array([180]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) assert isinstance(tracker_data, dict) - expect = {'tracker_theta': 0, 'aoi': 10, 'surface_azimuth': 90, - 'surface_tilt': 0} + expect = { + "tracker_theta": 0, + "aoi": 10, + "surface_azimuth": 90, + "surface_tilt": 0, + } for k, v in expect.items(): assert_allclose(tracker_data[k], v, atol=1e-7) @@ -62,31 +95,48 @@ def test_arrays(): def test_nans(): apparent_zenith = np.array([10, np.nan, 10]) apparent_azimuth = np.array([180, 180, np.nan]) - with np.errstate(invalid='ignore'): - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - expect = {'tracker_theta': np.array([0, nan, nan]), - 'aoi': np.array([10, nan, nan]), - 'surface_azimuth': np.array([90, nan, nan]), - 'surface_tilt': np.array([0, nan, nan])} + with np.errstate(invalid="ignore"): + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + expect = { + "tracker_theta": np.array([0, nan, nan]), + "aoi": np.array([10, nan, nan]), + "surface_azimuth": np.array([90, nan, nan]), + "surface_tilt": np.array([0, nan, nan]), + } for k, v in expect.items(): assert_allclose(tracker_data[k], v, atol=1e-7) # repeat with Series because nans can differ apparent_zenith = pd.Series(apparent_zenith) apparent_azimuth = pd.Series(apparent_azimuth) - with np.errstate(invalid='ignore'): - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - expect = pd.DataFrame(np.array( - [[ 0., 10., 90., 0.], - [nan, nan, nan, nan], - [nan, nan, nan, nan]]), - columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']) + with np.errstate(invalid="ignore"): + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + expect = pd.DataFrame( + np.array( + [ + [0.0, 10.0, 90.0, 0.0], + [nan, nan, nan, nan], + [nan, nan, nan, nan], + ] + ), + columns=["tracker_theta", "aoi", "surface_azimuth", "surface_tilt"], + ) assert_frame_equal(tracker_data, expect) @@ -95,10 +145,15 @@ def test_arrays_multi(): apparent_azimuth = np.array([[180, 180], [180, 180]]) # singleaxis should fail for num dim > 1 with pytest.raises(ValueError): - tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) + tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) # uncomment if we ever get singleaxis to support num dim > 1 arrays # assert isinstance(tracker_data, dict) # expect = {'tracker_theta': np.full_like(apparent_zenith, 0), @@ -113,24 +168,41 @@ def test_azimuth_north_south(): apparent_zenith = pd.Series([60]) apparent_azimuth = pd.Series([90]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=180, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'tracker_theta': -60, 'aoi': 0, - 'surface_azimuth': 90, 'surface_tilt': 60}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=180, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "tracker_theta": -60, + "aoi": 0, + "surface_azimuth": 90, + "surface_tilt": 60, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) - expect['tracker_theta'] *= -1 + expect["tracker_theta"] *= -1 assert_frame_equal(expect, tracker_data) @@ -138,14 +210,26 @@ def test_azimuth_north_south(): def test_max_angle(): apparent_zenith = pd.Series([60]) apparent_azimuth = pd.Series([90]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=45, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 15, 'surface_azimuth': 90, - 'surface_tilt': 45, 'tracker_theta': 45}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=45, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 15, + "surface_azimuth": 90, + "surface_tilt": 45, + "tracker_theta": 45, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -154,14 +238,26 @@ def test_max_angle(): def test_min_angle(): apparent_zenith = pd.Series([60]) apparent_azimuth = pd.Series([270]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=(-45, 50), backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 15, 'surface_azimuth': 270, - 'surface_tilt': 45, 'tracker_theta': -45}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=(-45, 50), + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 15, + "surface_azimuth": 270, + "surface_tilt": 45, + "tracker_theta": -45, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -171,26 +267,50 @@ def test_backtrack(): apparent_zenith = pd.Series([80]) apparent_azimuth = pd.Series([90]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=False, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 0, 'surface_azimuth': 90, - 'surface_tilt': 80, 'tracker_theta': 80}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=False, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 0, + "surface_azimuth": 90, + "surface_tilt": 80, + "tracker_theta": 80, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 52.5716, 'surface_azimuth': 90, - 'surface_tilt': 27.42833, 'tracker_theta': 27.4283}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 52.5716, + "surface_azimuth": 90, + "surface_tilt": 27.42833, + "tracker_theta": 27.4283, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -200,27 +320,50 @@ def test_axis_tilt(): apparent_zenith = pd.Series([30]) apparent_azimuth = pd.Series([135]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=30, axis_azimuth=180, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 7.286245, 'surface_azimuth': 142.65730, - 'surface_tilt': 35.98741, - 'tracker_theta': -20.88121}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=30, + axis_azimuth=180, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 7.286245, + "surface_azimuth": 142.65730, + "surface_tilt": 35.98741, + "tracker_theta": -20.88121, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=30, axis_azimuth=0, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 47.6632, 'surface_azimuth': 50.96969, - 'surface_tilt': 42.5152, 'tracker_theta': 31.6655}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=30, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 47.6632, + "surface_azimuth": 50.96969, + "surface_tilt": 42.5152, + "tracker_theta": 31.6655, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -230,14 +373,26 @@ def test_axis_azimuth(): apparent_zenith = pd.Series([30]) apparent_azimuth = pd.Series([90]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=90, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 30, 'surface_azimuth': 180, - 'surface_tilt': 0, 'tracker_theta': 0}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=90, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 30, + "surface_azimuth": 180, + "surface_tilt": 0, + "tracker_theta": 0, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -245,14 +400,26 @@ def test_axis_azimuth(): apparent_zenith = pd.Series([30]) apparent_azimuth = pd.Series([180]) - tracker_data = tracking.singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=90, - max_angle=90, backtrack=True, - gcr=2.0/7.0) - - expect = pd.DataFrame({'aoi': 0, 'surface_azimuth': 180, - 'surface_tilt': 30, 'tracker_theta': 30}, - index=[0], dtype=np.float64) + tracker_data = tracking.singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=90, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + ) + + expect = pd.DataFrame( + { + "aoi": 0, + "surface_azimuth": 180, + "surface_tilt": 30, + "tracker_theta": 30, + }, + index=[0], + dtype=np.float64, + ) expect = expect[SINGLEAXIS_COL_ORDER] assert_frame_equal(expect, tracker_data) @@ -266,13 +433,24 @@ def test_horizon_flat(): solar_zenith = pd.Series(solar_zenith) # depending on platform and numpy versions this will generate # RuntimeWarning: invalid value encountered in > < >= - out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=0, - axis_azimuth=180, backtrack=False, max_angle=180) - expected = pd.DataFrame(np.array( - [[ nan, nan, nan, nan], - [ 0., 45., 270., 0.], - [ nan, nan, nan, nan]]), - columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']) + out = tracking.singleaxis( + solar_zenith, + solar_azimuth, + axis_tilt=0, + axis_azimuth=180, + backtrack=False, + max_angle=180, + ) + expected = pd.DataFrame( + np.array( + [ + [nan, nan, nan, nan], + [0.0, 45.0, 270.0, 0.0], + [nan, nan, nan, nan], + ] + ), + columns=["tracker_theta", "aoi", "surface_azimuth", "surface_tilt"], + ) assert_frame_equal(out, expected) @@ -282,26 +460,44 @@ def test_horizon_tilted(): solar_zenith = np.full_like(solar_azimuth, 45) solar_azimuth = pd.Series(solar_azimuth) solar_zenith = pd.Series(solar_zenith) - out = tracking.singleaxis(solar_zenith, solar_azimuth, axis_tilt=90, - axis_azimuth=180, backtrack=False, max_angle=180) - expected = pd.DataFrame(np.array( - [[-180., 45., 0., 90.], - [ 0., 45., 180., 90.], - [ 179., 45., 359., 90.]]), - columns=['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']) + out = tracking.singleaxis( + solar_zenith, + solar_azimuth, + axis_tilt=90, + axis_azimuth=180, + backtrack=False, + max_angle=180, + ) + expected = pd.DataFrame( + np.array( + [ + [-180.0, 45.0, 0.0, 90.0], + [0.0, 45.0, 180.0, 90.0], + [179.0, 45.0, 359.0, 90.0], + ] + ), + columns=["tracker_theta", "aoi", "surface_azimuth", "surface_tilt"], + ) assert_frame_equal(out, expected) def test_low_sun_angles(): # GH 656, 824 result = tracking.singleaxis( - apparent_zenith=80, apparent_azimuth=338, axis_tilt=30, - axis_azimuth=180, max_angle=60, backtrack=True, gcr=0.35) + apparent_zenith=80, + apparent_azimuth=338, + axis_tilt=30, + axis_azimuth=180, + max_angle=60, + backtrack=True, + gcr=0.35, + ) expected = { - 'tracker_theta': np.array([60.0]), - 'aoi': np.array([80.420987]), - 'surface_azimuth': np.array([253.897886]), - 'surface_tilt': np.array([64.341094])} + "tracker_theta": np.array([60.0]), + "aoi": np.array([80.420987]), + "surface_azimuth": np.array([253.897886]), + "surface_tilt": np.array([64.341094]), + } for k, v in result.items(): assert_allclose(expected[k], v) @@ -310,13 +506,13 @@ def test_calc_axis_tilt(): # expected values expected_axis_tilt = 2.239 # [degrees] expected_side_slope = 9.86649274360294 # [degrees] - expected = DATA_DIR / 'singleaxis_tracker_wslope.csv' - expected = pd.read_csv(expected, index_col='timestamp', parse_dates=True) + expected = DATA_DIR / "singleaxis_tracker_wslope.csv" + expected = pd.read_csv(expected, index_col="timestamp", parse_dates=True) # solar positions - starttime = '2017-01-01T00:30:00-0300' - stoptime = '2017-12-31T23:59:59-0300' + starttime = "2017-01-01T00:30:00-0300" + stoptime = "2017-12-31T23:59:59-0300" lat, lon = -27.597300, -48.549610 - times = pd.DatetimeIndex(pd.date_range(starttime, stoptime, freq='h')) + times = pd.DatetimeIndex(pd.date_range(starttime, stoptime, freq="h")) solpos = pvlib.solarposition.get_solarposition(times, lat, lon) # singleaxis tracker w/slope data slope_azimuth, slope_tilt = 77.34, 10.1149 @@ -326,83 +522,130 @@ def test_calc_axis_tilt(): gcr = 0.33292759 # GCR = length / horizontal_pitch = 1.64 / 5 / cos(9.86) # calculate tracker axis zenith axis_tilt = tracking.calc_axis_tilt( - slope_azimuth, slope_tilt, axis_azimuth=axis_azimuth) + slope_azimuth, slope_tilt, axis_azimuth=axis_azimuth + ) assert np.isclose(axis_tilt, expected_axis_tilt) # calculate cross-axis tilt and relative rotation cross_axis_tilt = tracking.calc_cross_axis_tilt( - slope_azimuth, slope_tilt, axis_azimuth, axis_tilt) + slope_azimuth, slope_tilt, axis_azimuth, axis_tilt + ) assert np.isclose(cross_axis_tilt, expected_side_slope) sat = tracking.singleaxis( - solpos.apparent_zenith, solpos.azimuth, axis_tilt, axis_azimuth, - max_angle, backtrack=True, gcr=gcr, cross_axis_tilt=cross_axis_tilt) + solpos.apparent_zenith, + solpos.azimuth, + axis_tilt, + axis_azimuth, + max_angle, + backtrack=True, + gcr=gcr, + cross_axis_tilt=cross_axis_tilt, + ) np.testing.assert_allclose( - sat['tracker_theta'], expected['tracker_theta'], atol=1e-7) - np.testing.assert_allclose(sat['aoi'], expected['aoi'], atol=1e-7) + sat["tracker_theta"], expected["tracker_theta"], atol=1e-7 + ) + np.testing.assert_allclose(sat["aoi"], expected["aoi"], atol=1e-7) np.testing.assert_allclose( - sat['surface_azimuth'], expected['surface_azimuth'], atol=1e-7) + sat["surface_azimuth"], expected["surface_azimuth"], atol=1e-7 + ) np.testing.assert_allclose( - sat['surface_tilt'], expected['surface_tilt'], atol=1e-7) + sat["surface_tilt"], expected["surface_tilt"], atol=1e-7 + ) def test_slope_aware_backtracking(): """ Test validation data set from https://www.nrel.gov/docs/fy20osti/76626.pdf """ - index = pd.date_range('2019-01-01T08:00', '2019-01-01T17:00', freq='h') - index = index.tz_localize('Etc/GMT+5') - expected_data = pd.DataFrame(index=index, data=[ - ( 2.404287, 122.79177, -84.440, -10.899), - (11.263058, 133.288729, -72.604, -25.747), - (18.733558, 145.285552, -59.861, -59.861), - (24.109076, 158.939435, -45.578, -45.578), - (26.810735, 173.931802, -28.764, -28.764), - (26.482495, 189.371536, -8.475, -8.475), - (23.170447, 204.13681, 15.120, 15.120), - (17.296785, 217.446538, 39.562, 39.562), - ( 9.461862, 229.102218, 61.587, 32.339), - ( 0.524817, 239.330401, 79.530, 5.490), - ], columns=['ApparentElevation', 'SolarAzimuth', - 'TrueTracking', 'Backtracking']) + index = pd.date_range("2019-01-01T08:00", "2019-01-01T17:00", freq="h") + index = index.tz_localize("Etc/GMT+5") + expected_data = pd.DataFrame( + index=index, + data=[ + (2.404287, 122.79177, -84.440, -10.899), + (11.263058, 133.288729, -72.604, -25.747), + (18.733558, 145.285552, -59.861, -59.861), + (24.109076, 158.939435, -45.578, -45.578), + (26.810735, 173.931802, -28.764, -28.764), + (26.482495, 189.371536, -8.475, -8.475), + (23.170447, 204.13681, 15.120, 15.120), + (17.296785, 217.446538, 39.562, 39.562), + (9.461862, 229.102218, 61.587, 32.339), + (0.524817, 239.330401, 79.530, 5.490), + ], + columns=[ + "ApparentElevation", + "SolarAzimuth", + "TrueTracking", + "Backtracking", + ], + ) expected_axis_tilt = 9.666 expected_slope_angle = -2.576 slope_azimuth, slope_tilt = 180.0, 10.0 axis_azimuth = 195.0 axis_tilt = tracking.calc_axis_tilt( - slope_azimuth, slope_tilt, axis_azimuth) + slope_azimuth, slope_tilt, axis_azimuth + ) assert np.isclose(axis_tilt, expected_axis_tilt, rtol=1e-3, atol=1e-3) cross_axis_tilt = tracking.calc_cross_axis_tilt( - slope_azimuth, slope_tilt, axis_azimuth, axis_tilt) + slope_azimuth, slope_tilt, axis_azimuth, axis_tilt + ) assert np.isclose( - cross_axis_tilt, expected_slope_angle, rtol=1e-3, atol=1e-3) + cross_axis_tilt, expected_slope_angle, rtol=1e-3, atol=1e-3 + ) sat = tracking.singleaxis( - 90.0-expected_data['ApparentElevation'], expected_data['SolarAzimuth'], - axis_tilt, axis_azimuth, max_angle=90.0, backtrack=True, gcr=0.5, - cross_axis_tilt=cross_axis_tilt) - assert_series_equal(sat['tracker_theta'], - expected_data['Backtracking'].rename('tracker_theta'), - check_less_precise=True) + 90.0 - expected_data["ApparentElevation"], + expected_data["SolarAzimuth"], + axis_tilt, + axis_azimuth, + max_angle=90.0, + backtrack=True, + gcr=0.5, + cross_axis_tilt=cross_axis_tilt, + ) + assert_series_equal( + sat["tracker_theta"], + expected_data["Backtracking"].rename("tracker_theta"), + check_less_precise=True, + ) truetracking = tracking.singleaxis( - 90.0-expected_data['ApparentElevation'], expected_data['SolarAzimuth'], - axis_tilt, axis_azimuth, max_angle=90.0, backtrack=False, gcr=0.5, - cross_axis_tilt=cross_axis_tilt) - assert_series_equal(truetracking['tracker_theta'], - expected_data['TrueTracking'].rename('tracker_theta'), - check_less_precise=True) + 90.0 - expected_data["ApparentElevation"], + expected_data["SolarAzimuth"], + axis_tilt, + axis_azimuth, + max_angle=90.0, + backtrack=False, + gcr=0.5, + cross_axis_tilt=cross_axis_tilt, + ) + assert_series_equal( + truetracking["tracker_theta"], + expected_data["TrueTracking"].rename("tracker_theta"), + check_less_precise=True, + ) def test_singleaxis_aoi_gh1221(): # vertical tracker loc = pvlib.location.Location(40.1134, -88.3695) dr = pd.date_range( - start='02-Jun-1998 00:00:00', end='02-Jun-1998 23:55:00', freq='5min', - tz='Etc/GMT+6') + start="02-Jun-1998 00:00:00", + end="02-Jun-1998 23:55:00", + freq="5min", + tz="Etc/GMT+6", + ) sp = loc.get_solarposition(dr) tr = pvlib.tracking.singleaxis( - sp['apparent_zenith'], sp['azimuth'], axis_tilt=90, axis_azimuth=180, - max_angle=0.001, backtrack=False) - fixed = pvlib.irradiance.aoi(90, 180, sp['apparent_zenith'], sp['azimuth']) - fixed[np.isnan(tr['aoi'])] = np.nan - assert np.allclose(tr['aoi'], fixed, equal_nan=True) + sp["apparent_zenith"], + sp["azimuth"], + axis_tilt=90, + axis_azimuth=180, + max_angle=0.001, + backtrack=False, + ) + fixed = pvlib.irradiance.aoi(90, 180, sp["apparent_zenith"], sp["azimuth"]) + fixed[np.isnan(tr["aoi"])] = np.nan + assert np.allclose(tr["aoi"], fixed, equal_nan=True) def test_calc_surface_orientation_types(): @@ -411,23 +654,24 @@ def test_calc_surface_orientation_types(): expected_tilts = np.array([10, 0, 10], dtype=float) expected_azimuths = np.array([270, 90, 90], dtype=float) out = tracking.calc_surface_orientation(tracker_theta=rotations) - np.testing.assert_allclose(expected_tilts, out['surface_tilt']) - np.testing.assert_allclose(expected_azimuths, out['surface_azimuth']) + np.testing.assert_allclose(expected_tilts, out["surface_tilt"]) + np.testing.assert_allclose(expected_azimuths, out["surface_azimuth"]) # pandas Series rotations = pd.Series(rotations) - expected_tilts = pd.Series(expected_tilts).rename('surface_tilt') - expected_azimuths = pd.Series(expected_azimuths).rename('surface_azimuth') + expected_tilts = pd.Series(expected_tilts).rename("surface_tilt") + expected_azimuths = pd.Series(expected_azimuths).rename("surface_azimuth") out = tracking.calc_surface_orientation(tracker_theta=rotations) - assert_series_equal(expected_tilts, out['surface_tilt']) - assert_series_equal(expected_azimuths, out['surface_azimuth']) + assert_series_equal(expected_tilts, out["surface_tilt"]) + assert_series_equal(expected_azimuths, out["surface_azimuth"]) # float for rotation, expected_tilt, expected_azimuth in zip( - rotations, expected_tilts, expected_azimuths): + rotations, expected_tilts, expected_azimuths + ): out = tracking.calc_surface_orientation(rotation) - assert out['surface_tilt'] == pytest.approx(expected_tilt) - assert out['surface_azimuth'] == pytest.approx(expected_azimuth) + assert out["surface_tilt"] == pytest.approx(expected_tilt) + assert out["surface_azimuth"] == pytest.approx(expected_azimuth) def test_calc_surface_orientation_kwargs(): @@ -435,11 +679,11 @@ def test_calc_surface_orientation_kwargs(): rotations = np.array([-10, 0, 10]) expected_tilts = np.array([22.2687445, 20.0, 22.2687445]) expected_azimuths = np.array([152.72683041, 180.0, 207.27316959]) - out = tracking.calc_surface_orientation(rotations, - axis_tilt=20, - axis_azimuth=180) - np.testing.assert_allclose(out['surface_tilt'], expected_tilts) - np.testing.assert_allclose(out['surface_azimuth'], expected_azimuths) + out = tracking.calc_surface_orientation( + rotations, axis_tilt=20, axis_azimuth=180 + ) + np.testing.assert_allclose(out["surface_tilt"], expected_tilts) + np.testing.assert_allclose(out["surface_azimuth"], expected_azimuths) def test_calc_surface_orientation_special(): @@ -448,16 +692,16 @@ def test_calc_surface_orientation_special(): expected_tilts = np.array([180, 90, 0, 0, 90, 180], dtype=float) expected_azimuths = [270, 270, 90, 90, 90, 90] out = tracking.calc_surface_orientation(rotations) - np.testing.assert_allclose(out['surface_tilt'], expected_tilts) - np.testing.assert_allclose(out['surface_azimuth'], expected_azimuths) + np.testing.assert_allclose(out["surface_tilt"], expected_tilts) + np.testing.assert_allclose(out["surface_azimuth"], expected_azimuths) # special case for axis_tilt rotations = np.array([-10, 0, 10]) expected_tilts = np.array([90, 90, 90], dtype=float) expected_azimuths = np.array([350, 0, 10], dtype=float) out = tracking.calc_surface_orientation(rotations, axis_tilt=90) - np.testing.assert_allclose(out['surface_tilt'], expected_tilts) - np.testing.assert_allclose(out['surface_azimuth'], expected_azimuths) + np.testing.assert_allclose(out["surface_tilt"], expected_tilts) + np.testing.assert_allclose(out["surface_azimuth"], expected_azimuths) # special cases for axis_azimuth rotations = np.array([-10, 0, 10]) @@ -465,10 +709,15 @@ def test_calc_surface_orientation_special(): expected_azimuth_offsets = np.array([-90, 90, 90], dtype=float) for axis_azimuth in [0, 90, 180, 270, 360]: expected_azimuths = (axis_azimuth + expected_azimuth_offsets) % 360 - out = tracking.calc_surface_orientation(rotations, - axis_azimuth=axis_azimuth) - np.testing.assert_allclose(out['surface_tilt'], expected_tilts) + out = tracking.calc_surface_orientation( + rotations, axis_azimuth=axis_azimuth + ) + np.testing.assert_allclose(out["surface_tilt"], expected_tilts) # the rounding is a bit ugly, but necessary to test approximately equal # in a modulo-360 sense. - np.testing.assert_allclose(np.round(out['surface_azimuth'], 4) % 360, - expected_azimuths, rtol=1e-5, atol=1e-5) + np.testing.assert_allclose( + np.round(out["surface_azimuth"], 4) % 360, + expected_azimuths, + rtol=1e-5, + atol=1e-5, + ) diff --git a/pvlib/tests/test_transformer.py b/pvlib/tests/test_transformer.py index 0739a9e95a..dd1b1c3105 100644 --- a/pvlib/tests/test_transformer.py +++ b/pvlib/tests/test_transformer.py @@ -6,34 +6,37 @@ def test_simple_efficiency(): - # define test inputs - input_power = pd.Series([ - -800.0, - 436016.609823837, - 1511820.16603752, - 1580687.44677249, - 1616441.79660171 - ]) + input_power = pd.Series( + [ + -800.0, + 436016.609823837, + 1511820.16603752, + 1580687.44677249, + 1616441.79660171, + ] + ) no_load_loss = 0.002 load_loss = 0.007 transformer_rating = 2750000 # define expected test results - expected_output_power = pd.Series([ - -6300.10103234071, - 430045.854892526, - 1500588.39919874, - 1568921.77089526, - 1604389.62839879 - ]) + expected_output_power = pd.Series( + [ + -6300.10103234071, + 430045.854892526, + 1500588.39919874, + 1568921.77089526, + 1604389.62839879, + ] + ) # run test function with test inputs calculated_output_power = transformer.simple_efficiency( input_power=input_power, no_load_loss=no_load_loss, load_loss=load_loss, - transformer_rating=transformer_rating + transformer_rating=transformer_rating, ) # determine if expected results are obtained @@ -48,13 +51,13 @@ def test_simple_efficiency_known_values(): # verify correct behavior at no-load condition assert_allclose( - transformer.simple_efficiency(no_load_loss*rating, *args), - 0.0 + transformer.simple_efficiency(no_load_loss * rating, *args), 0.0 ) # verify correct behavior at rated condition assert_allclose( - transformer.simple_efficiency(rating*(1 + no_load_loss + load_loss), - *args), + transformer.simple_efficiency( + rating * (1 + no_load_loss + load_loss), *args + ), rating, ) diff --git a/pvlib/tools.py b/pvlib/tools.py index c8d4d6e309..26831fc56c 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -137,9 +137,9 @@ def localize_to_utc(time, location): time_utc = time.astimezone(pytz.utc) else: try: - time_utc = time.tz_convert('UTC') + time_utc = time.tz_convert("UTC") except TypeError: - time_utc = time.tz_localize(location.tz).tz_convert('UTC') + time_utc = time.tz_localize(location.tz).tz_convert("UTC") return time_utc @@ -165,12 +165,12 @@ def datetime_to_djd(time): time_utc = time.astimezone(pytz.utc) djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12)) - djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24) + djd = (time_utc - djd_start).total_seconds() * 1.0 / (60 * 60 * 24) return djd -def djd_to_datetime(djd, tz='UTC'): +def djd_to_datetime(djd, tz="UTC"): """ Converts a Dublin Julian Day float to a datetime.datetime object @@ -228,7 +228,7 @@ def _pandas_to_utc(pd_object): pandas object localized to or assumed to be UTC. """ try: - pd_object_utc = pd_object.tz_convert('UTC') + pd_object_utc = pd_object.tz_convert("UTC") except TypeError: pd_object_utc = pd_object return pd_object_utc @@ -247,8 +247,8 @@ def _doy_to_datetimeindex(doy, epoch_year=2014): ------- pd.DatetimeIndex """ - doy = np.atleast_1d(doy).astype('float') - epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1)) + doy = np.atleast_1d(doy).astype("float") + epoch = pd.Timestamp("{}-12-31".format(epoch_year - 1)) timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy] return pd.DatetimeIndex(timestamps) @@ -326,8 +326,10 @@ def _build_args(keys, input_dict, dict_name): args = [input_dict[key] for key in keys] except KeyError as e: missing_key = e.args[0] - msg = (f"Missing required parameter '{missing_key}'. Found " - f"{input_dict} in {dict_name}.") + msg = ( + f"Missing required parameter '{missing_key}'. Found " + f"{input_dict} in {dict_name}." + ) raise KeyError(msg) return args @@ -378,43 +380,43 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): -------- pvlib.singlediode._pwr_optfcn """ - if np.any(upper - lower < 0.): - raise ValueError('upper >= lower is required') + if np.any(upper - lower < 0.0): + raise ValueError("upper >= lower is required") phim1 = (np.sqrt(5) - 1) / 2 df = params.copy() # shallow copy to avoid modifying caller's dict - df['VH'] = upper - df['VL'] = lower + df["VH"] = upper + df["VL"] = lower converged = False while not converged: + phi = phim1 * (df["VH"] - df["VL"]) + df["V1"] = df["VL"] + phi + df["V2"] = df["VH"] - phi - phi = phim1 * (df['VH'] - df['VL']) - df['V1'] = df['VL'] + phi - df['V2'] = df['VH'] - phi - - df['f1'] = func(df, 'V1') - df['f2'] = func(df, 'V2') - df['SW_Flag'] = df['f1'] > df['f2'] + df["f1"] = func(df, "V1") + df["f2"] = func(df, "V2") + df["SW_Flag"] = df["f1"] > df["f2"] - df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag']) - df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) + df["VL"] = df["V2"] * df["SW_Flag"] + df["VL"] * (~df["SW_Flag"]) + df["VH"] = df["V1"] * ~df["SW_Flag"] + df["VH"] * (df["SW_Flag"]) - err = abs(df['V2'] - df['V1']) + err = abs(df["V2"] - df["V1"]) # handle all NaN case gracefully with warnings.catch_warnings(): - warnings.filterwarnings(action='ignore', - message='All-NaN slice encountered') + warnings.filterwarnings( + action="ignore", message="All-NaN slice encountered" + ) converged = np.all(err[~np.isnan(err)] < atol) # best estimate of location of maximum - df['max'] = 0.5 * (df['V1'] + df['V2']) - func_result = func(df, 'max') - x = np.where(np.isnan(func_result), np.nan, df['max']) - if np.isscalar(df['max']): + df["max"] = 0.5 * (df["V1"] + df["V2"]) + func_result = func(df, "max") + x = np.where(np.isnan(func_result), np.nan, df["max"]) + if np.isscalar(df["max"]): # np.where always returns an ndarray, converting scalars to 0d-arrays x = x.item() @@ -422,10 +424,10 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): def _get_sample_intervals(times, win_length): - """ Calculates time interval and samples per window for Reno-style clear + """Calculates time interval and samples per window for Reno-style clear sky detection functions """ - deltas = np.diff(times.values) / np.timedelta64(1, '60s') + deltas = np.diff(times.values) / np.timedelta64(1, "60s") # determine if we can proceed if times.inferred_freq and len(np.unique(deltas)) == 1: @@ -435,9 +437,9 @@ def _get_sample_intervals(times, win_length): return sample_interval, samples_per_window else: message = ( - 'algorithm does not yet support unequal time intervals. consider ' - 'resampling your data and checking for gaps from missing ' - 'periods, leap days, etc.' + "algorithm does not yet support unequal time intervals. consider " + "resampling your data and checking for gaps from missing " + "periods, leap days, etc." ) raise NotImplementedError(message) @@ -460,11 +462,11 @@ def _degrees_to_index(degrees, coordinate): in the Linke turbidity lookup table. """ # Assign inputmin, inputmax, and outputmax based on degree type. - if coordinate == 'latitude': + if coordinate == "latitude": inputmin = 90 inputmax = -90 outputmax = 2160 - elif coordinate == 'longitude': + elif coordinate == "longitude": inputmin = -180 inputmax = 180 outputmax = 4320 @@ -472,12 +474,13 @@ def _degrees_to_index(degrees, coordinate): raise IndexError("coordinate must be 'latitude' or 'longitude'.") inputrange = inputmax - inputmin - scale = outputmax/inputrange # number of indices per degree + scale = outputmax / inputrange # number of indices per degree center = inputmin + 1 / scale / 2 # shift to center of index outputmax -= 1 # shift index to zero indexing index = (degrees - center) * scale - err = IndexError('Input, %g, is out of range (%g, %g).' % - (degrees, inputmin, inputmax)) + err = IndexError( + "Input, %g, is out of range (%g, %g)." % (degrees, inputmin, inputmax) + ) # If the index is still out of bounds after rounding, raise an error. # 0.500001 is used in comparisons instead of 0.5 to allow for a small @@ -500,14 +503,14 @@ def _degrees_to_index(degrees, coordinate): return index -EPS = np.finfo('float64').eps # machine precision NumPy-1.20 -DX = EPS**(1/3) # optimal differential element +EPS = np.finfo("float64").eps # machine precision NumPy-1.20 +DX = EPS ** (1 / 3) # optimal differential element def _first_order_centered_difference(f, x0, dx=DX, args=()): # simple replacement for scipy.misc.derivative, which is scheduled for # removal in scipy 1.12.0 - df = f(x0+dx, *args) - f(x0-dx, *args) + df = f(x0 + dx, *args) - f(x0 - dx, *args) return df / 2 / dx @@ -529,7 +532,7 @@ def get_pandas_index(*args): """ return next( (a.index for a in args if isinstance(a, (pd.DataFrame, pd.Series))), - None + None, ) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index afdaab2adf..622a9332ba 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -6,9 +6,16 @@ from pvlib import shading -def singleaxis(apparent_zenith, apparent_azimuth, - axis_tilt=0, axis_azimuth=0, max_angle=90, - backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0): +def singleaxis( + apparent_zenith, + apparent_azimuth, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0 / 7.0, + cross_axis_tilt=0, +): """ Determine the rotation angle of a single-axis tracker when given particular solar zenith and azimuth angles. @@ -127,7 +134,7 @@ def singleaxis(apparent_zenith, apparent_azimuth, apparent_zenith = np.atleast_1d(apparent_zenith) if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1: - raise ValueError('Input dimensions must not exceed 1') + raise ValueError("Input dimensions must not exceed 1") # The ideal tracking angle, omega_ideal, is the rotation to place the sun # position vector (xp, yp, zp) in the (x, z) plane, which is normal to @@ -153,23 +160,24 @@ def singleaxis(apparent_zenith, apparent_azimuth, if backtrack: # distance between rows in terms of rack lengths relative to cross-axis # tilt - axes_distance = 1/(gcr * cosd(cross_axis_tilt)) + axes_distance = 1 / (gcr * cosd(cross_axis_tilt)) # NOTE: account for rare angles below array, see GH 824 temp = np.abs(axes_distance * cosd(omega_ideal - cross_axis_tilt)) # backtrack angle using [1], Eq. 14 - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): omega_correction = np.degrees( - -np.sign(omega_ideal)*np.arccos(temp)) + -np.sign(omega_ideal) * np.arccos(temp) + ) # NOTE: in the middle of the day, arccos(temp) is out of range because # there's no row-to-row shade to avoid, & backtracking is unnecessary # [1], Eqs. 15-16 - with np.errstate(invalid='ignore'): + with np.errstate(invalid="ignore"): tracker_theta = omega_ideal + np.where( - temp < 1, omega_correction, - 0) + temp < 1, omega_correction, 0 + ) else: tracker_theta = omega_ideal @@ -188,14 +196,19 @@ def singleaxis(apparent_zenith, apparent_azimuth, # Calculate auxiliary angles surface = calc_surface_orientation(tracker_theta, axis_tilt, axis_azimuth) - surface_tilt = surface['surface_tilt'] - surface_azimuth = surface['surface_azimuth'] - aoi = irradiance.aoi(surface_tilt, surface_azimuth, - apparent_zenith, apparent_azimuth) + surface_tilt = surface["surface_tilt"] + surface_azimuth = surface["surface_azimuth"] + aoi = irradiance.aoi( + surface_tilt, surface_azimuth, apparent_zenith, apparent_azimuth + ) # Bundle DataFrame for return values and filter for sun below horizon. - out = {'tracker_theta': tracker_theta, 'aoi': aoi, - 'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt} + out = { + "tracker_theta": tracker_theta, + "aoi": aoi, + "surface_azimuth": surface_azimuth, + "surface_tilt": surface_tilt, + } if index is not None: out = pd.DataFrame(out, index=index) out[zen_gt_90] = np.nan @@ -237,25 +250,30 @@ def calc_surface_orientation(tracker_theta, axis_tilt=0, axis_azimuth=0): Tracking of One-Axis Trackers", Technical Report NREL/TP-6A20-58891, July 2013. :doi:`10.2172/1089596` """ - with np.errstate(invalid='ignore', divide='ignore'): + with np.errstate(invalid="ignore", divide="ignore"): surface_tilt = acosd(cosd(tracker_theta) * cosd(axis_tilt)) # clip(..., -1, +1) to prevent arcsin(1 + epsilon) issues: - azimuth_delta = asind(np.clip(sind(tracker_theta) / sind(surface_tilt), - a_min=-1, a_max=1)) + azimuth_delta = asind( + np.clip( + sind(tracker_theta) / sind(surface_tilt), a_min=-1, a_max=1 + ) + ) # Combine Eqs 2, 3, and 4: - azimuth_delta = np.where(abs(tracker_theta) < 90, - azimuth_delta, - -azimuth_delta + np.sign(tracker_theta) * 180) + azimuth_delta = np.where( + abs(tracker_theta) < 90, + azimuth_delta, + -azimuth_delta + np.sign(tracker_theta) * 180, + ) # handle surface_tilt=0 case: azimuth_delta = np.where(sind(surface_tilt) != 0, azimuth_delta, 90) surface_azimuth = (axis_azimuth + azimuth_delta) % 360 out = { - 'surface_tilt': surface_tilt, - 'surface_azimuth': surface_azimuth, + "surface_tilt": surface_tilt, + "surface_azimuth": surface_azimuth, } - if hasattr(tracker_theta, 'index'): + if hasattr(tracker_theta, "index"): out = pd.DataFrame(out) return out @@ -326,8 +344,8 @@ def _calc_tracker_norm(ba, bg, dg): sin_bg = sind(bg) sin_dg = sind(dg) vx = sin_dg * cos_ba * cos_bg - vy = sind(ba)*sin_bg + cosd(dg)*cos_ba*cos_bg - vz = -sin_dg*sin_bg*cos_ba + vy = sind(ba) * sin_bg + cosd(dg) * cos_ba * cos_bg + vz = -sin_dg * sin_bg * cos_ba return vx, vy, vz @@ -351,12 +369,13 @@ def _calc_beta_c(v, dg, ba): """ vnorm = np.sqrt(np.dot(v, v)) beta_c = np.arcsin( - ((v[0]*cosd(dg) - v[1]*sind(dg)) * sind(ba) + v[2]*cosd(ba)) / vnorm) + ((v[0] * cosd(dg) - v[1] * sind(dg)) * sind(ba) + v[2] * cosd(ba)) + / vnorm + ) return beta_c -def calc_cross_axis_tilt( - slope_azimuth, slope_tilt, axis_azimuth, axis_tilt): +def calc_cross_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth, axis_tilt): """ Calculate the angle, relative to horizontal, of the line formed by the intersection between the slope containing the tracker axes and a plane diff --git a/pvlib/transformer.py b/pvlib/transformer.py index 3b66b0beb3..afc71fdba6 100644 --- a/pvlib/transformer.py +++ b/pvlib/transformer.py @@ -6,9 +6,9 @@ def simple_efficiency( - input_power, no_load_loss, load_loss, transformer_rating + input_power, no_load_loss, load_loss, transformer_rating ): - r''' + r""" Calculate the power at the output terminal of the transformer after taking into account efficiency using a simple calculation. @@ -103,7 +103,7 @@ def simple_efficiency( .. [1] Central Station Engineers of the Westinghouse Electric Corporation, "Electrical Transmission and Distribution Reference Book" 4th Edition. pg. 101. - ''' # noqa: E501 + """ # noqa: E501 input_power_normalized = input_power / transformer_rating @@ -111,7 +111,7 @@ def simple_efficiency( b = 1 c = no_load_loss - input_power_normalized - output_power_normalized = (-b + (b**2 - 4*a*c)**0.5) / (2 * a) + output_power_normalized = (-b + (b**2 - 4 * a * c) ** 0.5) / (2 * a) output_power = output_power_normalized * transformer_rating return output_power diff --git a/scripts/update_top_ranking_issues.py b/scripts/update_top_ranking_issues.py index 3608d43344..5bcbfd58be 100644 --- a/scripts/update_top_ranking_issues.py +++ b/scripts/update_top_ranking_issues.py @@ -15,8 +15,8 @@ def main(): # but we can place it in our env when running the script locally, # for convenience local_github_token: str | None = None - github_token: str | None = ( - local_github_token or os.getenv("GITHUB_ACCESS_TOKEN") + github_token: str | None = local_github_token or os.getenv( + "GITHUB_ACCESS_TOKEN" ) github = Github(github_token) @@ -35,7 +35,7 @@ def main(): # --- Actions --- # Get sorted issues query: str = ( - f'repo:{repository.full_name} is:open is:issue sort:reactions-+1-desc' + f"repo:{repository.full_name} is:open is:issue sort:reactions-+1-desc" ) issues = github.search_issues(query) @@ -54,8 +54,8 @@ def main(): break markdown_bullet_point: str = ( - f"{issue.html_url} " + - f"({issue._rawData['reactions']['+1']} :thumbsup:)" + f"{issue.html_url} " + + f"({issue._rawData['reactions']['+1']} :thumbsup:)" ) markdown_bullet_point = f"{i}. {markdown_bullet_point}" @@ -67,9 +67,7 @@ def main(): ) header = "Top Ranking Issues" new_body = header + "\n" + "\n".join(ranked_issues) - top_issues_card.edit( - body=new_body - ) + top_issues_card.edit(body=new_body) print(top_issues_card.body) diff --git a/setup.py b/setup.py index 60d1a5e8ba..0f0ef3a807 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ spa_depends = ["pvlib/spa_c_files/spa.h"] spa_all_file_paths = map( lambda x: os.path.join(os.path.dirname(__file__), x), - spa_sources + spa_depends + spa_sources + spa_depends, ) if all(map(os.path.exists, spa_all_file_paths)):