コード例 #1
0
def test_calc_simulated_energy():
    turbines = load_turbines()
    wind_speed = load_wind_speed(2016, 7)
    simulated_energy_timeseries_gwh = calc_simulated_energy(
        wind_speed, turbines)
    np.testing.assert_almost_equal(
        simulated_energy_timeseries_gwh.isel(time=0).values, 15773.1596734)
コード例 #2
0
def plot_history_turbines():
    turbines = load_turbines()

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

    # wow is this complicated to get rid of NaN-warnings by median()! isn't there an easier way?
    idcs_not_nan = {'turbines': ~np.isnan(turbines.t_cap)}
    per_year = turbines.t_cap[idcs_not_nan].groupby(
        turbines.p_year[idcs_not_nan])
    per_year.median(dim='turbines').plot(
        label='Median capacity of new turbines [kW]',
        ax=ax,
        marker='o',
        color='#efc220')

    ax.legend()
    plt.xlabel('Year')
    plt.ylabel('Capacity [kW]')
    plt.grid(True)

    ax2 = ax.twinx()
    idcs_not_nan = {'turbines': ~np.isnan(turbines.t_rd)}
    per_year = turbines.t_rd[idcs_not_nan].groupby(
        turbines.p_year[idcs_not_nan])
    per_year.median(dim='turbines').plot(
        label='Median rotor diameter of new turbines [m]',
        marker='o',
        color='#0d8085',
        ax=ax2)
    plt.ylabel("Rotor diameter [m]")
    ax2.legend(loc=1)

    return fig
コード例 #3
0
def test_calc_bounding_box_usa():
    turbines = load_turbines()
    north, west, south, east = calc_bounding_box_usa(turbines)

    # TODO this might be a very specific test, testing also turbines file...
    assert "{}".format(north) == '67.839905'
    assert (west, south, east) == (-172.713074, 16.970871, -64.610001)
コード例 #4
0
def main():
    # API documentation for downloading a subset:
    # https://confluence.ecmwf.int/display/CKB/Global+data%3A+Download+data+from+ECMWF+for+a+particular+area+and+resolution
    # https://retostauffer.org/code/Download-ERA5/

    download_dir = EXTERNAL_DIR / 'wind_velocity_usa_era5'

    turbines = load_turbines()
    north, west, south, east = calc_bounding_box_usa(turbines)

    # Format for downloading ERA5: North/West/South/East
    bounding_box = "{}/{}/{}/{}".format(north, west, south, east)

    logging.info("Downloading bounding_box=%s for years=%s and months=%s",
                 bounding_box, YEARS, MONTHS)

    c = cdsapi.Client()

    for year in YEARS:
        for month in MONTHS:
            filename = download_dir / f'wind_velocity_usa_{year}-{month:02d}.nc'

            if filename.exists():
                logging.info(f"Skipping {filename}, already exists!")
                continue

            logging.info(f"Starting download of {filename}...")

            for i in range(5):
                try:
                    c.retrieve(
                        'reanalysis-era5-single-levels', {
                            'product_type':
                            'reanalysis',
                            'format':
                            'netcdf',
                            'variable': [
                                '100m_u_component_of_wind',
                                '100m_v_component_of_wind',
                                '10m_u_component_of_wind',
                                '10m_v_component_of_wind'
                            ],
                            'year':
                            f'{year}',
                            'month': [f'{month:02d}'],
                            'area':
                            bounding_box,
                            'day': [f"{day:02d}" for day in range(1, 32)],
                            'time': [f"{hour:02d}:00" for hour in range(24)],
                        }, f"{filename}.part")
                except Exception as e:
                    logging.warning("Download failed: %s", e)
                else:
                    logging.info(f"Download of {filename} successful!")
                    os.rename(f"{filename}.part", filename)
                    break
            else:
                logging.warning("Download failed permanently!")
