Sat 01 February 2020
Math + Code

The purpose of this article is to document how to setup log files on PARI/GP, SageMath and Magma. The motivations of this article are:

  • The desire to automatically save all your interactions with each computer algebra system.
  • The desire to properly and quickly recover data which took hours of computation from each computer algebra system.

The first motivation is useful, for example, to go back to computations you thought were not useful before but suddenly realized was actually useful. With all the logs saved automatically, finding the code you used to recover something you computed once months ago is just a matter of figuring out when you computed it (and maybe using grep to find it). It is also useful when you switch back and forth between the computer algebra systems and forget proper syntax (i.e. How do you declare an elliptic curve in PARI/SAGE/Magma again?). The second motivation is useful to save time, in order to not need to recompute something that has been computed before.

This article assumes that you already have a working installation of the above computer algebra systems and are looking for a quick way to get things working. It also assumes that you are working in Ubuntu/Linux.

PARI/GP

The default location of the PARI/GP startup script is in ~/.gprc. If you compiled PARI/GP yourself, you should find a misc folder on the directory where the PARI installation files are located. There is a gprc.dft file which is basically a sample gprc file. If you ever need to look something up, an HTML documentation and PDF documentations are available.

Log Files

One can also set log files in gprc. To do so, append these lines to your gprc file:

logfile = "~/logs/%Y%m%d-%H%M.parilog"
log = 1

Every time you open a GP session, PARI creates a file whose filename is of the form logfile. My system saves the log file in the directory ~/logs/ with a filename depending on the system date and time (i.e. it will be of the form 20191231-2359.parilog).

Recording Output

Another useful option that one can set in gprc is the number of lines of the output results. To set the limit, one puts the following in gprc.

lines = 20

One would typically set this low for readability. However, you might want to change this to a huge number (or 0 for no limit) so that GP prints the output completely and thus is also recorded in the log file.

Additionally, output in PARI is quite transparent. Initializing a number field via bnfinit returns a vector containing all the data PARI needs to efficiently compute most number field computations you require. If you ever need to work with the same number field again, you can simply copy the vector and paste it onto another PARI session to get almost exactly the same number field structure (pay attention to precision, etc).

Magma

The default location for the magma startup script is ~/.magmarc.

Log Files

The way I made log files work in Magma is to append the following scirpt to ~/.magmarc (or wherever your startup script is).

D := POpen("date +'~/logs/%Y%m%d-%H%M.magmalog'", "r");
F := Gets(D);
SetLogFile(F);

The log file records the input and output, as it appears on the console. If you are curious on why this works, POpen executes the shell command date and outputs our desired filename for the log file. The command Gets interprets this output into a Magma string and SetLogFile does what you expect it to do.

SAGE

As noted in its documentation, the default location for the SAGE startup script is ~/.sage/init.sage.

Log Files

The script to generate the log file is based on this stackexchange answer. I leave it up to the reader to install the necessary python imports. Without further ado, here is what you would need to append to your init.sage file:

import atexit
import os
import time

ip = get_ipython()
LIMIT = 0 # limit the size of the history

def save_history():
    """save the IPython history to a plaintext file"""
    F = time.strftime('/home/guissmo/logs/%Y%m%d-%H%M.sagelog');
    histfile = os.path.join(os.path.expanduser(F);)
    print("Saving plaintext history to %s" % histfile)
    lines = []
    # get previous lines
    # this is only necessary because we truncate the history,
    # otherwise we chould just open with mode='a'
    if os.path.exists(histfile):
        with open(histfile, 'r') as f:
            lines = f.readlines()

    # add any new lines from this session
    lines.extend(record[2] + '\n' for record in ip.history_manager.get_range())

    with open(histfile, 'w') as f:
        # limit to LIMIT entries
        f.writelines(lines[-LIMIT:])

# do the save at exit
atexit.register(save_history)

As you can see, this code is much more involved. And it even only records the input! If you ran a command that takes a long time to receive the output, then you must run it again to recover the data. At least you have the commands to redo it!

Saving and Loading Hours of Work

I did not bother to figure out how to copy everything in SAGE console and save it in a log file. In any case, simply copying what is visible in the console is not enough to recover output which took hours to find.

Suppose hardtocompute contains data which took a long time to compute, one can execute dumps(hardtocompute) and save the result to a file. In a future SAGE session, accessing the result (by reading the saved file) and using loads then recovers whatever hardtocompute used to be. I have not tested extensively to what extent the structure is recovered. Perhaps let me know if you figure out some of the intricacies.

This dumps/loads strategy is automated in this script.

import atexit
import os
import time

ip = get_ipython()
LIMIT = 0 # limit the size of the history

F = time.strftime('~/logs/%Y%m%d-%H%M.sagelog');
D = time.strftime('~/logs/%Y%m%d-%H%M.sagedump');
histfile = os.path.join(os.path.expanduser(F));
dumpfile = os.path.join(os.path.expanduser(D));
print("Recording input history to %s" % histfile);
print("Recording output history to %s" % dumpfile);

def dumper(oh, i):
     if i in oh.keys():
         return dumps(oh[i])
     else:
         return 0

def undumper(l, i):
     if l[i] != 0:
         return loads(l[i])
     else:
         return 0

def save_history():
    """save the IPython history to a plaintext file"""
    lines = []
    # get previous lines
    # this is only necessary because we truncate the history,
    # otherwise we chould just open with mode='a'
    if os.path.exists(histfile):
        with open(histfile, 'r') as f:
            lines = f.readlines()

    # add any new lines from this session
    lines.extend(record[2] + '\n' for record in ip.history_manager.get_range())

    with open(histfile, 'w') as f:
        # limit to LIMIT entries
        f.writelines(lines[-LIMIT:])

    print("Saving input history to %s" % histfile)

    with open(dumpfile, 'w') as f:
        l = [0] + [dumper(_oh, i) for i in range(1,max(_oh)+1)];
        f.write(dumps(l));

    print("Saving output dumps to %s" % dumpfile)

def read_dump(s):
    f = open(s, 'r');
    l = loads(f.read());
    f.close();
    return [ undumper(l, i) for i in range(1, len(l)) ];

# do the save at exit
atexit.register(save_history)

This script automatically saves the input history and the output history into two separate files. To recover the output as an array later on, use read_dump(s) where s is the complete file path and file name of the dump of the session you wish to recover. The output of read_dump is an array whose ith entry is the corresponding output of the ith input, if any. Otherwise, it is 0 (in the case where the ith input does not return any data).

Conclusion

Do let me know if you found this useful. If you have suggestions or comments to improve this, please feel free to share.