Module nemo.polygons

Support code for the 43 polygons of the AEMO study.

Expand source code
# Copyright (C) 2014, 2015, 2016 The University of New South Wales
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

"""Support code for the 43 polygons of the AEMO study."""

from math import atan2, cos, radians, sin, sqrt

import numpy as np

from nemo import regions

# The fraction of a region's load in each polygon.
regions.nsw.polygons = {21: 0, 22: 0, 23: 0, 24: .05, 28: 0, 29: 0,
                        30: 0, 31: .8, 33: 0, 34: 0, 35: .05, 36: .1}
regions.qld.polygons = {1: .04, 2: 0, 3: 0, 4: .11, 5: 0, 6: 0,
                        7: .27, 8: 0, 9: 0, 10: 0, 11: .02, 14: 0, 15: 0,
                        16: .14, 17: .42}
regions.sa.polygons = {12: 0, 13: 0, 18: 0, 19: 0, 20: 0, 25: 0,
                       26: 0, 27: .1, 32: .9}
regions.snowy.polygons = {}
regions.tas.polygons = {40: .2, 41: .2, 42: 0, 43: .6}
regions.vic.polygons = {37: .2, 38: .1, 39: .7}

# Ensure all weights sum to one.
for r in regions.All:
    if r.polygons:
        assert round(sum(r.polygons.values())) == 1

# Useful for testing
WILDCARD = 31

