def _setup_array(self) -> None: """ Setup the solar panel array within the grid as a Point per panel """ self.site = FlickerMismatch.get_turb_site(self.blade_length * 2) self.site_points, self.heat_map_template = self._setup_heatmap_template( self.site.bounds, self.gridcell_width, self.gridcell_height) min_y, max_y = self.site.bounds[1], self.site.bounds[3] string, string_points = create_pv_string_points( 0, min_y, self.gridcell_width, self.gridcell_height, self.gridcell_width, max_y - min_y) # where solar strings are self.array = [] x_pos = self.site.bounds[0] while x_pos < xs[-1]: tmp_string = translate(string, x_pos, 0) self.array.append(tmp_string) x_pos += self.gridcell_width logger.info("setup_turbines_and_arrays success") # Create points centered on each module self.array_string_points = [] x_pos = self.site.bounds[0] while x_pos < xs[-1]: array_points = translate(string_points, x_pos, 0) self.array_string_points.append( self._setup_string_points(array_points)) x_pos += self.gridcell_width logger.info("setup_point_maps success")
def _setup_irradiance(self): """ Compute solar azimuth and elevation degrees; Compute plane-of-array irradiance for a single-axis tracking PVwatts system :return: """ pv_model = pv.default("PVWattsNone") pv_model.SystemDesign.array_type = 2 pv_model.SystemDesign.gcr = .1 if self.solar_resource_data is None: filename = str(self.lat) + "_" + str( self.lon) + "_psmv3_60_2012.csv" weather_path = Path( __file__ ).parent.parent.parent / "resource_files" / "solar" / filename if not weather_path.is_file(): SolarResource(self.lat, self.lon, year=2012) if not weather_path.is_file(): raise ValueError("resource file does not exist") pv_model.SolarResource.solar_resource_file = str(weather_path) else: pv_model.SolarResource.solar_resource_data = self.solar_resource_data pv_model.execute(0) self.poa = np.array(pv_model.Outputs.poa) logger.info("get_irradiance success")
def _setup_array(self) -> None: """ Setup the solar panel array as a Point per panel """ self.turb_pos = [] theta = np.radians(self.grid_angle) # where the turbines are self.turb_pos, self.site = create_turbines_in_grid( self.turbine_dx, self.turbine_dy, theta, FlickerMismatchGrid.n_turbines_per_side) # find the center grid which is symmetrical to all inner grids center_grid_coordinates = [(self.turb_pos[t][0], self.turb_pos[t][1]) for t in (5, 6, 10, 9)] self.center_grid = Polygon(center_grid_coordinates) self.site_points, self.heat_map_template = self._setup_heatmap_template( self.center_grid.bounds) # where solar strings are string_width = module_width string_height = self.center_grid.bounds[3] - self.center_grid.bounds[1] y_pos = self.center_grid.bounds[1] biggest_string_coordinates = ((0, y_pos), (string_width, y_pos), (string_width, y_pos + string_height), (0, y_pos + string_height)) biggest_string = Polygon(biggest_string_coordinates) self.array = [] x_pos = self.center_grid.bounds[0] while x_pos < self.center_grid.bounds[2]: string = translate(biggest_string, x_pos, 0) string = string.intersection(self.center_grid) if string.area > 0: self.array.append(string) x_pos += string_width logger.info("setup_turbines_and_arrays success") # Create points centered on each module self.array_string_points = [] for array in self.array: array_points = self.site_points.intersection( array.buffer(module_height / 4)) if array_points.is_empty: continue string_points = self._setup_string_points(array_points) self.array_string_points.append(string_points) logger.info("setup_point_maps success")
def run_parallel(self, n_procs: int, weight_option: tuple, intervals: Optional[Sequence[range]] = None): """ Runs create_heat_maps_irradiance in parallel :param n_procs: :param weight_option: tuple of selected weighting options, producing a heatmap each - "poa": weight by plane-of-array irradiance - "power": weight by power loss of pvmismatch module - "time": weight by number of timesteps shaded :param intervals: list of ranges to simulate; if none, simulate entire weather file's records :return: heat_map_shadow, heat_map_flicker """ logger.info("run_parallel with {} processes".format(n_procs)) pool = self._create_pool(n_procs) if intervals is None: intervals = self.step_intervals if 'power' in weight_option or 'poa' in weight_option: self._setup_irradiance() results = pool.imap( functools.partial(self.create_heat_maps, weight_option=weight_option), intervals) # aggregate results and renormalize heat_maps_to_return = [ copy.deepcopy(self.heat_map_template[0]) for _ in weight_option ] if 'power' in weight_option: subhourly_poa = np.repeat(self.poa, FlickerMismatch.steps_per_hour) total_poa = sum([sum(subhourly_poa[i]) for i in intervals]) total_steps = sum([len(i) for i in intervals]) for r, i in zip(results, intervals): for j, hm in enumerate(heat_maps_to_return): if weight_option[j] == 'poa': hm += r[j] * sum(self.poa[i]) / total_poa elif weight_option[j] == 'power' or weight_option[j] == 'time': hm += r[j] * len(i) / total_steps logger.info("Create_heat_map success") return tuple(heat_maps_to_return)
def __init__(self, lat: float, lon: float, turbine_nx: float, turbine_ny: float, angle: float = 0, blade_length: int = 35, angles_per_step: int = 1): """ :param lat: latitude :param lon: longitude :param turbine_nx: number of turbine diameters for horizontal spacing in grid :param turbine_ny: number of turbine diameters for vertical spacing in grid :param angle: degree of rotation for turbine grid :param blade_length: meters :param angles_per_step: number of blade angles per step of the hour """ FlickerMismatch.periodic = True self.center_grid = None self.turbine_dx = turbine_nx * blade_length * 2 self.turbine_dy = turbine_ny * blade_length * 2 self.grid_angle = int(angle) % 90 self.n_rows_modules = int(turbine_nx / (0.124 * 12)) super().__init__(lat, lon, blade_length=blade_length, angles_per_step=angles_per_step) self.filename_full = "{}_{}_{}_{}_{}_{}_{}".format( self.lat, self.lon, self.steps_per_hour, self.angles_per_step, self.turbine_dx, self.turbine_dy, self.grid_angle) self.grid_turbine_shadow_file = Path(__file__).parent / "data" / str( self.filename_full + "_shd.pkl") logger.info( "Creating FlickerMismatchModel with filename_full {}".format( self.filename_full))
def create_heat_maps( self, steps: range, weight_option: tuple, ) -> tuple: """ Create shadow and flicker heat maps for a given range of simulation steps :param weight_option: tuple of selected weighting options, producing a heatmap each - "poa": weight by plane-of-array irradiance - "power": weight by power loss of pvmismatch module - "time": weight by number of timesteps shaded :param steps: which steps to run, must be within range calculated by steps_per_hour x angles_per_step :return: shadow heat map, flicker heat map """ proc_id = mp.current_process().name logger.info("Proc {}: Starting heat maps {}".format(proc_id, steps)) step_to_minute = 60 / self.steps_per_hour self.azi_ang, self.elv_ang, _ = get_sun_pos(self.lat, self.lon, step_to_minute, steps=steps) self.turbine_shadow = get_turbine_shadows_timeseries( self.blade_length, steps, self.angles_per_step, self.azi_ang, self.elv_ang, self.wind_dir, FlickerMismatch.turbine_tower_shadow) by_poa = by_power = by_time = False for i in weight_option: if i == "poa": by_poa = True heat_map_shadow = copy.deepcopy(self.heat_map_template[0]) elif i == "power": by_power = True heat_map_flicker = copy.deepcopy(self.heat_map_template[0]) elif i == "time": by_time = True heat_map_time = copy.deepcopy(self.heat_map_template[0]) else: raise ValueError("Unrecognized 'weight_option'") if not (by_poa or by_power or by_time): raise ValueError( "No valid 'weight_option' provided. Provide a list of selected ways to weight the shading " "from the set ('poa', 'power', 'time')") if by_poa or by_power: if not isinstance(self.poa, Sequence): self._setup_irradiance() total_poa = sum(self.poa[steps]) progress_size = int(len(steps) / min(10, len(steps))) for i, step in enumerate(steps): if i % progress_size == 0: logger.info( "Proc {} created heat maps for {} / 100 steps".format( proc_id, int(i / len(steps) * 100))) hr = int(step / FlickerMismatch.steps_per_hour) shadows = self._calculate_turbine_shadow(i) if not shadows: continue if by_poa: poa_weight = self.poa[hr] / total_poa FlickerMismatch._calculate_shading(poa_weight, shadows, self.site_points, heat_map_shadow, self.gridcell_width, self.gridcell_height) if by_power: FlickerMismatch._calculate_power_loss(self.poa[hr], self.elv_ang[i], shadows, self.array_string_points, heat_map_flicker, self.gridcell_width, self.gridcell_height) if by_time: FlickerMismatch._calculate_shading(1, shadows, self.site_points, heat_map_time, self.gridcell_width, self.gridcell_height, normalize_by_area=True) # normalize by angles per hour (since each will use the same weight) or by number of hours total step_normalize = self.angles_per_step if self.angles_per_step else 1 if by_poa: heat_map_shadow /= step_normalize if by_power: heat_map_flicker /= step_normalize * len(steps) if by_time: heat_map_time /= step_normalize * len(steps) heat_maps_to_return = [] for i in weight_option: if i == 'poa': heat_maps_to_return.append(heat_map_shadow) elif i == 'power': heat_maps_to_return.append(heat_map_flicker) elif i == 'time': heat_maps_to_return.append(heat_map_time) logger.info("Finished heat maps") return tuple(heat_maps_to_return)