7
$\begingroup$

I'd like to be able to start and control a Mathematica kernel/front-end from python and be able to evaluate things in a notebook and query results on the python side too.

Is this possible?

$\endgroup$
7
$\begingroup$

For this I'll make use of a little thing I recently wrote to answer a much deeper question: Control Front End in "mathlink" mode

The heart of this will be a little package to make a kernel that both looks for stuff on stdin and from its notebook interface. That package lives here. We can load it simply by running:

Get["https://raw.githubusercontent.com/b3m2a1/mathematica-tools/master/SubprocessKernel.wl"];
SubprocessKernel`OpenSubprocessNotebook[]

There's much more we can do with this, but for now and for proof-of-principle this will suffice.

Once we have that package everything we need to do is really on the python side, writing a little script that starts a kernel in a subprocess (I use pexpect since it makes life much nicer), links it to a front-end, and creates a shell that can play with this kernel. Here's my quick implementation of that:

import pexpect, re, time

### Just for the shell
import tkinter as tk
from GUITools import InterpreterText as shell

### Create a Mathematica instance bound to a controlled kernel

wolfker="/Applications/Mathematica.app/Contents/MacOS/WolframKernel"
proc=pexpect.spawn(wolfker)
script='https://raw.githubusercontent.com/b3m2a1/mathematica-tools/master/SubprocessKernel.wl'
cmd='Get["{}"]; SubprocessKernel`OpenSubprocessNotebook[];'.format(script)
proc.sendline(cmd)
proc.expect('Starting kernel\.\.\.')

### Convenience function for getting junk out of Mathematica

def call_mathematica(cmd):
    proc.expect(".*")
    proc.sendline(cmd)
    proc.expect(r"(>> ------ Output: \d+ ------ >>)|(Out\[)")
    leader=proc.after.decode()
    if leader.startswith('>> ------ Output:'):
        proc.expect('<< ------ Output: \d+ ------ <<')
        leader=leader+proc.before.decode()+proc.after.decode()
    proc.expect(".+")
    res=leader+proc.after.decode()
    chunks=re.findall(r'>> ------ Output: \d+ ------ >>.*?<< ------ Output: \d+ ------ <<', res, re.DOTALL)

    if len(chunks)==0:
        chunks=res
    else:
        chunks=["\n".join(chunk.splitlines()[1:-2]) for chunk in chunks]

    if len(chunks)==1:
        chunks=chunks[0]

    return chunks

### Create a little shell to interact with the made Mathematica instance

mathshell=shell(variables={'Mathematica':proc, 'call_mathematica':call_mathematica});
mathshell.pack(expand=True, fill='both');
mathshell.mainloop()

If you're going to actually use this you can also change the initialization under cmd so that you set the $SubprocessREPLSettings to things that will treat you better (in particular you can change the function that writes results to stdout so that they're in, say, JSON so that python can read them directly).

You could even potentially make use of the stuff I have here and here that attempts to provide an interface for that kind of thing. In particular one could use this as part of a bidirectional interface to allow convenient control of Mathematica from python with some level of native type conversion.

For now, here's a screenshot showing that it does indeed work:

enter image description here

The python terminal is in the lower right and you can see that I was able to access variables defined in the Mathematica notebook in the python terminal.

There's much more we could do (and maybe should) but that's for another day...

$\endgroup$
4
$\begingroup$

Here's a much better answer that uses native data exchange, more convenient syntax, and a slew of other improvements. We'll use the PJLink package I've been working on recently. I wrote about it here. There's a nice demo there that I'll discuss again here.

First I load up python in whatever way I want (I chose Jupyter there). Then I start one of these SubprocessKernels as in my other answer. This time it'll connect via MathLink, though.

import os, sys
pjlink_path = "~/Documents/Python/IDEA/PJLink" #this path will change
sys.path.insert(0, os.path.expanduser(pjlink_path))

from SubprocessKernel import SubprocessKernel
from SubprocessKernel import MathematicaBlock, LinkEnvironment

ker = SubprocessKernel()
def MEval(expr, wait = True, kernel = ker):
  kernel.drain()
  return kernel.evaluate (expr, wait = wait)
ker.start()
ker.start_evaluator()

with MathematicaBlock():
  res = MEval(Set (M.hi, "Hello from python!"))

res

#Out:
'Hello from python!'

And as discussed in that linked post, we can also call into python from the notebook that ker.start() opened.

Here's the final screenshot I had from my demo there (sorry about the bad resolution):

enter image description here

We see there that we've gotten bidirectional control and even got a plot that was made in Mathematica to show up in a Jupyer notebook (Mathematica-inspired front-end for python).

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.