# Vertices of the closed polygons (nb. must be closed)
_polygons = {
    1: (
        (144.602, -13.838),
        (145.602, -13.838),
        (148.447, -18.282),
        (146.646, -19.041),
        (145.107, -18.792),
        (143.459, -18.480),
        (144.602, -13.838),
    ),
    2: (
        (140.949, -18.099),
        (143.459, -18.480),
        (143.701, -20.715),
        (140.949, -20.612),
        (140.949, -18.099),
    ),
    3: (
        (143.459, -18.480),
        (145.107, -18.792),
        (146.646, -20.797),
        (143.701, -20.715),
        (143.459, -18.480),
    ),
    4: (
        (148.447, -18.282),
        (150.754, -21.545),
        (147.810, -22.513),
        (146.646, -20.797),
        (145.107, -18.792),
        (146.646, -19.041),
        (148.447, -18.282),
    ),
    5: (
        (140.949, -20.612),
        (143.701, -20.715),
        (143.987, -23.665),
        (140.949, -23.665),
        (140.949, -20.612),
    ),
    6: (
        (143.701, -20.715),
        (146.646, -20.797),
        (147.810, -22.513),
        (148.887, -23.665),
        (146.382, -23.665),
        (143.987, -23.665),
        (143.701, -20.715),
    ),
    7: (
        (150.754, -21.545),
        (152.974, -24.137),
        (149.639, -24.817),
        (148.887, -23.665),
        (147.810, -22.513),
        (150.754, -21.545),
    ),
    8: (
        (140.949, -23.665),
        (143.987, -23.665),
        (144.141, -25.958),
        (140.999, -25.996),
        (140.949, -23.665),
    ),
    9: (
        (143.987, -23.665),
        (146.382, -23.665),
        (147.458, -25.958),
        (144.141, -25.958),
        (143.987, -23.665),
    ),
    10: (
        (146.382, -23.665),
        (148.887, -23.665),
        (149.639, -24.817),
        (150.529, -25.958),
        (147.458, -25.958),
        (146.382, -23.665),
    ),
    11: (
        (152.974, -24.137),
        (154.457, -25.958),
        (150.529, -25.958),
        (149.639, -24.817),
        (152.974, -24.137),
    ),
    12: (
        (135.183, -25.999),
        (137.933, -25.997),
        (137.933, -29.075),
        (135.187, -29.075),
        (135.183, -25.999),
    ),
    13: (
        (137.933, -25.997),
        (140.999, -25.996),
        (140.999, -28.999),
        (137.933, -29.075),
        (137.933, -25.997),
    ),
    14: (
        (140.999, -25.996),
        (144.141, -25.958),
        (144.232, -28.999),
        (141.001, -28.999),
        (140.999, -25.996),
    ),
    15: (
        (144.141, -25.958),
        (147.458, -25.958),
        (147.550, -28.999),
        (144.232, -28.999),
        (144.141, -25.958),
    ),
    16: (
        (147.458, -25.958),
        (150.529, -25.958),
        (150.688, -28.999),
        (147.550, -28.999),
        (147.458, -25.958),
    ),
    17: (
        (154.457, -25.958),
        (154.852, -29.075),
        (150.688, -28.999),
        (150.529, -25.958),
        (154.457, -25.958),
    ),
    18: (
        (131.199, -29.075),
        (135.187, -29.075),
        (135.187, -31.325),
        (131.199, -31.325),
        (131.199, -29.075),
    ),
    19: (
        (135.187, -29.075),
        (137.933, -29.075),
        (137.933, -31.325),
        (135.187, -31.325),
        (135.187, -29.075),
    ),
    20: (
        (137.933, -29.075),
        (140.999, -28.999),
        (141.001, -30.999),
        (141.001, -31.354),
        (137.933, -31.325),
        (137.933, -29.075),
    ),
    21: (
        (141.001, -28.999),
        (144.232, -28.999),
        (144.141, -31.109),
        (141.001, -30.998),
        (141.001, -28.999),
    ),
    22: (
        (144.232, -28.999),
        (147.550, -28.999),
        (146.843, -31.840),
        (144.141, -31.766),
        (144.141, -31.109),
        (144.232, -28.999),
    ),
    23: (
        (147.550, -28.999),
        (150.688, -28.999),
        (149.359, -31.878),
        (146.843, -31.840),
        (147.550, -28.999),
    ),
    24: (
        (154.852, -29.075),
        (153.798, -32.639),
        (149.359, -31.878),
        (150.688, -28.999),
        (154.852, -29.075),
    ),
    25: (
        (131.199, -31.325),
        (135.187, -31.325),
        (133.638, -34.053),
        (131.199, -32.658),
        (131.199, -31.325),
    ),
    26: (
        (135.187, -31.325),
        (137.933, -31.325),
        (137.900, -33.852),
        (137.856, -36.985),
        (135.700, -36),
        (133.638, -34.053),
        (135.187, -31.325),
    ),
    27: (
        (137.933, -31.326),
        (141.001, -31.354),
        (141.002, -33.311),
        (141.003, -33.982),
        (140.964, -33.981),
        (137.900, -33.852),
        (137.933, -31.326),
    ),
    28: (
        (141.001, -30.998),
        (144.141, -31.109),
        (144.141, -31.766),
        (143.954, -33.303),
        (141.002, -33.311),
        (141.001, -31.354),
        (141.001, -30.998),
    ),
    29: (
        (144.141, -31.766),
        (146.843, -31.840),
        (146.250, -34.053),
        (143.943, -34.016),
        (143.954, -33.303),
        (144.141, -31.766),
    ),
    30: (
        (146.843, -31.840),
        (149.359, -31.878),
        (148.315, -34.107),
        (146.250, -34.053),
        (146.843, -31.840),
    ),
    31: (
        (153.798, -32.639),
        (152.260, -34.724),
        (148.315, -34.107),
        (149.359, -31.878),
        (153.798, -32.639),
    ),
    32: (
        (137.900, -33.852),
        (140.964, -33.981),
        (140.964, -33.990),
        (140.963, -33.990),
        (140.962, -34.110),
        (140.966, -35.237),
        (140.966, -35.389),
        (140.964, -35.749),
        (140.974, -37.359),
        (140.971, -37.791),
        (140.966, -38.056),
        (140.966, -38.568),
        (137.856, -36.985),
        (137.900, -33.852),
    ),
    33: (
        (141.002, -33.311),
        (143.954, -33.303),
        (143.943, -34.016),
        (143.811, -35.299),
        (140.966, -35.237),
        (140.962, -34.108),
        (140.963, -33.990),
        (140.964, -33.990),
        (140.964, -33.981),
        (141.003, -33.982),
        (141.002, -33.311),
    ),
    34: (
        (143.943, -34.016),
        (146.250, -34.053),
        (145.698, -35.989),
        (143.811, -35.299),
        (143.943, -34.016),
    ),
    35: (
        (146.250, -34.053),
        (148.315, -34.107),
        (147.118, -36.510),
        (145.698, -35.989),
        (146.250, -34.053),
    ),
    36: (
        (152.260, -34.724),
        (150.590, -37.831),
        (147.118, -36.510),
        (148.315, -34.107),
        (152.260, -34.724),
    ),
    37: (
        (140.966, -35.237),
        (143.811, -35.299),
        (143.483, -39.249),
        (140.968, -38.568),
        (140.966, -38.056),
        (140.971, -37.791),
        (140.975, -37.359),
        (140.963, -35.742),
        (140.966, -35.389),
        (140.966, -35.237),
    ),
    38: (
        (146.426, -40.581),
        (145.698, -35.989),
        (147.118, -36.510),
        (150.590, -37.831),
        (146.426, -40.581),
    ),
    39: (
        (145.698, -35.989),
        (146.426, -40.581),
        (143.481, -39.249),
        (143.811, -35.299),
        (145.698, -35.989),
    ),
    40: (
        (143.483, -39.249),
        (146.426, -40.581),
        (146.426, -42.033),
        (144.097, -42.033),
        (143.483, -39.249),
    ),
    41: (
        (146.426, -40.581),
        (149.052, -38.849),
        (149.052, -42.033),
        (146.426, -42.033),
        (146.426, -40.581),
    ),
    42: (
        (144.097, -42.033),
        (146.426, -42.033),
        (146.426, -44),
        (144.097, -43.747),
        (144.097, -42.033),
    ),
    43: (
        (146.426, -42.033),
        (149.052, -42.033),
        (149.052, -43.747),
        (146.426, -44),
        (146.426, -42.033),
    ),
}