コード例 #5
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
コード例 #6
0
def save_figures():
    turbines = load_turbines()

    savefig_history_turbines()
    savefig_power_curves()
    if COMPUTE_CONSTANT_DISTANCE_FACTORS:
        savefig_repower_potential()
    savefig_repower_potential_direction()
    savefig_mean_wind_speed_and_turbines(turbines)
    savefig_optimized_cluster(turbines)
    savefig_simulated_energy_time_series()
    if COMPUTE_CONSTANT_DISTANCE_FACTORS:
        savefig_min_distances(turbines)
    savefig_distances(turbines)
コード例 #7
0
def calc_simulated_energy_years(years, turbines=None, power_curve=None, capacity_scaling=True,
                                only_built_turbines=True):
    """Load wind speed data from processed files and calculate energy in a loop to avoid large
    data in memory for the parts where dask chunks are not correctly working yet.

    Parameters
    ----------
    years : iterable
        eg. range(2004, 2017)
    turbines : xr.DataSet
        as returned by load_turbines()
    power_curve : callable
        see calc_simulated_energy()

    Returns
    -------
    xr.DataArray

    """
    eta = False
    simulated_energy_gwh = []
    if turbines is None:
        turbines = load_turbines()

    with ProgressBar():
        for year in years:
            for month in range(1, 13):
                t0 = time.time()
                logging.info("Calculating {}-{:02d}...".format(year, month))
                wind_speed = load_wind_speed(year, month)
                simulated_energy_gwh.append(
                    calc_simulated_energy(
                        wind_speed=wind_speed,
                        turbines=turbines,
                        power_curve=power_curve,
                        capacity_scaling=capacity_scaling,
                        only_built_turbines=only_built_turbines
                    )
                )

                if not eta:
                    logging.info("ETA: %s seconds", (time.time() - t0) * 12 * len(years))
                    eta = True

    simulated_energy_gwh = xr.concat(simulated_energy_gwh, dim='time')
    return simulated_energy_gwh
コード例 #8
0
def main():
    setup_logging()

    turbines = load_turbines()
    with ProgressBar():
        for year in YEARS:
            for month in MONTHS:
                fname = (
                    INTERIM_DIR / 'wind_speed_usa_era5' /
                    'wind_speed_usa_era5-{}-{:02d}.nc'.format(year, month))

                # here is a poor man Makefile, because it takes some while to convert all files
                if fname.exists():
                    logging.debug("Skipping %s", fname)
                    continue

                logging.info("Processing %s...", fname)

                wind_velocity = load_wind_velocity(year=year, month=month)
                wind_speed = calc_wind_speed_at_turbines(
                    wind_velocity, turbines)

                wind_speed.to_netcdf(fname)
コード例 #9
0
def test_find_optimal_locations():
    turbines = load_turbines()
    distance_factor = 2.5e-2
    turbine_model = Turbine(name='test turbine',
                            file_name='test_turbine',
                            power_curve=lambda x: x,
                            capacity_mw=1,
                            rotor_diameter_m=100.,
                            hub_height_m=200.)
    power_generation = xr.DataArray(np.ones((1, len(turbines.turbines))),
                                    dims=('turbine_model', 'turbines'))
    min_distance_km = distance_factor * turbine_model.rotor_diameter_m * METER_TO_KM
    cluster_per_location, _, _ = calc_location_clusters(
        turbines, min_distance_km)

    is_optimal_location = calc_optimal_locations(
        power_generation=power_generation,
        turbine_models=[turbine_model],
        cluster_per_location=cluster_per_location,
        distance_factor=distance_factor,
    )

    # there are 6 (erroneous) locations with distance < 5.5m to the closest location
    idcs_pairs = [[8489, 56587], [13268, 13391], [54214, 54216],
                  [54215, 54217]]

    # Note that there are erroneous turbine locations, which are only filtered in
    # calc_min_distances_cluster(), which is not used here. It might make sense to clean this
    # data in load_turbines() and use a higher distance_factor for this test.

    for idcs in idcs_pairs:
        assert np.sum(is_optimal_location[
            0, idcs]) == 1.  # either one or the other is optimal

    assert all(
        np.delete(is_optimal_location[0],
                  np.array(idcs_pairs).flatten()) == 1.)
