diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index 1afae70ab0..677a13f3c5 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New These are new features and improvements of note in each release. +.. include:: whatsnew/v0.4.0.txt .. include:: whatsnew/v0.3.3.txt .. include:: whatsnew/v0.3.2.txt .. include:: whatsnew/v0.3.1.txt diff --git a/docs/sphinx/source/whatsnew/v0.4.0.txt b/docs/sphinx/source/whatsnew/v0.4.0.txt new file mode 100644 index 0000000000..67451e99ca --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.4.0.txt @@ -0,0 +1,23 @@ +.. _whatsnew_0400: + +v0.4.0 (June xx, 2016) +----------------------- + +This is a major release from 0.3.3. +We recommend that all users upgrade to this version. + +Enhancements +~~~~~~~~~~~~ + +* Splits ``ModelChain`` into a ``SingleDiode`` chain and a ``SAPM`` chain. + (:issue:`151`) + + +Bug fixes +~~~~~~~~~ + + +Contributors +~~~~~~~~~~~~ + +* Will Holmgren diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 69832611b9..0b4059cf14 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -211,12 +211,9 @@ def get_orientation(strategy, **kwargs): class ModelChain(object): """ - An experimental class that represents all of the modeling steps + An experimental base class that represents all of the modeling steps necessary for calculating power or energy for a PV system at a given - location using the SAPM. - - CEC module specifications and the single diode model are not yet - supported. + location. Parameters ---------- @@ -284,9 +281,10 @@ def orientation_strategy(self, strategy): self._orientation_strategy = strategy - def run_model(self, times, irradiance=None, weather=None): + def prepare_inputs(self, times, irradiance=None, weather=None): """ - Run the model. + Prepare the solar position, irradiance, and weather inputs to + the model. Parameters ---------- @@ -307,8 +305,9 @@ def run_model(self, times, irradiance=None, weather=None): self Assigns attributes: times, solar_position, airmass, irradiance, - total_irrad, weather, temps, aoi, dc, ac + total_irrad, weather, aoi """ + self.times = times self.solar_position = self.location.get_solarposition(self.times) @@ -316,6 +315,9 @@ def run_model(self, times, irradiance=None, weather=None): self.airmass = self.location.get_airmass( solar_position=self.solar_position, model=self.airmass_model) + self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) + if irradiance is None: irradiance = self.location.get_clearsky( self.solar_position.index, self.clearsky_model, @@ -358,13 +360,53 @@ def run_model(self, times, irradiance=None, weather=None): weather = {'wind_speed': 0, 'temp_air': 20} self.weather = weather + return self + + def run_model(self): + """ + A stub function meant to be subclassed. + """ + + raise NotImplementedError( + 'you must subclass ModelChain and implement this method') + + +class SAPM(ModelChain): + """ + Uses the SAPM to calculate cell temperature, DC power and AC power. + """ + + def run_model(self, times, irradiance=None, weather=None): + """ + Run the model. + + Parameters + ---------- + times : DatetimeIndex + Times at which to evaluate the model. + + irradiance : None or DataFrame + If None, calculates clear sky data. + Columns must be 'dni', 'ghi', 'dhi'. + + weather : None or DataFrame + If None, assumes air temperature is 20 C and + wind speed is 0 m/s. + Columns must be 'wind_speed', 'temp_air'. + + Returns + ------- + self + + Assigns attributes: times, solar_position, airmass, irradiance, + total_irrad, weather, aoi, temps, dc, ac. + """ + self.prepare_inputs(times, irradiance, weather) + self.temps = self.system.sapm_celltemp(self.total_irrad['poa_global'], self.weather['wind_speed'], self.weather['temp_air']) - self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) - self.dc = self.system.sapm(self.total_irrad['poa_direct'], self.total_irrad['poa_diffuse'], self.temps['temp_cell'], @@ -376,3 +418,68 @@ def run_model(self, times, irradiance=None, weather=None): self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) return self + + +class SingleDiode(ModelChain): + """ + Uses the DeSoto and single diode models to calculate the DC power, + and the SAPM models to calculate cell temperature and AC power. + """ + + def run_model(self, times, irradiance=None, weather=None): + """ + Run the model. + + Parameters + ---------- + times : DatetimeIndex + Times at which to evaluate the model. + + irradiance : None or DataFrame + If None, calculates clear sky data. + Columns must be 'dni', 'ghi', 'dhi'. + + weather : None or DataFrame + If None, assumes air temperature is 20 C and + wind speed is 0 m/s. + Columns must be 'wind_speed', 'temp_air'. + + Returns + ------- + self + + Assigns attributes: times, solar_position, airmass, irradiance, + total_irrad, weather, aoi, temps, dc, ac. + """ + + self.prepare_inputs(times, irradiance, weather) + + self.aoi_mod = self.system.ashraeiam(self.aoi).fillna(0) + self.total_irrad['poa_global_aoi'] = ( + self.total_irrad['poa_direct'] * self.aoi_mod + + self.total_irrad['poa_diffuse']) + + self.temps = self.system.sapm_celltemp( + self.total_irrad['poa_global_aoi'], + self.weather['wind_speed'], + self.weather['temp_air']) + + (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) = ( + self.system.calcparams_desoto(self.total_irrad['poa_global_aoi'], + self.temps['temp_cell'])) + + self.desoto = (photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + + self.dc = self.system.singlediode( + photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + + self.dc = self.dc.fillna(0) + + self.dc = self.system.scale_voltage_current_power(self.dc) + + self.ac = self.system.snlinverter(self.dc['v_mp'], self.dc['p_mp']) + + return self diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index e590325113..2b4e285258 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -3,13 +3,15 @@ from numpy import nan from pvlib import modelchain, pvsystem -from pvlib.modelchain import ModelChain +from pvlib.modelchain import ModelChain, SAPM, SingleDiode from pvlib.pvsystem import PVSystem from pvlib.tracking import SingleAxisTracker from pvlib.location import Location from pandas.util.testing import assert_series_equal, assert_frame_equal -from nose.tools import with_setup, raises +from nose.tools import raises + +from . import incompatible_conda_linux_py3 # should store this test data locally, but for now... sam_data = {} @@ -19,7 +21,7 @@ def retrieve_sam_network(): sam_data['cecinverter'] = pvsystem.retrieve_sam('cecinverter') -def mc_setup(): +def get_sapm_module_parameters(): # limit network usage try: modules = sam_data['sandiamod'] @@ -27,21 +29,82 @@ def mc_setup(): retrieve_sam_network() modules = sam_data['sandiamod'] - module = modules.Canadian_Solar_CS5P_220M___2009_.copy() + module = 'Canadian_Solar_CS5P_220M___2009_' + module_parameters = modules[module].copy() + + return module_parameters + + +def get_cec_module_parameters(): + # limit network usage + try: + modules = sam_data['cecmod'] + except KeyError: + retrieve_sam_network() + modules = sam_data['cecmod'] + + module = 'Canadian_Solar_CS5P_220M' + module_parameters = modules[module].copy() + module_parameters['b'] = 0.05 + module_parameters['EgRef'] = 1.121 + module_parameters['dEgdT'] = -0.0002677 + + return module_parameters + + +def get_cec_inverter_parameters(): + inverters = sam_data['cecinverter'] - inverter = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'].copy() + inverter = 'ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_' + inverter_parameters = inverters[inverter].copy() + + return inverter_parameters + + +def sapm_setup(tracker=False): + + module_parameters = get_sapm_module_parameters() - system = PVSystem(module_parameters=module, - inverter_parameters=inverter) + inverter_parameters = get_cec_inverter_parameters() + + if tracker: + system = SingleAxisTracker(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) + else: + system = PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) + + location = Location(32.2, -111, altitude=700) + + mc = SAPM(system, location) + + return mc + + +def singlediode_setup(tracker=False): + + module_parameters = get_cec_module_parameters() + + inverter_parameters = get_cec_inverter_parameters() + + if tracker: + system = SingleAxisTracker(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) + else: + system = PVSystem(module_parameters=module_parameters, + inverter_parameters=inverter_parameters) location = Location(32.2, -111, altitude=700) - return system, location + mc = SingleDiode(system, location) + + return mc def test_ModelChain_creation(): - system, location = mc_setup() - mc = ModelChain(system, location) + system = PVSystem() + location = Location(32.2, -111, altitude=700) + ModelChain(system, location) def test_orientation_strategy(): @@ -61,65 +124,95 @@ def run_orientation_strategy(strategy, expected): # the || accounts for the coercion of 'None' to None assert (mc.orientation_strategy == strategy or - mc.orientation_strategy == None) + mc.orientation_strategy is None) assert system.surface_tilt == expected[0] assert system.surface_azimuth == expected[1] -def test_run_model(): - system, location = mc_setup() +@raises(NotImplementedError) +def test_run_model_ModelChain(): + system = PVSystem() + location = Location(32.2, -111, altitude=700) mc = ModelChain(system, location) + mc.run_model() + + +@incompatible_conda_linux_py3 +def test_run_model(): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac + expected = {} + expected[SAPM] = \ + pd.Series(np.array([1.82033564e+02, -2.00000000e-02]), index=times) + expected[SingleDiode] = \ + pd.Series(np.array([179.15260, -2.00000000e-02]), index=times) - expected = pd.Series(np.array([ 1.82033564e+02, -2.00000000e-02]), - index=times) - assert_series_equal(ac, expected) + def run_test(mc): + ac = mc.run_model(times).ac + assert_series_equal(ac, expected[type(mc)]) + for mc in (sapm_setup(), singlediode_setup()): + yield run_test, mc + +@incompatible_conda_linux_py3 def test_run_model_with_irradiance(): - system, location = mc_setup() - mc = ModelChain(system, location) 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(times, irradiance=irradiance).ac + irradiance = pd.DataFrame( + {'dni': [900, 0], 'ghi': [600, 50], 'dhi': [150, 50]}, index=times) + expected = {} + expected[SAPM] = \ + pd.Series(np.array([1.90054749e+02, -2.00000000e-02]), index=times) + expected[SingleDiode] = \ + pd.Series(np.array([ 186.46774125, 7.8941746 ]), index=times) - expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), - index=times) - assert_series_equal(ac, expected) + def run_test(mc): + ac = mc.run_model(times, irradiance=irradiance).ac + assert_series_equal(ac, expected[type(mc)]) + for mc in (sapm_setup(), singlediode_setup()): + yield run_test, mc + +@incompatible_conda_linux_py3 def test_run_model_with_weather(): - system, location = mc_setup() - mc = ModelChain(system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - weather = pd.DataFrame({'wind_speed':5, 'temp_air':10}, index=times) - ac = mc.run_model(times, weather=weather).ac + weather = pd.DataFrame({'wind_speed': 5, 'temp_air': 10}, index=times) + expected = {} + expected[SAPM] = \ + pd.Series(np.array([1.99952400e+02, -2.00000000e-02]), index=times) + expected[SingleDiode] = \ + pd.Series(np.array([ 1.97456536e+02, -2.00000000e-02]), index=times) - expected = pd.Series(np.array([ 1.99952400e+02, -2.00000000e-02]), - index=times) - assert_series_equal(ac, expected) + def run_test(mc): + ac = mc.run_model(times, weather=weather).ac + assert_series_equal(ac, expected[type(mc)]) + + for mc in (sapm_setup(), singlediode_setup()): + yield run_test, mc def test_run_model_tracker(): - system, location = mc_setup() - system = SingleAxisTracker(module_parameters=system.module_parameters, - inverter_parameters=system.inverter_parameters) - mc = ModelChain(system, location) times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') - ac = mc.run_model(times).ac - expected = pd.Series(np.array([ 121.421719, -2.00000000e-02]), - index=times) - assert_series_equal(ac, expected) + expected_ac = {} + expected_ac[SAPM] = \ + pd.Series(np.array([ 121.421719, -2.00000000e-02]), index=times) + expected_ac[SingleDiode] = \ + pd.Series(np.array([ 1.22736286e+02, -2.00000000e-02]), index=times) - expected = pd.DataFrame(np. + expected_tracking = pd.DataFrame(np. array([[ 54.82513187, 90. , 11.0039221 , 11.0039221 ], [ nan, 0. , 0. , nan]]), columns=['aoi', 'surface_azimuth', 'surface_tilt', 'tracker_theta'], index=times) - assert_frame_equal(mc.tracking, expected) + + def run_test(mc): + ac = mc.run_model(times).ac + assert_series_equal(ac, expected_ac[type(mc)]) + assert_frame_equal(mc.tracking, expected_tracking) + + for mc in (sapm_setup(tracker=True), singlediode_setup(tracker=True)): + yield run_test, mc @raises(ValueError) @@ -134,10 +227,9 @@ def test_basic_chain_required(): latitude = 32 longitude = -111 altitude = 700 - modules = sam_data['sandiamod'] - module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - inverters = sam_data['cecinverter'] - inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + module_parameters = get_sapm_module_parameters() + inverter_parameters = get_cec_inverter_parameters() dc, ac = modelchain.basic_chain(times, latitude, longitude, module_parameters, inverter_parameters, @@ -149,20 +241,18 @@ def test_basic_chain_alt_az(): end='20160101 1800-0700', freq='6H') latitude = 32.2 longitude = -111 - altitude = 700 surface_tilt = 0 surface_azimuth = 0 - modules = sam_data['sandiamod'] - module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - inverters = sam_data['cecinverter'] - inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + module_parameters = get_sapm_module_parameters() + inverter_parameters = get_cec_inverter_parameters() dc, ac = modelchain.basic_chain(times, latitude, longitude, module_parameters, inverter_parameters, surface_tilt=surface_tilt, surface_azimuth=surface_azimuth) - expected = pd.Series(np.array([ 1.14490928477e+02, -2.00000000e-02]), + expected = pd.Series(np.array([1.14490928477e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -173,17 +263,15 @@ def test_basic_chain_strategy(): latitude = 32.2 longitude = -111 altitude = 700 - modules = sam_data['sandiamod'] - module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - inverters = sam_data['cecinverter'] - inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] - dc, ac = modelchain.basic_chain(times, latitude, longitude, - module_parameters, inverter_parameters, - orientation_strategy='south_at_latitude_tilt', - altitude=altitude) + module_parameters = get_sapm_module_parameters() + inverter_parameters = get_cec_inverter_parameters() + + dc, ac = modelchain.basic_chain( + times, latitude, longitude, module_parameters, inverter_parameters, + orientation_strategy='south_at_latitude_tilt', altitude=altitude) - expected = pd.Series(np.array([ 1.82033563543e+02, -2.00000000e-02]), + expected = pd.Series(np.array([1.82033563543e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -196,10 +284,9 @@ def test_basic_chain_altitude_pressure(): altitude = 700 surface_tilt = 0 surface_azimuth = 0 - modules = sam_data['sandiamod'] - module_parameters = modules['Canadian_Solar_CS5P_220M___2009_'] - inverters = sam_data['cecinverter'] - inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_'] + + module_parameters = get_sapm_module_parameters() + inverter_parameters = get_cec_inverter_parameters() dc, ac = modelchain.basic_chain(times, latitude, longitude, module_parameters, inverter_parameters, @@ -207,7 +294,7 @@ def test_basic_chain_altitude_pressure(): surface_azimuth=surface_azimuth, pressure=93194) - expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), + expected = pd.Series(np.array([1.15771428788e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected) @@ -217,6 +304,6 @@ def test_basic_chain_altitude_pressure(): surface_azimuth=surface_azimuth, altitude=altitude) - expected = pd.Series(np.array([ 1.15771428788e+02, -2.00000000e-02]), + expected = pd.Series(np.array([1.15771428788e+02, -2.00000000e-02]), index=times) assert_series_equal(ac, expected)