1 | #!/usr/bin/env python3 |
---|
2 | |
---|
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") |
---|
15 | parser.add_argument("-r", "--RunsPerNumThds", default=5, help="Number of trials per # of threads. Default is 5") |
---|
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) |
---|
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) |
---|
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 | |
---|
113 | # print("CurrNode: " + str(currNode)) |
---|
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]: |
---|
123 | # print("LocalVisited, curr: " + str(currSum) + ", max: " + str(maxCycleSum) + ", Start: " + str(startNode) ) |
---|
124 | # if the cycle contains the start node check if it is our new max cycle |
---|
125 | if currNode == startNode and currSum > maxCycleSum: |
---|
126 | # print("NewMax") |
---|
127 | maxCycleSum = currSum |
---|
128 | maxCycle = currCycle.copy() |
---|
129 | return |
---|
130 | |
---|
131 | visited[currNode] = True |
---|
132 | |
---|
133 | for idx, val in enumerate(handoff[currNode]): |
---|
134 | # continue if no edge |
---|
135 | if val == 0: |
---|
136 | continue |
---|
137 | |
---|
138 | currCycle.append(currNode) |
---|
139 | findLargestCycle(startNode, idx, visited, globalVisited, currSum + val, currCycle) |
---|
140 | currCycle.pop() |
---|
141 | |
---|
142 | visited[currNode] = False |
---|
143 | |
---|
144 | |
---|
145 | |
---|
146 | def analyzeRun(): |
---|
147 | global handoff, sumPerRow, handoffTotal, maxCycle, maxCycleSum, minBound, maxBound, expected |
---|
148 | currThds = len(handoff) |
---|
149 | |
---|
150 | # find largest cycle |
---|
151 | globalVisited = [False] * currThds |
---|
152 | for i in range(currThds): |
---|
153 | visited = [False] * currThds |
---|
154 | findLargestCycle(i, i, visited, globalVisited, 0, []) |
---|
155 | globalVisited[i] = True |
---|
156 | |
---|
157 | # calculate stats |
---|
158 | cycleHandoffs = [] |
---|
159 | cycleHandoffs.append(handoff[maxCycle[-1]][maxCycle[0]]) |
---|
160 | sumOfMaxCycleRows = sumPerRow[maxCycle[0]] |
---|
161 | |
---|
162 | # expected handoff is MULT P( handoff ) for each handoff in max cycle |
---|
163 | expectedConvoy = handoff[maxCycle[-1]][maxCycle[0]] / sumPerRow[maxCycle[-1]] |
---|
164 | for idx, val in enumerate(maxCycle[1:]): |
---|
165 | cycleHandoffs.append(handoff[maxCycle[idx]][val]) |
---|
166 | sumOfMaxCycleRows += sumPerRow[val] |
---|
167 | expectedConvoy = expectedConvoy * handoff[maxCycle[idx]][val] / sumPerRow[maxCycle[idx]] |
---|
168 | |
---|
169 | # adjust expected bound |
---|
170 | # if max cycle contains all nodes, sumOfMaxCycleRows / handoffTotal == 1 |
---|
171 | # else this adjusts the expected bound to compensate for the cycle not visiting all nodes |
---|
172 | # also mult by 100 to turn into percentage from decimal |
---|
173 | expectedConvoy = expectedConvoy * sumOfMaxCycleRows * 100 / handoffTotal |
---|
174 | |
---|
175 | # upper bound is the percentage of all handoffs that occur in the maximum possible cycle |
---|
176 | # The cycle is possible min(cycleHandoffs) number of times |
---|
177 | maxFeasibleHandoff = min(cycleHandoffs) |
---|
178 | upperBoundConvoy = maxFeasibleHandoff * len(maxCycle) * 100 / handoffTotal |
---|
179 | lowerBoundConvoy = 1 |
---|
180 | for val in maxCycle: |
---|
181 | lowerBoundConvoy = lowerBoundConvoy * maxFeasibleHandoff / sumPerRow[val] |
---|
182 | |
---|
183 | # adjust lower bound. See comment for expectedConvoy adjustment to explain why |
---|
184 | lowerBoundConvoy = lowerBoundConvoy * sumOfMaxCycleRows * 100 / handoffTotal |
---|
185 | |
---|
186 | maxBound.append(upperBoundConvoy) |
---|
187 | minBound.append(lowerBoundConvoy) |
---|
188 | expected.append(expectedConvoy) |
---|
189 | |
---|
190 | if args.Verbose or args.Single: |
---|
191 | output('Convoying bounds: {:.2f}%-{:.2f}%, Expected convoying: {:.2f}%'.format(lowerBoundConvoy,upperBoundConvoy, expectedConvoy)) |
---|
192 | |
---|
193 | |
---|
194 | if args.Single: |
---|
195 | output("N: " + str(args.MaxThreads)) |
---|
196 | goToLineByStartingChars(str(args.MaxThreads)+' ') |
---|
197 | readInMatrix(int(args.MaxThreads)) |
---|
198 | analyzeRun() |
---|
199 | reset() |
---|
200 | else: |
---|
201 | for i in range(args.MaxThreads): |
---|
202 | output("N: " + str(i+1)) |
---|
203 | |
---|
204 | goToLineByStartingChars(str(i+1)+' ') |
---|
205 | for j in range(args.RunsPerNumThds): |
---|
206 | readInMatrix(i+1) |
---|
207 | analyzeRun() |
---|
208 | reset() |
---|
209 | |
---|
210 | output('Mean convoying bounds: {:.2f}%-{:.2f}%, Mean expected convoying: {:.2f}%'.format(st.mean(minBound), st.mean(maxBound),st.mean(expected))) |
---|
211 | output('Median convoying bounds: {:.2f}%-{:.2f}%, Median expected convoying: {:.2f}%'.format(st.median(minBound), st.median(maxBound),st.median(expected))) |
---|
212 | output('') |
---|
213 | thdReset() |
---|
214 | |
---|
215 | readFile.close() |
---|
216 | |
---|
217 | if writeFile: |
---|
218 | writeFile.close() |
---|