コード例 #10
0
def test_find_optimal_locations_only_outliers():
    turbines = load_turbines()
    distance_factor = 1e-3
    turbine_model = Turbine(name='test turbine',
                            file_name='test_turbine',
                            power_curve=lambda x: x,
                            capacity_mw=1,
                            rotor_diameter_m=150.,
                            hub_height_m=200.)
    power_generation = xr.DataArray(np.ones((1, len(turbines.turbines))),
                                    dims=('turbine_model', 'turbines'))
    min_distance_km = distance_factor * turbine_model.rotor_diameter_m * METER_TO_KM
    cluster_per_location, _, _ = calc_location_clusters(
        turbines, min_distance_km)

    is_optimal_location = calc_optimal_locations(
        power_generation=power_generation,
        turbine_models=[turbine_model],
        cluster_per_location=cluster_per_location,
        distance_factor=distance_factor,  # needs to be > 0
    )
    # with vanishing distance_factor, we can built at all locations:
    assert np.all(is_optimal_location == 1.)
    assert is_optimal_location.shape == (1, len(turbines.turbines))
コード例 #11
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')
コード例 #12
0
def test_turbine_locations():
    turbines = load_turbines()
    locations = turbine_locations(turbines)
    assert locations.shape == (turbines.sizes['turbines'], 2)
コード例 #13
0
def calc_optimal_locations(power_generation,
                           turbine_models,
                           cluster_per_location,
                           distance_factor=None,
                           distance_factors=None,
                           prevail_wind_direction=None,
                           turbines=None):
    """For each (old) turbine location (all turbines from `load_turbines()`), pick at maximum one
    model from `turbine_models` to be installed such that total power_generation is maximized and
    distance thresholds are not violated.

    Parameters
    ----------
    power_generation : xr.DataArray, dims: turbine_model, turbines
        for each turbine (N turbines) and model 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)
    turbine_models : list of turbine_models.Turbine
        will try to find an optimal configuration of these turbine models (mixed also within one
        cluster), this is mostly used for rotor diameter, order of list needs to match with
        dimension `turbine_model` in parameter `power_generation`
    cluster_per_location : xr.DataArray (dims: turbines)
        for each turbine location one cluster id, see calc_location_clusters()
    distance_factor : float
        fixed distance factor, provide either this or `dstance_factors`
    distance_factors : xr.DataArray (dim: direction)
        distance factor per direction relative to prevailing wind direction, will be expanded by
        pre-pending the last element in the beginning (2-pi periodic) and the first at the end
        this should not contain dim=turbines!
    prevail_wind_direction : xr.DataArray (dim: turbines)
        prevailing wind direction for each turbine
    turbines : xr.DataSet
        as returned by load_turbines()

    Returns
    -------
    is_optimal_location : xr.DataArray
        1 if model is optimal (dims: turbine_model, turbines)

    """
    # TODO to support multiple turbine models, at least the bug for outliers below needs to be fixed
    assert len(
        turbine_models
    ) == 1, "multiple turbine models not yet supported"  # still valid?

    assert (distance_factors is None) ^ (distance_factor is None), \
        "provide either distance_factor or distance_factors"

    if distance_factor is not None:
        eps = 0.1  # safety margin for interpolation, probably paranoia
        distance_factors = xr.DataArray(
            [distance_factor, distance_factor],
            dims='direction',
            coords={'direction': [-np.pi - eps, np.pi + eps]})
    else:
        distance_factors = _extend_distance_factors(distance_factors)

    if turbines is None:
        turbines = load_turbines()

    clusters = np.unique(cluster_per_location)

    # FIXME for outliers clusters==-1 use (only) largest turbine in dim=turbine_models!
    is_optimal_location = np.ones(
        (len(turbine_models), len(cluster_per_location)), dtype=np.int64)

    # clusters[0] should be cluster -1, i.e. outliers which are always optimal for largest turbine
    assert clusters[0] == -1, "first cluster does not have index -1"

    # TODO this should be replaced by looping over groupby() --> speedup by a couple of minutes
    for i, cluster in enumerate(clusters[1:]):
        if i % 10 == 0:
            logging.info(
                f"Optimizing cluster {cluster} ({[tm.file_name for tm in turbine_models]},"
                f" df={distance_factor})...")
        locations_in_cluster = cluster == cluster_per_location
        is_optimal_location_cluster, problem = calc_optimal_locations_cluster(
            turbines=turbines.sel(turbines=locations_in_cluster),
            turbine_models=turbine_models,
            distance_factors=distance_factors,
            prevail_wind_direction=prevail_wind_direction,
            power_generation=power_generation.sel(
                turbines=locations_in_cluster))

        is_optimal_location[:,
                            locations_in_cluster] = is_optimal_location_cluster

    # FIXME turbine coords missing! This is tragic because one turbine is removed!
    is_optimal_location = xr.DataArray(is_optimal_location,
                                       dims=('turbine_model', 'turbines'),
                                       name='is_optimal_location')

    assert np.all(is_optimal_location.groupby(cluster_per_location).sum(dim='turbines') > 0),\
        "not all clusters have at least one optimal location"

    return is_optimal_location
