Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/output/attributeDiff.py
169673 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2014-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 attributeDiff.py
15
# @author Jakob Erdmann
16
# @date 2022-12-20
17
18
"""
19
compute difference between attributes in two files
20
"""
21
from __future__ import absolute_import
22
from __future__ import print_function
23
24
import os
25
import sys
26
from collections import defaultdict
27
from lxml import etree as ET
28
29
if 'SUMO_HOME' in os.environ:
30
tools = os.path.join(os.environ['SUMO_HOME'], 'tools')
31
sys.path.append(os.path.join(tools))
32
import sumolib
33
else:
34
sys.exit("please declare environment variable 'SUMO_HOME'")
35
36
37
def get_options():
38
optParser = sumolib.options.ArgumentParser()
39
optParser.add_option("old", help="the first xml file")
40
optParser.add_option("new", help="the second xml file")
41
optParser.add_option("-v", "--verbose", action="store_true", default=False,
42
help="Give more output")
43
optParser.add_option("-e", "--element", help="element to analyze")
44
optParser.add_option("-a", "--attribute", help="attribute to analyze")
45
optParser.add_option("-i", "--id-attribute", dest="idAttribute",
46
help="extra attribute to distinguish values")
47
optParser.add_option("-o", "--xml-output", help="write differences to xml file")
48
optParser.add_option("-m", "--only-first-output", dest="onlyFirst",
49
help="write statistic on elements that occur only in the first input file to the given FILE")
50
optParser.add_option("-M", "--only-second-output", dest="onlySecond",
51
help="write statistic on elements that occur only in the second input file to the given FILE")
52
optParser.add_option("-p", "--precision", type=int, default=2,
53
help="Set output precision")
54
options = optParser.parse_args()
55
56
if options.attribute:
57
options.attribute = options.attribute.split(',')
58
if options.idAttribute:
59
options.idAttribute = options.idAttribute.split(',')
60
if options.element:
61
options.element = options.element.split(',')
62
63
return options
64
65
66
def write(fname, values, root):
67
with open(fname, 'w') as f:
68
sumolib.writeXMLHeader(f, root=root)
69
for elem_id in sorted(values.keys()):
70
parts = elem_id.split('|')
71
elem = parts[0]
72
f.write(' <%s' % elem)
73
if len(parts) > 1:
74
f.write(' id="%s"' % '|'.join(parts[1:]))
75
for attr, d in sorted(values[elem_id].items()):
76
f.write(' %s="%s"' % (attr, d))
77
f.write('/>\n')
78
f.write('</%s>\n' % root)
79
80
81
def main():
82
options = get_options()
83
84
oldValues = defaultdict(dict) # elem->attr->value
85
differences = defaultdict(dict) # elem->attr->difference
86
missingAttr = defaultdict(set)
87
missingAttr2 = defaultdict(set)
88
onlySecondValues = defaultdict(dict) # elem->attr->value
89
invalidType = defaultdict(set)
90
91
def elements(fname):
92
stack = []
93
idStack = []
94
with sumolib.openz(fname, 'rb') as f:
95
for event, node in ET.iterparse(f, events=('start', 'end')):
96
if options.element is not None and node.tag not in options.element:
97
continue
98
if event == 'start':
99
stack.append(node.tag)
100
if options.idAttribute:
101
idStack.append([])
102
for attr in options.idAttribute:
103
if node.get(attr) is not None:
104
idStack[-1].append(node.get(attr))
105
106
else:
107
stack.pop()
108
if options.idAttribute:
109
idStack.pop()
110
continue
111
112
tags = tuple(stack[1:]) if options.element is None else tuple(stack) # exclude root
113
elementDescription = '.'.join(tags)
114
if options.idAttribute:
115
for ids in idStack:
116
if ids:
117
elementDescription += '|' + '|'.join(ids)
118
119
if options.attribute is None:
120
for k, v in node.items():
121
yield elementDescription, k, v
122
else:
123
for attr in options.attribute:
124
if node.get(attr) is not None or options.element is not None:
125
yield elementDescription, attr, node.get(attr)
126
127
# parse old
128
for tag, attr, stringVal in elements(options.old):
129
if stringVal is not None:
130
try:
131
if '_' in stringVal:
132
# float() accepts '_' but this doesn't play nice with lane ids
133
raise Exception
134
val = sumolib.miscutils.parseTime(stringVal)
135
oldValues[tag][attr] = val
136
except Exception:
137
invalidType[attr].add(stringVal)
138
else:
139
missingAttr[attr].add(tag)
140
141
# parse new and compute difference
142
for tag, attr, stringVal in elements(options.new):
143
if stringVal is not None:
144
try:
145
if '_' in stringVal:
146
# float() accepts '_' but this doesn't play nice with lane ids
147
raise Exception
148
val = sumolib.miscutils.parseTime(stringVal)
149
if tag in oldValues and attr in oldValues[tag]:
150
differences[tag][attr] = val - oldValues[tag][attr]
151
else:
152
missingAttr2[attr].add(tag)
153
onlySecondValues[tag][attr] = val
154
except Exception:
155
invalidType[attr].add(stringVal)
156
else:
157
missingAttr2[attr].add(tag)
158
159
if options.verbose or options.xml_output is None:
160
for tag in sorted(differences.keys()):
161
print("%s: %s" % (tag, ' '.join(["%s=%s" % av for av in sorted(differences[tag].items())])))
162
163
if missingAttr:
164
for attr in sorted(missingAttr.keys()):
165
if options.verbose:
166
print("In file %s, Elements %s did not provide attribute '%s'" % (
167
options.old, ','.join(sorted(missingAttr[attr])), attr))
168
169
if missingAttr2:
170
for attr in sorted(missingAttr2.keys()):
171
if options.verbose:
172
print("In file %s, Elements %s did not provide attribute '%s'" % (
173
options.new, ','.join(sorted(missingAttr2[attr])), attr))
174
175
if invalidType and options.attribute is not None:
176
for attr in sorted(invalidType.keys()):
177
sys.stderr.write(("%s distinct values of attribute '%s' could not be interpreted " +
178
"as numerical value or time. Example values: '%s'\n") %
179
(len(invalidType[attr]), attr, "', '".join(sorted(invalidType[attr])[:10])))
180
181
if options.xml_output is not None:
182
write(options.xml_output, differences, "attributeDiff")
183
184
if options.onlySecond is not None:
185
write(options.onlySecond, onlySecondValues, "attributeDiff-onlySecond")
186
187
if options.onlyFirst is not None:
188
onlyFirstValues = defaultdict(dict) # elem->attr->value
189
for elem_id in oldValues.keys():
190
for attr, oldV in oldValues[elem_id].items():
191
if elem_id not in differences or attr not in differences[elem_id]:
192
onlyFirstValues[elem_id][attr] = oldV
193
write(options.onlyFirst, onlyFirstValues, "attributeDiff-onlyFirst")
194
195
196
if __name__ == "__main__":
197
main()
198
199