Search code examples
pythonanacondajupytersagejupyter-kernel

How to install SageMath kernel in Anaconda?


I'm trying to use Sage in Anaconda 3 but it looks that the libraries are not imported.

I firstly created a new environment 'ipykernel_py2' and then installed Python 2 as explained in here. With this I can have both Python 3 and Python 3 up and running in Anaconda 3.

Then I went to the kernel's folder created (C:\Users\YOUR_USERNAME\AppData\Local\Continuum\anaconda3\envs\ipykernel_py2\share\jupyter\kernels) and pasted Sage's kernel (taken from C:\Program Files\SageMath 8.2\runtime\opt\sagemath-8.2\local\share\jupyter\kernels). This allows to create new SageMath files in Jupyter but the kernel is dead. To activate the kernel I used Anaconda Prompt and typed:

activate ipykernel_py2
python -m ipykernel install --user --name sagemath --display-name "SageMath 8.2"

So the kernel is now activated and I can create and run Sage files. However the libraries are still not working. It seems that the file is running like a normal Python 2 file.

Does anyone know how to fix this? Do I need to create a seperate environment?


Solution

  • Sage for Windows runs under a UNIX emulation environment called Cygwin. Looking at the sagemath/kernel.json it contains:

    {"display_name": "SageMath 8.2", "argv": ["/opt/sagemath-8.2/local/bin/sage", "--python", "-m", "sage.repl.ipython_kernel", "-f", "{connection_file}"]}
    

    You can see here that it has a UNIX-style path to the sage executable. This path only makes sense to other programs running under Sage's Cygwin environment, and is meaningless to native Windows programs. Simply converting it to the equivalent Windows path won't work either, because bin/sage is actually a shell script. At the very least you need to provide a Windows path to the bash that comes with Cygwin and pass it the UNIX path to the sage executable (the same as the one above). Without a login shell, most environment variables needed won't be set either, so you probably need bash -l.

    So, something like:

    {"display_name": "SageMath 8.2", "argv": ["C:\\Program Files\\SageMath 8.2\\runtime\\bin\\bash.exe", "-l", "/opt/sagemath-8.2/local/bin/sage", "--python", "-m", "sage.repl.ipython_kernel", "-f", "{connection_file}"]}
    

    might work. The one thing I'm not sure about is whether the {connection_file} argument will be handled properly either. I haven't tested it.

    Update: Indeed, the above partially works, but there are a few problems: The {connection_file} argument as passed as the absolute Windows path to the file. While Cygwin can normally translate transparently from Windows paths to a corresponding UNIX path, there is a known issue that Python's os.path module on Cygwin does not handle Windows-style paths well, and this leads to issues.

    The other major problem I encountered was that IPKernelApp, the class that drives generic Jupyter kernels, has a thread which polls to see if the kernel's parent process (in this case the notebook server) has exited, so it can appropriately shut down if the parent shuts down. This is how kernels know to automatically shut down when you kill the notebook server.

    How this is done is very different depending on the platform--Windows versus UNIX-like. Because Sage's kernel runs in Cygwin, it chooses the UNIX-like poller. However, this is wrong if the notebook server happens to be a native Windows process, as is the case when running the Sage kernel in a Windows-native Jupyter. Remarkably, the parent poller for Windows can work just as well on Cygwin since it accesses the Windows API through ctypes. Therefore, this can be worked around by providing a wrapper to IPKernelApp that forces uses of ParentPollerWindows.

    A possible solution then looks something like this: From within the SageMath Shell do:

    $ cd "$SAGE_LOCAL"
    $ mkdir -p ./share/jupyter/kernels/sagemath
    $ cd ./share/jupyter/kernels/sagemath
    $ cat <<_EOF_ > kernel-wrapper.sh
    #!/bin/sh
    here="$(dirname "$0")"
    connection_file="$(cygpath -u -a "$1")"
    exec /opt/sagemath-8.2/local/bin/sage --python "${here}/kernel-wrapper.py" -f "${connection_file}"
    _EOF_
    $ cat <<_EOF_ > kernel-wrapper.py
    from ipykernel.kernelapp import IPKernelApp as OrigIPKernelApp
    from ipykernel.parentpoller import ParentPollerWindows
    from sage.repl.ipython_kernel.kernel import SageKernel
    
    
    class IPKernelApp(OrigIPKernelApp):
        """
        Although this kernel runs under Cygwin, its parent is a native Windows
        process, so we force use of the ParentPollerWindows.
        """
    
        def init_poller(self):
            if self.interrupt or self.parent_handle:
                self.poller = ParentPollerWindows(self.interrupt,
                                                  self.parent_handle)
    
    
    IPKernelApp.launch_instance(kernel_class=SageKernel)
    _EOF_
    

    Now edit the kernel.json (in its existing location under share\jupyter\kernels\sagemath) to read:

    {"display_name": "SageMath 8.2", "argv": ["C:\\Program Files\\SageMath 8.2\\runtime\\bin\\bash.exe", "-l", "/opt/sagemath-8.2/local/share/jupyter/kernels/sagemath/kernel-wrapper.sh", "{connection_file}"]}
    

    This runs kernel-wrapper.sh which in turn runs kernel-wrapper.py. (There are a few simplifications I could make to get rid of the need for kernel-wrapper.sh completely, but that would be easier in SageMath 8.3 which includes PyCygwin.)

    Make sure to change every "8.2" to the appropriate "X.Y" version for your Sage installation.

    Update: Made some updates thanks to feedback from a user, but I haven't tested these changes yet, so please make sure instead of blindly copy/pasting that every file/directory path in my instructions exists and looks correct.

    As you can see, this was not trivial, and was never by design meant to be possible. But it can be done. Once the kernel itself is up and running it's just a matter of talking to it over TCP/IP sockets so there's not too much magic involved after that. I believe there are some small improvements that could be made on both the Jupyter side and on the Sage side that would facilitate this sort of thing in the future...