def compute_objective(self, candidate: HybridSimulationVariables): """ Annual energy production of wind and solar with the given layout """ conforming_candidate, squared_error = self.make_conforming_candidate_and_get_penalty( candidate) penalty = max( 0.0, self.penalty_scale * max(0.0, squared_error - self.max_unpenalized_distance)) # wind wind_model: windpower.Windpower = self._scenario['Wind'][0] wind_model.Farm.wind_farm_xCoordinates = conforming_candidate.turb_pos_x wind_model.Farm.wind_farm_yCoordinates = conforming_candidate.turb_pos_y wind_score = self._scenario['Wind'][1](wind_model) / 1000 # get solar capacity after flicker losses net_solar_capacities = [] flicker_losses = 1 for area in conforming_candidate.solar_areas: solar_capacity = self.module_power * area.num_modules # gcr_loss = self.solar_gcr_loss_multiplier(area.gcr) # solar_capacity *= gcr_loss flicker_loss = get_flicker_loss_multiplier( self._flicker_data, conforming_candidate.turb_pos_x, conforming_candidate.turb_pos_y, self.turb_diam, area.strands, (self.module_width, self.module_height)) solar_capacity *= flicker_loss flicker_losses *= flicker_loss net_solar_capacities.append(solar_capacity) total_solar_capacity = sum(net_solar_capacities) if total_solar_capacity == 0: return 0 # solar capacity after gcr losses avg_gcr = np.dot( np.array(net_solar_capacities) / total_solar_capacity, np.array([area.gcr for area in conforming_candidate.solar_areas])) solar_model: pvwatts.Pvwattsv7 = self._scenario['Solar'][0] solar_model.SystemDesign.gcr = avg_gcr solar_model.SystemDesign.system_capacity = float(total_solar_capacity) solar_score = self._scenario['Solar'][1](solar_model) / 1000 score = wind_score + solar_score # report losses gcr_losses = (1 - self.solar_gcr_loss_multiplier(avg_gcr)) * 100 logger.info( "Evaluative objective with score {} = {} w + {} s. " "Wake losses {}%, gcr losses {}%, flicker losses {}%".format( score - penalty, wind_score, solar_score, wind_model.Outputs.wake_losses, gcr_losses, (1 - flicker_losses) * 100)) return score - penalty, score, wind_score, solar_score, wind_model.Outputs.wake_losses, gcr_losses, ( 1 - flicker_losses) * 100
def __init__(self, inner_problem: HybridOptimizationProblem, ) -> None: """ The site must be a Polygon (i.e. a single polygon) :param inner_problem: wind layout optimization problem """ super().__init__(inner_problem, HybridOptimizationProblem, HybridCandidate) logger.info("Created HybridOptimizationProblemBGRM")
def make_conforming_candidate_and_get_penalty( self, candidate: HybridSimulationVariables ) -> Tuple[HybridSimulationVariables, float]: """ Penalize turbines out of bounds while moving them within the boundary + always generates a feasible solution + provides a smooth surface to descend into a good solution - requires tuning of penalty """ candidate.turb_pos_x, candidate.turb_pos_y, squared_error = \ move_turbines_within_boundary(candidate.turb_pos_x, candidate.turb_pos_y, self.site_info.polygon.boundary, self.site_info.valid_region) logger.info("Made conforming candidate {}".format(vars(candidate))) return candidate, squared_error
def __init__( self, site_info: SiteInfo, num_turbines: int, solar_capacity: float, min_spacing: float = 200., penalty_scale: float = .1, max_unpenalized_distance: float = 0.0, # [m] min_gcr: float = .2, max_gcr: float = .8, min_spacing_between_solar_and_wind: float = 100, # [m] module_power: float = .321) -> None: """ Setup turbine flicker data and hybrid simulations :param site_info: location, site and resource info :param num_turbines: number of turbines to place on site :param min_spacing: min spacing between turbines :param penalty_scale: tuning parameter :param max_unpenalized_distance: tuning parameter :param min_gcr: minimum gcr for the solar panels :param max_gcr: max gcr :param min_spacing_between_solar_and_wind: :param module_power: [kw] generation capacity per solar module """ super().__init__(site_info, num_turbines, min_spacing) self.candidate_type = HybridSimulationVariables self.solar_capacity_kw: float = solar_capacity self.penalty_scale: float = penalty_scale self.max_unpenalized_distance: float = max_unpenalized_distance self.min_gcr: float = min_gcr self.max_gcr: float = max_gcr self.min_spacing_between_solar_and_wind: float = min_spacing_between_solar_and_wind self.module_power: float = module_power self.turb_diam = 77 self.module_width: float = module_width self.module_height: float = module_height self.max_num_modules: int = int( floor(self.solar_capacity_kw / self.module_power)) self.min_strand_length: int = FlickerMismatch.modules_per_string self._scenario = None self._solar_size_aep_multiplier = None self._solar_gcr_loss_multiplier = dict() self._flicker_data = self._load_flicker_data() self._setup_simulation() logger.info("Created HybridOptimizationProblem")
def _setup_simulation(self) -> None: """ Wind simulation -> PySAM windpower model Solar simulation -> Surrogate model of PySAM Pvwatts model since the AEP scales linearly and independently w.r.t solar capacity and gcr """ def run_wind_model(windmodel: windpower.Windpower): windmodel.Farm.system_capacity = \ max(windmodel.Turbine.wind_turbine_powercurve_powerout) * len(windmodel.Farm.wind_farm_xCoordinates) windmodel.execute(0) return windmodel.Outputs.annual_energy def run_pv_model(pvmodel: pvwatts.Pvwattsv7): cap = pvmodel.SystemDesign.system_capacity gcr = pvmodel.SystemDesign.gcr est = cap * self._solar_size_aep_multiplier * self.solar_gcr_loss_multiplier( gcr) # pvmodel.execute() # rl = pvmodel.Outputs.annual_energy # err = (rl - est)/rl # if err > 0.05: # print("High approx error found with {} kwh and {} gcr of {}".format(cap, gcr, err)) return est # create wind model self._scenario = dict() wind_model = windpower.default("WindPowerSingleOwner") wind_model.Resource.wind_resource_data = self.site_info.wind_resource.data self.turb_diam = wind_model.Turbine.wind_turbine_rotor_diameter wind_model.Farm.wind_farm_wake_model = 2 # use eddy viscosity wake model self._scenario['Wind'] = (wind_model, run_wind_model) # create pv model solar_model = pvwatts.default("PVWattsSingleOwner") solar_model.SolarResource.solar_resource_data = self.site_info.solar_resource.data solar_model.SystemDesign.array_type = 2 # single-axis tracking solar_model.SystemDesign.tilt = 0 # setup surrogate solar_model.execute(0) self._solar_size_aep_multiplier = solar_model.Outputs.annual_energy / solar_model.SystemDesign.system_capacity solar_model.SystemDesign.gcr = 0.01 # lowest possible gcr solar_model.SystemDesign.system_capacity = 1 solar_model.execute(0) if solar_model.Outputs.annual_energy > 0: self._solar_gcr_loss_multiplier[ 'unit'] = solar_model.Outputs.annual_energy else: raise RuntimeError( "Solar GCR Loss Multiplier: Setup failed due to 0 for unit value" ) self._scenario['Solar'] = (solar_model, run_pv_model) # estimate max AEP self.upper_bounds = calculate_max_hybrid_aep(self.site_info, self.num_turbines, self.solar_capacity_kw) logger.info( "Setup Wind and Solar models. Max AEP is {} for wind, {} solar, {} total" .format(self.upper_bounds['wind'], self.upper_bounds['solar'], self.upper_bounds['total']))
def make_inner_candidate_from_parameters( self, parameters: HybridCandidate, ) -> Tuple[float, Tuple[HybridSimulationVariables, Polygon, BaseGeometry]]: """ Transforms parameters into inner problem candidate (i.e. a set of wind turbine coordinates) 1. Place the section of solar panels -> height and width; x and y position; east, west and south buffers 2. Place turbines according to Wind BGMD :param parameters: :return: candidate to hybrid layout problem """ logger.info("Starting inner candidate: {}".format(vars(parameters))) ''' - x or y position does not change actual solar placement - want to get a solution with x and y as centered as possible - get bounds of solar placement and bounds of solar region - compute distance from most centered point - larger buffer has no effect because it goes out of bounds - want a solution with the smallest buffer possible - get bounds of buffer region and buffer in bounds - compute distance from smallest buffer possible ''' penalty = 0.0 max_num_turbines: int = self.inner_problem.num_turbines site_shape = self.inner_problem.site_info.polygon min_spacing = self.inner_problem.min_spacing # place solar area site_sw_bound = np.array([site_shape.bounds[0], site_shape.bounds[1]]) site_ne_bound = np.array([site_shape.bounds[2], site_shape.bounds[3]]) site_bounds_size = site_ne_bound - site_sw_bound solar_center = site_sw_bound + site_bounds_size * \ np.array([parameters.solar_x_position, parameters.solar_y_position]) # place solar max_solar_width = self.inner_problem.module_width * self.inner_problem.max_num_modules \ / self.inner_problem.min_strand_length solar_aspect = np.exp(parameters.solar_aspect_power) solar_x_size, num_modules, strands, solar_region, solar_bounds = \ find_best_solar_size( self.inner_problem.max_num_modules, self.inner_problem.min_strand_length, site_shape, solar_center, 0.0, self.inner_problem.module_width, self.inner_problem.module_height, parameters.solar_gcr, solar_aspect, self.inner_problem.module_width, max_solar_width, ) solar_x_buffer_length = min_spacing * (1 + parameters.solar_x_buffer) solar_s_buffer_length = min_spacing * (1 + parameters.solar_s_buffer) solar_buffer_shape = make_polygon_from_bounds( solar_bounds[0] - np.array([solar_x_buffer_length, solar_s_buffer_length]), solar_bounds[1] + np.array([solar_x_buffer_length, 0])) def get_bounds_center(shape): bounds = shape.bounds return Point(.5 * (bounds[0] + bounds[2]), .5 * (bounds[1] + bounds[3])) def get_excess_buffer_penalty(buffer, solar_region, bounding_shape): penalty = 0.0 buffer_intersection = buffer.intersection(bounding_shape) shape_center = get_bounds_center(buffer) intersection_center = get_bounds_center(buffer_intersection) shape_center_delta = \ np.abs(np.array(shape_center.coords) - np.array(intersection_center.coords)) / site_bounds_size shape_center_penalty = np.sum(shape_center_delta**2) penalty += shape_center_penalty bounds = buffer.bounds intersection_bounds = buffer_intersection.bounds west_excess = intersection_bounds[0] - bounds[0] south_excess = intersection_bounds[1] - bounds[1] east_excess = bounds[2] - intersection_bounds[2] north_excess = bounds[3] - intersection_bounds[3] solar_bounds = solar_region.bounds actual_aspect = (solar_bounds[3] - solar_bounds[1]) / \ (solar_bounds[2] - solar_bounds[0]) aspect_error = fabs(np.log(actual_aspect) - np.log(solar_aspect)) penalty += aspect_error**2 # excess buffer, minus minimum size # excess buffer is how much extra there is, but we must not penalise minimum sizes # # excess_x_buffer = max(0.0, es - min_spacing) # excess_y_buffer = max(0.0, min(ee, ew) - min_spacing) # if buffer has excess, then we need to penalize any excess buffer length beyond the minimum minimum_s_buffer = max(solar_s_buffer_length - south_excess, min_spacing) excess_x_buffer = (solar_s_buffer_length - minimum_s_buffer) / min_spacing penalty += excess_x_buffer**2 minimum_w_buffer = max(solar_x_buffer_length - west_excess, min_spacing) minimum_e_buffer = max(solar_x_buffer_length - east_excess, min_spacing) excess_y_buffer = (solar_x_buffer_length - max( minimum_w_buffer, minimum_e_buffer)) / min_spacing penalty += excess_y_buffer**2 return penalty penalty += get_excess_buffer_penalty(solar_buffer_shape, solar_region, site_shape) solar_buffer_region = site_shape.intersection(solar_buffer_shape) wind_shape = site_shape.difference( solar_buffer_shape) # compute valid wind layout shape # place border turbines turbine_positions: [Point] = [] if not isinstance(wind_shape, MultiPolygon): wind_shape = MultiPolygon([ wind_shape, ]) border_spacing = (parameters.border_spacing + 1) * min_spacing for bounding_shape in wind_shape: turbine_positions.extend( get_evenly_spaced_points_along_border( bounding_shape.exterior, border_spacing, parameters.border_offset, max_num_turbines - len(turbine_positions), )) valid_wind_shape = self.subtract_turbine_exclusion_zone( wind_shape, turbine_positions) # place interior grid turbines max_num_interior_turbines = max_num_turbines - len(turbine_positions) grid_aspect = np.exp(parameters.grid_aspect_power) intrarow_spacing, grid_sites = get_best_grid( valid_wind_shape, wind_shape.centroid, parameters.grid_angle, grid_aspect, parameters.row_phase_offset, min_spacing * 10000, min_spacing, max_num_interior_turbines, ) turbine_positions.extend(grid_sites) inner_candidate = self.inner_problem.candidate_type( turbine_positions, ((parameters.solar_gcr, num_modules, strands), )) return penalty, (inner_candidate, solar_buffer_shape, solar_region)
def run(default_config: Dict) -> None: config, output_path, run_name = setup_run(default_config) recorder = DataRecorder.make_data_recorder(output_path) max_evaluations = config['max_evaluations'] location_index = config['location'] location = locations[location_index] site = config['site'] site_data = None if site == 'circular': site_data = make_circular_site(lat=location[0], lon=location[1], elev=location[2]) elif site == 'irregular': site_data = make_irregular_site(lat=location[0], lon=location[1], elev=location[2]) else: raise Exception("Unknown site '" + site + "'") site_info = SiteInfo(site_data) inner_problem = HybridOptimizationProblem(site_info, config['num_turbines'], config['solar_capacity']) problem = HybridParametrization(inner_problem) optimizer = ParametrizedOptimizationDriver(problem, recorder=recorder, **config['optimizer_config']) figure = plt.figure(1) axes = figure.add_subplot(111) axes.set_aspect('equal') plt.grid() plt.tick_params(which='both', labelsize=15) plt.xlabel('x (m)', fontsize=15) plt.ylabel('y (m)', fontsize=15) site_info.plot() score, evaluation, best_solution = optimizer.central_solution() score, evaluation = problem.objective(best_solution) if score is None else score print(-1, ' ', score, evaluation) print('setup 1') num_substeps = 1 figure, axes = plt.subplots(dpi=200) axes.set_aspect(1) animation_writer = PillowWriter(2 * num_substeps) animation_writer.setup(figure, os.path.join(output_path, 'trajectory.gif'), dpi=200) print('setup 2') _, _, central_solution = optimizer.central_solution() print('setup 3') bounds = problem.inner_problem.site_info.polygon.bounds site_sw_bound = np.array([bounds[0], bounds[1]]) site_ne_bound = np.array([bounds[2], bounds[3]]) site_center = .5 * (site_sw_bound + site_ne_bound) max_delta = max(bounds[2] - bounds[0], bounds[3] - bounds[1]) reach = (max_delta / 2) * 1.3 min_plot_bound = site_center - reach max_plot_bound = site_center + reach print('setup 4') best_score, best_evaluation, best_solution = 0.0, 0.0, None def plot_candidate(candidate): nonlocal best_score, best_evaluation, best_solution axes.cla() axes.set(xlim=(min_plot_bound[0], max_plot_bound[0]), ylim=(min_plot_bound[1], max_plot_bound[1])) wind_color = (153 / 255, 142 / 255, 195 / 255) solar_color = (241 / 255, 163 / 255, 64 / 255) central_color = (.5, .5, .5) conforming_candidate, _, __ = problem.make_conforming_candidate_and_get_penalty(candidate) problem.plot_candidate(conforming_candidate, figure, axes, central_color, central_color, alpha=.7) if best_solution is not None: conforming_best, _, __ = problem.make_conforming_candidate_and_get_penalty(best_solution) problem.plot_candidate(conforming_best, figure, axes, wind_color, solar_color, alpha=1.0) axes.set_xlabel('Best Solution AEP: {}'.format(best_evaluation)) else: axes.set_xlabel('') axes.legend([ Line2D([0], [0], color=wind_color, lw=8), Line2D([0], [0], color=solar_color, lw=8), Line2D([0], [0], color=central_color, lw=8), ], ['Wind Layout', 'Solar Layout', 'Mean Search Vector'], loc='lower left') animation_writer.grab_frame() print('plot candidate') plot_candidate(central_solution) central_prev = central_solution # TODO: make a smooth transition between points # TODO: plot exclusion zones print('begin') try: while optimizer.num_evaluations() < max_evaluations: print('step start') logger.info("Starting step, num evals {}".format(optimizer.num_evaluations())) optimizer.step() print('step end') proportion = min(1.0, optimizer.num_evaluations() / max_evaluations) g = 1.0 * proportion b = 1.0 - g a = .5 color = (b, g, b) best_score, best_evaluation, best_solution = optimizer.best_solution() central_score, central_evaluation, central_solution = optimizer.central_solution() a1 = optimizer.converter.convert_from(central_prev) b1 = optimizer.converter.convert_from(central_solution) a = np.array(a1, dtype=np.float64) b = np.array(b1, dtype=np.float64) for i in range(num_substeps): p = (i + 1) / num_substeps c = (1 - p) * a + p * b candidate = optimizer.converter.convert_to(c) plot_candidate(candidate) central_prev = central_solution print(optimizer.num_iterations(), ' ', optimizer.num_evaluations(), best_score, best_evaluation) except: raise RuntimeError("Optimizer error encountered. Try modifying the config to use larger generation_size if" " encountering singular matrix errors.") animation_writer.finish() optimizer.close() print("Results and animation written to " + os.path.abspath(output_path))