Skip to content

Commit 6f9b8da

Browse files
committed
Merge branch 'class_based'
2 parents 903bf65 + c7dd2ce commit 6f9b8da

17 files changed

Lines changed: 73579 additions & 702 deletions

blocksequence/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .algorithms.blockorder import *
2+
from .algorithms.edgeorder import *
3+
from .algorithms.evolution import *
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .edgeorder import *
2+
from .blockorder import *
3+
from .evolution import *
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
Ordering of child geographies (block polygons) within a parent geography.
3+
"""
4+
5+
from .evolution import *
6+
import logging
7+
import numpy as np
8+
import pandas as pd
9+
10+
logger = logging.getLogger(__name__)
11+
12+
__all__ = ['BlockOrder']
13+
14+
15+
class BlockOrder:
16+
"""Calculate an optimal order for the child geographies within a parent geography.
17+
18+
This is implemented as a Genetic Algorithm to find a best effort approximation to solve the Travelling Salesman
19+
Problem. The block list is initially loaded as a random tour and a distance calculated. The tour is then evolved,
20+
swapping the block order on each evolution to try and find the shortest possible route through all the blocks.
21+
22+
Inspired by https://gist.github.com/turbofart/3428880
23+
"""
24+
25+
def __init__(self, block_data, cgeo_attr, x_field, y_field, block_order_field_name = 'block_order', max_evolution_count=150):
26+
"""Initialize the object
27+
28+
Parameters
29+
----------
30+
block_data : pandas.DataFrame
31+
DataFrame of block information. Must include the block UID, and x/y coordinates or a representative point.
32+
33+
cgeo_attr : String
34+
The name of the field in block_data containing the UID value
35+
36+
x_field : String
37+
The name of the field that contains the X value of the block representative point.
38+
39+
y_field : String
40+
The name of the field that contains the Y value of the block representative point.
41+
42+
max_evolution_count : Integer, 150
43+
The maximum number of evolutions to try before returning a result.
44+
"""
45+
46+
logger.debug("BlockSequence class initialization started")
47+
48+
self.block_data = block_data
49+
self.cgeo_attr = cgeo_attr
50+
self.max_evolution_count = max_evolution_count
51+
52+
self.x_field = x_field
53+
self.y_field = y_field
54+
55+
self.bo_name = block_order_field_name
56+
57+
# Create a new tour manager to hold all the possible tours for this set of blocks
58+
self.tourmanager = TourManager()
59+
60+
# Initialize a tour with the data provided
61+
self._initialize_tour()
62+
63+
# Create the initial population from the tourmanager
64+
self.block_count = len(self.block_data)
65+
self.block_population = Population(self.tourmanager, self.block_count, True)
66+
67+
def _initialize_tour(self):
68+
"""Initialize the tour manager with the block data."""
69+
70+
logger.debug("Adding each block to tour manager")
71+
for index, point in self.block_data.iterrows():
72+
block_rep = Block(point[self.cgeo_attr], point[self.x_field], point[self.y_field])
73+
# logger.debug("Adding block %s to tour manager", block_rep)
74+
self.tourmanager.add_block(block_rep)
75+
76+
def get_optimal_order(self):
77+
"""Determine the optimal block order.
78+
79+
Returns
80+
-------
81+
df : pandas.DataFrame
82+
A DataFrame containing the block UID and an associated block order value for every block in the input.
83+
"""
84+
85+
logger.debug("get_optimal_order start")
86+
87+
# Tours with only one or two blocks have no value in being evolved, so the random order
88+
# calculated earlier is used to save processing time.
89+
if self.block_count > 2:
90+
# For areas with very sall numbers of blocks, it doesn't make sense to use the maximum possible number of
91+
# evolutions to find the optimal order. The number of possible combinations is a factorial of the initial
92+
# block count, so that is used to determine the maximum number of evolutions up to generation_count.
93+
evolution_count = self.max_evolution_count
94+
block_count_factorial = np.math.factorial(self.block_count)
95+
if block_count_factorial < evolution_count:
96+
evolution_count = block_count_factorial
97+
98+
logger.debug("Evolving the block population %s times to find an optimal solution", evolution_count)
99+
# Set up the genetic algorithm on the set of tours
100+
block_ga = GenetricAlgorithm(self.tourmanager)
101+
102+
# Perform an initial evolution on the population to set things up
103+
self.block_population = block_ga.evolve_population(self.block_population)
104+
105+
# Perform the maximum number of evolutions allowed to try and find the shortest path between blocks
106+
for i in range(evolution_count):
107+
self.block_population = block_ga.evolve_population(self.block_population)
108+
109+
# Return the best path that was found
110+
logger.debug("Final distance: %s", self.block_population.get_fittest().get_distance())
111+
chosen_block_order = self.block_population.get_fittest().get_block_order()
112+
113+
# Create a DataFrame to put the order on the block ID
114+
df = pd.DataFrame(list(zip(chosen_block_order, range(1, len(chosen_block_order) + 1))),
115+
columns=[self.cgeo_attr, self.bo_name])
116+
return df

0 commit comments

Comments
 (0)