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()