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
Esempio n. 2
0
 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']))
Esempio n. 6
0
    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)
Esempio n. 7
0
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))