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