Writing lisp code from Python

| categories: lisp, python | tags:

Some time ago I wrote about converting python data structures to lisp . I have expanded on that idea to writing lisp programs from Python! The newly expanded code that makes this possible can be found at https://github.com/jkitchin/pycse/blob/master/pycse/lisp.py .

Here are the simple data types known to pycse.lisp:

import pycse.lisp
import numpy as np

print("a string".lisp)
a = 5
b = 5.0
print(a.lisp)
print(b.lisp)
print([1, 2, 3].lisp)
print((1, 2, 3).lisp)
print({'a': 4}.lisp)
print(np.array([1, 2, 3]).lisp)
print(np.array([1.0, 2.0, 3.0]).lisp)
"a string"
5
5.0
(1 2 3)
(1 2 3)
(:a 4)
(1 2 3)
(1.0 2.0 3.0)

There are also some more complex types.

import pycse.lisp as pl

print(pl.Symbol('lambda'))
print(pl.Quote('lambda'))
print(pl.SharpQuote('lambda'))
print(pl.Cons("a", 5))
print(pl.Alist(["a", 2, "b", 5]))
print(pl.Vector([1, 2, 3]))

print(pl.Backquote([]))
print(pl.Comma([1, 2, 3]))
print(pl.Splice([1, 2, 3]))
lambda
'lambda
#'lambda
("a" . 5)
(("a" . 2) ("b" . 5))
[1 2 3]
`()
,(1 2 3)
,@(1 2 3)

You can nest these too.

import pycse.lisp as pl
print(pl.Quote(pl.Alist(["a", 2, "b", 5])))
print(pl.Backquote([pl.Symbol('+'), pl.Comma(pl.Symbol('b')), 5]))
'(("a" . 2) ("b" . 5))
`(+ ,b 5)

All that means we can use Python code to generate lisp programs. Here is an example where we make two sub-programs, and combine them into an overall program, then add one more subprogram to it. We wrap the results in an emacs-lisp block, then actually run the block!

import pycse.lisp as pl

p1 = [pl.Symbol('mapcar'),
      [pl.Symbol('lambda'),
       [pl.Symbol('x')],
       [pl.Symbol('*'),
        pl.Symbol('x'),
        pl.Symbol('x')]],
      pl.Quote([1, 2, 3, 4])]

p2 = [pl.Symbol('princ'), "Hello world"]

p = [pl.Symbol('list'), p1, p2]
p.append([pl.Symbol('+'), 5, 5])

