-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathEnumeratePatches.py
324 lines (253 loc) · 9.56 KB
/
EnumeratePatches.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# Enumerating all patches of parcels with at least a given area.
# (c) James R. Wilcox 2017
# Parcel: atomic unit of land. Consists of:
# - unique identifier
# - area
# - price (ignored for now)
# - list of adjacent parcels
# See class Parcel below.
# Patch: a geographically contiguous set of parcels.
# See class Patch below.
import copy
import csv
from itertools import *
import sys
# Main algorithm.
#
# The basic idea is to start with every parcel in its own patch, and
# then iteratively expand patches with adjacent parcels.
#
# The only tricky part is making sure you never consider the same
# patch twice. One easy solution would be to check whether the patch
# had been seen before, but this wastes the work of generating it more
# than once.
#
# Instead, we keep track not only of which parcels have been added to
# the patch, but also which parcels have been considered but then
# *not* added. When considering a patch's adjacent parcels, we leave
# out any that have already been considered.
def enumerate_patches(parcels, min_area):
"Find all (minimal) patches with at least the given area"
worklist = []
# Initialize the worklist with all singleton patches.
old_parcels = []
for p in parcels:
patch = Patch.singleton(p)
patch.mark_all(old_parcels)
worklist.append(patch)
old_parcels.append(p)
# Now start the actual processing.
while len(worklist) > 0:
patch = worklist.pop()
# The problem asks for *minimal* patches, so once we hit the
# area bound, we stop expanding. "Stop expanding" here means
# "don't put anything back on the worklist".
if patch.area >= min_area:
yield patch
continue
# Consider all possible one-step expansions of current patch.
old_parcels = []
for parcel in patch.adjacent():
new_patch = copy.copy(patch)
new_patch.add(parcel)
new_patch.mark_all(old_parcels)
worklist.append(new_patch)
old_parcels.append(parcel)
class Parcel(object):
"""An atomic unit of land. Consists of:
- unique identifier
- area
- price (ignored for now)
- list of adjacent parcels"""
# Since parcel ids are unique, we can look up a parcel by its id,
# implemented by the following map.
_instances = {}
@staticmethod
def fromId(id):
"Look up the Parcel corresponding to the given id."
return Parcel._instances[id]
def __init__(self, id, area, price):
if id in Parcel._instances:
raise RuntimeError("duplicate parcel id {}".format(id))
self.id = id
self.area = area
self.price = price
# The list of adjacent parcels must be filled out elsewhere.
self.adjacent = []
Parcel._instances[id] = self
def __str__(self):
return "Parcel(id={}, area={}, price={})".format(self.id, self.area, self.price)
def __repr__(self):
return str(self)
class Patch(object):
"""A geographically contiguous set of parcels.
The total area of the patch is available in the field `area`."""
# Internally, a patch is represented as a dictionary mapping parcels to booleans.
# If a parcel is mapped to True, then it is in the patch.
# If it is mapped to False, then it is *marked* as considered, but not added.
# Marked parcels are never considered adjacent to the patch.
def __init__(self):
self.parcels = {}
self.area = 0
def __str__(self):
l = [p.id for (p, v) in self.parcels.iteritems() if v]
l.sort()
return "Patch({})".format(l)
def __copy__(self):
p = Patch()
p.parcels = self.parcels.copy()
p.area = self.area
return p
@staticmethod
def singleton(parcel):
"Construct a patch consisting of just the given parcel."
result = Patch()
result.add(parcel)
return result
def add(self, p):
if p in self.parcels:
raise RuntimeError("add parcel was already present")
self.parcels[p] = True
self.area += p.area
def mark(self, p):
"""Mark a parcel as considered but *not* added.
The parcel must not already be added present in the patch (either added or marked)."""
if p in self.parcels:
raise RuntimeError("mark parcel was already present")
self.parcels[p] = False
def mark_all(self, l):
"Mark all parcels in the given list, if they are not already present."
for p in l:
self.mark(p)
def adjacent(self):
"A generator of (non-marked) parcels adjacent to this patch."
visited = set()
for (p, added) in self.parcels.iteritems():
if added:
for q in p.adjacent:
if q not in visited:
visited.add(q)
if q not in self.parcels:
yield q
def sorted_parcels(self):
l = [p for (p, present) in self.parcels.iteritems() if present]
l.sort(key=lambda parcel: parcel.id)
return l
def load(parcel_file, adjacency_file):
"Load parcel and adjacency data from the given files."
parcels = []
reader = csv.reader(parcel_file)
for row in reader:
parcel = Parcel(int(row[0]), float(row[1]), float(row[2]))
parcels.append(parcel)
reader = csv.reader(adjacency_file)
for row in reader:
a, b = Parcel.fromId(int(row[0])), Parcel.fromId(int(row[1]))
a.adjacent.append(b)
return parcels
def dump_patches(dump_file, patches):
"""Dump patches to CSV file.
The format is one patch per row.
Each row consists of a unique patch id followed by the list of parcel ids in that patch."""
def patch_to_row(patch):
l = [p.id for (p, present) in patch.parcels.items() if present]
l.sort()
return [patch.id] + l
csv.writer(dump_file).writerows([patch_to_row(patch) for patch in patches])
def dump_interference(dump_file, patches):
"""Dump patch interference data to CSV file.
Each row contains the unqiue ids of two interfering patches."""
l = []
for patch in patches:
tmp = []
for other in patch.interfering:
tmp.append([patch.id, other.id])
tmp.sort()
l.extend(tmp)
csv.writer(dump_file).writerows(l)
def compute_reverse_index(parcels, patches):
reverse_index = {}
for parcel in parcels:
reverse_index[parcel] = []
for patch in patches:
for (p, present) in patch.parcels.iteritems():
if present:
reverse_index[p].append(patch)
return reverse_index
def assign_ids(patches):
n = 1
for patch in patches:
patch.id = n
n += 1
def compute_interference(patches, reverse_index):
for patch in patches:
patch.interfering = set()
def mark_interfering(patch, others):
for other in others:
if patch is not other: # nobody interferes with themselves
patch.interfering.add(other)
for patch in patches:
for (parcel, present) in patch.parcels.iteritems():
if present:
# if another patch shares this parcel, then it overlaps -> interferes
mark_interfering(patch, reverse_index[parcel])
# if another patch contains an adjacent parcel, it interferes
for adj_parcel in parcel.adjacent:
mark_interfering(patch, reverse_index[adj_parcel])
def proper_nonempty_subsets(l):
def go(l, i):
if i >= len(l):
yield ()
raise StopIteration
for x in go(l, i+1):
yield (l[i],) + x
yield x
return ifilter(lambda t: (0 < len(t) and len(t) < len(l)), go(l, 0))
def minimize(patches):
S = set()
for patch in patches:
S.add(tuple(patch.sorted_parcels()))
l = []
for patch in patches:
keep = True
for t2 in proper_nonempty_subsets(tuple(patch.sorted_parcels())):
if t2 in S:
keep = False
if keep:
l.append(patch)
return l
def compute_patches(parcels):
patches = list(enumerate_patches(parcels, 50))
patches = minimize(patches)
patches.sort(key=lambda patch: patch.sorted_parcels())
return patches
def dump_cplex(cplex_file, patches):
def var(patch):
return "x" + str(patch.id)
print >> cplex_file, "maximize"
print >> cplex_file, " ", " + ".join(var(patch) for patch in patches)
print >> cplex_file, "subject to"
for patch in patches:
for other in patch.interfering:
if patch.id < other.id:
print >> cplex_file, " {} + {} <= 1".format(var(patch), var(other))
print >> cplex_file, "binary"
for patch in patches:
print >> cplex_file, " {}".format(var(patch))
print >> cplex_file, "end"
def main():
# (Edit these paths to point to the data.)
with open('/Users/jrw12/Downloads/Parcels.txt') as parcel_file, \
open('/Users/jrw12/Downloads/Adjacency.txt') as adjacency_file:
parcels = load(parcel_file, adjacency_file)
patches = compute_patches(parcels)
assign_ids(patches)
compute_interference(patches, compute_reverse_index(parcels, patches))
with open('Patches.lp', 'w') as cplex_file:
dump_cplex(cplex_file, patches)
with open('Patches.txt', 'w') as patches_file:
dump_patches(patches_file, patches)
with open('Interference.txt', 'w') as interference_file:
dump_interference(interference_file, patches)
if __name__ == "__main__":
main()