def test_shadows_coords_left_right_of_cut_point(): """Test that coords left and right of cut point are created correctly""" # Ground inputs shadow_coords = np.array( [[[[0], [0]], [[2], [0]]], [[[3], [0]], [[5], [0]]]], dtype=float) overlap = [False] # --- Create timeseries ground cut_point = TsPointCoords([2.5], [0]) ts_ground = TsGround.from_ordered_shadows_coords( shadow_coords, flag_overlap=overlap, cut_point_coords=[cut_point]) # Get left and right shadows shadows_left = ts_ground.shadow_coords_left_of_cut_point(0) shadows_right = ts_ground.shadow_coords_right_of_cut_point(0) # Reformat for testing shadows_left = [shadow.as_array for shadow in shadows_left] shadows_right = [shadow.as_array for shadow in shadows_right] expected_shadows_left = [ shadow_coords[0], [cut_point.as_array, cut_point.as_array] ] expected_shadows_right = [[cut_point.as_array, cut_point.as_array], shadow_coords[1]] # Test that correct np.testing.assert_allclose(shadows_left, expected_shadows_left) np.testing.assert_allclose(shadows_right, expected_shadows_right) # --- Case where pv rows are flat, cut point are inf cut_point = TsPointCoords([np.inf], [0]) ts_ground = TsGround.from_ordered_shadows_coords( shadow_coords, flag_overlap=overlap, cut_point_coords=[cut_point]) # Get right shadows shadows_right = ts_ground.shadow_coords_right_of_cut_point(0) # Test that correct maxi = MAX_X_GROUND expected_shadows_right = np.array([[[[maxi], [0.]], [[maxi], [0.]]], [[[maxi], [0.]], [[maxi], [0.]]]]) shadows_right = [shadow.as_array for shadow in shadows_right] np.testing.assert_allclose(shadows_right, expected_shadows_right) # --- Case where pv rows are flat, cut point are - inf cut_point = TsPointCoords([-np.inf], [0]) ts_ground = TsGround.from_ordered_shadows_coords( shadow_coords, flag_overlap=overlap, cut_point_coords=[cut_point]) # Get left shadows shadows_left = ts_ground.shadow_coords_left_of_cut_point(0) # Test that correct mini = MIN_X_GROUND expected_shadows_left = np.array([[[[mini], [0.]], [[mini], [0.]]], [[[mini], [0.]], [[mini], [0.]]]]) shadows_left = [shadow.as_array for shadow in shadows_left] np.testing.assert_allclose(shadows_left, expected_shadows_left)
def test_calculate_aoi_angles(): """Make sure calculation of AOI angles is correct""" u_vector = np.array([[1, 2, 3], [0, 0, 0]]) centroid = TsPointCoords(np.array([0.5, 1, 1]), np.array([0, 0, 0])) point = TsPointCoords(np.array([1, 0, 1]), np.array([0.5, 1, 5])) aoi_angles = AOIMethods._calculate_aoi_angles(u_vector, centroid, point) expected_aoi_angles = [45, 135, 90] np.testing.assert_allclose(aoi_angles, expected_aoi_angles)
def from_ts_pvrows_and_angles(cls, list_ts_pvrows, alpha_vec, rotation_vec, y_ground=Y_GROUND, flag_overlap=None, param_names=None): """Create timeseries ground from list of timeseries PV rows, and PV array and solar angles. Parameters ---------- list_ts_pvrows : \ list of :py:class:`~pvfactors.geometry.pvrow.TsPVRow` Timeseries PV rows to use to calculate timeseries ground shadows alpha_vec : np.ndarray Angle made by 2d solar vector and PV array x-axis [rad] rotation_vec : np.ndarray Timeseries rotation values of the PV row [deg] y_ground : float, optional Fixed y coordinate of flat ground [m] (Default = Y_GROUND constant) flag_overlap : list of bool, optional Flags indicating if the ground shadows are overlapping, for all time steps (Default=None). I.e. is there direct shading on pv rows? param_names : list of str, optional List of names of surface parameters to use when creating geometries (Default = None) """ rotation_vec = np.deg2rad(rotation_vec) n_steps = len(rotation_vec) # Calculate coords of ground shadows and cutting points ground_shadow_coords = [] cut_point_coords = [] for ts_pvrow in list_ts_pvrows: # Get pvrow coords x1s_pvrow = ts_pvrow.full_pvrow_coords.b1.x y1s_pvrow = ts_pvrow.full_pvrow_coords.b1.y x2s_pvrow = ts_pvrow.full_pvrow_coords.b2.x y2s_pvrow = ts_pvrow.full_pvrow_coords.b2.y # --- Shadow coords calculation # Calculate x coords of shadow x1s_shadow = x1s_pvrow - (y1s_pvrow - y_ground) / np.tan(alpha_vec) x2s_shadow = x2s_pvrow - (y2s_pvrow - y_ground) / np.tan(alpha_vec) # Order x coords from left to right x1s_on_left = x1s_shadow <= x2s_shadow xs_left_shadow = np.where(x1s_on_left, x1s_shadow, x2s_shadow) xs_right_shadow = np.where(x1s_on_left, x2s_shadow, x1s_shadow) # Append shadow coords to list ground_shadow_coords.append( [[xs_left_shadow, y_ground * np.ones(n_steps)], [xs_right_shadow, y_ground * np.ones(n_steps)]]) # --- Cutting points coords calculation dx = (y1s_pvrow - y_ground) / np.tan(rotation_vec) cut_point_coords.append( TsPointCoords(x1s_pvrow - dx, y_ground * np.ones(n_steps))) ground_shadow_coords = np.array(ground_shadow_coords) return cls.from_ordered_shadows_coords( ground_shadow_coords, flag_overlap=flag_overlap, cut_point_coords=cut_point_coords, param_names=param_names, y_ground=y_ground)
def test_ts_ground_elements_surfaces(): """Check timeseries ground elements are created correctly""" # Create timeseries coords gnd_element_coords = TsLineCoords.from_array( np.array([[[-1, -1], [0, 0]], [[1, 1], [0, 0]]])) pt_coords_1 = TsPointCoords.from_array(np.array([[-0.5, -1], [0, 0]])) pt_coords_2 = TsPointCoords.from_array(np.array([[0.5, 0], [0, 0]])) # Create gnd element gnd_element = TsGroundElement( gnd_element_coords, list_ordered_cut_pts_coords=[pt_coords_1, pt_coords_2]) # Check that structures contain the correct number of ts surfaces assert len(gnd_element.surface_list) == 3 assert len(gnd_element.surface_dict[0]['left']) == 1 assert len(gnd_element.surface_dict[1]['left']) == 2 assert len(gnd_element.surface_dict[0]['right']) == 2 assert len(gnd_element.surface_dict[1]['right']) == 1 # Check that the objects are the same assert ( gnd_element.surface_list[0] == gnd_element.surface_dict[0]['left'][0]) assert ( gnd_element.surface_list[0] == gnd_element.surface_dict[1]['left'][0]) assert ( gnd_element.surface_list[1] == gnd_element.surface_dict[0]['right'][0]) assert ( gnd_element.surface_list[1] == gnd_element.surface_dict[1]['left'][1]) assert ( gnd_element.surface_list[2] == gnd_element.surface_dict[0]['right'][1]) assert ( gnd_element.surface_list[2] == gnd_element.surface_dict[1]['right'][0]) # Now check surfaces lengths np.testing.assert_allclose(gnd_element.surface_list[0].length, [0.5, 0]) np.testing.assert_allclose(gnd_element.surface_list[1].length, [1, 1]) np.testing.assert_allclose(gnd_element.surface_list[2].length, [0.5, 1]) # Check coords of surfaces np.testing.assert_allclose(gnd_element.surface_list[0].b1.x, [-1, -1]) np.testing.assert_allclose(gnd_element.surface_list[0].b2.x, [-0.5, -1])
def test_ts_ground_to_geometry(): # There should be an overlap shadow_coords = np.array([[[[0, 0], [0, 0]], [[2, 1], [0, 0]]], [[[1, 2], [0, 0]], [[5, 5], [0, 0]]]]) overlap = [True, False] cut_point_coords = [TsPointCoords.from_array(np.array([[2, 2], [0, 0]]))] # Test with overlap ts_ground = TsGround.from_ordered_shadows_coords( shadow_coords, flag_overlap=overlap, cut_point_coords=cut_point_coords) # Run some checks for index 0 pvground = ts_ground.at(0, merge_if_flag_overlap=False, with_cut_points=False) assert pvground.n_surfaces == 4 assert pvground.list_segments[0].illum_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.length == 5 np.testing.assert_allclose(pvground.shaded_length, 5) # Run some checks for index 1 pvground = ts_ground.at(1, with_cut_points=False) assert pvground.n_surfaces == 5 assert pvground.list_segments[0].illum_collection.n_surfaces == 3 assert pvground.list_segments[0].shaded_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.length == 4 np.testing.assert_allclose(pvground.shaded_length, 4) # Run some checks for index 0, when merging pvground = ts_ground.at(0, merge_if_flag_overlap=True, with_cut_points=False) assert pvground.n_surfaces == 3 assert pvground.list_segments[0].illum_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.n_surfaces == 1 assert pvground.list_segments[0].shaded_collection.length == 5 np.testing.assert_allclose(pvground.shaded_length, 5) # Run some checks for index 0, when merging and with cut points pvground = ts_ground.at(0, merge_if_flag_overlap=True, with_cut_points=True) assert pvground.n_surfaces == 4 assert pvground.list_segments[0].illum_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.n_surfaces == 2 assert pvground.list_segments[0].shaded_collection.length == 5 np.testing.assert_allclose(pvground.shaded_length, 5)
def _calculate_aoi_angles_w_obstruction(self, u_vector, centroid, point_gnd, point_obstr, gnd_surf_is_left): """Calculate AOI angles for a PV row surface of the :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray` that sees a ground surface, while being potentially obstructed by another PV row Parameters ---------- u_vector : np.ndarray Direction vector of the surface for which to calculate AOI angles centroid : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Centroid point of PV row surface for which to calculate AOI angles point : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Point of ground surface that will determine AOI angle point_obstr: :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Potentially obstructing point for the view aoi angle calculation gnd_surf_is_left : bool Flag specifying whether ground surface is left of PV row's cut point or not Returns ------- np.ndarray AOI angles formed by remote point and centroid on surface, measured against surface direction vector, accounting for potential obstruction [degrees] """ if point_obstr is None: # There is no obstruction point = point_gnd else: # Determine if there is obstruction by using the angles made by # specific strings with the x-axis alpha_pv = self._angle_with_x_axis(point_gnd, centroid) alpha_ob = self._angle_with_x_axis(point_gnd, point_obstr) if gnd_surf_is_left: is_obstructing = alpha_pv > alpha_ob else: is_obstructing = alpha_pv < alpha_ob x = np.where(is_obstructing, point_obstr.x, point_gnd.x) y = np.where(is_obstructing, point_obstr.y, point_gnd.y) point = TsPointCoords(x, y) aoi_angles = self._calculate_aoi_angles(u_vector, centroid, point) return aoi_angles
def _create_shaded_side_coords(xy_center, width, shaded_length, mask_tilted_to_left, rotation_vec, side_lowest_pt): """ Create the timeseries line coordinates for the shaded portion of a PV row side, based on inputted shaded length. Parameters ---------- xy_center : tuple of float x and y coordinates of the PV row center point (invariant) width : float width of the PV rows [m] shaded_length : np.ndarray Timeseries values of side shaded length [m] tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left rotation_vec : np.ndarray Timeseries rotation vector of the PV rows in [deg] side_lowest_pt : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries coordinates of lowest point of considered PV row side Returns ------- side_shaded_coords : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries coordinates of the shaded portion of the PV row side """ # Get invariant values x_center, y_center = xy_center radius = width / 2. # Calculate coords of shading point r_shade = radius - shaded_length x_sh = np.where( mask_tilted_to_left, r_shade * cosd(rotation_vec + 180.) + x_center, r_shade * cosd(rotation_vec) + x_center) y_sh = np.where( mask_tilted_to_left, r_shade * sind(rotation_vec + 180.) + y_center, r_shade * sind(rotation_vec) + y_center) side_shaded_coords = TsLineCoords(TsPointCoords(x_sh, y_sh), side_lowest_pt) return side_shaded_coords
def vf_aoi_pvrow_to_sky(self, ts_pvrows, ts_ground, tilted_to_left, vf_matrix): """Calculate the view factors between timeseries PV row surface and sky while accounting for AOI losses, and assign values to the passed view factor matrix using the surface indices. Parameters ---------- ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array ts_ground : :py:class:`~pvfactors.geometry.timeseries.TsGround` Timeseries ground of the PV array tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left vf_matrix : np.ndarray View factor matrix to update during calculation. Should have 3 dimensions as follows: [n_surfaces, n_surfaces, n_timesteps] """ sky_index = vf_matrix.shape[0] - 1 # --- Build list of dummy sky surfaces # create sky left open area pt_1 = TsPointCoords(ts_ground.x_min * np.ones_like(tilted_to_left), ts_ground.y_ground * np.ones_like(tilted_to_left)) pt_2 = ts_pvrows[0].highest_point sky_left = TsSurface(TsLineCoords(pt_1, pt_2)) # create sky right open area pt_1 = TsPointCoords(ts_ground.x_max * np.ones_like(tilted_to_left), ts_ground.y_ground * np.ones_like(tilted_to_left)) pt_2 = ts_pvrows[-1].highest_point sky_right = TsSurface(TsLineCoords(pt_2, pt_1)) # Add sky surfaces in-between PV rows dummy_sky_surfaces = [sky_left] for idx_pvrow, ts_pvrow in enumerate(ts_pvrows[:-1]): right_ts_pvrow = ts_pvrows[idx_pvrow + 1] pt_1 = ts_pvrow.highest_point pt_2 = right_ts_pvrow.highest_point sky_surface = TsSurface(TsLineCoords(pt_1, pt_2)) dummy_sky_surfaces.append(sky_surface) # Add sky right open area dummy_sky_surfaces.append(sky_right) # Now calculate vf_aoi for all PV row surfaces to sky for idx_pvrow, ts_pvrow in enumerate(ts_pvrows): # Get dummy sky surfaces sky_left = dummy_sky_surfaces[idx_pvrow] sky_right = dummy_sky_surfaces[idx_pvrow + 1] # Calculate vf_aoi for surfaces in PV row # front side front = ts_pvrow.front for front_surf in front.all_ts_surfaces: vf_aoi_left = self._vf_aoi_surface_to_surface(front_surf, sky_left, is_back=False) vf_aoi_right = self._vf_aoi_surface_to_surface(front_surf, sky_right, is_back=False) vf_aoi = np.where(tilted_to_left, vf_aoi_left, vf_aoi_right) vf_matrix[front_surf.index, sky_index, :] = vf_aoi # back side back = ts_pvrow.back for back_surf in back.all_ts_surfaces: vf_aoi_left = self._vf_aoi_surface_to_surface(back_surf, sky_left, is_back=True) vf_aoi_right = self._vf_aoi_surface_to_surface(back_surf, sky_right, is_back=True) vf_aoi = np.where(tilted_to_left, vf_aoi_right, vf_aoi_left) vf_matrix[back_surf.index, sky_index, :] = vf_aoi
def calculate_vf_to_gnd(self, pvrow_element_coords, pvrow_idx, n_pvrows, n_steps, y_ground, cut_point_coords, pvrow_element_length, tilted_to_left, ts_pvrows): """Calculate view factors from timeseries pvrow_element to the entire ground. Parameters ---------- pvrow_element_coords : :py:class:`~pvfactors.geometry.timeseries.TsLineCoords` Timeseries line coordinates of pvrow element pvrow_idx : int Index of the timeseries PV row on the which the pvrow_element is n_pvrows : int Number of timeseries PV rows in the PV array n_steps : int Number of timesteps for which to calculate the pvfactors y_ground : float Y-coordinate of the flat ground [m] cut_point_coords : list of :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` List of cut point coordinates, as calculated for timeseries PV rows pvrow_element_length : float or np.ndarray Length (width) of the timeseries pvrow_element [m] tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` Timeseries PV row geometries that will be used in the calculation Returns ------- vf_to_gnd : np.ndarray View factors from timeseries pvrow_element to the entire ground """ pvrow_lowest_pt = ts_pvrows[pvrow_idx].full_pvrow_coords.lowest_point if pvrow_idx == 0: # There is no obstruction to view of the ground on the left coords_left_gnd = TsLineCoords( TsPointCoords(MIN_X_GROUND * np.ones(n_steps), y_ground), TsPointCoords(np.minimum(MAX_X_GROUND, cut_point_coords.x), y_ground)) vf_left_ground = self._vf_surface_to_surface( pvrow_element_coords, coords_left_gnd, pvrow_element_length) else: # The left PV row obstructs the view of the ground on the left left_pt_neighbor = \ ts_pvrows[pvrow_idx - 1].full_pvrow_coords.lowest_point coords_gnd_proxy = TsLineCoords(left_pt_neighbor, pvrow_lowest_pt) vf_left_ground = self._vf_surface_to_surface( pvrow_element_coords, coords_gnd_proxy, pvrow_element_length) if pvrow_idx == (n_pvrows - 1): # There is no obstruction of the view of the ground on the right coords_right_gnd = TsLineCoords( TsPointCoords(np.maximum(MIN_X_GROUND, cut_point_coords.x), y_ground), TsPointCoords(MAX_X_GROUND * np.ones(n_steps), y_ground)) vf_right_ground = self._vf_surface_to_surface( pvrow_element_coords, coords_right_gnd, pvrow_element_length) else: # The right PV row obstructs the view of the ground on the right right_pt_neighbor = \ ts_pvrows[pvrow_idx + 1].full_pvrow_coords.lowest_point coords_gnd_proxy = TsLineCoords(pvrow_lowest_pt, right_pt_neighbor) vf_right_ground = self._vf_surface_to_surface( pvrow_element_coords, coords_gnd_proxy, pvrow_element_length) # Merge the views of the ground for the back side vf_ground = np.where(tilted_to_left, vf_right_ground, vf_left_ground) return vf_ground