[8e64cb4] | 1 | #!/usr/bin/env python3 |
---|
| 2 | |
---|
[93e0603] | 3 | import os |
---|
| 4 | import sys |
---|
| 5 | import math |
---|
| 6 | import argparse |
---|
| 7 | import statistics as st |
---|
| 8 | |
---|
| 9 | # Parsing Logic |
---|
| 10 | parser = argparse.ArgumentParser(prog = 'GenConvoyStats', |
---|
| 11 | description = 'Analyzes handoff matrix output and uses Markov chain modelling to provide upper and lower bounds on the largest long term convoy', |
---|
| 12 | epilog = '') |
---|
| 13 | |
---|
| 14 | parser.add_argument("Filename") |
---|
[fd6e3a4] | 15 | parser.add_argument("-r", "--RunsPerNumThds", default=5, help="Number of trials per # of threads. Default is 5") |
---|
[93e0603] | 16 | parser.add_argument("-m", "--MaxThreads", default=32, help="Maximum number of threads. Default is 32") |
---|
| 17 | parser.add_argument("-o", "--OutputFile", default='', help="File to write output to") |
---|
| 18 | parser.add_argument("-v", "--Verbose", help="Verbose output. Will print per run stats alongside aggregates", action='count', default=0) |
---|
[8e64cb4] | 19 | parser.add_argument("-s", "--Single", help="Indicates that input only contains a single run. Assumes the number of threads is the value passed to -m", action='count', default=0) |
---|
[93e0603] | 20 | |
---|
| 21 | args = parser.parse_args() |
---|
| 22 | |
---|
| 23 | # handoff data |
---|
| 24 | handoff = [] |
---|
| 25 | sumPerRow = [] |
---|
| 26 | handoffTotal = 0 |
---|
| 27 | maxCycleSum = 0 |
---|
| 28 | maxCycle = [] |
---|
| 29 | |
---|
| 30 | # per thread data (across runs) |
---|
| 31 | minBound = [] |
---|
| 32 | maxBound = [] |
---|
| 33 | expected = [] |
---|
| 34 | |
---|
| 35 | # input file descriptor |
---|
| 36 | readFile = open(args.Filename, "r") |
---|
| 37 | |
---|
| 38 | writeFile = False |
---|
| 39 | if args.OutputFile != '': |
---|
| 40 | writeFile = open(args.Filename, "w") |
---|
| 41 | |
---|
| 42 | def reset(): |
---|
| 43 | global handoff, sumPerRow, handoffTotal, readFile, maxCycleSum, maxCycle |
---|
| 44 | handoff.clear() |
---|
| 45 | sumPerRow.clear() |
---|
| 46 | handoffTotal = 0 |
---|
| 47 | maxCycleSum = 0 |
---|
| 48 | maxCycle.clear() |
---|
| 49 | |
---|
| 50 | def thdReset(): |
---|
| 51 | global minBound, maxBound, expected |
---|
| 52 | minBound.clear() |
---|
| 53 | maxBound.clear() |
---|
| 54 | expected.clear() |
---|
| 55 | |
---|
| 56 | def output(string): |
---|
| 57 | global writeFile |
---|
| 58 | if writeFile: |
---|
| 59 | writeFile.write(string + '\n') |
---|
| 60 | |
---|
| 61 | print(string) |
---|
| 62 | |
---|
| 63 | # reads in handoff matrix for a single run and accumulates row/matrix total at the same time |
---|
| 64 | def readInMatrix(currThds): |
---|
| 65 | global handoff, sumPerRow, handoffTotal, readFile |
---|
| 66 | for i in range(currThds): |
---|
| 67 | line = readFile.readline() |
---|
| 68 | |
---|
| 69 | # Deal with EOF |
---|
| 70 | if not line: |
---|
| 71 | print("Incorrect arguments or file format: error in readInMatrix") |
---|
| 72 | sys.exit(1) |
---|
| 73 | |
---|
| 74 | # deal with any empty lines |
---|
| 75 | while line == '\n': |
---|
| 76 | line = readFile.readline() |
---|
| 77 | |
---|
| 78 | row = [] |
---|
| 79 | rowSum = 0 |
---|
| 80 | |
---|
| 81 | # convert row into list of ints and accumulate |
---|
| 82 | for val in line.replace(',','').split(): |
---|
| 83 | row.append(int(val)) |
---|
| 84 | rowSum += int(val) |
---|
| 85 | |
---|
| 86 | #store row in global state |
---|
| 87 | handoff.append(row) |
---|
| 88 | sumPerRow.append(rowSum) |
---|
| 89 | handoffTotal += rowSum |
---|
| 90 | |
---|
| 91 | # moves current non empty line in readFile to line after line described by first two chars |
---|
| 92 | def goToLineByStartingChars(string): |
---|
| 93 | global readFile |
---|
| 94 | # find start of relevant data |
---|
| 95 | line = "" |
---|
| 96 | while True: |
---|
| 97 | line = readFile.readline() |
---|
| 98 | if not line: |
---|
| 99 | print("Incorrect arguments or file format: error in goToLineByStartingChars") |
---|
| 100 | sys.exit(1) |
---|
| 101 | |
---|
| 102 | # strip after checking for EOF so we can distinguish EOF vs empty line |
---|
| 103 | line = line.strip() |
---|
| 104 | |
---|
| 105 | # discard lines until we see the column line in output |
---|
| 106 | if line and line[0:len(string)] == string: |
---|
| 107 | break |
---|
| 108 | |
---|
| 109 | # recursively find largest cycle included a specific node using DFS |
---|
| 110 | def findLargestCycle(startNode, currNode, visited, globalVisited, currSum, currCycle): |
---|
| 111 | global handoff, maxCycle, maxCycleSum |
---|
| 112 | |
---|
[ebbe941] | 113 | # print("CurrNode: " + str(currNode) + " StartNode: " + str(startNode)) |
---|
[93e0603] | 114 | # print(currCycle) |
---|
| 115 | # if we visit a node from a previous call then return since we have already |
---|
| 116 | # looked at all cycles containing that node |
---|
| 117 | if globalVisited[currNode]: |
---|
| 118 | # print("globalVisited") |
---|
| 119 | return |
---|
| 120 | |
---|
| 121 | # found a cycle |
---|
| 122 | if visited[currNode]: |
---|
[ebbe941] | 123 | # if currNode == startNode: |
---|
[93e0603] | 124 | # print("LocalVisited, curr: " + str(currSum) + ", max: " + str(maxCycleSum) + ", Start: " + str(startNode) ) |
---|
| 125 | # if the cycle contains the start node check if it is our new max cycle |
---|
| 126 | if currNode == startNode and currSum > maxCycleSum: |
---|
| 127 | # print("NewMax") |
---|
| 128 | maxCycleSum = currSum |
---|
| 129 | maxCycle = currCycle.copy() |
---|
| 130 | return |
---|
| 131 | |
---|
| 132 | visited[currNode] = True |
---|
[ebbe941] | 133 | # print(visited) |
---|
[93e0603] | 134 | for idx, val in enumerate(handoff[currNode]): |
---|
| 135 | # continue if no edge |
---|
[ebbe941] | 136 | # if val == 0: |
---|
| 137 | # continue |
---|
[93e0603] | 138 | |
---|
| 139 | currCycle.append(currNode) |
---|
| 140 | findLargestCycle(startNode, idx, visited, globalVisited, currSum + val, currCycle) |
---|
| 141 | currCycle.pop() |
---|
[ebbe941] | 142 | # print(currNode) |
---|
[93e0603] | 143 | visited[currNode] = False |
---|
| 144 | |
---|
| 145 | |
---|
| 146 | |
---|
| 147 | def analyzeRun(): |
---|
| 148 | global handoff, sumPerRow, handoffTotal, maxCycle, maxCycleSum, minBound, maxBound, expected |
---|
| 149 | currThds = len(handoff) |
---|
| 150 | |
---|
[ebbe941] | 151 | |
---|
| 152 | # This is NP-Hard and is currently not tractable so we just estimate the largest cycle |
---|
| 153 | # the code to do so is commented below |
---|
| 154 | |
---|
| 155 | # find largest cycle |
---|
| 156 | # globalVisited = [False] * currThds |
---|
| 157 | # for i in range(currThds): |
---|
| 158 | # # print(i) |
---|
| 159 | # visited = [False] * currThds |
---|
| 160 | # findLargestCycle(i, i, visited, globalVisited, 0, []) |
---|
| 161 | # globalVisited[i] = True |
---|
| 162 | |
---|
| 163 | # # calculate stats |
---|
| 164 | # cycleHandoffs = [] |
---|
| 165 | # cycleHandoffs.append(handoff[maxCycle[-1]][maxCycle[0]]) |
---|
| 166 | # sumOfMaxCycleRows = sumPerRow[maxCycle[0]] |
---|
| 167 | |
---|
| 168 | # # expected handoff is MULT P( handoff ) for each handoff in max cycle |
---|
| 169 | # expectedConvoy = handoff[maxCycle[-1]][maxCycle[0]] / sumPerRow[maxCycle[-1]] |
---|
| 170 | # for idx, val in enumerate(maxCycle[1:]): |
---|
| 171 | # cycleHandoffs.append(handoff[maxCycle[idx]][val]) |
---|
| 172 | # sumOfMaxCycleRows += sumPerRow[val] |
---|
| 173 | # expectedConvoy = expectedConvoy * handoff[maxCycle[idx]][val] / sumPerRow[maxCycle[idx]] |
---|
| 174 | |
---|
| 175 | # # adjust expected bound |
---|
| 176 | # # if max cycle contains all nodes, sumOfMaxCycleRows / handoffTotal == 1 |
---|
| 177 | # # else this adjusts the expected bound to compensate for the cycle not visiting all nodes |
---|
| 178 | # # also mult by 100 to turn into percentage from decimal |
---|
| 179 | # expectedConvoy = expectedConvoy * sumOfMaxCycleRows * 100 / handoffTotal |
---|
| 180 | |
---|
| 181 | ################################ |
---|
| 182 | |
---|
| 183 | #start of approximation code is here: |
---|
| 184 | |
---|
| 185 | # instead we take the maximum handoff from each row and assume it is all a cycle as an approximation |
---|
| 186 | maxCycle = [] |
---|
| 187 | maxCycleSum = 0 |
---|
[93e0603] | 188 | cycleHandoffs = [] |
---|
[ebbe941] | 189 | expectedConvoy = 100 |
---|
| 190 | for i in range(currThds): |
---|
| 191 | currMax = max(handoff[i]) |
---|
| 192 | maxCycle.append(i) |
---|
| 193 | cycleHandoffs.append(currMax) |
---|
| 194 | maxCycleSum += currMax |
---|
| 195 | expectedConvoy = expectedConvoy * currMax / sumPerRow[i] |
---|
| 196 | |
---|
| 197 | sumOfMaxCycleRows = handoffTotal |
---|
| 198 | |
---|
| 199 | # end of approximation code |
---|
| 200 | ################################################### |
---|
[93e0603] | 201 | |
---|
| 202 | # upper bound is the percentage of all handoffs that occur in the maximum possible cycle |
---|
| 203 | # The cycle is possible min(cycleHandoffs) number of times |
---|
| 204 | maxFeasibleHandoff = min(cycleHandoffs) |
---|
| 205 | upperBoundConvoy = maxFeasibleHandoff * len(maxCycle) * 100 / handoffTotal |
---|
| 206 | lowerBoundConvoy = 1 |
---|
| 207 | for val in maxCycle: |
---|
| 208 | lowerBoundConvoy = lowerBoundConvoy * maxFeasibleHandoff / sumPerRow[val] |
---|
| 209 | |
---|
| 210 | # adjust lower bound. See comment for expectedConvoy adjustment to explain why |
---|
| 211 | lowerBoundConvoy = lowerBoundConvoy * sumOfMaxCycleRows * 100 / handoffTotal |
---|
| 212 | |
---|
| 213 | maxBound.append(upperBoundConvoy) |
---|
| 214 | minBound.append(lowerBoundConvoy) |
---|
| 215 | expected.append(expectedConvoy) |
---|
| 216 | |
---|
[8e64cb4] | 217 | if args.Verbose or args.Single: |
---|
[93e0603] | 218 | output('Convoying bounds: {:.2f}%-{:.2f}%, Expected convoying: {:.2f}%'.format(lowerBoundConvoy,upperBoundConvoy, expectedConvoy)) |
---|
| 219 | |
---|
| 220 | |
---|
[8e64cb4] | 221 | if args.Single: |
---|
| 222 | output("N: " + str(args.MaxThreads)) |
---|
| 223 | goToLineByStartingChars(str(args.MaxThreads)+' ') |
---|
| 224 | readInMatrix(int(args.MaxThreads)) |
---|
| 225 | analyzeRun() |
---|
| 226 | reset() |
---|
| 227 | else: |
---|
| 228 | for i in range(args.MaxThreads): |
---|
| 229 | output("N: " + str(i+1)) |
---|
| 230 | |
---|
| 231 | goToLineByStartingChars(str(i+1)+' ') |
---|
| 232 | for j in range(args.RunsPerNumThds): |
---|
| 233 | readInMatrix(i+1) |
---|
| 234 | analyzeRun() |
---|
| 235 | reset() |
---|
| 236 | |
---|
| 237 | output('Mean convoying bounds: {:.2f}%-{:.2f}%, Mean expected convoying: {:.2f}%'.format(st.mean(minBound), st.mean(maxBound),st.mean(expected))) |
---|
| 238 | output('Median convoying bounds: {:.2f}%-{:.2f}%, Median expected convoying: {:.2f}%'.format(st.median(minBound), st.median(maxBound),st.median(expected))) |
---|
| 239 | output('') |
---|
| 240 | thdReset() |
---|
[93e0603] | 241 | |
---|
| 242 | readFile.close() |
---|
| 243 | |
---|
| 244 | if writeFile: |
---|
| 245 | writeFile.close() |
---|