Source code for pvfactors.engine

"""This module contains the engine class that will run the complete
timeseries simulations."""

import numpy as np
from pvfactors.viewfactors import VFCalculator
from pvfactors.irradiance import HybridPerezOrdered
from pvfactors.config import DEFAULT_RHO_FRONT, DEFAULT_RHO_BACK


[docs]class PVEngine(object): """Class putting all of the calculations together into simple workflows. """
[docs] def __init__(self, pvarray, vf_calculator=None, irradiance_model=None, fast_mode_pvrow_index=None, fast_mode_segment_index=None): """Create pv engine class, and initialize timeseries parameters. Parameters ---------- pvarray : BasePVArray (or child) object The initialized PV array object that will be used for calculations vf_calculator : vf calculator object, optional Calculator that will be used to calculate the view factor matrices, will use :py:class:`~pvfactors.viewfactors.calculator.VFCalculator` if None (Default = None) irradiance_model : irradiance model object, optional The irradiance model that will be applied to the PV array, will use :py:class:`~pvfactors.irradiance.models.HybridPerezOrdered` if None (Default = None) fast_mode_pvrow_index : int, optional If a pvrow index is passed, then the PVEngine fast mode will be activated and the engine calculation will be done only for the back surface of the pvrow with the corresponding index (Default = None) fast_mode_segment_index : int, optional If a segment index is passed, then the PVEngine fast mode will calculate back surface irradiance only for the selected segment of the selected back surface (Default = None) """ # Initialize the attributes of the PV engine self.vf_calculator = vf_calculator or VFCalculator() self.irradiance = irradiance_model or HybridPerezOrdered() self.pvarray = pvarray # Save fast mode indices self.fast_mode_pvrow_index = fast_mode_pvrow_index self.fast_mode_segment_index = fast_mode_segment_index # These values will be updated at fitting time self.n_points = None self.skip_step = None
@classmethod def with_rho_initialization( cls, pvarray, vf_calculator, irradiance_model, fast_mode_pvrow_index=None, fast_mode_segment_index=None): """Before creating the PV engine object, update the front and back reflectivity scalars using the faoi functions, if those values weren't passed originally Parameters ---------- pvarray : :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray` The initialized PV array object that will be used for calculations vf_calculator : \ :py:class:`~pvfactors.viewfactors.calculator.VFCalculator` Calculator that will be used to calculate the view factor matrices, and AOI losses irradiance_model : irradiance model object The irradiance model that will be applied to the PV array, for instance :py:class:`~pvfactors.irradiance.models.HybridPerezOrdered` fast_mode_pvrow_index : int, optional If a pvrow index is passed, then the PVEngine fast mode will be activated and the engine calculation will be done only for the back surface of the pvrow with the corresponding index (Default = None) fast_mode_segment_index : int, optional If a segment index is passed, then the PVEngine fast mode will calculate back surface irradiance only for the selected segment of the selected back surface (Default = None) Returns ------- PV engine PV engine where the rho values have been initialized """ # Calculate global average reflectivity using VF calculator is_back = False rho_front_calculated = (vf_calculator.vf_aoi_methods .rho_from_faoi_fn(is_back)) is_back = True rho_back_calculated = (vf_calculator.vf_aoi_methods .rho_from_faoi_fn(is_back)) # Initialize rho values for irradiance model irradiance_model.rho_front = irradiance_model.initialize_rho( irradiance_model.rho_front, rho_front_calculated, DEFAULT_RHO_FRONT) irradiance_model.rho_back = irradiance_model.initialize_rho( irradiance_model.rho_back, rho_back_calculated, DEFAULT_RHO_BACK) return cls(pvarray, vf_calculator=vf_calculator, irradiance_model=irradiance_model, fast_mode_pvrow_index=fast_mode_pvrow_index, fast_mode_segment_index=fast_mode_segment_index) def fit(self, timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, albedo, ghi=None): """Fit the timeseries data to the engine. More specifically, save all the parameters that needs to be saved, and fit the PV array and irradiance models to the data (i.e. perform all the intermediate vector-based calculations). Note that all angles follow the pvlib-python angle convention: North - 0 deg, East - 90 deg, etc. Parameters ---------- timestamps : array-like or timestamp-like List of timestamps of the simulation. DNI : array-like or float Direct normal irradiance values [W/m2] DHI : array-like or float Diffuse horizontal irradiance values [W/m2] solar_zenith : array-like or float Solar zenith angles [deg] solar_azimuth : array-like or float Solar azimuth angles [deg] surface_tilt : array-like or float Surface tilt angles, from 0 to 180 [deg] surface_azimuth : array-like or float Surface azimuth angles [deg] albedo : array-like or float Albedo values (ground reflectivity) ghi : array-like, optional Global horizontal irradiance [W/m2], if not provided, will be calculated from DNI and DHI if needed (Default = None) """ # Format inputs to numpy arrays if it looks like floats where inputted if np.isscalar(DNI): timestamps = [timestamps] DNI = np.array([DNI]) DHI = np.array([DHI]) solar_zenith = np.array([solar_zenith]) solar_azimuth = np.array([solar_azimuth]) surface_tilt = np.array([surface_tilt]) surface_azimuth = np.array([surface_azimuth]) ghi = None if ghi is None else np.array([ghi]) # Format albedo self.n_points = len(DNI) albedo = (albedo * np.ones(self.n_points) if np.isscalar(albedo) else albedo) # Fit PV array self.pvarray.fit(solar_zenith, solar_azimuth, surface_tilt, surface_azimuth) # Fit irradiance model self.irradiance.fit(timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, albedo, ghi=ghi) # Add timeseries irradiance results to pvarray self.irradiance.transform(self.pvarray) # Fit VF calculator self.vf_calculator.fit(self.n_points) # Skip timesteps when: # - solar zenith > 90, ie the sun is down # - DNI or DHI is negative, which does not make sense # - DNI and DHI are both zero self.skip_step = (solar_zenith > 90) | (DNI < 0) | (DHI < 0) \ | ((DNI == 0) & (DHI == 0)) def run_full_mode(self, fn_build_report=None): """Run all simulation timesteps using the full mode, which calculates the equilibrium of reflections in the system, and returns a report that will be built by the function passed by the user. Parameters ---------- fn_build_report : function, optional Function that will build the report of the simulation (Default value = None) Returns ------- report Saved results from the simulation, as specified by user's report function. If no function is passed, nothing will be returned. """ # Get pvarray pvarray = self.pvarray # Get the irradiance modeling matrices # shape = n_surfaces + 1, n_timesteps irradiance_mat, rho_mat, invrho_mat, _ = \ self.irradiance.get_full_ts_modeling_vectors(pvarray) # --- Calculate view factors # shape = n_surfaces + 1, n_surfaces + 1, n_timesteps ts_vf_matrix = self.vf_calculator.build_ts_vf_matrix(pvarray) pvarray.ts_vf_matrix = ts_vf_matrix # Reshape for broadcasting and inverting # shape = n_timesteps, n_surfaces + 1, n_surfaces + 1 ts_vf_matrix_reshaped = np.moveaxis(ts_vf_matrix, -1, 0) # --- Solve mathematical problem # shape of system = n_timesteps, n_surfaces + 1, n_surfaces + 1 shape_system = ts_vf_matrix_reshaped.shape # Build 3d matrix of inverse reflectivities: diagonal for each ts # shape = n_timesteps, n_surfaces + 1, n_surfaces + 1 invrho_ts_diag = np.zeros(shape_system) for idx_surf in range(shape_system[1]): invrho_ts_diag[:, idx_surf, idx_surf] = invrho_mat[idx_surf, :] # Subtract matrices: will rely on broadcasting # shape = n_timesteps, n_surfaces + 1, n_surfaces + 1 a_mat = invrho_ts_diag - ts_vf_matrix_reshaped # Calculate inverse, requires specific shape # shape = n_timesteps, n_surfaces + 1, n_surfaces + 1 inv_a_mat = np.linalg.inv(a_mat) # Use einstein sum to get final timeseries radiosities # shape = n_surfaces + 1, n_timesteps q0 = np.einsum('ijk,ki->ji', inv_a_mat, irradiance_mat) # Calculate incident irradiance: will rely on broadcasting # shape = n_surfaces + 1, n_timesteps qinc = np.einsum('ijk,ki->ji', invrho_ts_diag, q0) # --- Derive other irradiance terms # shape = n_surfaces, n_timesteps isotropic_mat = ts_vf_matrix[:-1, -1, :] * irradiance_mat[-1, :] reflection_mat = qinc[:-1, :] - irradiance_mat[:-1, :] - isotropic_mat # --- Calculate AOI losses and absorbed irradiance # Create tiled reflection matrix of # shape = n_surfaces + 1, n_surfaces + 1, n_timestamps rho_ts_tiled = np.moveaxis(np.tile(rho_mat.T, (shape_system[1], 1, 1)), -1, 0) # Get vf AOI matrix # shape [n_surfaces + 1, n_surfaces + 1, n_timestamps] vf_aoi_matrix = (self.vf_calculator .build_ts_vf_aoi_matrix(pvarray, rho_ts_tiled)) pvarray.ts_vf_aoi_matrix = vf_aoi_matrix # Get absorbed irradiance matrix # shape [n_surfaces, n_timestamps] irradiance_abs_mat = ( self.irradiance.get_summed_components(pvarray, absorbed=True)) # Calculate absorbed irradiance qabs = (np.einsum('ijk,jk->ik', vf_aoi_matrix, q0)[:-1, :] + irradiance_abs_mat) # --- Update surfaces with values: the lists are ordered by index for idx_surf, ts_surface in enumerate(pvarray.all_ts_surfaces): ts_surface.update_params( {'q0': q0[idx_surf, :], 'qinc': qinc[idx_surf, :], 'isotropic': isotropic_mat[idx_surf, :], 'reflection': reflection_mat[idx_surf, :], 'qabs': qabs[idx_surf, :]}) # Return report if function was passed report = None if fn_build_report is None else fn_build_report(pvarray) return report def run_fast_mode(self, fn_build_report=None, pvrow_index=None, segment_index=None): """Run all simulation timesteps using the fast mode for the back surface of a PV row, and assuming that the incident irradiance on all other surfaces is known (all but back surfaces). The function will return a report that will be built by the function passed by the user. Parameters ---------- fn_build_report : function, optional Function that will build the report of the simulation (Default value = None) pvrow_index : int, optional Index of the PV row for which we want to calculate back surface irradiance; if used, this will override the ``fast_mode_pvrow_index`` passed at engine initialization (Default = None) segment_index : int, optional Index of the segment on the PV row's back side for which we want to calculate the incident irradiance; if used, this will override the ``fast_mode_segment_index`` passed at engine initialization (Default = None) Returns ------- report Results from the simulation, as specified by user's report function. If no function is passed, nothing will be returned. """ # Prepare variables pvrow_idx = (self.fast_mode_pvrow_index if pvrow_index is None else pvrow_index) segment_idx = (self.fast_mode_segment_index if segment_index is None else segment_index) ts_pvrow = self.pvarray.ts_pvrows[pvrow_idx] # Run calculations if segment_idx is None: # Run calculation for all segments of back surface for ts_segment in ts_pvrow.back.list_segments: self._calculate_back_ts_segment_qinc(ts_segment, pvrow_idx) else: # Run calculation for selected segment of back surface ts_segment = ts_pvrow.back.list_segments[segment_idx] self._calculate_back_ts_segment_qinc(ts_segment, pvrow_idx) # Create report report = (None if fn_build_report is None else fn_build_report(self.pvarray)) return report def _calculate_back_ts_segment_qinc(self, ts_segment, pvrow_idx): """Calculate the incident irradiance on a timeseries segment's surfaces for the back side of a PV row, using the fast mode, so assuming that the incident irradiance on all other surfaces is known. Nothing is returned by the function, but the segment's surfaces' parameter values are updated. Parameters ---------- ts_segment : :py:class:`~pvfactors.geometry.timeseries.TsDualSegment` Timeseries segment for which we want to calculate the incident irradiance pvrow_idx : int Index of the PV row on which the segment is located """ # Get all timeseries surfaces in segment list_ts_surfaces = ts_segment.all_ts_surfaces # Get irradiance vectors for calculation albedo = self.irradiance.albedo rho_front = self.irradiance.rho_front irr_gnd_shaded = self.irradiance.gnd_shaded irr_gnd_illum = self.irradiance.gnd_illum irr_pvrow_shaded = self.irradiance.pvrow_shaded irr_pvrow_illum = self.irradiance.pvrow_illum irr_sky = self.irradiance.sky_luminance for ts_surface in list_ts_surfaces: # Calculate view factors for timeseries surface vf = self.vf_calculator.get_vf_ts_pvrow_element( pvrow_idx, ts_surface, self.pvarray.ts_pvrows, self.pvarray.ts_ground, self.pvarray.rotation_vec, self.pvarray.width) # Update sky terms of timeseries surface self.irradiance.update_ts_surface_sky_term(ts_surface) # Calculate incident irradiance on illuminated surface gnd_shadow_refl = vf['to_gnd_shaded'] * albedo * irr_gnd_shaded gnd_illum_refl = vf['to_gnd_illum'] * albedo * irr_gnd_illum pvrow_shadow_refl = (vf['to_pvrow_shaded'] * rho_front * irr_pvrow_shaded) pvrow_illum_refl = (vf['to_pvrow_illum'] * rho_front * irr_pvrow_illum) reflections = (gnd_shadow_refl + gnd_illum_refl + pvrow_shadow_refl + pvrow_illum_refl) isotropic = vf['to_sky'] * irr_sky qinc = (gnd_shadow_refl + gnd_illum_refl + pvrow_shadow_refl + pvrow_illum_refl + isotropic + ts_surface.get_param('sky_term')) # Update parameters of timeseries surface object ts_surface.update_params( {'qinc': qinc, 'reflection_gnd_shaded': gnd_shadow_refl, 'reflection_gnd_illum': gnd_illum_refl, 'reflection_pvrow_shaded': pvrow_shadow_refl, 'reflection_pvrow_illum': pvrow_illum_refl, 'isotropic': isotropic, 'reflection': reflections, 'view_factors': vf})