Tutorial 2: Fields and Operators

This tutorials covers the basics of setting up and interacting with field and operator objects in Dedalus.

First, we’ll import the public interface and suppress some of the logging messages:

In [1]:
from dedalus import public as de
import numpy as np
import matplotlib.pyplot as plt

de.logging_setup.rootlogger.setLevel('ERROR')
%matplotlib inline

2.1: Fields

Creating a field

Field objects represent scalar fields defined over a domain. A field can be directly instantiated from the Field class by passing a domain object, or using the domain.new_field method. Let’s set up a 2D domain and build a field:

In [2]:
xbasis = de.Fourier('x', 64, interval=(0,2*np.pi), dealias=3/2)
ybasis = de.Chebyshev('y', 64, interval=(-1,1), dealias=3/2)
domain = de.Domain([xbasis, ybasis], grid_dtype=np.float64, mesh=[1])
f = domain.new_field(name='f')

We also gave the field a name – something which is automatically done for the state fields when solving a problem in Dedalus (we’ll see more about this in the next notebook), but we’ve just done it manually, for now.

Manipulating field data

The layout attribute of each field is a reference to the layout object (discussed in tutorial 1) describing the current transform/distribution state of that field’s data.

In [3]:
f.layout.grid_space
Out[3]:
array([False, False])

Field data can be written and retrieved in any layout by indexing a field a layout object. In most cases it’s just the full grid and full coefficient data that’s most useful to interact with, and these layouts can be easily accessed using 'g' and 'c' keys as shortcuts. Field objects also have several methods that can be used to convert the data between layouts.

Remember that when accessing field data, you’re manipulating just the local data of the globally distributed dataset. Details about the data distribution are available through methods on the layout objects.

Let’s assign our field some data using the local domain grids.

In [4]:
x, y = domain.grids(scales=1)
# Set data in grid space using 'g' key
f['g'] = np.cos(4*x) / np.cosh(3*y)

And let’s take a look at this data using some helper functions from the plot_tools module.

In [5]:
from dedalus.extras.plot_tools import plot_bot_2d
In [6]:
plot_bot_2d(f);
../_images/notebooks_dedalus_tutorial_fields_operators_16_0.png

Now let’s take a look at the magnitude of the coefficients:

In [7]:
# Convert data to coefficient space by indexing with 'c' key
f['c']
# Plot log of magnitude of data
def log_magnitude(xmesh, ymesh, data):
    return xmesh, ymesh, np.log10(np.abs(data))
plot_bot_2d(f, func=log_magnitude);
../_images/notebooks_dedalus_tutorial_fields_operators_18_0.png

We can see that the specified function is pretty well-resolved.

In addition to the key interface, we can change the layout of a field using the require_coeff_space, require_grid_space, require_layout, and require_local methods.

Field scale factors

The set_scales method is used to change the scaling factors used when transforming field data into grid space. When setting a field’s data using grid arrays, shape errors will result if there is a mismatch between the field and grid’s scale factors.

Large scale factors can be used to interpolate the field data onto a high-resolution grid, while small scale factors can be used to view a lower-resolution grid representation of a field. Beware: using scale factors less than 1 will result in a loss of data when transforming to grid space.

Let’s take a look at a high-resolution sampling of our field, by increasing the scales.

In [8]:
f.set_scales(2)
f['g']
plot_bot_2d(f);
../_images/notebooks_dedalus_tutorial_fields_operators_22_0.png

2.2: Operators

Arithmetic with fields

Mathematical operations on fields, such as arithmetic, differentiation, integration, and interpolation, are represented by operator classes. An instance of an operator class represents a specific mathematical operation, and provides an interface for the deferred evaluation of that operation with respect to it’s potentially evolving arguments.

Arithmetic operations between fields, or fields and scalars, are produced simply using Python’s infix operators for arithmetic. Let’s start with a very simple example of adding a scalar to the field we previously defined.

In [9]:
add_op = 10 + f
add_op
Out[9]:
Add(<Scalar 4644516864>, <Field 4603650344>)

The object we get is not another field, but an operator object representing the addition of 10 and our field. To actually compute this operation, we use the evaluate method, which returns a new field with the result. The dealias scale factors set during basis instantiation are used for the evaluation of all operators.

