1#!/usr/bin/env python3
2# ^shebang: load the correct Python interpreter and environment.
3#============================================================================
4#
5# NAME
6#
7# PrimpolyRunningTimeAnalysis.py
8#
9# DESCRIPTION
10#
11# Primitive polynomial running time plot in Python using Sympy and Numpy.
12#
13# USAGE
14#
15# Collect data using the script
16#
17# runningTime.sh
18#
19# Copy the times to this script.
20#
21# NOTES
22#
23# numpy: http://www.numpy.org
24# matplotlib https://matplotlib.org
25# Python interpreter: http://www.python.org
26# Python tutorial and reference: htttp://docs.python.org/lib/lib.html
27#
28# LEGAL
29#
30# Primpoly Version 16.3 - A Program for Computing Primitive Polynomials.
31# Copyright (C) 1999-2024 by Sean Erik O'Connor. All Rights Reserved.
32#
33# This program is free software: you can redistribute it and/or modify
34# it under the terms of the GNU General Public License as published by
35# the Free Software Foundation, either version 3 of the License, or
36# (at your option) any later version.
37#
38# This program is distributed in the hope that it will be useful,
39# but WITHOUT ANY WARRANTY; without even the implied warranty of
40# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41# GNU General Public License for more details.
42#
43# You should have received a copy of the GNU General Public License
44# along with this program. If not, see <http://www.gnu.org/licenses/>.
45#
46# The author's address is seanerikoconnor!AT!gmail!DOT!com
47# with the !DOT! replaced by . and the !AT! replaced by @
48#
49#============================================================================
50
51import os # Path
52import sys # argc, argv, read/write I/O
53import platform # platform, uname
54import re # Regular expression support
55import math
56import numpy as np
57import pylab as py
58import subprocess
59import pickle # Python's own data file format.
60from deepdiff import DeepDiff # Deep difference for complicated data structures.
61
62def write_to_pickle_file( data, file_name ):
63 """Write data to a pickle file."""
64 try:
65 with open(file_name, 'wb') as f:
66 pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
67
68 if not os.path.exists( file_name ):
69 print( f"ERROR: Problem writing to {file_name} file doesn't exist." )
70 return None
71
72 except Exception as detail:
73 print( "ERROR: Problem writing to file {file_name} Caught RuntimeError exception; detail = {detail}")
74 return None
75
76
77def read_from_pickle_file( file_name ):
78 """Read data from a pickle file."""
79
80 if not os.path.exists( file_name ):
81 print( f"ERROR: {file_name} does not exist" )
82 return None
83 try:
84 with open( file_name, 'rb') as f:
85 data = pickle.load(f)
86 except Exception as detail:
87 print( "ERROR: Problem reading file {file_name} Caught RuntimeError exception; detail = {detail}")
88 return None
89
90 return data
91
92def data_differences( data1, data2 ):
93 """Return the differences between to dictionaries."""
94
95 changes = DeepDiff(data1, data2)
96 return changes
97
98
99def get_float_pattern():
100 """Generate a regular expression which matches floating point numbers.
101
102 Regular expressions format for Python is referenced here: https://docs.python.org/3/howto/regex.html
103
104 From [how to extract floating number from string](https://stackoverflow.com/questions/4703390/how-to-extract-a-floating-number-from-a-string)
105 Some people, when confronted with a problem, think 'I know, I'll use regular expressions.' Now they have two problems.
106 -Jamie Zawinski <jwz@netscape.com> wrote on Tue, 12 Aug 1997 13:16:22 -0700:
107 """
108
109 float_pattern = r"""
110 [-+]? # Optional sign: + or - or nothing
111 (?:
112 (?: \d* \. \d+) # .1 .12, 1.2 1.23
113 |
114 (?: \d+ \.?) # 1. 12. 1 12
115 )
116 (?: [Ee] [+-]? \d+ )? # Optional exponent: e1 e-2
117 """
118
119 return float_pattern
120
121
122def parse_primpoly_timing_output(line_of_file, **kwargs):
123 """
124 Parse the output of the command
125 time Primipoly p n
126 to get the running time.
127 """
128
129 real_time = None
130
131 # kwargs is a dictionary.
132 for key, value in kwargs.items():
133 if key in kwargs:
134 print( f"key = {key} value = {value}" )
135
136 debug = False
137 debug_value = kwargs.get("debug")
138 if debug_value is not None and debug_value is True:
139 debug = True
140
141 if line_of_file is None or len(line_of_file) == 0:
142 return None
143
144 #first_char = line_of_file[0]
145 # print("first char = ", first_char)
146
147 # Don't forget to backslash the vertical bar: \| since we want to match this char.
148 # Match all spaces with \s+ even those within a line.
149 # Tremendous speedup if we check the first char in the line before applying regular expressions.
150 #if first_char == 'X':
151 pattern1 = fr"""
152 real
153 \s+
154 (?P<real_time>
155 {get_float_pattern()}
156 )
157 """
158
159 pattern2 = fr"""
160 real
161 \s+
162 (?P<real_time>
163 {get_float_pattern()}
164 )
165 """
166 if debug:
167 print(f"pattern1 = {pattern1}")
168 print(f"pattern2 = {pattern2}")
169 print(f"line of file =\n|{line_of_file}|")
170
171 p1 = re.compile(pattern1, re.VERBOSE | re.IGNORECASE)
172 p2 = re.compile(pattern2, re.VERBOSE | re.IGNORECASE)
173
174 # Try both matches.
175 m1 = p1.search(line_of_file)
176 m2 = p2.search(line_of_file)
177
178 if m1:
179 real_time = float(m1.group('real_time'))
180 if debug:
181 print( "Match pattern 1. real_time = {real_time}")
182 elif m2:
183 real_time = float(m2.group('real_time'))
184 if debug:
185 print( "Match pattern 2. real_time = {real_time}")
186
187 return real_time
188
189
190def time_primpoly(**kwargs):
191
192 # kwargs is a dictionary.
193 for key, value in kwargs.items():
194 if key in kwargs:
195 print( f"key = {key} value = {value}" )
196
197 # Enable debugging?
198 debug = False
199 debug_value = kwargs.get("debug")
200 if debug_value is not None and debug_value is True:
201 debug = True
202
203 # All the degrees we wish to test.
204 degrees = [ 2, 10, 30, 50, 70, 90, 100, 120, 140, 145, 150, 160, 180, 200, 202, 210, 300, 400, 500, 550 ]
205 times = []
206
207 p = 2
208 for n in degrees:
209 # Run time Primpoly and collect the outputs.
210 print( f"Primpoly p = {p} n = {n}", file=sys.stderr )
211
212 standard_output = None
213 standard_error = None
214
215 # Set up the executable and the arguments for Linux. Try that first.
216 executableFileName="Bin/Primpoly.exe"
217 timeCommand="/usr/bin/time"
218 timeOptions = "-f 'real %e'" # %e is Wall clock time in seconds
219 completed_process=subprocess.run([timeCommand, timeOptions, executableFileName, str(p), str(n)], capture_output=True)
220 if completed_process.returncode == 0:
221 standard_output = completed_process.stdout.decode("utf-8")
222 standard_error = completed_process.stderr.decode("utf-8")
223 else:
224 if debug:
225 print("Could not run Linux version of time Primpoly. Running macOS version of time Primpoly")
226 # Didn't work? Set up the executable and the arguments for macOS. Try that next.
227 executableFile="Bin/Primpoly.exe"
228 timeCommand="time"
229 completed_process=subprocess.run([timeCommand, executableFileName, str(p), str(n)], capture_output=True)
230 if completed_process.returncode == 0:
231 standard_output = completed_process.stdout.decode("utf-8")
232 standard_error = completed_process.stderr.decode("utf-8")
233 # Still didn't work? Then abort.
234 else:
235 print( "ERROR: Could not run time command on the Primpoly executable. Did you build it with make? Aborting..." )
236 return None
237
238 if debug:
239 for line in standard_output.split('\n'):
240 print( f">>> {line}" )
241 for line in standard_error.split('\n'):
242 print( f"!!! {line}" )
243
244 # Parse the results to get the running time.
245 running_time_sec = parse_primpoly_timing_output(standard_error)
246 if running_time_sec is None:
247 print( "ERROR: Could not parse the running time. Aborting..." )
248 return None
249 else:
250 print( f"\trunning time = {running_time_sec}", file=sys.stderr )
251
252 times.append( running_time_sec )
253
254 return degrees, times
255
256
257def plot_running_times( file_name, **kwargs ):
258 """Plot the running times"""
259
260 debug = False
261 debug_value = kwargs.get("debug")
262 if debug_value is not None and debug_value is True:
263 debug = True
264
265 if (timing_data := read_from_pickle_file( file_name )) == None:
266 print( f"ERROR: No data in the running times file or no file {file_name}")
267 return
268 elif debug:
269 print( f"timing data (computer name, degrees and times = \n\t{timing_data}" )
270
271 try:
272 py.figure('Primpoly Running Time', figsize=(20,10))
273 py.title('Running Time vs Degree for Mod 2 Primitive Polynomials')
274 py.xlabel('Polynomial Degree')
275 py.ylabel('Running Time, Seconds')
276 py.semilogy()
277
278 legends = []
279 for computer_name, degrees_and_times in timing_data.items():
280 degrees, times = degrees_and_times
281 if debug:
282 print(f"degrees = {degrees}")
283 print(f"times = {times}")
284 print(f"name = {computer_name}")
285
286 py.plot( degrees, times )
287 legends.append( computer_name )
288
289 py.legend(legends)
290 py.show()
291 return
292 except Exception as err:
293 print( f"ERROR: Plotting went haywire for file {file_name}. Caught a general Exception {err}" )
294 return
295
296def compute_running_time_and_record(file_name):
297 """Time my primitive polynomial program and record the times."""
298
299 # Do we have any previously recorded timing data? If not, create an empty dictionary for it.
300 if (timing_data := read_from_pickle_file( file_name )) == None:
301 timing_data = {}
302
303 # Find out which type of computer system we are running on.
304 if platform.uname().system == 'Darwin' and platform.uname().machine == 'arm64' and platform.uname().processor == 'arm':
305 computer_name = "MacBook Pro 2021 Apple M1 Max macOS"
306 elif platform.uname().system == 'Linux' and platform.uname().machine == 'x86_64' and platform.uname().processor == 'x86_64':
307 computer_name = "CyberPowerPC Model C Series AMD Intel Ubuntu/Linux 22.04 LTS Jammy Jellyfish"
308 else:
309 computer_name = None
310 print( f"ERROR: unknown computer type" )
311 return
312
313 # Run the timing analysis.
314 degrees, times = time_primpoly()
315
316 if computer_name not in timing_data:
317 # If we don't yet have data for this computer, create a new dictionary entry.
318 timing_data[ computer_name ] = [degrees, times]
319 else:
320 # Else overwrite the old timing data.
321 timing_data[ computer_name ] = [degrees, times]
322
323 # Record to file.
324 write_to_pickle_file( timing_data, file_name )
325
326# timing_data_results = read_from_pickle_file( file_name )
327# diff = data_differences( timing_data, timing_data_results )
328# if diff is None or len(diff) == 0:
329# print( "no differences" )
330
331
332def update_running_times_and_plot_all():
333 """Read the running time from file and plot it"""
334
335 print( "Running Python Version {0:d}.{1:d}.{2:d}".format( sys.version_info[ 0 ], sys.version_info[ 1 ], sys.version_info[ 2 ] ) )
336
337 file_name = "primpoly_running_time.pickle"
338
339 # Compute running time on this system and record to file.
340 compute_running_time_and_record(file_name)
341
342 # Plot all running times so far recorded.
343 plot_running_times(file_name)
344
345if __name__ == '__main__':
346 """ If you run this as a standalone program:
347 chmod +x PrimpolyRunningTimeAnalysis.py
348 ./PrimpolyRunningTimeAnalysis.py
349 """
350
351 update_running_times_and_plot_all()