def calc_dist_in_direction_cluster(turbines,
                                   prevail_wind_direction,
                                   bin_size_deg=15):
    """Same as calc_dist_in_direction(), but intended for one cluster only. Calculates a squared
    distance matrix (and a squared direction matrix) and therefore RAM usage is O(len(turbines)^2).

    Parameters
    ----------
    turbines : xr.DataSet
        as returned by load_turbines()
    prevail_wind_direction : xr.DataArray  (dim = turbines)
        will be used to orientate distances relative to prevailing wind direction,
        pass an xr.DataArray with zeros to get distances per absolute directions (not relative to
        prevailing wind direction)
    bin_size_deg : float
        size of direction bins in degrees

    Returns
    -------
    xr.DataArray
        dims: turbines, direction
        direction is relative to prevail_wind_direction, i.e. 0° = in prevailing wind direction,
        and otherwise counter-clockwise relative to 0°

    """
    directions = calc_directions(turbines, prevail_wind_direction)

    # directions is actually not used here, because bins and range are provided (except for dtype)
    bin_edges = np.histogram_bin_edges(directions,
                                       bins=360 // bin_size_deg,
                                       range=(-np.pi, np.pi))

    num_bins = len(bin_edges) - 1  # Attention, fencepost problem!

    # np.digitize does not return the n-th bin, but the n+1-th bin!
    # This is not a symmetric matrix, directions get flipped by 180° if dims is provided in wrong
    # order, but it is not at all clear how xarray defines the order (probably the order of
    # usage of dims 'targets' and 'turbines' in the arctan2() call above).
    bin_idcs = np.digitize(directions, bin_edges) - 1
    bin_idcs = xr.DataArray(
        bin_idcs,
        dims=('targets', 'turbines'),  # targets = closest turbines
        coords={'turbines': turbines.turbines})

    locations = turbine_locations(turbines)
    distances = geolocation_distances(locations)

    # set distance to itself to INF to avoid zero distance minimums later
    distances[np.diag_indices_from(distances)] = np.inf
    distances = xr.DataArray(distances,
                             dims=('turbines', 'targets'),
                             coords={'turbines': turbines.turbines})

    bin_centers = edges_to_center(bin_edges)
    direction_bins = xr.DataArray(np.arange(num_bins),
                                  dims='direction',
                                  coords={'direction': bin_centers})

    return xr.where(bin_idcs == direction_bins, distances,
                    np.inf).min(dim='targets')
Esempio n. 2
0
def plot_locations(turbines=None, idcs=None, directions=None, colors=None):
    """Plot turbine locations and add arrows to indicate wind directions.

    FIXME does not use proper projection, probably valid only for small regions.

    Parameters
    ----------
    turbines : xr.DataSet
        as returned by load_turbines()
    idcs : array_like of type boolean
        select turbines to plot
    directions : dict of form label: array_like
        array contains directions in rad
    colors : iterable
        color of arrows for each item in directions

    """
    if turbines is None:
        turbines = load_turbines()
    if idcs is None:
        idcs = np.ones_like(turbines.xlong).astype(np.bool)
    if directions is None:
        directions = {}
    if colors is None:
        colors = [None] * len(directions)

    fig, ax = plt.subplots(1, 1, figsize=FIGSIZE)

    locations = turbine_locations(turbines.sel(turbines=idcs))

    ax.plot(locations.T[1], locations.T[0], 'o', label='Wind turbine location')

    for (label, values), color in zip(directions.items(), colors):
        ax.quiver(
            locations.T[1],
            locations.T[0],
            np.cos(values.sel(turbines=idcs)),
            np.sin(values.sel(turbines=idcs)),
            width=0.002,
            label=label,
            color=color,
        )

    ax.set_aspect('equal')

    plt.xlabel('Longitude [deg]')
    plt.ylabel('Latitude [deg]')
    plt.legend()

    return fig, ax
def calc_location_clusters(turbines, min_distance_km=0.5):
    """Calculate a partitioning of locations given in lang/long into clusters using the DBSCAN
    algorithm.

    Runtime: about 10-15 seconds for all turbines.

    Parameters
    ----------
    turbines : xr.DataSet
        as returned by load_turbines()
    min_distance_km : float

    Returns
    -------
    cluster_per_location : xr.DataArray (dims: turbines)
        for each location location the cluster index, -1 for outliers, see
        ``sklearn.cluster.DBSCAN``
    clusters : np.ndarray of shape (M,)
        M is the number of clusters
    cluster_sizes : np.ndarray of shape (M,)
        the size for each cluster

    References
    ----------
    https://geoffboeing.com/2014/08/clustering-to-reduce-spatial-data-set-size

    """
    locations = turbine_locations(turbines)

    # Parameters for haversine formula
    kms_per_radian = EARTH_RADIUS_KM
    epsilon = min_distance_km / kms_per_radian

    clustering = DBSCAN(eps=epsilon,
                        min_samples=2,
                        algorithm='ball_tree',
                        metric='haversine').fit(np.radians(locations))

    cluster_per_location = clustering.labels_
    clusters, cluster_sizes = np.unique(cluster_per_location,
                                        return_counts=True)

    cluster_per_location = xr.DataArray(
        cluster_per_location,
        dims='turbines',
        coords={'turbines': turbines.turbines},
        name='cluster_per_location')  # TODO rename to cluster?

    return cluster_per_location, clusters, cluster_sizes