In [10]:
add_result = add_op.evaluate()
add_result.require_grid_space()
plot_bot_2d(add_result);
../_images/notebooks_dedalus_tutorial_fields_operators_28_0.png

Building expressions

Operator instances can be passed as arguments to other operators, building trees that represent more complicated expressions:

In [11]:
tree_op = 2*add_op + f*f
tree_op
Out[11]:
Add(Mul(<Scalar 4645745944>, Add(<Scalar 4644516864>, <Field 4603650344>)), Mul(<Field 4603650344>, <Field 4603650344>))

Reading these signatures can be a little cumbersome, but we can plot the operator’s structure using a helper from dedalus.tools:

In [12]:
from dedalus.tools.plot_op import plot_operator
plot_operator(tree_op, figsize=8, fontsize=12)
../_images/notebooks_dedalus_tutorial_fields_operators_33_0.png

And evaluating it:

In [13]:
tree_result = tree_op.evaluate()
tree_result.require_grid_space
plot_bot_2d(tree_result);
../_images/notebooks_dedalus_tutorial_fields_operators_35_0.png

Deferred evaluation

A key point is that the operator objects symbolically represent an operation on the field arguments, and are evaluated using deferred evaluation. If we change the data of the field arguments and re-evaluate an operator, we get a new result.

In [14]:
# Set scales back to 1 to build new grid data
f.set_scales('1', keep_data=False)
f['g'] = np.sin(3*x) * np.cos(6*y)
plot_bot_2d(f);
../_images/notebooks_dedalus_tutorial_fields_operators_38_0.png
In [15]:
tree_result = tree_op.evaluate()
tree_result.require_grid_space
plot_bot_2d(tree_result);
../_images/notebooks_dedalus_tutorial_fields_operators_39_0.png

Differentiation, integration, interpolation

In addition to arithmetic, operators are used for differentiation, integration, and interpolation. There are three ways to apply these operators to a field.

First, the operators performing these operations along a given dimension of the data can be accessed through the basis.Differentiate, basis.Integrate, and basis.Interpolate methods of the corresponding basis object.

In [16]:
# Let's change the data back to our first example, with a constant offset
f.set_scales('1', keep_data=False)
f['g'] = np.cos(4*x) / np.cosh(3*y) + 1

# Let's setup some operations using the basis operators
fx_op = ybasis.Differentiate(f)
int_f_op = ybasis.Integrate(f)
f0_op = ybasis.Interpolate(f, position=0)

# And we'll just evaluate and plot one of them
fx = fx_op.evaluate()
fx.require_grid_space()
plot_bot_2d(fx);
../_images/notebooks_dedalus_tutorial_fields_operators_42_0.png

Second, more general interfaces taking bases as arguments are available through the operators module. The operators.differentiate factory allows us to easily construct higher-order and mixed derivatives involving different bases. The operators.integrate and operators.interpolate factories allow us to integrate/interpolate along multiple axes, as well.

In [17]:
# Some examples using the constructors from the operators module
fxxyy_op = de.operators.differentiate(f, x=2, y=2)
total_int_f_op = de.operators.integrate(f, 'x', 'y')
f00_op = de.operators.interpolate(f, x=0, y=0)

fxxyy = fxxyy_op.evaluate()
total_int_f = total_int_f_op.evaluate()
f00 = f00_op.evaluate()

print('Total integral:', total_int_f['g'][0,0])
print('f at (x,y)=(0,0):', f00['g'][0,0])

fxxyy.require_grid_space()
plot_bot_2d(fxxyy);
Total integral: 12.566370614359172
f at (x,y)=(0,0): 1.9999999999999727
../_images/notebooks_dedalus_tutorial_fields_operators_44_1.png

Third, the differentiate, integrate, and interpolate field methods provide short-cuts for building and evaluating these operations, directly returning the resulting fields.

In [18]:
# The same results, directly from field methods
fxxyy = f.differentiate(x=2, y=2)
total_int_f = f.integrate('x', 'y')
f00 = f.interpolate(x=0, y=0)

print('Total integral:', total_int_f['g'][0,0])
print('f at (x,y)=(0,0):', f00['g'][0,0])

fxxyy.require_grid_space()
plot_bot_2d(fxxyy);
Total integral: 12.566370614359172
f at (x,y)=(0,0): 1.9999999999999727
../_images/notebooks_dedalus_tutorial_fields_operators_46_1.png