Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/net/remap_network.py
169673 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2009-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 remap_network.py
15
# @author Jakob Erdmann
16
# @date 2025-03-26
17
18
from __future__ import print_function
19
from __future__ import absolute_import
20
import os
21
import sys
22
23
if 'SUMO_HOME' in os.environ:
24
sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))
25
import sumolib # noqa
26
from sumolib.geomhelper import distance # noqa
27
from sumolib.xml import parse # noqa
28
from sumolib.net import lane2edge # noqa
29
from sumolib.statistics import Statistics # noqa
30
import sumolib.geomhelper as gh # noqa
31
32
SLACK = 1
33
34
35
def get_options(args=None):
36
desc = "Identify which edges in the target network correspond to edges in the original network"
37
ap = sumolib.options.ArgumentParser(description=desc)
38
ap.add_argument("--orig-net", dest="origNet", required=True, category="input", type=ap.net_file,
39
help="SUMO network with edges of interest", metavar="FILE")
40
ap.add_argument("--target-net", dest="targetNet", required=True, category="input", type=ap.net_file, metavar="FILE",
41
help="SUMO network with edge ids that should be used to represent the original edges")
42
ap.add_argument("-o", "--output-file", dest="output", required=True, category="output", type=ap.file,
43
help="File for writing mapping information", metavar="FILE")
44
ap.add_argument("-s", "--success-output", dest="successOutput", category="output", type=ap.file,
45
help="File for writing mapping success information", metavar="FILE")
46
ap.add_argument("--radius", type=float, default=1.6,
47
help="radius for finding candidate edges")
48
ap.add_argument("--min-common", type=float, dest="minCommon", default=0.1,
49
help="minimum common length in meters")
50
ap.add_argument("--filter-ids", dest="filterIds",
51
help="only handle the given edges")
52
ap.add_argument("-v", "--verbose", action="store_true", dest="verbose",
53
default=False, help="tell me what you are doing")
54
options = ap.parse_args()
55
if options.filterIds:
56
options.filterIds = set(options.filterIds.split(','))
57
if not options.successOutput:
58
options.successOutput = options.output + ".success"
59
return options
60
61
62
def compareEdge(edge, shape, edge2, radius):
63
shape2 = edge2.getShape()
64
off_dists = [(point,) + gh.polygonOffsetAndDistanceToPoint(point, shape2) for point in shape]
65
lastOffset = None
66
lastPoint = None
67
commonLength = 0
68
distSum = 0
69
distCount = 0
70
for point, offset, dist in off_dists:
71
if (lastOffset is None or offset >= lastOffset) and dist <= radius:
72
if lastOffset is not None:
73
commonLength += gh.distance(lastPoint, point)
74
lastOffset = offset
75
lastPoint = point
76
distSum += dist
77
distCount += 1
78
else:
79
firstOffset = off_dists[0][1]
80
offset2, dist2 = gh.polygonOffsetAndDistanceToPoint(shape2[-1], shape, True)
81
if lastOffset is not None and firstOffset + offset2 >= lastOffset and dist2 <= radius:
82
# edge2 ends before reaching the end of shape
83
commonLength = offset2
84
distSum += dist2
85
distCount += 1
86
break
87
88
permissionMatch = 1
89
for vClass in ["passenger", "bus", "taxi", "tram", "rail", "bicycle", "pedestrian"]:
90
if edge.allows(vClass) and not edge2.allows(vClass):
91
permissionMatch *= 0.9
92
quality = (radius - distSum / distCount) / radius if distCount > 0 else 0
93
laneMatch = 1 / (1 + abs(edge.getLaneNumber() - edge2.getLaneNumber()))
94
95
score = commonLength * quality * permissionMatch * laneMatch
96
return score, commonLength
97
98
99
def cutOff(shape, commonLength):
100
shapelen = gh.polyLength(shape)
101
if commonLength + SLACK > shapelen:
102
return []
103
else:
104
index, seen = gh.indexAtShapeOffset(shape, commonLength)
105
start = gh.positionAtShapeOffset(shape, commonLength)
106
return [start] + shape[index + 1:]
107
108
109
def mapEdge(options, edge):
110
success = 0
111
results = [] # [(targetEdge, targetFraction, commonLength), ...]
112
origShape = edge.getShape()
113
shape = [options.remap_xy(xy) for xy in origShape]
114
shapelen = gh.polyLength(shape)
115
116
usedEdges = set()
117
while shape:
118
x2, y2 = shape[0]
119
edges2 = options.net2.getNeighboringEdges(x2, y2, options.radius)
120
best = None
121
bestCommon = 0
122
bestScore = 0
123
for edge2, _ in edges2:
124
if edge2 in usedEdges:
125
continue
126
score, commonLength = compareEdge(edge, shape, edge2, options.radius)
127
if score > bestScore:
128
best = edge2
129
bestCommon = commonLength
130
bestScore = score
131
if bestCommon < options.minCommon:
132
break
133
bestLength = gh.polyLength(best.getShape())
134
fraction = min(1.0, bestCommon / bestLength)
135
results.append((best, fraction, bestCommon))
136
cutFraction = bestCommon / shapelen
137
shape = cutOff(shape, bestCommon)
138
success += cutFraction
139
usedEdges.add(best)
140
# print(edge.getID(), best.getID(), commonLength, len(edges2))
141
if options.verbose:
142
print(edge.getID(), success)
143
if success >= 1.01:
144
print("implausibly high success %s for edge %s" % (success, edge.getID()), file=sys.stderr)
145
success = round(success, 2)
146
success = min(success, 1)
147
return success, results
148
149
150
def main(options):
151
if options.verbose:
152
print("Reading orig-net '%s'" % options.origNet)
153
options.net = sumolib.net.readNet(options.origNet)
154
if options.verbose:
155
print("Reading target-net '%s'" % options.targetNet)
156
options.net2 = sumolib.net.readNet(options.targetNet)
157
158
if options.net.hasGeoProj() and options.net2.hasGeoProj():
159
def remap_xy(xy):
160
lon, lat = options.net.convertXY2LonLat(*xy)
161
return options.net2.convertLonLat2XY(lon, lat)
162
options.remap_xy = remap_xy
163
else:
164
options.remap_xy = lambda x: x
165
166
successStats = Statistics("completeness")
167
with open(options.output, 'w') as fout, open(options.successOutput, 'w') as sout:
168
fout.write(';'.join(["origEdge", "targetEdge", "targetFrom", "targetTo", "targetFraction", "common"]) + '\n')
169
sout.write(';'.join(["origEdge", "success"]) + '\n')
170
for edge in options.net.getEdges():
171
if options.filterIds and edge.getID() not in options.filterIds:
172
continue
173
success, targets = mapEdge(options, edge)
174
sout.write(';'.join([edge.getID(), "%.2f" % success]) + '\n')
175
successStats.add(success, edge.getID())
176
for edge2, fraction, common in targets:
177
fout.write(';'.join([
178
edge.getID(), edge2.getID(),
179
edge2.getFromNode().getID(), edge2.getToNode().getID(),
180
"%.2f" % fraction, "%.2f" % common]) + '\n')
181
print(successStats)
182
183
184
if __name__ == "__main__":
185
main(get_options())
186
187