コード例 #14
0
def test_load_turbines():
    turbines = load_data.load_turbines()
    assert np.isnan(turbines.t_cap).sum() == 3967
    assert turbines.p_year.min() == 1981
    assert turbines.p_year.max() == 2018
コード例 #15
0
def test_calc_wind_speed_at_turbines():
    turbines = load_turbines()
    year = 2017
    month = 3
    wind_velocity = load_wind_velocity(year, month)
    calc_wind_speed_at_turbines(wind_velocity, turbines)
コード例 #16
0
def calc_dist_in_direction(cluster_per_location,
                           prevail_wind_direction,
                           turbines=None,
                           bin_size_deg=15):
    """Directions between 0° and 360° will be grouped into bins of size ``bin_size_deg``,
    then for each turbine location the distance to the next turbine is calculated for each
    direction bin. Assumes that distance between clusters is infinite and therefore computation
    can be done for each cluster independently.

    Parameters
    ----------
    cluster_per_location : array_like of int
        cluster index for each turbine
    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)
    turbines : xr.DataSet
        as returned by load_turbines()
    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. 0rad = in prevailing wind direction,
        and otherwise counter-clockwise relative to 0rad

    """
    if turbines is None:
        turbines = load_turbines()

    n_bins = 360 // bin_size_deg
    distances = np.ones((turbines.sizes['turbines'], n_bins)) * np.nan

    distances = xr.DataArray(distances, dims=('turbines', 'direction'))

    d = None

    iterator = zip(turbines.groupby(cluster_per_location),
                   prevail_wind_direction.groupby(cluster_per_location))

    # TODO this loop could be parallelized, but a lock is needed for writing to distances, right?
    #  how about using dask.bag.foldby? would it help to use dask.delayed to speed up the inner
    #  loop and then combine results sequential?
    for ((idx_turbine, turbines_cluster), (idx_prevail,
                                           prevail_cluster)) in iterator:
        if idx_turbine == -1:  # single turbine per cluster
            continue

        d = calc_dist_in_direction_cluster(
            turbines_cluster,
            prevail_wind_direction=prevail_cluster,
            bin_size_deg=bin_size_deg)
        idcs = cluster_per_location == idx_turbine
        distances.loc[{'turbines': idcs}] = d

    if d is None:
        raise ValueError(
            "no location found for given clusters and cluster_per_location: "
            f"cluster_per_location={cluster_per_location}")

    # This is dangerously assuming that calc_dist_in_direction_cluster() always returns same
    # coordinates for dim=direction (which should be the case because bins and range in
    # np.histogram_bin_edges() is fixed) and that distances contains all turbines.
    distances = distances.assign_coords(direction=d.direction,
                                        turbines=turbines.turbines)

    return distances