Source code for feedinlib.models

# -*- coding: utf-8 -*-
"""
@author: oemof development group
"""

from abc import ABC, abstractmethod
import os
import numpy as np
import pandas as pd
import pvlib
from windpowerlib import basicmodel as windmodel
import requests


[docs]class Base(ABC): r""" The base class of feedinlib models. Parameters ---------- required : list of strings, optional Containing the names of the required parameters to use the model. """ def __init__(self, **kwargs): self._required = kwargs.get("required") @property @abstractmethod def required(self): """ The (names of the) parameters this model requires in order to calculate the feedin. As this is an abstract property, you have to override it in a subclass so that the model can be instantiated. This forces implementors to make the required parameters for a model explicit, even if they are empty, and gives them a good place to document them. By default, this property is settable and its value can be specified via and argument on construction. If you want to keep this functionality, simply delegate all calls to the superclass. """ return self._required @required.setter def required(self, names): self._required = names # Returning None rarely makes sense, IMHO. # Returning self at least allows for method chaining. return self
[docs]class PvlibBased(Base): r"""Model to determine the output of a photovoltaik module The calculation is based on the library pvlib. [1]_ Parameters ---------- PvlibBased.required (list of strings, optional) List of required parameters of the model Notes ----- For more information about the photovoltaic model check the documentation of the pvlib library. https://readthedocs.org/projects/pvlib-python/ References ---------- .. [1] `pvlib on github <https://github.com/pvlib/pvlib-python>`_ Examples -------- >>> from feedinlib import models >>> pv_model = models.PvlibBased() See Also -------- Base SimpleWindTurbine """ def __init__(self, **kwargs): super().__init__(**kwargs) self.area = None self.peak = None @property def required(self): r""" The parameters this model requires to calculate a feedin. In this feedin model the required parameters are: :modul_name: (string) - name of a pv module from the sam.nrel database [12]_ :tilt: (float) - tilt angle of the pv module (horizontal=0°) :azimuth: (float) - azimuth angle of the pv module (south=180°) :albedo: (float) - albedo factor arround the module """ if super().required is not None: return super().required return ["azimuth", "tilt", "module_name", "albedo"]
[docs] def feedin(self, **kwargs): r""" Feedin time series for the given pv module. In contrast to :py:func:`turbine_power_output <feedinlib.models.PvlibBased.get_pv_power_output>` it returns just the feedin series instead of the whole DataFrame. Parameters ---------- see : :py:func:`turbine_power_output <feedinlib.models.PvlibBased.get_pv_power_output>` Returns ------- pandas.Series A time series of the power output for the given pv module. """ return self.get_pv_power_output(**kwargs).p_mp
[docs] def solarposition_hourly_mean(self, location, data, **kwargs): r""" Determine the position of the sun as an hourly mean of all angles above the horizon. Parameters ---------- location : pvlib.location.Location A pvlib location object containing the longitude, latitude and the timezone of the location data : pandas.DataFrame Containing the time index of the location. method : string, optional Method to calulate the position of the sun according to the methods provided by the pvlib function (default: 'ephemeris') 'pvlib.solarposition.get_solarposition'. [2]_ freq : string, optional The time interval to create the hourly mean (default: '5Min'). pandas.DataFrame The DataFrame contains the following new columns: azimuth, zenith, elevation Notes ----- Combining hourly values for irradiation with discrete values for the position of the sun can lead to unrealistic results. Using hourly values for the position minimizes these errors. References ---------- .. [2] `pvlib solarposition <http://pvlib-python.readthedocs.org/en/ latest/pvlib.html#pvlib.solarposition.get_solarposition>`_. See Also -------- solarposition : calculates the position of the sun at a given time """ data_5min = pd.DataFrame( index=pd.date_range(data.index[0], periods=data.shape[0]*12, freq='5Min', tz=kwargs['weather'].timezone)) data_5min = pvlib.solarposition.get_solarposition( time=data_5min.index, latitude=location.latitude, longitude=location.longitude, method='ephemeris') return pd.concat( [data, data_5min.clip_lower(0).resample('H').mean()], axis=1, join='inner')
[docs] def solarposition(self, location, data, **kwargs): r""" Determine the position of the sun unsing the time of the time index. Parameters ---------- location : pvlib.location.Location A pvlib location object containing the longitude, latitude and the timezone of the location data : pandas.DataFrame Containing the timeseries of the weather data and the time index of the location. method : string, optional Method to calulate the position of the sun according to the methods provided by the pvlib function (default: 'ephemeris') 'pvlib.solarposition.get_solarposition'. [2]_ Returns ------- pandas.DataFrame The DataFrame contains the following new columns: azimuth, zenith, elevation Notes ----- This method is not used in favour to solarposition_hourly_mean. Examples -------- >>> import pvlib >>> import pandas as pd >>> from feedinlib import models >>> loc = pvlib.location.Location(52, 13, 'Europe/Berlin') >>> pvmodel = models.PvlibBased() >>> data = pd.DataFrame(index=pd.date_range(pd.datetime(2010, 1, 1, 0), ... periods=8760, freq='H', tz=loc.tz)) >>> elevation = pvmodel.solarposition(loc, data).elevation >>> print(round(elevation[12], 3)) 14.968 See Also -------- solarposition_hourly_mean : calculates the position of the sun as an hourly mean. """ return pd.concat( [data, pvlib.solarposition.get_solarposition( time=data.index, latitude=location.latitude, longitude=location.longitude, method=kwargs.get('method', 'ephemeris'))], axis=1, join='inner')
[docs] def angle_of_incidence(self, data, **kwargs): r""" Determine the angle of incidence using the pvlib aoi funktion. [4]_ Parameters ---------- data : pandas.DataFrame Containing the timeseries of the azimuth and zenith angle tilt : float Tilt angle of the pv module (horizontal=0°). azimuth : float Azimuth angle of the pv module (south=180°). Returns ------- pandas.Series Angle of incidence in degrees. See Also -------- solarposition_hourly_mean, solarposition References ---------- .. [4] `pvlib angle of incidence <http://pvlib-python.readthedocs.org/ en/latest/pvlib.html#pvlib.irradiance.aoi>`_. """ return pvlib.irradiance.aoi( solar_azimuth=data['azimuth'], solar_zenith=data['zenith'], surface_tilt=self.powerplant.tilt, surface_azimuth=self.powerplant.azimuth)
[docs] def global_in_plane_irradiation(self, data, **kwargs): r""" Determine the global irradiaton on the tilted surface. This method determines the global irradiation in plane knowing the direct and diffuse irradiation, the incident angle and the orientation of the surface. The method uses the pvlib.irradiance.globalinplane function [5]_ and some other functions of the pvlib.atmosphere [6]_ and the pvlib.solarposition [2]_ module to provide the input parameters for the globalinplane function. Parameters ---------- data : pandas.DataFrame Containing the time index of the location and columns with the following timeseries: (dirhi, dhi, zenith, azimuth, aoi) tilt : float Tilt angle of the pv module (horizontal=0°). azimuth : float Azimuth angle of the pv module (south=180°). albedo : float Albedo factor around the module Returns ------- pandas.DataFrame The DataFrame contains the following new columns: poa_global, poa_diffuse, poa_direct References ---------- .. [5] `pvlib globalinplane <http://pvlib-python.readthedocs.org/en/ latest/pvlib.html#pvlib.irradiance.globalinplane>`_. .. [6] `pvlib atmosphere <http://pvlib-python.readthedocs.org/en/ latest/pvlib.html#module-pvlib.atmosphere>`_. See Also -------- solarposition_hourly_mean, solarposition, angle_of_incidenc """ # Determine the extraterrestrial radiation data['dni_extra'] = pvlib.irradiance.extraradiation( datetime_or_doy=data.index.dayofyear) # Determine the relative air mass data['airmass'] = pvlib.atmosphere.relativeairmass(data['zenith']) # Determine direct normal irradiation data['dni'] = (data['dirhi']) / np.sin(np.radians(90 - data['zenith'])) # what for?? data['dni'][data['zenith'] > 88] = data['dirhi'] # Determine the sky diffuse irradiation in plane # with model of Perez (modell switch would be good) data['poa_sky_diffuse'] = pvlib.irradiance.perez( surface_tilt=self.powerplant.tilt, surface_azimuth=self.powerplant.azimuth, dhi=data['dhi'], dni=data['dni'], dni_extra=data['dni_extra'], solar_zenith=data['zenith'], solar_azimuth=data['azimuth'], airmass=data['airmass']) # Set NaN values to zero data['poa_sky_diffuse'][ pd.isnull(data['poa_sky_diffuse'])] = 0 # Determine the diffuse irradiation from ground reflection in plane data['poa_ground_diffuse'] = pvlib.irradiance.grounddiffuse( ghi=data['dirhi'] + data['dhi'], albedo=self.powerplant.albedo, surface_tilt=self.powerplant.tilt) # Determine total in-plane irradiance data = pd.concat( [data, pvlib.irradiance.globalinplane( aoi=data['aoi'], dni=data['dni'], poa_sky_diffuse=data['poa_sky_diffuse'], poa_ground_diffuse=data['poa_ground_diffuse'])], axis=1, join='inner') return data
[docs] def fetch_module_data(self, lib='sandia-modules', **kwargs): r""" Fetch the module data from the Sandia Module library The file is saved in the ~/.oemof folder and loaded from there to save time and to make it possible to work if the server is down. Parameters ---------- module_name : string Name of a pv module from the sam.nrel database [9]_. Returns ------- dictionary The necessary module data for the selected module to use the pvlib sapm pv model. [8]_ Examples -------- >>> from feedinlib import models >>> pvmodel = models.PvlibBased() >>> name = 'Yingli_YL210__2008__E__' >>> print(pvmodel.fetch_module_data(module_name=name).Area) 1.7 See Also -------- pv_module_output """ if kwargs.get('module_name') is None: kwargs['module_name'] = self.powerplant.module_name basic_path = os.path.join(os.path.expanduser("~"), '.oemof') url = 'https://sam.nrel.gov/sites/default/files/' filename = os.path.join(basic_path, 'sam-library-sandia-modules.csv') if not os.path.exists(basic_path): os.makedirs(basic_path) if not os.path.isfile(filename): url_file = 'sam-library-sandia-modules-2015-6-30.csv' req = requests.get(url + url_file) with open(filename, 'wb') as fout: fout.write(req.content) if kwargs.get('module_name') == 'all': module_data = pvlib.pvsystem.retrieve_sam(path=filename) else: module_data = (pvlib.pvsystem.retrieve_sam(path=filename) [kwargs['module_name']]) self.area = module_data.Area self.peak = module_data.Impo * module_data.Vmpo return module_data
[docs] def pv_module_output(self, data, **kwargs): r""" Determine the output of pv-system. Using the pvlib.pvsystem.sapm function of the pvlib [8]_. Parameters ---------- module_name : string Name of a pv module from the sam.nrel database [9]_. data : pandas.DataFrame Containing the time index of the location and columns with the following timeseries: (temp_air [K], v_wind, poa_global, poa_diffuse, poa_direct, airmass, aoi) method : string, optional Method to calulate the position of the sun according to the methods provided by the pvlib function (default: 'ephemeris') 'pvlib.solarposition.get_solarposition'. [10]_ Returns ------- pandas.DataFrame The DataFrame contains the following new columns: p_pv_norm, p_pv_norm_area References ---------- .. [8] `pvlib pv-system <http://pvlib-python.readthedocs.org/en/ latest/pvlib.html#pvlib.pvsystem.sapm>`_. .. [9] `module library <https://sam.nrel.gov/sites/default/files/ sam-library-sandia-modules-2015-6-30.csv>`_. .. [10] `pvlib get_solarposition <http://pvlib-python.readthedocs.org /en/latest/pvlib.html#pvlib.solarposition.get_solarposition>`_. See Also -------- global_in_plane_irradiation """ # Determine module and cell temperature data['temp_air_celsius'] = data['temp_air'] - 273.15 data = pd.concat([data, pvlib.pvsystem.sapm_celltemp( poa_global=data['poa_global'], wind_speed=data['v_wind'], temp_air=data['temp_air_celsius'], model='Open_rack_cell_polymerback')], axis=1, join='inner') # Retrieve the module data object module_data = self.fetch_module_data(**kwargs) data['effective_irradiance'] = pvlib.pvsystem.sapm_effective_irradiance( poa_direct=data['poa_direct'], poa_diffuse=data['poa_diffuse'], airmass_absolute=data['airmass'], aoi=data['aoi'], module=module_data) # Apply the Sandia PV Array Performance Model (SAPM) to get a data = pd.concat([data, pvlib.pvsystem.sapm( effective_irradiance=data['effective_irradiance'], temp_cell=data['temp_cell'], module=module_data)], axis=1, join='inner') # Set NaN values to zero data['p_mp'][ pd.isnull(data['p_mp'])] = 0 return data
[docs] def get_pv_power_output(self, **kwargs): r""" Output of the given pv module. For the theoretical background see the pvlib documentation [11]_. Parameters ---------- weather : feedinlib.weather.FeedinWeather object Instance of the feedinlib weather object (see class :py:class:`FeedinWeather<feedinlib.weather.FeedinWeather>` for more details) Notes ----- See :py:func:`method required <feedinlib.models.PvlibBased.required>` for all required parameters of this model. Returns ------- pandas.DataFrame The DataFrame contains the following new columns: p_pv_norm, p_pv_norm_area and all timeseries calculated before. References ---------- .. [11] `pvlib documentation <https://readthedocs.org/projects/ pvlib-python/>`_. .. [12] `module library <https://sam.nrel.gov/sites/default/files/ sam-library-sandia-modules-2015-6-30.csv>`_. See Also -------- pv_module_output, feedin """ data = kwargs['weather'].data # Create a location object location = pvlib.location.Location(kwargs['weather'].latitude, kwargs['weather'].longitude, kwargs['weather'].timezone) # Determine the position of the sun data = self.solarposition_hourly_mean(location, data, **kwargs) # A zenith angle greater than 90° means, that the sun is down. data['zenith'][data['zenith'] > 90] = 90 # Determine the angle of incidence data['aoi'] = self.angle_of_incidence(data, **kwargs) # Determine the irradiation in plane data = self.global_in_plane_irradiation(data, **kwargs) # Determine the output of the pv module data = self.pv_module_output(data, **kwargs) return data
[docs]class SimpleWindTurbine(Base): r"""Model to determine the output of a wind turbine Parameters ---------- required : list of strings Containing the names of the required parameters to use the model. Examples -------- >>> from feedinlib import models >>> required_ls = ['h_hub', 'd_rotor', 'wind_conv_type', 'data_height'] >>> wind_model = models.SimpleWindTurbine(required=required_ls) See Also -------- Base PvlibBased """ def __init__(self, **kwargs): super().__init__(**kwargs) self.nominal_power_wind_turbine = None @property def required(self): r""" The parameters this model requires to calculate a feedin. In this feedin model the required parameters are: :h_hub: (float) - Height of the hub of the wind turbine :d_rotor: (float) - 'Diameter of the rotor [m]', :wind_conv_type: (string) - Name of the wind converter type. Use self.get_wind_pp_types() to see a list of all possible wind converters. """ if super().required is not None: return super().required return ["h_hub", "d_rotor", "wind_conv_type"]
[docs] def feedin(self, **kwargs): r""" Alias for :py:func:`turbine_power_output <feedinlib.models.SimpleWindTurbine.turbine_power_output>`. """ my_turbine = windmodel.SimpleWindTurbine( wind_conv_type=kwargs.pop('wind_conv_type'), h_hub=kwargs.pop('h_hub'), d_rotor=kwargs.pop('d_rotor')) self.nominal_power_wind_turbine = my_turbine.nominal_power return my_turbine.turbine_power_output( weather=kwargs['weather'].data, data_height=kwargs['weather'].data_height)
if __name__ == "__main__": import doctest doctest.testmod()