Esempio n. 4
0
import sys
import logging

from wind_repower_usa.config import INTERIM_DIR, COMPUTE_CONSTANT_DISTANCE_FACTORS
from wind_repower_usa.geographic_coordinates import calc_min_distances
from wind_repower_usa.load_data import load_turbines
from wind_repower_usa.logging_config import setup_logging
from wind_repower_usa.util import turbine_locations

setup_logging()

if not COMPUTE_CONSTANT_DISTANCE_FACTORS:
    logging.warning("Skipping because constant distance factors are disabled!")
    sys.exit()

turbines = load_turbines()
locations = turbine_locations(turbines)

min_distances = calc_min_distances(locations)
min_distances.to_netcdf(INTERIM_DIR / 'min_distances' / 'min_distances.nc')
Esempio n. 5
0
def plot_optimized_cluster(turbines,
                           cluster_per_location,
                           is_optimal_location,
                           turbine,
                           distance_factors,
                           prevail_wind_direction,
                           step=3):
    plot_optimal_locations = plot_wind_directions = plot_thresholds = False
    plot_non_optimal_locations = True
    if step > 0:
        plot_wind_directions = True
    if step > 1:
        plot_thresholds = True
    if step > 2:
        plot_optimal_locations = True
    if step > 3:
        plot_non_optimal_locations = False
        plot_wind_directions = False

    fig, ax = plt.subplots(1, 1, figsize=FIGSIZE)

    # some arbitrary cluster with 70-100 turbines or so
    # probably: cluster=812 (but depends on clustering, therefore pinning via long/lat)
    x, y = -99.0, 45.92  # some point in cluster
    some_turbine_idx = (((turbines.xlong - x)**2 +
                         (turbines.ylat - y)**2)**0.5).argmin()
    cluster = cluster_per_location[some_turbine_idx].values
    loc = 'upper left'

    locations = turbine_locations(turbines)
    idcs = cluster_per_location == cluster
    is_optimal_location = is_optimal_location.sum(dim='turbine_model')
    is_optimal_location = is_optimal_location.astype(np.bool)
    locations_old_ylat, locations_old_xlon = locations[idcs].T
    locations_new_ylat, locations_new_xlon = locations[idcs
                                                       & is_optimal_location].T

    if plot_non_optimal_locations:
        ax.plot(locations_old_xlon,
                locations_old_ylat,
                'o',
                markersize=4,
                color='#0d8085',
                label='Current location of wind turbine')

    def radial_plot(ax, angles, radius, center, label):
        angles = np.append(angles, angles[0])
        radius = np.append(radius, radius[0])

        points_complex = center + radius * np.exp(1j * angles)
        alpha = None if plot_thresholds else 0.  # ugly hack to avoid changing figure size
        if not plot_thresholds:
            label = None
        ax.plot(points_complex.real,
                points_complex.imag,
                '-',
                color='gray',
                linewidth=0.4,
                label=label,
                alpha=alpha)
        return ax

    rotor_diameter = turbine.rotor_diameter_m

    has_label = False
    for idx in turbines.turbines[idcs & is_optimal_location]:
        radius = distance_factors / (EARTH_RADIUS_KM * 2 * np.pi) * 360
        radius = radius * METER_TO_KM * rotor_diameter
        center = turbines.isel(
            turbines=idx).xlong + turbines.isel(turbines=idx).ylat * 1j
        radial_plot(
            ax,
            angles=distance_factors.direction +
            prevail_wind_direction.sel(turbines=idx),
            radius=radius,
            center=center.values,
            label='Minimum distance to other turbine' if not has_label else '')
        has_label = True

    if plot_optimal_locations:
        ax.plot(locations_new_xlon,
                locations_new_ylat,
                'o',
                markersize=7,
                color='#c72321',
                fillstyle='none',
                label='Optimal location for {}'.format(turbine.name))

    ax.legend(loc=loc)

    def add_arrow(label):
        # not sure why quiver key is not working
        # https://stackoverflow.com/a/22349717/859591
        from matplotlib.legend_handler import HandlerPatch
        import matplotlib.patches as mpatches

        def make_legend_arrow(legend, orig_handle, xdescent, ydescent, width,
                              height, fontsize):
            p = mpatches.FancyArrow(0,
                                    0.5 * height,
                                    width,
                                    0,
                                    length_includes_head=True,
                                    head_width=0.5 * height)
            return p

        arrow = plt.arrow(0, 0, 1, 1, color='k')
        handles, labels = ax.get_legend_handles_labels()

        labels = labels[:1] + [label] + labels[1:]
        handles = handles[:1] + [arrow] + handles[1:]
        plt.legend(handles,
                   labels,
                   handler_map={
                       mpatches.FancyArrow:
                       HandlerPatch(patch_func=make_legend_arrow),
                   },
                   loc=loc)

    if plot_wind_directions:
        ax.quiver(locations_old_xlon,
                  locations_old_ylat,
                  np.cos(prevail_wind_direction.sel(turbines=idcs)),
                  np.sin(prevail_wind_direction.sel(turbines=idcs)),
                  width=0.0017,
                  color='k')

        add_arrow('Prevailing wind direction')

    ax.set_aspect('equal')
    plt.xlabel("Longitude [deg]")
    plt.ylabel("Latitude [deg]")

    return fig