print(p.lisp)
(list (mapcar (lambda (x) (* x x)) '(1 2 3 4)) (princ "Hello world") (+ 5 5))
(1 4 9 16) Hello world 10

Wow, it worked! Here is another example of setting up a macro and then running it.

import pycse.lisp as pl
s = pl.Symbol
bq = pl.Backquote
c = pl.Comma

p1 = [s('defmacro'), s('f'), [s('x')],
      "A docstring",
      bq([s('*'), c(s('x')), 5])]


p2 = [s('f'), 5]

print(p1.lisp)

print(p2.lisp)
(defmacro f (x) "A docstring" `(* ,x 5))
(f 5)
25

I am not too sure where this will be super useful, but it is an interesting proof of concept. I haven't tested this much beyond the original post and this one. Let me know if you find issues with it.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter

Expanding orgmode.py to get better org-python integration

| categories: orgmode, python | tags:

I have only ever been about 80% satisfied with Python/org-mode integration. I have developed a particular workflow that I like a lot, and works well for solving scientific and engineering problems. I typically use stand-alone Python blocks, i.e. not sessions. I tend to use print statements to create output that I want to see, e.g. the value of a calculation. I also tend to create multiple figures in a single block, which I want to display in the buffer. This workflow is represented extensively in PYCSE and dft-book which collectively have 700+ src blocks! So I use it alot ;)

There are some deficiencies though. For one, I have had to hand build any figures/tables that are generated from the code blocks. That means duplicating filenames, adding the captions, etc… It is not that easy to update captions from the code blocks, and there has been limited ability to use markup in the output.

Well finally I had some ideas to change this. The ideas are:

  1. Patch matplotlib so that savefig actually returns a figure link that can be printed to the output. savefig works the same otherwise.
  2. Patch matplotlib.pyplot.show to save the figure, and print a figure link in thhe output.
  3. Create special functions to generate org tables and figures.
  4. Create some other functions to generate some blocks and elements.

Then we could just import the library in our Python scripts (or add it as a prologue) and get this nice functionality. You can find the code for this here:

https://github.com/jkitchin/pycse/blob/master/pycse/orgmode.py

Finally, it seems like a good idea to specify that we want our results to be an org drawer. This makes the figures/tables export, and allows us to generate math and other markup in our programs. That has the downside of making exported results not be in the "verbatim" markup I am used to, but that may be solvable in other ways. We can make the org drawer output the default like this:

(setq org-babel-default-header-args:python
      (cons '(:results . "output org drawer replace")
            (assq-delete-all :results org-babel-default-header-args)))

With these, using Python blocks in org-mode gets quite a bit better!

Here is the first example, with savefig. I have the savefig function return the link, so we have to print it. We use this feature later. The figure is automatically inserted to the buffer. Like magic!

Here is a fun figure from http://matplotlib.org/xkcd/examples/pie_and_polar_charts/polar_scatter_demo.html

import pycse.orgmode

import numpy as np
import matplotlib.pyplot as plt
plt.xkcd()

N = 150
r = 2 * np.random.rand(N)
theta = 2 * np.pi * np.random.rand(N)
area = 200 * r**2 * np.random.rand(N)
colors = theta

ax = plt.subplot(111, polar=True)
c = plt.scatter(theta, r, c=colors, s=area, cmap=plt.cm.hsv)
c.set_alpha(0.75)

print(plt.savefig('test.png'))

How about another example with show. This just prints the link directly. It seems to make sense to do it that way. This is from http://matplotlib.org/xkcd/examples/showcase/xkcd.html .

import pycse.orgmode as org

from matplotlib import pyplot as plt
import numpy as np

plt.xkcd()

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.xticks([])
plt.yticks([])
ax.set_ylim([-30, 10])

data = np.ones(100)
data[70:] -= np.arange(30)

plt.annotate(
    'THE DAY I REALIZED\nI COULD COOK BACON\nWHENEVER I WANTED',
    xy=(70, 1), arrowprops=dict(arrowstyle='->'), xytext=(15, -10))

plt.plot(data)

plt.xlabel('time')
plt.ylabel('my overall health')
plt.show()

# An intermediate result
print('Some intermediate result for x - 4 = 6:')
x = 6 + 4
org.fixed_width('x = {}'.format(x))

# And another figure
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.bar([-0.125, 1.0-0.125], [0, 100], 0.25)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.set_xticks([0, 1])
ax.set_xlim([-0.5, 1.5])
ax.set_ylim([0, 110])
ax.set_xticklabels(['CONFIRMED BY\nEXPERIMENT', 'REFUTED BY\nEXPERIMENT'])
plt.yticks([])

plt.title("CLAIMS OF SUPERNATURAL POWERS")

plt.show()

Some intermediate result for x - 4 = 6:

x = 10

See, the figures show where they belong, with intermediate results that have some formatting, and they export correctly. Nice.

1 A Figure from Python

It has been a long desire of mine to generate full figures with captions from code blocks, and to get them where I want like this one:

Figure 3: An italicized histogram of 10000 points

Here is the code to generate the full figure. Note we use the output of savefig as the filename. That lets us save some intermediate variable construction. That seems nice.

import pycse.orgmode as org
import matplotlib.pyplot as plt
plt.xkcd()

import numpy as np
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt

# example data
mu = 100 # mean of distribution
sigma = 15 # standard deviation of distribution
x = mu + sigma * np.random.randn(10000)

num_bins = 50
# the histogram of the data
n, bins, patches = plt.hist(x, num_bins, normed=1, facecolor='green', alpha=0.5)
# add a 'best fit' line
y = mlab.normpdf(bins, mu, sigma)
plt.plot(bins, y, 'r--')
plt.xlabel('Smarts')
plt.ylabel('Probability')
plt.title(r'Histogram of IQ: $\mu=100$, $\sigma=15$')

# Tweak spacing to prevent clipping of ylabel
plt.subplots_adjust(left=0.15)

org.figure(plt.savefig('smarts.png'),
           label='fig:1',
           caption='An italicized /histogram/ of {} points'.format(len(x)),
           attributes=[('LATEX', ':width 3in'),
                       ('HTML', ':width 300'),
                       ('ORG', ':width 300')])

That is pretty awesome. You cannot put figures in more than one place like this, and you might not want to mix results with this, but it is still pretty awesome!

2 An example table.

Finally, I have wanted the same thing for tables. Here is the resulting table.

Table 1: Dependence of the energy on the encut value.
ENCUT Energy (eV)
100 11.233
200 21.233
300 31.233
400 41.233
500 51.233

Here is the code block that generated it.

import pycse.orgmode as org

data = [['<5>', '<11>'],  # Column aligners
        ['ENCUT', 'Energy (eV)'],
        None]

for encut in [100, 200, 300, 400, 500]:
    data += [[encut, 1.233 + 0.1 * encut]]

org.table(data,
          name='table-1',
          caption='Dependence of the energy on the encut value.')

The only obvious improvement on this is similar to getting images to redisplay after running a code block, it might be nice to reformat tables to make sure they are pretty looking. Otherwise this is good.

Let's go ahead and try that. Here we narrow down to the results, and align the tables in that region.

(defun org-align-visible-tables ()
  "Align all the tables in the results."
  (let ((location (org-babel-where-is-src-block-result)) start)
    (when location
      (setq start (- location 1))
      (save-restriction
        (save-excursion
          (goto-char location) (forward-line 1)
          (narrow-to-region start (org-babel-result-end))
          (goto-char (point-min))
          (while (re-search-forward org-table-any-line-regexp nil t)
            (save-excursion (org-table-align))
            (or (looking-at org-table-line-regexp)
                (forward-char 1)))
          (re-search-forward org-table-any-border-regexp nil 1))))))

(add-hook 'org-babel-after-execute-hook
          (lambda () (org-align-visible-tables)))
lambda nil (org-align-visible-tables)
lambda nil (org-refresh-images)

And that seems to solve that problem now too!

3 Miscellaneous outputs

Here are some examples of getting org-output from the pycse.orgmode module.

import pycse.orgmode as org

org.verbatim('One liner verbatim')

org.verbatim('''multiline
output
   with indentation
       at a few levels
that is verbatim.''')

org.fixed_width('your basic result')

org.fixed_width('''your
  basic
    result
on a few lines.''')

# A latex block
org.latex('\(e^{i\pi} - 1 = 0\)')

org.org(r'The equation is \(E = h \nu\).')

One liner

multiline
output
   with indentation
       at a few levels
that is verbatim.
your basic result
your
  basic
    result
on a few lines.

The equation is \(E = h \nu\).

4 Summary

This looks promising to me. There are a few things to get used to, like always having org output, and some minor differences in making figures. On the whole this looks like a big improvement though! I look forward to working with it more.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter

When in python do as Pythonistas unless...

| categories: python | tags:

Many lisps have a when/unless conditional syntax that works like this:

(when t (print "when evaluated"))

(unless nil (print "unless evaluated"))
"when evaluated"

"unless evaluated"

Those are actually just macros that expand to the more verbose if function:

(macroexpand '(unless nil (print "unless evaluated")))
(if nil nil
  (print "unless evaluated"))

In Python, we only have this syntax for this kind of construct:

if True: print "when equivalent"

if not False: print "unless equivalent"
when equivalent
unless equivalent

I thought is would be fun to get as close as possible to the lisp syntax in Python. It is not that easy though. The benefit of a macro is we do not evaluate the arguments until they need to be evaluated. In Python, all arguments of functions are immediately evaluated.

One way to avoid this is to put code inside a function. Then it will not be executed until the function is called. So, here is an example of how to get an unless function in Python that conditionally evaluates a function.

def unless(condition, f):
    if not condition:
        return f()

def func():
    return "executed. Condition was not true."


print unless(1 > 0, func)

print unless(1 < 0, func)
None
executed. Condition was not true.

That is close, but requires us to wrap our code in a function. There does not seem to be any alternative though. It thought maybe a context manager could be used, but there does not seem to be a way to bypass the execution of the code (https://www.python.org/dev/peps/pep-0377/ ). Still, it might be a useful way to change how to think about doing some things differently in Python.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter

Automatic decorating of class methods to run them in a context

| categories: python | tags:

We previously examined approaches to running code in a context. With hy, we even managed to remove the need for a with statement through the use of a macro that expanded behind the scenes to manage the context. In our jasp code, we frequently need a context manager that temporarily changes the working directory to run some code, then changes back. The use of the context manager was a design decision to avoid decorating every single function. Why? There are a lot of functions that need decorating, and they are spread over a lot of files. Not all of the entries from the next block are methods, but most of them are.

from jasp import *

c = Vasp()
print(dir(c))
['__doc__', '__init__', '__module__', '__repr__', '__str__', 'add_to_db', 'archive', 'atoms', 'bader', 'bool_params', 'calculate', 'calculation_required', 'check_state', 'chgsum', 'clean', 'clone', 'create_metadata', 'dict', 'dict_params', 'exp_params', 'float_params', 'get_atoms', 'get_beefens', 'get_bz_k_points', 'get_charge_density', 'get_default_number_of_electrons', 'get_dipole_moment', 'get_eigenvalues', 'get_elapsed_time', 'get_electronic_temperature', 'get_elf', 'get_energy_components', 'get_fermi_level', 'get_forces', 'get_ibz_k_points', 'get_ibz_kpoints', 'get_infrared_intensities', 'get_k_point_weights', 'get_local_potential', 'get_magnetic_moment', 'get_magnetic_moments', 'get_name', 'get_nearest_neighbor_table', 'get_neb', 'get_nonselfconsistent_energies', 'get_number_of_bands', 'get_number_of_electrons', 'get_number_of_grid_points', 'get_number_of_ionic_steps', 'get_number_of_iterations', 'get_number_of_spins', 'get_occupation_numbers', 'get_orbital_occupations', 'get_potential_energy', 'get_property', 'get_pseudo_density', 'get_pseudo_wavefunction', 'get_pseudopotentials', 'get_required_memory', 'get_spin_polarized', 'get_stress', 'get_valence_electrons', 'get_version', 'get_vibrational_frequencies', 'get_vibrational_modes', 'get_xc_functional', 'initialize', 'input_params', 'int_params', 'is_neb', 'job_in_queue', 'json', 'list_params', 'name', 'nbands', 'org', 'output_template', 'plot_neb', 'positions', 'post_run_hooks', 'prepare_input_files', 'pretty_json', 'python', 'read', 'read_convergence', 'read_default_number_of_electrons', 'read_dipole', 'read_eigenvalues', 'read_electronic_temperature', 'read_energy', 'read_fermi', 'read_forces', 'read_ibz_kpoints', 'read_incar', 'read_k_point_weights', 'read_kpoints', 'read_ldau', 'read_magnetic_moment', 'read_magnetic_moments', 'read_metadata', 'read_nbands', 'read_number_of_electrons', 'read_number_of_iterations', 'read_occupation_numbers', 'read_outcar', 'read_potcar', 'read_relaxed', 'read_stress', 'read_version', 'read_vib_freq', 'register_post_run_hook', 'register_pre_run_hook', 'restart', 'restart_load', 'results', 'run', 'run_counts', 'set', 'set_atoms', 'set_nbands', 'set_results', 'special_params', 'string_params', 'strip', 'strip_warnings', 'todict', 'track_output', 'update', 'write_incar', 'write_kpoints', 'write_metadata', 'write_potcar', 'write_sort_file', 'xml']

The use of a context manager is really useful for a single calculation, and it saves us a lot of boilerplate code to manage changing directories. It limits us though for looping through calculations. We are stuck with traditional for loops that have the with statement embedded in them. We also cannot get too functional, e.g. with list comprehension.

In other words, this is ok:

E = []
for d in np.linspace(1, 1.5):
    atoms = Atoms(...,d)
    with jasp('calculated-name-{}'.format(d),
              ...,
              atoms=atoms) as calc:
        E.append(atoms.get_potential_energy())

But this code is not possible:

bond_lengths = np.linspace(1, 1.5)

A = [Atoms(...,d) for d in bond_lengths]

calcs = [JASP('calculated-name-{}'.format(d),...,atoms=atoms)
         for d, atoms in zip(bond-lengths, A)]

E = [atoms.get_potential_energy() for atoms in A]

It is not legal syntax to embed a with statement inside a list comprehension. The code will not work because to get the potential energy we have to switch into the calculation directory and read it from a file there, then switch back.

To make that possible, we need to decorate the class functions so that the right thing happens when needed. I still do not want to decorate each function manually. Although there is a case to make for it, as I mentioned earlier, the functions are all over the place, and numerous. Now is not the time to fix it.

Instead, we consider a solution that will automatically decorate class functions for us! Enter the Metaclass. This is a class that modifies how classes are created. The net effect of the code below is our Calculator class now has all functions automatically decorated with a function that changes to the working directory, runs the function, and then ensures we change back even in the event of an exception. This approach is adapted from http://stackoverflow.com/questions/3467526/attaching-a-decorator-to-all-functions-within-a-class .

I am pretty sure this is the right way to do this. We cannot simply decorate the functions of ase.calculators.vasp.Vasp because our decorator needs access to the directory defined in a class instance. That is what the init method of the metaclass enables.

We will put this code into a library called meta_calculator.py for reuse in later examples.

import os
import types

class WithCurrentDirectory(type):
   """Metaclass that decorates all of its methods except __init__."""
   def __new__(cls, name, bases, attrs):
      return super(WithCurrentDirectory, cls).__new__(cls, name, bases, attrs)

   def __init__(cls, name, bases, attrs):
      """Decorate all the methods of the class instance with the classmethod cd.

      We skip __init__ because that is where the attributes are actually set.
      It is an error to access them before they are set.
      """
      for attr_name, attr_value in attrs.iteritems():
         if attr_name != '__init__' and isinstance(attr_value, types.FunctionType):
            setattr(cls, attr_name, cls.cd(attr_value))


   @classmethod
   def cd(cls, func):
      """Decorator to temporarily run cls.func in the directory stored in cls.wd.

      The working directory is restored to the original directory afterwards.
      """
      def wrapper(self, *args, **kwargs):
         if self.verbose:
            print('\nRunning {}'.format(func.__name__))
            print("Started in {}".format(os.getcwd()))
         os.chdir(self.wd)
         if self.verbose:
            print("  Entered {}".format(os.getcwd()))
         try:
            result = func(self, *args, **kwargs)
            if self.verbose:
               print('  {}'.format(result))
            return result
         except Exception, e:
            # this is where you would use an exception handling function
            print('  Caught {}'.format(e))
            pass
         finally:
            os.chdir(self.owd)
            if self.verbose:
               print("  Exited to {}\n".format(os.getcwd()))

      wrapper.__name__ = func.__name__
      wrapper.__doc__ = func.__doc__
      return wrapper


class Calculator(object):
   """Class we use for a calculator.

   Every method is decorated by the metaclass so it runs in the working
   directory defined by the class instance.

   """

   __metaclass__ = WithCurrentDirectory

   def __init__(self, wd, verbose=False):
      self.owd = os.getcwd()
      self.wd = wd
      self.verbose = verbose
      if not os.path.isdir(wd):
         os.makedirs(wd)


   def create_input(self, **kwargs):
      with open('INCAR', 'w') as f:
         for key, val in kwargs.iteritems():
            f.write('{} = {}\n'.format(key, val))


   def exc(self):
      "This raises an exception to see what happens"
      1 / 0

   def read_input(self):
      with open('INCAR', 'r') as f:
         return f.read()

   def __str__(self):
      return 'In {}. Contains: {}'.format(os.getcwd(),
                                          os.listdir('.'))

Here is how we might use it.

from meta_calculator import *

c = Calculator('/tmp/calc-1', verbose=True)
print c.create_input(xc='PBE', encut=450)
print c.read_input()
print c.exc()
print c
Running create_input
Started in /Users/jkitchin/blogofile-jkitchin.github.com/_blog
  Entered /private/tmp/calc-1
  None
  Exited to /Users/jkitchin/blogofile-jkitchin.github.com/_blog

None

Running read_input
Started in /Users/jkitchin/blogofile-jkitchin.github.com/_blog
  Entered /private/tmp/calc-1
  xc = PBE
encut = 450

  Exited to /Users/jkitchin/blogofile-jkitchin.github.com/_blog

xc = PBE
encut = 450


Running exc
Started in /Users/jkitchin/blogofile-jkitchin.github.com/_blog
  Entered /private/tmp/calc-1
  Caught integer division or modulo by zero
  Exited to /Users/jkitchin/blogofile-jkitchin.github.com/_blog

None

Running __str__
Started in /Users/jkitchin/blogofile-jkitchin.github.com/_blog
  Entered /private/tmp/calc-1
  In /private/tmp/calc-1. Contains: ['INCAR']
  Exited to /Users/jkitchin/blogofile-jkitchin.github.com/_blog

In /private/tmp/calc-1. Contains: ['INCAR']

As we can see, in each function call, we evidently do change into the path that /tmp/calc-1 points to (it is apparently /private/tmp on Mac OSX), runs the method, and then changes back out, even when exceptions occur.

Here is a functional approach to using our new calculator.

from meta_calculator import *

encuts = [100, 200, 300, 400]
calcs = [Calculator('encut-{}'.format(encut)) for encut in encuts]

# list-comprehension for the side-effect
[calc.create_input(encut=encut) for calc,encut in zip(calcs, encuts)]

inputs = [calc.read_input() for calc in calcs]

print(inputs)
print([calc.wd for calc in calcs])
['encut = 100\n', 'encut = 200\n', 'encut = 300\n', 'encut = 400\n']
['encut-100', 'encut-200', 'encut-300', 'encut-400']

Sweet. And here is our evidence that the directories got created and have the files in them.

find . -type f -print | grep "encut-[1-4]00" | xargs -n 1 -I {} -i bash -c 'echo {}; cat {}; echo'
./encut-100/INCAR
encut = 100

./encut-200/INCAR
encut = 200

./encut-300/INCAR
encut = 300

./encut-400/INCAR
encut = 400

This looks like another winner that will be making its way into jasp soon. I guess it will require at least some minor surgery on a class in ase.calculators.vasp. It might be time to move a copy of it all the way into jasp.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter

Another approach to docstrings and validation of args and kwargs in Python

| categories: python | tags:

We have been exploring various ways to add documentation and validation to arbitrary arguments that our molecular simulation codes use. In our previous we derived a method where we created functions that provide docstrings, and validate the input. One issue we had was the duplication of keywords and function names. Here we consider an approach that allows this kind of syntax:

calc = Calculator('/tmp',
                  encut(400),
                  xc('PBE'),
                  generic('kpts', [1, 1, 1]))

Those are regular *args, not **kwargs.

Compare this to:

calc = Calculator('/tmp',
                  encut=encut(400),
                  xc=xc('PBE'),
                  kpts=generic('kpts', [1, 1, 1]))

where those are kwargs. The duplication of the keywords is what we aim to eliminate, because 1) they are redundant, 2) why type things twice?

Here we work out an approach with *args that avoids the duplication. We use the same kind of validation functions as before, but we will decorate each one so it returns a tuple of (key, value), where key is based on the function name. This is so we don't have to duplicate the function name ourselves; we let the decorator do it for us. Then, in our Calculator class init function, we use this tuple to assign the values to self.key as the prototypical way to handle the *args. Other setter functions could also be used.

Here is the template for this approach.

def input(func):
    """Input decorator to convert a validation function to input function."""
    def inner(*args, **kwargs):
        res = func.__name__, func(*args, **kwargs)
        print('{} validated to {}'.format(func.__name__, res))
        return res
    # magic incantations to make the decorated function look like the old one.
    inner.__name__ = func.__name__
    inner.__doc__ = func.__doc__
    return inner

@input
def encut(cutoff):
    "Planewave cutoff in eV."
    assert isinstance(cutoff, int) and (cutoff > 0)
    return cutoff

@input
def xc(s):
    """Exchange-correlation functional.

    Should be 'PBE' or 'LDA'

    """
    assert isinstance(s, str)
    assert s in ['PBE', 'LDA']
    return s

def generic(key, val):
    """Generic function with no validation.

    Use this for other key,val inputs not yet written.

    """
    return (key, val)

class Calculator(object):
    def __init__(self, wd, *args):
        """each arg should be of the form (attr, val)."""
        self.wd = wd
        self.args = args
        for attr, val in args:
            setattr(self, attr, val)

    def __str__(self):
        return '\n'.join(['{}'.format(x) for x in self.args])

##################################################################

calc = Calculator('/tmp',
                  encut(400),
                  xc('PBE'),
                  generic('kpts', [1, 1, 1]))

print(calc)

print(help(encut))
encut validated to ('encut', 400)
xc validated to ('xc', 'PBE')
('encut', 400)
('xc', 'PBE')
('kpts', [1, 1, 1])
Help on function encut in module __main__:

encut(*args, **kwargs)
    Planewave cutoff in eV.

None

This approach obviously works. I don't think I like the syntax as much, although in most python editors, it should directly give access to the docstrings of the functions. This is pretty explicit in what is happening, which is an advantage. Compare this to the following approach, which uses our traditional kwarg syntax, with dynamic, but hidden validation.

def encut(cutoff):
    "Planewave cutoff in eV."
    assert isinstance(cutoff, int) and (cutoff > 0)
    return cutoff

def xc(s):
    """Exchange-correlation functional.

    Should be 'PBE' or 'LDA'.

    """
    assert isinstance(s, str), "xc should be a string"
    assert s in ['PBE', 'LDA'], "xc should be 'PBE' or 'LDA'"
    return s


class Calculator(object):
    def __init__(self, wd, **kwargs):
        """each arg should be of the form (attr, val)."""
        self.wd = wd

        for kwarg, val in kwargs.iteritems():
            f = globals().get(kwarg, None)
            if f is not None:
                print('{} evaluated to {}'.format(kwarg, f(val)))
            else:
                print('No validation for {}'.format(kwarg))

            setattr(self, kwarg, val)

##################################################################

calc = Calculator('/tmp',
                  encut=400,
                  xc='PBE',
                  kpts=[1, 1, 1])

print(calc.encut)
help(xc)
xc evaluated to PBE
No validation for kpts
encut evaluated to 400
400
Help on function xc in module __main__:

xc(s)
    Exchange-correlation functional.

    Should be 'PBE' or 'LDA'.

The benefit of this approach is no change in the syntax we are used to. We still get access to docstrings via tools like pydoc. It should not be too hard to get helpful tooltips in Emacs for this, using pydoc to access the docstrings. This might be the winner.

It is up for debate if we should use assert or Exceptions. If anyone uses python with -O the assert statements are ignored. That might not be desirable though. Maybe it would be better to use Exceptions, with a user customizable variable that determines if validation is performed.

Copyright (C) 2016 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

Discuss on Twitter
« Previous Page -- Next Page »