NUMPOLYGONS = len(_polygons)

# Dictionary mapping polygon number to region.
_region_table = {}
for rgn in [regions.nsw, regions.qld, regions.sa, regions.tas, regions.vic]:
    for poly in rgn.polygons:
        _region_table[poly] = rgn


def region(polygon):
    """
    Return the region a polygon resides in.

    >>> region(1)
    QLD1
    >>> region(40)
    TAS1
    """
    return _region_table[polygon]


# These build limits come from the ROAM Consulting report on wind and
# solar modelling for the AEMO 100% Renewables project. See
# nemo.ozlabs.org for a link to this report.

wind_limit = [None, 80.3, 0, 36.9, 6.5, 15.6, 1.5, 6.9, 2.6, 0, 4.1,
              1.5, 2.1, 0.9, 30.3, 0, 0, 40.5, 0.2, 0, 49.1, 2.3, 0,
              1.7, 116.3, 3.3, 71.9, 128.3, 11.7, 0.5, 0.6, 52.5,
              20.0, 0, 0, 0.9, 101.0, 9.15, 10.2, 15.6, 11.4, 14.1,
              0.5, 29.1]

pv_limit = [None, 133, 1072, 217, 266, 1343, 1424, 287, 1020, 657,
            175, 47, 488, 749, 1338, 1497, 1093, 243, 558, 647, 639,
            921, 1310, 1182, 125, 81, 493, 689, 937, 736, 522, 31,
            527, 535, 618, 339, 26, 670, 78, 347, 13, 21, 0.21, 5]

cst_limit = [None, 102, 822, 166, 204, 1030, 1092, 220, 782, 504, 134,
             36, 374, 574, 1026, 1148, 838, 186, 428, 496, 490, 706,
             1004, 906, 96, 62, 378, 528, 718, 564, 400, 24, 404, 410,
             474, 260, 20, 514, 60, 266, 10, 16, 0.16, 4]

# Only these four polygons have been chosen for off-shore wind farm siting.
offshore_wind_limit = {31: 10, 36: 10, 38: 10, 40: 10}


def _centroid(vertices):
    """Find the centroid of a polygon."""
    # pylint: disable=invalid-name
    # Ensure the polygon is closed
    assert vertices[0] == vertices[-1]
    thesum = 0
    vsum = (0, 0)
    for i in range(len(vertices) - 1):
        v1 = vertices[i]
        v2 = vertices[i + 1]
        cross = v1[0] * v2[1] - v1[1] * v2[0]
        thesum += cross
        vsum = (((v1[0] + v2[0]) * cross) + vsum[0],
                ((v1[1] + v2[1]) * cross) + vsum[1])
        z = 1. / (3. * thesum)
    return (vsum[0] * z, vsum[1] * z)