Esempio n. 6
0
def test_turbine_locations():
    turbines = load_turbines()
    locations = turbine_locations(turbines)
    assert locations.shape == (turbines.sizes['turbines'], 2)
def calc_optimal_locations_cluster(turbines, turbine_models, distance_factors,
                                   prevail_wind_direction, power_generation):
    """For a set of locations, this will calculate an optimal subset of locations where turbines
    are to be placed, such that the power generation is maximized and a distance threshold is not
    violated:

    Objective function: maximize sum(power_generation[i], for all turbines i if is optimal location)
    s.t.: distance(i, j) >= min_distance  for i,j where i and j are optimal locations

    This is meant to be run on a small set of locations, e.g. a couple of hundred (or thousands).

    Parameters
    ----------
    turbines : xr.DataSet
        as returned by load_turbines(), but intended to be a subset due to memory use O(N^2)
    turbine_models : list of turbine_models.Turbine
        used for rotor diameter
    distance_factors : xr.DataArray (dim: direction)
        distance factor per direction relative to prevailing wind direction, will be expanded
    prevail_wind_direction : xr.DataArray (dim: turbines)
        prevailing wind direction for each turbine
    power_generation : xr.DataArray, dims: turbine_model, turbines
        for each turbine (N turbines) an expected power generation, scaling does not matter,
        so it does not matter if it is in GW or GWh/yr or 5*GWh/yr (averaging over 5 years)

    Returns
    -------
    is_optimal_location : np.array of length K,N
        is_optimal_location[k,i] for a location i and model k:
        1 == model k should be built, 0 == model k is not optimal,
        sum(axis=0)[i] == 0 means nothing is to be built at location i

    """
    locations = turbine_locations(turbines)
    num_locations = locations.shape[0]
    num_models = len(turbine_models)

    assert len(locations.shape) == 2
    assert locations.shape[1] == 2
    assert power_generation.sizes['turbines'] == num_locations
    assert power_generation.sizes['turbine_model'] == num_models

    pairwise_distances = geolocation_distances(locations)

    # for each location, if True a new turbine should be built, otherwise only decommission old one
    is_optimal_location = cp.Variable((num_models, num_locations),
                                      boolean=True)

    rotor_diameter_km = np.array([x.rotor_diameter_m
                                  for x in turbine_models]) * METER_TO_KM

    pairwise_distances[np.diag_indices_from(pairwise_distances)] = np.inf

    pairwise_df = _calc_pairwise_df(turbines, distance_factors,
                                    prevail_wind_direction)

    # for a location i, a location j with j != i and a turbine model k at least one of the
    # following must hold:
    #  - k is not built at i    <==> right-hand-side of inequality equals 0 or -1
    #  - nothing is built at j  <==> right-hand-side of inequality equals 0 or -1
    #  - i is far enough away from j for all j != i
    constraints = [
        pairwise_distances[i, :] / pairwise_df[i, :] / rotor_diameter_km[k] >=
        (is_optimal_location[k, i] +
         cp.atoms.affine.sum.sum(is_optimal_location, axis=0) - 1)
        for k in range(num_models) for i in range(num_locations)
    ]

    constraints += [cp.atoms.affine.sum.sum(is_optimal_location, axis=0) <= 1]

    obj = cp.Maximize(
        cp.atoms.affine.sum.sum(
            cp.multiply(is_optimal_location, power_generation.values)))

    problem = cp.Problem(obj, constraints)

    problem.solve(solver=cp.GUROBI)

    if problem.status != 'optimal':
        raise RuntimeError("Optimization problem could not be"
                           f"solved optimally: {problem.status}")

    return is_optimal_location.value, problem