sumolib.miscutils

  1# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
  2# Copyright (C) 2012-2026 German Aerospace Center (DLR) and others.
  3# This program and the accompanying materials are made available under the
  4# terms of the Eclipse Public License 2.0 which is available at
  5# https://www.eclipse.org/legal/epl-2.0/
  6# This Source Code may also be made available under the following Secondary
  7# Licenses when the conditions for such availability set forth in the Eclipse
  8# Public License 2.0 are satisfied: GNU General Public License, version 2
  9# or later which is available at
 10# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
 11# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
 12
 13# @file    miscutils.py
 14# @author  Jakob Erdmann
 15# @author  Michael Behrisch
 16# @author  Mirko Barthauer
 17# @date    2012-05-08
 18
 19from __future__ import absolute_import
 20from __future__ import print_function
 21from __future__ import division
 22import sys
 23import time
 24import os
 25import math
 26import colorsys
 27import socket
 28import random
 29import gzip
 30import codecs
 31import io
 32from pathlib import Path
 33from types import ModuleType, FunctionType
 34from gc import get_referents
 35try:
 36    from urllib.request import urlopen
 37except ImportError:
 38    from urllib import urlopen
 39# needed for backward compatibility
 40from .statistics import Statistics, geh, uMax, uMin, round  # noqa
 41
 42PRACTICAL_INFINITY = 1e400
 43_BLACKLIST = type, ModuleType, FunctionType
 44
 45
 46def get_size(obj):
 47    """sum size of object & members.
 48    lifted from https://stackoverflow.com/a/30316760
 49    """
 50    if isinstance(obj, (_BLACKLIST)):
 51        raise TypeError('getsize() does not take argument of type: ' + str(type(obj)))
 52    seen_ids = set()
 53    size = 0
 54    objects = [obj]
 55    while objects:
 56        need_referents = []
 57        for obj in objects:
 58            if not isinstance(obj, _BLACKLIST) and id(obj) not in seen_ids:
 59                seen_ids.add(id(obj))
 60                size += sys.getsizeof(obj)
 61                need_referents.append(obj)
 62        objects = get_referents(*need_referents)
 63    return size
 64
 65
 66def benchmark(func):
 67    """
 68    decorator for timing a function
 69    """
 70    def benchmark_wrapper(*args, **kwargs):
 71        started = time.time()
 72        now = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime())
 73        print('function %s called at %s' % (func.__name__, now))
 74        sys.stdout.flush()
 75        result = func(*args, **kwargs)
 76        print('function %s finished after %f seconds' %
 77              (func.__name__, time.time() - started))
 78        sys.stdout.flush()
 79        return result
 80    return benchmark_wrapper
 81
 82
 83class Benchmarker:
 84    """
 85    class for benchmarking a function using a "with"-statement.
 86    Preferable over the "benchmark" function for the following use cases
 87    - benchmarking a code block that isn't wrapped in a function
 88    - benchmarking a function only in some calls
 89    """
 90
 91    def __init__(self, active, description):
 92        self.active = active
 93        self.description = description
 94
 95    def __enter__(self):
 96        self.started = time.time()
 97
 98    def __exit__(self, *args):
 99        if self.active:
100            duration = time.time() - self.started
101            print("%s finished after %s" % (self.description, humanReadableTime(duration)))
102
103
104class working_dir:
105    """
106    temporarily change working directory using 'with' statement
107    """
108
109    def __init__(self, dir):
110        self.dir = dir
111        self.origdir = os.getcwd()
112
113    def __enter__(self):
114        os.chdir(self.dir)
115
116    def __exit__(self, type, value, traceback):
117        os.chdir(self.origdir)
118
119
120class Colorgen:
121    DISTINCT = [
122        (0.17, 1.0, 0.5),
123        (0.0, 0.9, 1.0),
124        (0.35, 0.67, 0.71),
125        (0.14, 0.9, 1.0),
126        (0.56, 1.0, 0.78),
127        (0.07, 0.8, 0.96),
128        (0.79, 0.83, 0.71),
129        (0.5, 0.71, 0.94),
130        (0.84, 0.79, 0.94),
131        (0.2, 0.76, 0.96),
132        (0.0, 0.24, 0.98),
133        (0.5, 1.0, 0.5),
134        (0.77, 0.25, 1.0),
135        (0.09, 0.76, 0.67),
136        (0.15, 0.22, 1.0),
137        (0.0, 1.0, 0.5),
138        (0.38, 0.33, 1.0),
139        (0.67, 1.0, 0.5),
140    ]
141
142    def __init__(self, hsv, cycleLength=10.67):
143        self.hsv = hsv
144        self.cycle = [int(random.random() * 256) for x in self.hsv]
145        self.cycleOffset = int(round(256 / cycleLength))
146        self.distinctIndex = 0
147
148    def get_value(self, opt, index):
149        if opt == 'random':
150            return random.random()
151        if opt == 'cycle':
152            # the 255 below is intentional to get all color values when cycling long enough
153            self.cycle[index] = (self.cycle[index] + self.cycleOffset) % 255
154            return self.cycle[index] / 255.0
155        if opt == 'distinct':
156            if index == 0:
157                self.distinctIndex = (self.distinctIndex + 1) % len(self.DISTINCT)
158            return self.DISTINCT[self.distinctIndex][index]
159        return float(opt)
160
161    def floatTuple(self):
162        """return color as a tuple of floats each in [0,1]"""
163        return colorsys.hsv_to_rgb(*[self.get_value(o, i) for i, o in enumerate(self.hsv)])
164
165    def byteTuple(self):
166        """return color as a tuple of bytes each in [0,255]"""
167        return tuple([int(round(255 * x)) for x in self.floatTuple()])
168
169    def __call__(self):
170        """return constant or randomized rgb-color string"""
171        return ','.join(map(str, self.byteTuple()))
172
173
174class priorityDictionary(dict):
175
176    def __init__(self):
177        '''Initialize priorityDictionary by creating binary heap
178            of pairs (value,key).  Note that changing or removing a dict entry will
179            not remove the old pair from the heap until it is found by smallest() or
180            until the heap is rebuilt.'''
181        self.__heap = []
182        dict.__init__(self)
183
184    def smallest(self):
185        '''Find smallest item after removing deleted items from heap.'''
186        if len(self) == 0:
187            raise IndexError("smallest of empty priorityDictionary")
188        heap = self.__heap
189        while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
190            lastItem = heap.pop()
191            insertionPoint = 0
192            while 1:
193                smallChild = 2 * insertionPoint + 1
194                if smallChild + 1 < len(heap) and \
195                        heap[smallChild][0] > heap[smallChild + 1][0]:
196                    smallChild += 1
197                if smallChild >= len(heap) or lastItem <= heap[smallChild]:
198                    heap[insertionPoint] = lastItem
199                    break
200                heap[insertionPoint] = heap[smallChild]
201                insertionPoint = smallChild
202        return heap[0][1]
203
204    def __iter__(self):
205        '''Create destructive sorted iterator of priorityDictionary.'''
206        def iterfn():
207            while len(self) > 0:
208                x = self.smallest()
209                yield x
210                del self[x]
211        return iterfn()
212
213    def __setitem__(self, key, val):
214        '''Change value stored in dictionary and add corresponding
215            pair to heap.  Rebuilds the heap if the number of deleted items grows
216            too large, to avoid memory leakage.'''
217        dict.__setitem__(self, key, val)
218        heap = self.__heap
219        if len(heap) > 2 * len(self):
220            self.__heap = [(v, k) for k, v in self.items()]
221            self.__heap.sort()  # builtin sort likely faster than O(n) heapify
222        else:
223            newPair = (val, key)
224            insertionPoint = len(heap)
225            heap.append(None)
226            while insertionPoint > 0 and val < heap[(insertionPoint - 1) // 2][0]:
227                heap[insertionPoint] = heap[(insertionPoint - 1) // 2]
228                insertionPoint = (insertionPoint - 1) // 2
229            heap[insertionPoint] = newPair
230
231    def setdefault(self, key, val):
232        '''Reimplement setdefault to call our customized __setitem__.'''
233        if key not in self:
234            self[key] = val
235        return self[key]
236
237    def update(self, other):
238        for key in other.keys():
239            self[key] = other[key]
240
241
242def getFreeSocketPort(numTries=10):
243    for _ in range(numTries):
244        try:
245            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
246            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
247            s.bind(('', 0))
248            p = s.getsockname()[1]
249            s.close()
250            return p
251        except socket.error:
252            pass
253    return None
254
255
256def getSocketStream(port, mode='rb'):
257    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
258    s.bind(("localhost", port))
259    s.listen(1)
260    conn, _ = s.accept()
261    return conn.makefile(mode)
262
263
264# euclidean distance between two coordinates in the plane
265def euclidean(a, b):
266    return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
267
268
269def humanReadableTime(seconds):
270    result = ""
271    sign = '-' if seconds < 0 else ''
272    seconds = abs(seconds)
273    ds = 3600 * 24
274    if seconds > ds:
275        result = "%s:" % int(seconds / ds)
276        seconds = seconds % ds
277    result += "%02i:" % int(seconds / 3600)
278    seconds = seconds % 3600
279    result += "%02i:" % int(seconds / 60)
280    seconds = seconds % 60
281    if seconds == int(seconds):
282        seconds = int(seconds)
283    result += "%02i" % seconds
284    return sign + result
285
286
287SPECIAL_TIME_STRINGS = ["triggered", "containerTriggered", "split", "begin"]
288
289
290def parseTime(t, factor=1):
291    try:
292        return float(t) * factor
293    except ValueError:
294        pass
295    try:
296        # prepended zero is ignored if the date value already contains days
297        days, hours, minutes, seconds = ([0] + list(map(float, t.split(':'))))[-4:]
298        sign = -1 if t.strip()[0] == '-' else 1
299        return (3600 * 24 * days + 3600 * hours + 60 * minutes + seconds) * sign * factor
300    except ValueError:
301        if t in SPECIAL_TIME_STRINGS:
302            # signal special case but don't crash
303            return None
304        else:
305            raise
306
307
308def parseBool(val):
309    # see data/xsd/baseTypes:boolType
310    return val in ["true", "True", "x", "1", "yes", "on"]
311
312
313def getFlowNumber(flow):
314    """interpret number of vehicles from a flow parsed by sumolib.xml.parse"""
315    if flow.number is not None:
316        return int(flow.number)
317    if flow.end is not None:
318        duration = parseTime(flow.end) - parseTime(flow.begin)
319        period = 0
320        isFractional = False
321        if flow.period is not None:
322            if 'exp' in flow.period:
323                # use expected value
324                period = 1 / float(flow.period[4:-1])
325                isFractional = True
326            else:
327                period = float(flow.period)
328        elif flow.probability is not None:
329            # use expected value
330            period = 1 / float(flow.probability)
331            isFractional = True
332        for attr in ['perHour', 'vehsPerHour']:
333            if flow.hasAttribute(attr):
334                period = 3600 / float(flow.getAttribute(attr))
335        if period > 0:
336            count = duration / period
337            if isFractional:
338                return count
339            else:
340                # flows with a regular period always start at least one vehicle at the begin time
341                return math.ceil(count)
342        else:
343            return 1
344
345
346def intIfPossible(val):
347    if int(val) == val:
348        return int(val)
349    else:
350        return val
351
352
353def openz(fileOrURL, mode="r", **kwargs):
354    """
355    Opens transparently files, URLs and gzipped files for reading and writing.
356    Special file names "stdout" and "stderr" are handled as well.
357    Also enforces UTF8 on text output / input and should handle BOMs in input.
358    Should be compatible with python 2 and 3.
359    """
360    encoding = kwargs.get("encoding", "utf8" if "w" in mode else "utf-8-sig")
361    try:
362        if fileOrURL.startswith("http://") or fileOrURL.startswith("https://"):
363            return io.BytesIO(urlopen(fileOrURL).read())
364        if fileOrURL == "stdout":
365            return sys.stdout
366        if fileOrURL == "stderr":
367            return sys.stderr
368        if fileOrURL.endswith(".gz") and "w" in mode:
369            if "b" in mode:
370                return gzip.open(fileOrURL, mode="w")
371            return gzip.open(fileOrURL, mode="wt", encoding=encoding)
372        if kwargs.get("trySocket") and fileOrURL.isdigit():
373            return getSocketStream(int(fileOrURL), mode)
374        if kwargs.get("tryGZip", True) and "r" in mode:
375            with gzip.open(fileOrURL) as fd:
376                fd.read(1)
377            if "b" in mode:
378                return gzip.open(fileOrURL)
379            if sys.version_info[0] < 3:
380                return codecs.getreader('utf-8')(gzip.open(fileOrURL))
381            return gzip.open(fileOrURL, mode="rt", encoding=encoding)
382    except OSError as e:
383        if kwargs.get("printErrors"):
384            print(e, file=sys.stderr)
385    except IOError as e:
386        if kwargs.get("printErrors"):
387            print(e, file=sys.stderr)
388    if "b" in mode:
389        return io.open(fileOrURL, mode=mode)
390    return io.open(fileOrURL, mode=mode, encoding=encoding)
391
392
393def short_names(filenames, noEmpty):
394    if len(filenames) == 1:
395        return filenames
396    reversedNames = [''.join(reversed(f)) for f in filenames]
397    prefix = os.path.commonprefix(filenames)
398    suffix = os.path.commonprefix(reversedNames)
399    prefixLen = len(prefix)
400    suffixLen = len(suffix)
401    shortened = [f[prefixLen:-suffixLen] for f in filenames]
402    if noEmpty and any([not f for f in shortened]):
403        # make longer to avoid empty file names
404        base = os.path.basename(prefix)
405        shortened = [base + f for f in shortened]
406    return shortened
407
408
409def getBaseName(filename):
410    """strip extensions such as .net.xml.gz"""
411    if filename[-11:] == ".net.xml.gz" and len(filename) > 11:
412        return filename[:-11]
413    elif filename[-8:] == ".net.xml" and len(filename) > 8:
414        return filename[:-8]
415    elif filename[-7:] == ".xml.gz" and len(filename) > 7:
416        return filename[:-7]
417    elif filename[-4:] == ".xml" and len(filename) > 4:
418        return filename[:-4]
419    else:
420        return filename
421
422def flattenPath(filename, sep='_'):
423    """create a filename that encodes the original directory structure"""
424    parts = Path(filename).parts
425    clean = [p for p in parts if p not in ('/', '\\') and ':' not in p]
426    return sep.join(clean)
PRACTICAL_INFINITY = inf
def get_size(obj):
47def get_size(obj):
48    """sum size of object & members.
49    lifted from https://stackoverflow.com/a/30316760
50    """
51    if isinstance(obj, (_BLACKLIST)):
52        raise TypeError('getsize() does not take argument of type: ' + str(type(obj)))
53    seen_ids = set()
54    size = 0
55    objects = [obj]
56    while objects:
57        need_referents = []
58        for obj in objects:
59            if not isinstance(obj, _BLACKLIST) and id(obj) not in seen_ids:
60                seen_ids.add(id(obj))
61                size += sys.getsizeof(obj)
62                need_referents.append(obj)
63        objects = get_referents(*need_referents)
64    return size

sum size of object & members. lifted from https://stackoverflow.com/a/30316760

def benchmark(func):
67def benchmark(func):
68    """
69    decorator for timing a function
70    """
71    def benchmark_wrapper(*args, **kwargs):
72        started = time.time()
73        now = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime())
74        print('function %s called at %s' % (func.__name__, now))
75        sys.stdout.flush()
76        result = func(*args, **kwargs)
77        print('function %s finished after %f seconds' %
78              (func.__name__, time.time() - started))
79        sys.stdout.flush()
80        return result
81    return benchmark_wrapper

decorator for timing a function

class Benchmarker:
 84class Benchmarker:
 85    """
 86    class for benchmarking a function using a "with"-statement.
 87    Preferable over the "benchmark" function for the following use cases
 88    - benchmarking a code block that isn't wrapped in a function
 89    - benchmarking a function only in some calls
 90    """
 91
 92    def __init__(self, active, description):
 93        self.active = active
 94        self.description = description
 95
 96    def __enter__(self):
 97        self.started = time.time()
 98
 99    def __exit__(self, *args):
100        if self.active:
101            duration = time.time() - self.started
102            print("%s finished after %s" % (self.description, humanReadableTime(duration)))

class for benchmarking a function using a "with"-statement. Preferable over the "benchmark" function for the following use cases

  • benchmarking a code block that isn't wrapped in a function
  • benchmarking a function only in some calls
Benchmarker(active, description)
92    def __init__(self, active, description):
93        self.active = active
94        self.description = description
active
description
class working_dir:
105class working_dir:
106    """
107    temporarily change working directory using 'with' statement
108    """
109
110    def __init__(self, dir):
111        self.dir = dir
112        self.origdir = os.getcwd()
113
114    def __enter__(self):
115        os.chdir(self.dir)
116
117    def __exit__(self, type, value, traceback):
118        os.chdir(self.origdir)

temporarily change working directory using 'with' statement

working_dir(dir)
110    def __init__(self, dir):
111        self.dir = dir
112        self.origdir = os.getcwd()
dir
origdir
class Colorgen:
121class Colorgen:
122    DISTINCT = [
123        (0.17, 1.0, 0.5),
124        (0.0, 0.9, 1.0),
125        (0.35, 0.67, 0.71),
126        (0.14, 0.9, 1.0),
127        (0.56, 1.0, 0.78),
128        (0.07, 0.8, 0.96),
129        (0.79, 0.83, 0.71),
130        (0.5, 0.71, 0.94),
131        (0.84, 0.79, 0.94),
132        (0.2, 0.76, 0.96),
133        (0.0, 0.24, 0.98),
134        (0.5, 1.0, 0.5),
135        (0.77, 0.25, 1.0),
136        (0.09, 0.76, 0.67),
137        (0.15, 0.22, 1.0),
138        (0.0, 1.0, 0.5),
139        (0.38, 0.33, 1.0),
140        (0.67, 1.0, 0.5),
141    ]
142
143    def __init__(self, hsv, cycleLength=10.67):
144        self.hsv = hsv
145        self.cycle = [int(random.random() * 256) for x in self.hsv]
146        self.cycleOffset = int(round(256 / cycleLength))
147        self.distinctIndex = 0
148
149    def get_value(self, opt, index):
150        if opt == 'random':
151            return random.random()
152        if opt == 'cycle':
153            # the 255 below is intentional to get all color values when cycling long enough
154            self.cycle[index] = (self.cycle[index] + self.cycleOffset) % 255
155            return self.cycle[index] / 255.0
156        if opt == 'distinct':
157            if index == 0:
158                self.distinctIndex = (self.distinctIndex + 1) % len(self.DISTINCT)
159            return self.DISTINCT[self.distinctIndex][index]
160        return float(opt)
161
162    def floatTuple(self):
163        """return color as a tuple of floats each in [0,1]"""
164        return colorsys.hsv_to_rgb(*[self.get_value(o, i) for i, o in enumerate(self.hsv)])
165
166    def byteTuple(self):
167        """return color as a tuple of bytes each in [0,255]"""
168        return tuple([int(round(255 * x)) for x in self.floatTuple()])
169
170    def __call__(self):
171        """return constant or randomized rgb-color string"""
172        return ','.join(map(str, self.byteTuple()))
Colorgen(hsv, cycleLength=10.67)
143    def __init__(self, hsv, cycleLength=10.67):
144        self.hsv = hsv
145        self.cycle = [int(random.random() * 256) for x in self.hsv]
146        self.cycleOffset = int(round(256 / cycleLength))
147        self.distinctIndex = 0
DISTINCT = [(0.17, 1.0, 0.5), (0.0, 0.9, 1.0), (0.35, 0.67, 0.71), (0.14, 0.9, 1.0), (0.56, 1.0, 0.78), (0.07, 0.8, 0.96), (0.79, 0.83, 0.71), (0.5, 0.71, 0.94), (0.84, 0.79, 0.94), (0.2, 0.76, 0.96), (0.0, 0.24, 0.98), (0.5, 1.0, 0.5), (0.77, 0.25, 1.0), (0.09, 0.76, 0.67), (0.15, 0.22, 1.0), (0.0, 1.0, 0.5), (0.38, 0.33, 1.0), (0.67, 1.0, 0.5)]
hsv
cycle
cycleOffset
distinctIndex
def get_value(self, opt, index):
149    def get_value(self, opt, index):
150        if opt == 'random':
151            return random.random()
152        if opt == 'cycle':
153            # the 255 below is intentional to get all color values when cycling long enough
154            self.cycle[index] = (self.cycle[index] + self.cycleOffset) % 255
155            return self.cycle[index] / 255.0
156        if opt == 'distinct':
157            if index == 0:
158                self.distinctIndex = (self.distinctIndex + 1) % len(self.DISTINCT)
159            return self.DISTINCT[self.distinctIndex][index]
160        return float(opt)
def floatTuple(self):
162    def floatTuple(self):
163        """return color as a tuple of floats each in [0,1]"""
164        return colorsys.hsv_to_rgb(*[self.get_value(o, i) for i, o in enumerate(self.hsv)])

return color as a tuple of floats each in [0,1]

def byteTuple(self):
166    def byteTuple(self):
167        """return color as a tuple of bytes each in [0,255]"""
168        return tuple([int(round(255 * x)) for x in self.floatTuple()])

return color as a tuple of bytes each in [0,255]

class priorityDictionary(builtins.dict):
175class priorityDictionary(dict):
176
177    def __init__(self):
178        '''Initialize priorityDictionary by creating binary heap
179            of pairs (value,key).  Note that changing or removing a dict entry will
180            not remove the old pair from the heap until it is found by smallest() or
181            until the heap is rebuilt.'''
182        self.__heap = []
183        dict.__init__(self)
184
185    def smallest(self):
186        '''Find smallest item after removing deleted items from heap.'''
187        if len(self) == 0:
188            raise IndexError("smallest of empty priorityDictionary")
189        heap = self.__heap
190        while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
191            lastItem = heap.pop()
192            insertionPoint = 0
193            while 1:
194                smallChild = 2 * insertionPoint + 1
195                if smallChild + 1 < len(heap) and \
196                        heap[smallChild][0] > heap[smallChild + 1][0]:
197                    smallChild += 1
198                if smallChild >= len(heap) or lastItem <= heap[smallChild]:
199                    heap[insertionPoint] = lastItem
200                    break
201                heap[insertionPoint] = heap[smallChild]
202                insertionPoint = smallChild
203        return heap[0][1]
204
205    def __iter__(self):
206        '''Create destructive sorted iterator of priorityDictionary.'''
207        def iterfn():
208            while len(self) > 0:
209                x = self.smallest()
210                yield x
211                del self[x]
212        return iterfn()
213
214    def __setitem__(self, key, val):
215        '''Change value stored in dictionary and add corresponding
216            pair to heap.  Rebuilds the heap if the number of deleted items grows
217            too large, to avoid memory leakage.'''
218        dict.__setitem__(self, key, val)
219        heap = self.__heap
220        if len(heap) > 2 * len(self):
221            self.__heap = [(v, k) for k, v in self.items()]
222            self.__heap.sort()  # builtin sort likely faster than O(n) heapify
223        else:
224            newPair = (val, key)
225            insertionPoint = len(heap)
226            heap.append(None)
227            while insertionPoint > 0 and val < heap[(insertionPoint - 1) // 2][0]:
228                heap[insertionPoint] = heap[(insertionPoint - 1) // 2]
229                insertionPoint = (insertionPoint - 1) // 2
230            heap[insertionPoint] = newPair
231
232    def setdefault(self, key, val):
233        '''Reimplement setdefault to call our customized __setitem__.'''
234        if key not in self:
235            self[key] = val
236        return self[key]
237
238    def update(self, other):
239        for key in other.keys():
240            self[key] = other[key]
priorityDictionary()
177    def __init__(self):
178        '''Initialize priorityDictionary by creating binary heap
179            of pairs (value,key).  Note that changing or removing a dict entry will
180            not remove the old pair from the heap until it is found by smallest() or
181            until the heap is rebuilt.'''
182        self.__heap = []
183        dict.__init__(self)

Initialize priorityDictionary by creating binary heap of pairs (value,key). Note that changing or removing a dict entry will not remove the old pair from the heap until it is found by smallest() or until the heap is rebuilt.

def smallest(self):
185    def smallest(self):
186        '''Find smallest item after removing deleted items from heap.'''
187        if len(self) == 0:
188            raise IndexError("smallest of empty priorityDictionary")
189        heap = self.__heap
190        while heap[0][1] not in self or self[heap[0][1]] != heap[0][0]:
191            lastItem = heap.pop()
192            insertionPoint = 0
193            while 1:
194                smallChild = 2 * insertionPoint + 1
195                if smallChild + 1 < len(heap) and \
196                        heap[smallChild][0] > heap[smallChild + 1][0]:
197                    smallChild += 1
198                if smallChild >= len(heap) or lastItem <= heap[smallChild]:
199                    heap[insertionPoint] = lastItem
200                    break
201                heap[insertionPoint] = heap[smallChild]
202                insertionPoint = smallChild
203        return heap[0][1]

Find smallest item after removing deleted items from heap.

def setdefault(self, key, val):
232    def setdefault(self, key, val):
233        '''Reimplement setdefault to call our customized __setitem__.'''
234        if key not in self:
235            self[key] = val
236        return self[key]

Reimplement setdefault to call our customized __setitem__.

def update(self, other):
238    def update(self, other):
239        for key in other.keys():
240            self[key] = other[key]

D.update([E, ]**F) -> None. Update D from dict/iterable E and F. If E is present and has a .keys() method, then does: for k in E: D[k] = E[k] If E is present and lacks a .keys() method, then does: for k, v in E: D[k] = v In either case, this is followed by: for k in F: D[k] = F[k]

def getFreeSocketPort(numTries=10):
243def getFreeSocketPort(numTries=10):
244    for _ in range(numTries):
245        try:
246            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
247            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
248            s.bind(('', 0))
249            p = s.getsockname()[1]
250            s.close()
251            return p
252        except socket.error:
253            pass
254    return None
def getSocketStream(port, mode='rb'):
257def getSocketStream(port, mode='rb'):
258    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
259    s.bind(("localhost", port))
260    s.listen(1)
261    conn, _ = s.accept()
262    return conn.makefile(mode)
def euclidean(a, b):
266def euclidean(a, b):
267    return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
def humanReadableTime(seconds):
270def humanReadableTime(seconds):
271    result = ""
272    sign = '-' if seconds < 0 else ''
273    seconds = abs(seconds)
274    ds = 3600 * 24
275    if seconds > ds:
276        result = "%s:" % int(seconds / ds)
277        seconds = seconds % ds
278    result += "%02i:" % int(seconds / 3600)
279    seconds = seconds % 3600
280    result += "%02i:" % int(seconds / 60)
281    seconds = seconds % 60
282    if seconds == int(seconds):
283        seconds = int(seconds)
284    result += "%02i" % seconds
285    return sign + result
SPECIAL_TIME_STRINGS = ['triggered', 'containerTriggered', 'split', 'begin']
def parseTime(t, factor=1):
291def parseTime(t, factor=1):
292    try:
293        return float(t) * factor
294    except ValueError:
295        pass
296    try:
297        # prepended zero is ignored if the date value already contains days
298        days, hours, minutes, seconds = ([0] + list(map(float, t.split(':'))))[-4:]
299        sign = -1 if t.strip()[0] == '-' else 1
300        return (3600 * 24 * days + 3600 * hours + 60 * minutes + seconds) * sign * factor
301    except ValueError:
302        if t in SPECIAL_TIME_STRINGS:
303            # signal special case but don't crash
304            return None
305        else:
306            raise
def parseBool(val):
309def parseBool(val):
310    # see data/xsd/baseTypes:boolType
311    return val in ["true", "True", "x", "1", "yes", "on"]
def getFlowNumber(flow):
314def getFlowNumber(flow):
315    """interpret number of vehicles from a flow parsed by sumolib.xml.parse"""
316    if flow.number is not None:
317        return int(flow.number)
318    if flow.end is not None:
319        duration = parseTime(flow.end) - parseTime(flow.begin)
320        period = 0
321        isFractional = False
322        if flow.period is not None:
323            if 'exp' in flow.period:
324                # use expected value
325                period = 1 / float(flow.period[4:-1])
326                isFractional = True
327            else:
328                period = float(flow.period)
329        elif flow.probability is not None:
330            # use expected value
331            period = 1 / float(flow.probability)
332            isFractional = True
333        for attr in ['perHour', 'vehsPerHour']:
334            if flow.hasAttribute(attr):
335                period = 3600 / float(flow.getAttribute(attr))
336        if period > 0:
337            count = duration / period
338            if isFractional:
339                return count
340            else:
341                # flows with a regular period always start at least one vehicle at the begin time
342                return math.ceil(count)
343        else:
344            return 1

interpret number of vehicles from a flow parsed by sumolib.xml.parse

def intIfPossible(val):
347def intIfPossible(val):
348    if int(val) == val:
349        return int(val)
350    else:
351        return val
def openz(fileOrURL, mode='r', **kwargs):
354def openz(fileOrURL, mode="r", **kwargs):
355    """
356    Opens transparently files, URLs and gzipped files for reading and writing.
357    Special file names "stdout" and "stderr" are handled as well.
358    Also enforces UTF8 on text output / input and should handle BOMs in input.
359    Should be compatible with python 2 and 3.
360    """
361    encoding = kwargs.get("encoding", "utf8" if "w" in mode else "utf-8-sig")
362    try:
363        if fileOrURL.startswith("http://") or fileOrURL.startswith("https://"):
364            return io.BytesIO(urlopen(fileOrURL).read())
365        if fileOrURL == "stdout":
366            return sys.stdout
367        if fileOrURL == "stderr":
368            return sys.stderr
369        if fileOrURL.endswith(".gz") and "w" in mode:
370            if "b" in mode:
371                return gzip.open(fileOrURL, mode="w")
372            return gzip.open(fileOrURL, mode="wt", encoding=encoding)
373        if kwargs.get("trySocket") and fileOrURL.isdigit():
374            return getSocketStream(int(fileOrURL), mode)
375        if kwargs.get("tryGZip", True) and "r" in mode:
376            with gzip.open(fileOrURL) as fd:
377                fd.read(1)
378            if "b" in mode:
379                return gzip.open(fileOrURL)
380            if sys.version_info[0] < 3:
381                return codecs.getreader('utf-8')(gzip.open(fileOrURL))
382            return gzip.open(fileOrURL, mode="rt", encoding=encoding)
383    except OSError as e:
384        if kwargs.get("printErrors"):
385            print(e, file=sys.stderr)
386    except IOError as e:
387        if kwargs.get("printErrors"):
388            print(e, file=sys.stderr)
389    if "b" in mode:
390        return io.open(fileOrURL, mode=mode)
391    return io.open(fileOrURL, mode=mode, encoding=encoding)

Opens transparently files, URLs and gzipped files for reading and writing. Special file names "stdout" and "stderr" are handled as well. Also enforces UTF8 on text output / input and should handle BOMs in input. Should be compatible with python 2 and 3.

def short_names(filenames, noEmpty):
394def short_names(filenames, noEmpty):
395    if len(filenames) == 1:
396        return filenames
397    reversedNames = [''.join(reversed(f)) for f in filenames]
398    prefix = os.path.commonprefix(filenames)
399    suffix = os.path.commonprefix(reversedNames)
400    prefixLen = len(prefix)
401    suffixLen = len(suffix)
402    shortened = [f[prefixLen:-suffixLen] for f in filenames]
403    if noEmpty and any([not f for f in shortened]):
404        # make longer to avoid empty file names
405        base = os.path.basename(prefix)
406        shortened = [base + f for f in shortened]
407    return shortened
def getBaseName(filename):
410def getBaseName(filename):
411    """strip extensions such as .net.xml.gz"""
412    if filename[-11:] == ".net.xml.gz" and len(filename) > 11:
413        return filename[:-11]
414    elif filename[-8:] == ".net.xml" and len(filename) > 8:
415        return filename[:-8]
416    elif filename[-7:] == ".xml.gz" and len(filename) > 7:
417        return filename[:-7]
418    elif filename[-4:] == ".xml" and len(filename) > 4:
419        return filename[:-4]
420    else:
421        return filename

strip extensions such as .net.xml.gz

def flattenPath(filename, sep='_'):
423def flattenPath(filename, sep='_'):
424    """create a filename that encodes the original directory structure"""
425    parts = Path(filename).parts
426    clean = [p for p in parts if p not in ('/', '\\') and ':' not in p]
427    return sep.join(clean)

create a filename that encodes the original directory structure