def dist(poly1, poly2):
    """Return the distance between two polygon centroids.

    >>> dist(1,1)
    0
    >>> dist(1,43)
    2910
    >>> dist(1,43) == distances[1,43]
    True
    """
    # Code adapted from Chris Veness
    # pylint: disable=invalid-name
    radius = 6371  # km
    point1 = centroids[poly1]
    point2 = centroids[poly2]
    dlat = radians(point1[0] - point2[0])
    dlon = radians(point1[1] - point2[1])
    lat1 = radians(point1[0])
    lat2 = radians(point2[0])
    a = sin(dlat / 2) ** 2 + \
        sin(dlon / 2) ** 2 * cos(lat1) * cos(lat2)
    c = 2 * atan2(sqrt(a), sqrt(1 - a))
    return int(radius * c)


centroids = {}
for _i, _vertices in _polygons.items():
    _lon, _lat = _centroid(_vertices)
    centroids[_i] = (_lat, _lon)

# A proposed transmission network.

net = {
    1: {2: dist(1, 2), 3: dist(1, 3), 4: dist(1, 4)},
    2: {1: dist(2, 1), 3: dist(2, 3), 5: dist(2, 5)},
    3: {1: dist(3, 1), 2: dist(3, 2), 4: dist(3, 4), 6: dist(3, 6)},
    4: {1: dist(4, 1), 3: dist(4, 3), 6: dist(4, 6), 7: dist(4, 7),
        10: dist(4, 10)},
    5: {2: dist(5, 2), 6: dist(5, 6), 8: dist(5, 8)},
    6: {3: dist(6, 3), 4: dist(6, 4), 5: dist(6, 5), 9: dist(6, 9)},
    7: {4: dist(7, 4), 10: dist(7, 10), 11: dist(7, 11), 16: dist(7, 16)},
    8: {5: dist(8, 5), 9: dist(8, 9), 14: dist(8, 14)},
    9: {6: dist(9, 6), 8: dist(9, 8), 10: dist(9, 10), 15: dist(9, 15)},
    10: {4: dist(10, 4), 7: dist(10, 7), 9: dist(10, 9), 16: dist(10, 16)},
    11: {7: dist(7, 11), 16: dist(16, 11), 17: dist(11, 17)},
    12: {13: dist(12, 13), 19: dist(12, 19)},
    13: {12: dist(13, 12), 14: dist(13, 14), 20: dist(13, 20)},
    14: {8: dist(14, 8), 13: dist(14, 13), 15: dist(14, 15), 21: dist(14, 21)},
    15: {9: dist(15, 9), 14: dist(15, 14), 16: dist(15, 16), 22: dist(15, 22)},
    16: {
        7: dist(16, 7),
        10: dist(16, 10),
        11: dist(16, 11),
        15: dist(16, 15),
        17: dist(16, 17),
        23: dist(16, 23),
        24: dist(16, 24),
    },
    17: {11: dist(17, 11), 16: dist(17, 16), 24: dist(17, 24)},
    18: {19: dist(18, 19), 25: dist(18, 25)},
    19: {12: dist(19, 12), 18: dist(19, 18), 20: dist(19, 20),
         26: dist(19, 26)},
    20: {13: dist(20, 13), 19: dist(20, 19), 21: dist(20, 21),
         27: dist(20, 27)},
    21: {14: dist(21, 14), 20: dist(21, 20), 22: dist(21, 22),
         29: dist(21, 29)},
    22: {15: dist(22, 15), 21: dist(22, 21), 23: dist(22, 23),
         29: dist(22, 29)},
    23: {16: dist(23, 16), 22: dist(23, 22), 24: dist(23, 24),
         30: dist(23, 30)},
    24: {16: dist(24, 16), 17: dist(24, 17), 23: dist(24, 23),
         31: dist(23, 31)},
    25: {18: dist(25, 18), 26: dist(25, 26)},
    26: {19: dist(26, 19), 25: dist(26, 25), 27: dist(26, 27)},
    27: {
        20: dist(27, 20),
        26: dist(27, 26),
        28: dist(27, 28),
        32: dist(27, 32),
        33: dist(27, 33),
    },
    28: {21: dist(28, 21), 27: dist(28, 27), 29: dist(28, 29),
         33: dist(28, 33)},
    29: {22: dist(29, 22), 28: dist(29, 28), 30: dist(29, 30),
         34: dist(29, 34)},
    30: {23: dist(30, 23), 29: dist(30, 29), 31: dist(30, 31),
         35: dist(30, 35)},
    31: {24: dist(31, 24), 30: dist(31, 30), 36: dist(31, 36)},
    32: {27: dist(32, 27), 33: dist(32, 33), 37: dist(32, 37)},
    33: {
        27: dist(33, 27),
        28: dist(33, 28),
        32: dist(33, 32),
        34: dist(33, 34),
        37: dist(33, 37),
        39: dist(33, 39),
    },
    34: {29: dist(34, 29), 33: dist(34, 33), 35: dist(34, 35),
         39: dist(34, 39)},
    35: {
        30: dist(35, 30),
        34: dist(35, 34),
        36: dist(35, 36),
        38: dist(35, 38),
        39: dist(35, 39),
    },
    36: {31: dist(36, 31), 35: dist(36, 35), 38: dist(36, 38)},
    37: {32: dist(37, 32), 33: dist(37, 33), 39: dist(37, 39)},
    38: {35: dist(38, 35), 36: dist(38, 36), 39: dist(38, 39),
         41: dist(38, 41)},
    39: {
        33: dist(39, 33),
        34: dist(39, 34),
        35: dist(39, 35),
        37: dist(39, 37),
        38: dist(39, 38),
        40: dist(39, 40),
    },
    40: {39: dist(40, 39), 41: dist(40, 41), 42: dist(40, 42)},
    41: {38: dist(41, 38), 40: dist(41, 40), 43: dist(41, 43)},
    42: {40: dist(42, 40), 43: dist(42, 43)},
    43: {41: dist(43, 41), 42: dist(43, 42)},
}

