Friday, July 24, 2015

Uncertainty Extension for IPython

Wouldn't it be nice if we had uncertainty with a nice notation in IPython? The current method would be to use raw Python,

In [1]:
from uncertainties import ufloat
print(ufloat(12.34,.01))
12.340+/-0.010

Let's use the handy infix library to make the notation easier. We'll define |pm| to mean +/-.

Note: this is a very simple library that is less than a page long. Feel free to write the code yourself (as I do later in this notebook).

In [19]:
from infix import or_infix, custom_infix, base_infix
pm = or_infix(ufloat)
In [3]:
print(12.34 |pm| .01)
12.340+/-0.010

Aside

Our | operator has very low precedence. So, things like this are not very useful:

In [6]:
2 * 12.34 |pm| .01
Out[6]:
24.68+/-0.01

The highest order we can do with the infix library is mul,

In [12]:
pm = custom_infix('__rmul__','__mul__')(ufloat)
2 * 12.34 *pm* .01
Out[12]:
24.68+/-0.01

The library doesn't have support for python's only right-assosiative operator, **. We can add that easily, though:

In [23]:
class pow_infix(base_infix):
    def right(self, right):
        return self.func(right, self.__infix__)
    
    __pow__ = base_infix.left
    __rpow__ = right
In [24]:
pm = pow_infix(ufloat)
2 * 12.34 **pm** .01
Out[24]:
24.68+/-0.02

Extending IPython

We've made an improvement, but this is ugly notation. Let's patch IPython to find the notation +/- or ± and replace it with our infix operator before passing it to the underlying python kernel:

In [34]:
from IPython.core.inputtransformer import StatelessInputTransformer
@StatelessInputTransformer.wrap
def my_special_commands(line):
    return line.replace('+/-','**pm**').replace('±','**pm**')

We only needed a line transformer, and there was no need to even handle storing a state. Thankfully, we didn't need something like the AST this time, since it's a simple replacement only; that's the beauty of starting with a valid python construct for infix operations.

Now, let's use our transformer in the current IPython instance:

In [35]:
import IPython
ip = IPython.get_ipython()
ip.input_splitter.logical_line_transforms.append(my_special_commands())
ip.input_transformer_manager.logical_line_transforms.append(my_special_commands())
In [36]:
print(112 +/- 2 - 1321 +/- 2)
-1209.0+/-2.8
In [37]:
print(112 ± 2 - 1321 ± 2)
-1209.0+/-2.8

Loadable extension

Let's make a IPython extension that makes all of this automatic. I'm removing the need for the infix library (since it's only a few lines).

In [16]:
%%writefile uncert_ipython.py

from uncertainties import ufloat
import IPython
from IPython.core.inputtransformer import StatelessInputTransformer

class pow_infix(object):
    def __init__(self, func):
        self.func = func

    def __pow__(self, left):
        self.__infix__ = left
        return self

    def __rpow__(self, right):
        return self.func(right, self.__infix__)
   
pm = pow_infix(ufloat)

@StatelessInputTransformer.wrap
def my_special_commands(line):
    return line.replace('+/-','**pm**').replace('±','**pm**')

comm1 = my_special_commands()
comm2 = my_special_commands()

def load_ipython_extension(ipython):
    ipython.input_splitter.logical_line_transforms.append(comm1)
    ipython.input_transformer_manager.logical_line_transforms.append(comm2)
    ipython.user_ns['pm'] = pm
    
def unload_ipython_extension(ipython):
    ipython.input_splitter.logical_line_transforms.remove(comm1)
    ipython.input_transformer_manager.logical_line_transforms.remove(comm2)
Overwriting uncert_ipython.py

After restarting the kernel, we can test this:

In [17]:
%load_ext uncert_ipython
In [23]:
1 +/- .1
Out[23]:
1.0+/-0.1
In [24]:
%unload_ext uncert_ipython
In [25]:
1 +/- .1
  File "<ipython-input-25-5d2725f7d570>", line 1
    1 +/- .1
       ^
SyntaxError: invalid syntax

No comments:

Post a Comment