Note

This tutorial was generated from a Jupyter notebook that can be downloaded here.

Lazy versus greedy evaluation

tl;dr

Version 1.0 of the code evaluates things lazily by default, meaning that all internal values are nodes in a graph, stored as theano tensors. Lazy mode is required for interfacing with pymc3 to do inference (refer to the several tutorials on pymc3 sampling). If you really need the value of a theano object, you can always call its eval() method, but keep in mind that operation can be somewhat slow.

If, on the other hand, you’re not interested in using pymc3 or in any of the derivatives of starry models, you can disable lazy evaluation by typing

starry.config.lazy = False

at the top of your script, before you instantiate any starry maps. If you do that, starry will behave as it did in previous versions: you don’t have to call the eval() method or worry about any tensor nonsense.

Lazy mode

One of the big changes in version 1.0 of starry is lazy evaluation mode, which is now the default. Lazy evaluation means that the evaluation of all expressions in the code is delayed until a numerical value is needed (i.e., when outputting or plotting the result). This is as opposed to greedy or eager evaluation, in which all expressions are evaluated on-the-fly, as soon as the code encounters them. In lazy evaluation mode, expressions are compiled and stored in memory as nodes in a graph, which are only executed when a numerical value is required. This strategy allows for some cool compile-time optimization under the hood. But by far the greatest advantage of lazy evaluation (at least in our case) is that it makes it easy to autodifferentiate expressions using backpropagation. This lets us compute derivatives of all expressions extremely efficiently, and those can be seemlessly integrated into derivative-based MCMC sampling schemes such as Hamiltonian Monte Carlo or NUTS.

Version 1.0 of starry is built on top of the theano machine learning library, which handles all of the graph compiling and backpropagation. There’s lots of other software that does similar things (such as tensorflow and pytorch), but the advantage of theano is that it is also the backbone of exoplanet and pymc3. This allows us to easily integrate starry with all the cool inference machinery of those two packages.

Let’s look at some examples of how lazy evaluation works in starry. Let’s instantiate a regular starry map:

[4]:
import starry

map = starry.Map(ydeg=1)

We can give this map a simple dipole by assigning a value to the coefficient of the \(Y_{1,0}\) spherical harmonic:

[5]:
map[1, 0] = 0.5

Since the coefficient of the \(Y_{0,0}\) harmonic is fixed at unity, our spherical harmonic coefficients are now the vector \(y = (1, 0, \frac{1}{2}, 0)\). Here’s what that looks like:

[6]:
map.show()
../../_images/notebooks_LazyGreedy_11_0.png

Recall that the spherical harmonic coefficients are stored in the y attribute of the map. Let’s take a look:

[7]:
map.y
[7]:
AdvancedIncSubtensor1{no_inplace,set}.0

That doesn’t look right, but it is: the vector \(y\) is stored internally as a theano tensor and doesn’t yet have a numerical value:

[8]:
type(map.y)
[8]:
theano.tensor.var.TensorVariable

In order to access its value, I can call its eval method:

[9]:
map.y.eval()
[9]:
array([1. , 0. , 0.5, 0. ])

Which is what we expected.

A similar thing happens when we call a method such as flux:

[10]:
map.flux(xo=0.4, yo=0.3, ro=0.1, theta=30)
[10]:
Elemwise{mul,no_inplace}.0
[11]:
map.flux(xo=0.4, yo=0.3, ro=0.1, theta=30).eval()
[11]:
array([1.48179813])

As we mentioned above, it’s not generally a good idea to call the eval() method, since it can be quite slow. The whole point of lazy evaluation mode is so that starry can be easily integrated with pymc3. When building a pymc3 model, pymc3 handles all of the evaluations internally, so there’s no need to call eval(). Within a pymc3 model context, users can pass pymc3 variables, theano variables, and/or numpy arrays to any starry method; casting is handled internally in all cases. Check out the tutorials on inference with pymc3 for more information.

If, on the other hand, you’re not planning on integrating starry with pymc3, you should probably run it in greedy mode. See below.

Greedy mode

To run starry in greedy (i.e., not lazy) mode, you can add the following line somewhere near the top of your script:

[13]:
starry.config.lazy = False

(Note that if you try to change the evaluation mode after you’ve instantiated a starry map, the code will complain.)

In greedy mode, things behave as they did in previous versions of the code. Check it out:

[14]:
map = starry.Map(ydeg=1)
[15]:
map[1, 0] = 0.5
[16]:
map.y
[16]:
array([1. , 0. , 0.5, 0. ])
[17]:
type(map.y)
[17]:
numpy.ndarray

All methods are automatically compiled and return numerical outputs:

[18]:
map.flux(xo=0.4, yo=0.3, ro=0.1, theta=30)
[18]:
array([1.48179813])

Because theses methods are autocompiled, it’s much faster to use greedy mode than to call eval() every time.