Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/generateParkingAreaRerouters.py
169660 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2010-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file generateParkingAreaRerouters.py
15
# @author Lara CODECA
16
# @author Jakob Erdmann
17
# @author Mirko Barthauer
18
# @date 11-3-2019
19
20
""" Generate parking area rerouters from the parking area definition. """
21
22
from __future__ import print_function
23
from __future__ import absolute_import
24
import collections
25
import functools
26
import multiprocessing
27
import sys
28
import xml.etree.ElementTree
29
import numpy
30
import sumolib
31
32
if not hasattr(functools, "lru_cache"):
33
# python 2.7 fallback (lru_cache is a decorater with arguments: a function that returns a decorator)
34
def lru_cache_dummy(maxsize):
35
class Cache_info:
36
hits = -1
37
misses = -1
38
39
def deco(fun):
40
fun.cache_info = lambda: Cache_info()
41
return fun
42
return deco
43
functools.lru_cache = lru_cache_dummy
44
45
46
def get_options(cmd_args=None):
47
""" Argument Parser. """
48
parser = sumolib.options.ArgumentParser(
49
prog='generateParkingAreaRerouters.py', usage='%(prog)s [options]',
50
description='Generate parking area rerouters from the parking area definition.')
51
parser.add_argument(
52
'-a', '--parking-areas', type=parser.additional_file, category="input", dest='paFiles', required=True,
53
help='SUMO parkingArea definition.')
54
parser.add_argument(
55
'-n', '--sumo-net', type=parser.net_file, category="input", dest='sumo_net_definition', required=True,
56
help='SUMO network definition.')
57
parser.add_argument(
58
'-b', '--begin', type=float, dest='begin', default=0.0,
59
help='Rerouter interval begin')
60
parser.add_argument(
61
'-e', '--end', type=float, dest='end', default=86400.0,
62
help='Rerouter interval end')
63
parser.add_argument(
64
'--max-number-alternatives', type=int, category="processing", dest='num_alternatives', default=10,
65
help='Rerouter: max number of alternatives.')
66
parser.add_argument(
67
'--max-distance-alternatives', type=float, category="processing", dest='dist_alternatives', default=500.0,
68
help='Rerouter: max distance for the alternatives.')
69
parser.add_argument(
70
'--min-capacity-visibility-true', type=int, category="processing", dest='capacity_threshold', default=25,
71
help='Rerouter: parking capacity for the visibility threshold.')
72
parser.add_argument(
73
'--max-distance-visibility-true', type=float, category="processing", dest='dist_threshold', default=250.0,
74
help='Rerouter: parking distance for the visibility threshold.')
75
parser.add_argument(
76
'--opposite-visible', action="store_true", category="processing", dest='opposite_visible',
77
default=False, help="ParkingArea on the opposite side of the road is always visible")
78
parser.add_argument(
79
'--prefer-visible', action="store_true", category="processing", dest='prefer_visible',
80
default=False, help="ParkingAreas which are visible are preferentially")
81
parser.add_argument(
82
'--min-capacity', type=int, category="processing", dest='min_capacity', default=1,
83
help='Do no reroute to parkingAreas with less than min-capacity')
84
parser.add_argument(
85
'--distribute', dest='distribute', category="processing",
86
help='Distribute alternatives by distance according to the given weights. 3,1 '
87
+ 'means that 75 percent of the alternatives are below the median distance of all'
88
+ 'alternatives in range and 25 percent are above the median distance')
89
parser.add_argument(
90
'--visible-ids', dest='visible_ids', category="processing", default="",
91
help='set list of parkingArea ids as always visible')
92
parser.add_argument(
93
'--processes', type=int, category="processing", dest='processes', default=1,
94
help='Number of processes spawned to compute the distance between parking areas.')
95
parser.add_argument(
96
'-o', '--output', type=parser.additional_file, category="output", dest='output', required=True,
97
help='Name for the output file.')
98
parser.add_argument(
99
'--tqdm', dest='with_tqdm', category="processing", action='store_true',
100
help='Enable TQDM feature.')
101
parser.set_defaults(with_tqdm=False)
102
103
options = parser.parse_args(cmd_args)
104
105
if options.distribute is not None:
106
dists = options.distribute.split(',')
107
for x in dists:
108
try:
109
x = float(x)
110
except ValueError:
111
print("Value '%s' in option --distribute must be numeric" % x,
112
file=sys.stderr)
113
sys.exit()
114
115
options.visible_ids = set(options.visible_ids.split(','))
116
117
return options
118
119
120
def initRTree(all_parkings):
121
try:
122
import rtree # noqa
123
except ImportError:
124
sys.stdout.write("Warning: Module 'rtree' not available. Using slow brute-force search for alternative parkingAreas\n") # noqa
125
return None
126
127
result = None
128
result = rtree.index.Index()
129
result.interleaved = True
130
# build rtree for parkingAreas
131
for index, parking in enumerate(all_parkings.values()):
132
x, y = parking['pos']
133
r = 1
134
bbox = (x - r, y - r, x + r, y + r)
135
result.add(index, bbox)
136
return result
137
138
139
class ReroutersGeneration(object):
140
""" Generate parking area rerouters from the parking area definition. """
141
142
def __init__(self, options):
143
144
self._opt = options
145
self._parking_areas = dict()
146
self._sumo_rerouters = dict()
147
148
print('Loading SUMO network: {}'.format(options.sumo_net_definition))
149
self._sumo_net = sumolib.net.readNet(options.sumo_net_definition, withInternal=True)
150
for pafile in options.paFiles.split(','):
151
print('Loading parking file: {}'.format(pafile))
152
self._load_parking_areas_from_file(pafile)
153
154
self._generate_rerouters()
155
self._save_rerouters()
156
157
def _load_parking_areas_from_file(self, filename):
158
""" Load parkingArea from XML file. """
159
xml_tree = xml.etree.ElementTree.parse(filename).getroot()
160
sequence = None
161
if self._opt.with_tqdm:
162
from tqdm import tqdm
163
sequence = tqdm(xml_tree)
164
else:
165
sequence = xml_tree
166
for child in sequence:
167
if child.tag != "parkingArea":
168
continue
169
self._parking_areas[child.attrib['id']] = child.attrib
170
171
laneID = child.attrib['lane']
172
lane = self._sumo_net.getLane(laneID)
173
174
endPos = lane.getLength()
175
if 'endPos' in child.attrib:
176
endPos = float(child.attrib['endPos'])
177
if endPos < 0:
178
endPos = lane.getLength()
179
else:
180
child.attrib['endPos'] = endPos
181
182
if 'startPos' not in child.attrib:
183
child.attrib['startPos'] = 0
184
185
pa = self._parking_areas[child.attrib['id']]
186
pa['edge'] = lane.getEdge().getID()
187
pa['pos'] = sumolib.geomhelper.positionAtShapeOffset(lane.getShape(), endPos)
188
pa['capacity'] = (int(child.get('roadsideCapacity', 1 if len(child.findall('space')) == 0 else 0)) +
189
len(child.findall('space')))
190
191
# ---------------------------------------------------------------------------------------- #
192
# Rerouter Generation #
193
# ---------------------------------------------------------------------------------------- #
194
195
def _generate_rerouters(self):
196
""" Compute the rerouters for each parking lot for SUMO. """
197
print('Computing distances and sorting parking alternatives.')
198
with multiprocessing.Pool(processes=self._opt.processes) as pool:
199
list_parameters = list()
200
splits = numpy.array_split(list(self._parking_areas.keys()), self._opt.processes)
201
for parkings in splits:
202
parameters = {
203
'selection': parkings,
204
'all_parking_areas': self._parking_areas,
205
'net_file': self._opt.sumo_net_definition,
206
'with_tqdm': self._opt.with_tqdm,
207
'num_alternatives': self._opt.num_alternatives,
208
'dist_alternatives': self._opt.dist_alternatives,
209
'dist_threshold': self._opt.dist_threshold,
210
'capacity_threshold': self._opt.capacity_threshold,
211
'min_capacity': self._opt.min_capacity,
212
'opposite_visible': self._opt.opposite_visible,
213
'prefer_visible': self._opt.prefer_visible,
214
'distribute': self._opt.distribute,
215
'visible_ids': self._opt.visible_ids,
216
}
217
list_parameters.append(parameters)
218
for res in pool.imap_unordered(generate_rerouters_process, list_parameters):
219
for key, value in res.items():
220
self._sumo_rerouters[key] = value
221
print('Computed {} rerouters.'.format(len(self._sumo_rerouters.keys())))
222
223
# ---------------------------------------------------------------------------------------- #
224
# Save SUMO Additionals to File #
225
# ---------------------------------------------------------------------------------------- #
226
227
_REROUTER = """
228
<rerouter id="{rid}" edges="{edges}">
229
<interval begin="{begin}" end="{end}">
230
<!-- in order of distance --> {parkings}
231
</interval>
232
</rerouter>
233
"""
234
235
_RR_PARKING = """
236
<parkingAreaReroute id="{pid}" visible="{visible}"/> <!-- dist: {dist:.1f} -->"""
237
238
def _save_rerouters(self):
239
""" Save the parking lots into a SUMO XML additional file
240
with threshold visibility set to True. """
241
print("Creation of {}".format(self._opt.output))
242
with open(self._opt.output, 'w') as outfile:
243
sumolib.writeXMLHeader(outfile, "$Id$", "additional", options=self._opt) # noqa
244
# remove the randomness introduced by the multiprocessing and allows meaningful diffs
245
ordered_rerouters = sorted(self._sumo_rerouters.keys())
246
for rerouter_id in ordered_rerouters:
247
rerouter = self._sumo_rerouters[rerouter_id]
248
opposite = None
249
if self._opt.opposite_visible:
250
rrEdge = self._sumo_net.getEdge(rerouter['edge'])
251
for e in rrEdge.getToNode().getOutgoing():
252
if e.getToNode() == rrEdge.getFromNode():
253
opposite = e
254
break
255
256
alternatives = ''
257
for alt, dist in rerouter['rerouters']:
258
altEdge = self._sumo_net.getEdge(self._parking_areas[alt]['edge'])
259
if altEdge == opposite:
260
opposite = None
261
_visibility = isVisible(rerouter_id, alt, dist,
262
self._sumo_net,
263
self._parking_areas,
264
self._opt.dist_threshold,
265
self._opt.capacity_threshold,
266
self._opt.opposite_visible,
267
self._opt.visible_ids)
268
_visibility = str(_visibility).lower()
269
alternatives += self._RR_PARKING.format(pid=alt, visible=_visibility, dist=dist)
270
271
edges = [rerouter['edge']]
272
if opposite is not None:
273
# there is no rerouter on the opposite edge but the current
274
# parkingArea should be visible from there
275
edges.append(opposite.getID())
276
277
outfile.write(self._REROUTER.format(
278
rid=rerouter['rid'], edges=' '.join(edges), begin=self._opt.begin, end=self._opt.end,
279
parkings=alternatives))
280
outfile.write("</additional>\n")
281
print("{} created.".format(self._opt.output))
282
283
# ----------------------------------------------------------------------------------------- #
284
285
286
def isVisible(pID, altID, dist, net, parking_areas, dist_threshold,
287
capacity_threshold, opposite_visible, visible_ids):
288
if altID == pID:
289
return True
290
if (int(parking_areas[altID].get('roadsideCapacity', 0)) >= capacity_threshold):
291
return True
292
if dist <= dist_threshold:
293
return True
294
if opposite_visible:
295
rrEdge = net.getEdge(parking_areas[pID]['edge'])
296
altEdge = net.getEdge(parking_areas[altID]['edge'])
297
if rrEdge.getFromNode() == altEdge.getToNode() and rrEdge.getToNode() == altEdge.getFromNode():
298
return True
299
if altID in visible_ids:
300
return True
301
return False
302
303
304
def generate_rerouters_process(parameters):
305
""" Compute the rerouters for the given parking areas."""
306
307
sumo_net = sumolib.net.readNet(parameters['net_file'], withInternal=True)
308
rtree = initRTree(parameters['all_parking_areas'])
309
ret_rerouters = dict()
310
311
@functools.lru_cache(maxsize=None)
312
def _cached_get_shortest_path(from_edge, to_edge, fromPos, toPos):
313
""" Calls and caches sumolib: net.getShortestPath. """
314
return sumo_net.getShortestPath(from_edge, to_edge, fromPos=fromPos, toPos=toPos)
315
316
distances = collections.defaultdict(dict)
317
routes = collections.defaultdict(dict)
318
sequence = None
319
if parameters['with_tqdm']:
320
from tqdm import tqdm
321
sequence = tqdm(parameters['selection'])
322
else:
323
sequence = parameters['selection']
324
325
distWeights = None
326
distWeightSum = None
327
distThresholds = None
328
if parameters['distribute'] is not None:
329
distWeights = list(map(float, parameters['distribute'].split(',')))
330
distWeightSum = sum(distWeights)
331
332
for parking_id in sequence:
333
parking_a = parameters['all_parking_areas'][parking_id]
334
from_edge = sumo_net.getEdge(parking_a['edge'])
335
fromPos = float(parking_a['endPos'])
336
candidates = parameters['all_parking_areas'].values()
337
if rtree is not None:
338
allParkings = list(candidates)
339
candidates = []
340
x, y = parking_a['pos']
341
r = parameters['dist_alternatives']
342
for i in rtree.intersection((x - r, y - r, x + r, y + r)):
343
candidates.append(allParkings[i])
344
345
for parking_b in candidates:
346
if parking_a['id'] == parking_b['id']:
347
continue
348
toPos = float(parking_b['startPos'])
349
route, cost = _cached_get_shortest_path(from_edge,
350
sumo_net.getEdge(parking_b['edge']),
351
fromPos, toPos)
352
if route:
353
distances[parking_a['id']][parking_b['id']] = cost
354
routes[parking_a['id']][parking_b['id']] = route
355
356
cache_info = _cached_get_shortest_path.cache_info()
357
total = float(cache_info.hits + cache_info.misses)
358
perc = cache_info.hits * 100.0
359
if total:
360
perc /= float(cache_info.hits + cache_info.misses)
361
print('Cache: hits {}, misses {}, used {}%.'.format(
362
cache_info.hits, cache_info.misses, perc))
363
364
# select closest parking areas
365
sequence = None
366
if parameters['with_tqdm']:
367
from tqdm import tqdm
368
sequence = tqdm(distances.items())
369
else:
370
sequence = distances.items()
371
372
for pid, dists in sequence:
373
list_of_dist = [tuple(reversed(kv)) for kv in dists.items() if kv[1] is not None]
374
list_of_dist = sorted(list_of_dist)
375
temp_rerouters = [(pid, 0.0)]
376
377
numAlternatives = min(len(list_of_dist), parameters['num_alternatives'])
378
379
used = set()
380
if parameters['prefer_visible']:
381
for distance, parking in list_of_dist:
382
if parameters['all_parking_areas'][parking].get('capacity') < parameters['min_capacity']:
383
continue
384
if len(temp_rerouters) > parameters['num_alternatives']:
385
break
386
if isVisible(pid, parking, distance, sumo_net,
387
parameters['all_parking_areas'],
388
parameters['dist_threshold'],
389
parameters['capacity_threshold'],
390
parameters['opposite_visible'],
391
parameters['visible_ids']):
392
temp_rerouters.append((parking, distance))
393
used.add(parking)
394
395
dist = None
396
distThresholds = None
397
if distWeights is not None:
398
dist = [int(x / distWeightSum * numAlternatives) for x in distWeights]
399
distThresholdIndex = [int(i * len(list_of_dist) / len(dist)) for i in range(len(dist))]
400
distThresholds = [list_of_dist[i][0] for i in distThresholdIndex]
401
# print("distWeights=%s" % distWeights)
402
# print("dist=%s" % dist)
403
# print("distances=%s" % [x[0] for x in list_of_dist])
404
# print("distThresholdIndex=%s" % distThresholdIndex)
405
# print("distThresholds=%s" % distThresholds)
406
407
distIndex = 0
408
found = 0
409
required = dist[distIndex] if dist else None
410
for distance, parking in list_of_dist:
411
route = routes[pid][parking]
412
if parking in used:
413
found += 1
414
continue
415
416
if parameters['all_parking_areas'][parking].get('capacity') < parameters['min_capacity']:
417
continue
418
# optionally enforce distance distribution
419
# if dist is not None:
420
# print("found=%s required=%s distIndex=%s threshold=%s" % (found, required,
421
# distIndex, distThresholds[distIndex]))
422
423
if dist is not None and found >= required:
424
if distIndex + 1 < len(dist):
425
distIndex += 1
426
required += dist[distIndex]
427
if distThresholds is not None and distance < distThresholds[distIndex]:
428
continue
429
430
if len(temp_rerouters) > parameters['num_alternatives']:
431
break
432
if distance > parameters['dist_alternatives']:
433
break
434
found += 1
435
temp_rerouters.append((parking, distance))
436
437
if not list_of_dist:
438
print('Parking {} has 0 neighbours!'.format(pid))
439
440
ret_rerouters[pid] = {
441
'rid': pid,
442
'edge': parameters['all_parking_areas'][pid]['edge'],
443
'rerouters': temp_rerouters,
444
}
445
return ret_rerouters
446
447
448
def main(cmd_args):
449
""" Generate parking area rerouters from the parking area definition. """
450
args = get_options(cmd_args)
451
ReroutersGeneration(args)
452
print('Done.')
453
454
455
if __name__ == "__main__":
456
# see default start method 'fork' is unsafe and raises DeprecationWarning starting in Pythoon 3.12
457
multiprocessing.set_start_method('spawn')
458
main(sys.argv[1:])
459
460