distances = np.zeros((NUMPOLYGONS + 1, NUMPOLYGONS + 1))
# mark row 0 and column 0 as unused (there is no polygon #0)
distances[0] = np.nan
distances[::, 0] = np.nan
rows, cols = NUMPOLYGONS + 1, NUMPOLYGONS + 1
for p1 in range(1, rows):
    for p2 in range(1, cols):
        distances[p1, p2] = dist(p1, p2)

existing_net = np.zeros((NUMPOLYGONS + 1, NUMPOLYGONS + 1))
# mark row 0 and column 0 as unused (there is no polygon #0)
existing_net[0] = np.nan
existing_net[::, 0] = np.nan

for (p1, p2, limit) in \
    [(7, 4, 1100), (7, 16, 1000), (7, 11, 1100), (11, 17, 1100),
     (16, 7, 400), (17, 11, 200), (11, 7, 200), (16, 17, 3500),
     (16, 24, 1200), (17, 24, 250), (24, 16, 500), (24, 17, 100),
     (24, 31, 900), (31, 24, 1100), (31, 36, 2000), (36, 31, 2000),
     (36, 35, 1500), (35, 36, 2800), (33, 34, 100), (34, 33, 100),
     (32, 27, 100), (27, 32, 550), (32, 33, 150), (33, 32, 230),
     (32, 37, 900), (37, 32, 900), (37, 39, 900), (39, 37, 900),
     (33, 39, 500), (39, 33, 1300), (34, 39, 1000), (39, 34, 1300),
     (39, 38, 2500), (38, 39, 6400), (38, 41, 450), (41, 38, 600)]:
    assert p1 in list(net[p2].keys()), (p2, p1)
    assert p2 in list(net[p1].keys()), (p1, p2)
    existing_net[p1, p2] = limit

Functions

def dist(poly1, poly2)

Return the distance between two polygon centroids.

>>> dist(1,1)
0
>>> dist(1,43)
2910
>>> dist(1,43) == distances[1,43]
True
Expand source code
def dist(poly1, poly2):
    """Return the distance between two polygon centroids.

    >>> dist(1,1)
    0
    >>> dist(1,43)
    2910
    >>> dist(1,43) == distances[1,43]
    True
    """
    # Code adapted from Chris Veness
    # pylint: disable=invalid-name
    radius = 6371  # km
    point1 = centroids[poly1]
    point2 = centroids[poly2]
    dlat = radians(point1[0] - point2[0])
    dlon = radians(point1[1] - point2[1])
    lat1 = radians(point1[0])
    lat2 = radians(point2[0])
    a = sin(dlat / 2) ** 2 + \
        sin(dlon / 2) ** 2 * cos(lat1) * cos(lat2)
    c = 2 * atan2(sqrt(a), sqrt(1 - a))
    return int(radius * c)
def region(polygon)

Return the region a polygon resides in.

>>> region(1)
QLD1
>>> region(40)
TAS1
Expand source code
def region(polygon):
    """
    Return the region a polygon resides in.

    >>> region(1)
    QLD1
    >>> region(40)
    TAS1
    """
    return _region_table[polygon]