pyro: a python hydro code

http://github.com/zingale/pyro2

_images/pyro_plots.png

Introduction to pyro

_images/pyro_plots.png

pyro is a simple framework for implementing and playing with hydrodynamics solvers. It is designed to provide a tutorial for students in computational astrophysics (and hydrodynamics in general). We introduce simple implementations of some popular methods used in the field, with the code written to be easily understandable. All simulations use a single grid (no domain decomposition).

Note

pyro is not meant for demanding scientific simulations—given the choice between performance and clarity, clarity is taken.

pyro builds off of a finite-volume framework for solving PDEs. There are a number of solvers in pyro, allowing for the solution of hyperbolic (wave), parabolic (diffusion), and elliptic (Poisson) equations. In particular, the following solvers are developed:

  • linear advection
  • compressible hydrodynamics
  • multigrid
  • implicit thermal diffusion
  • incompressible hydrodynamics
  • low Mach number atmospheric hydrodynamics

Runtime visualization shows the evolution as the equations are solved.

In the pages that follow, the following format is adopted:

  • PDF notes provide the basic theory behind the methods. References are cited to provide more detail.
  • An overview of the use of the applicable modules from pyro provided.
  • Exercises interspersed fill in some of the detail.

Setting up pyro

You can clone pyro from github: http://github.com/zingale/pyro2

Note

It is strongly recommended that you use python 3.x. While python 2.x might still work, we do not test pyro under python 2, so it may break at any time in the future.

The following python packages are required:

  • numpy
  • matplotlib
  • f2py (part of NumPy)
  • pytest (for unit tests)

Additionally, you will need gfortran installed on your computer, for f2py to be able to compile the code.

The following steps are needed before running pyro:

  • add pyro/ to your PYTHONPATH environment variable. For the bash shell, this is done as:
export PYTHONPATH="/path/to/pyro/:${PYTHONPATH}"
  • build the Fortran modules, by running the mk.sh script. It should be sufficient to just do:
./mk.sh
  • define the environment variable PYRO_HOME to point to the pyro2/ directory (only needed for regression testing)
export PYRO_HOME="/path/to/pyro/"

Quick test

Run the advection solver to quickly test if things are setup correctly:

./pyro.py advection smooth inputs.smooth

You should see a plot window pop up with a smooth pulse advecting diagonally through the periodic domain.

Notes on the numerical methods

Detailed discussions and derivations of the numerical methods used in pyro are given in the set of notes Introduction to Computational Astrophysical Hydrodynamics, part of the Open Astrophysics Bookshelf.

Design ideas

pyro is written primarily in python (by default, we expect python 3), with a few low-level routines written in Fortran for performance. The numpy package is used for representing arrays throughout the python code and the matplotlib library is used for visualization. We use f2py (part of NumPy) to interface with some Fortran code. Finally, pytest is used for unit testing of some components.

All solvers are written for a 2-d grid. This gives a good balance between complexity and speed.

A paper describing the design philosophy of pyro was accepted to Astronomy & Computing [paper link].

Directory structure

The files for each solver are in their own sub-directory, with additional sub-directories for the mesh and utilities. Each solver has two sub-directories: problems/ and tests/. These store the different problem setups for the solver and reference output for testing.

Your PYTHONPATH environment variable should be set to include the top-level pyro2/ directory.

The overall structure is:

  • pyro2/: This is the top-level directory. The main driver, pyro.py, is here, and all pyro simulations should be run from this directory.
  • advection/: The linear advection equation solver using the CTU method. All advection-specific routines live here.
    • problems/: The problem setups for the advection solver.
    • tests/: Reference advection output files for comparison and regression testing.
  • advection_rk/: The linear advection equation solver using the method-of-lines approach.
    • problems/: This is a symbolic link to the advection/problems/ directory.
    • tests/: Reference advection output files for comparison and regression testing.
  • analysis/: Various analysis scripts for processing pyro output files.
  • compressible/: The compressible hydrodynamics solver using the CTU method. All source files specific to this solver live here.
    • problems/: The problem setups for the compressible hydrodynamics solver.
    • tests/: Reference compressible hydro output for regression testing.
  • compressible_rk/: The compressible hydrodynamics solver using method of lines integration.
    • problems/: This is a symbolic link to the compressible/problems/ directory.
    • tests/: Reference compressible hydro output for regression testing.
  • diffusion/: The implicit (thermal) diffusion solver. All diffusion-specific routines live here.
    • problems/: The problem setups for the diffusion solver.
    • tests/: Reference diffusion output for regression testing.
  • incompressible/: The incompressible hydrodynamics solver. All incompressible-specific routines live here.
    • problems/: The problem setups for the incompressible solver.
    • tests/: Reference incompressible hydro output for regression testing.
  • lm_atm/: The low Mach number hydrodynamics solver for atmospherical flows. All low-Mach-specific files live here.
    • problems/: The problem setups for the low Mach number solver.
    • tests/: Reference low Mach hydro output for regression testing.
  • mesh/: The main classes that deal with 2-d cell-centered grids and the data that lives on them. All the solvers use these classes to represent their discretized data.
  • multigrid/: The multigrid solver for cell-centered data. This solver is used on its own to illustrate how multigrid works, and directly by the diffusion and incompressible solvers.
    • problems/: The problem setups for when the multigrid solver is used in a stand-alone fashion.
    • tests/: Reference multigrid solver solutions (from when the multigrid solver is used stand-alone) for regression testing.
  • swe/: The shallow water solver.
    • problems/: The problem setups for the shallow water solver.
    • tests/: Reference shallow water output for regression testing.
  • util/: Various service modules used by the pyro routines, including runtime parameters, I/O, profiling, and pretty output modes.

Fortran

Fortran is used to speed up some critical portions of the code, and in many cases, provides more clarity than trying to write optimized python code using array operations in numpy. The Fortran code seemlessly integrates into python using f2py.

Wherever Fortran is used, we enforce the following design rule: the Fortran functions must be completely self-contained, with all information coming through the interface. No external dependencies are allowed. Each pyro module will have (at most) a single Fortran file and can be compiled into a library via a single f2py command line invocation.

A single script, mk.sh, in the top-level directory will compile all the Fortran source.

Main driver

All the solvers use the same driver, the main pyro.py script. The flowchart for the driver is:

  • parse runtime parameters
  • setup the grid (initialize() function from the solver)
    • initialize the data for the desired problem (init_data() function from the problem)
  • do any necessary pre-evolution initialization (preevolve() function from the solver)
  • evolve while t < tmax and n < max_steps
    • fill boundary conditions (fill_BC_all() method of the CellCenterData2d class)
    • get the timestep (compute_timestep() calls the solver’s method_compute_timestep() function from the solver)
    • evolve for a single timestep (evolve() function from the solver)
    • t = t + dt
    • output (write() method of the CellCenterData2d class)
    • visualization (dovis() function from the solver)
  • call the solver’s finalize() function to output any useful information at the end

This format is flexible enough for the advection, compressible, diffusion, and incompressible evolution solver. Each solver provides a Simulation class that provides the following methods (note: inheritance is used, so many of these methods come from the base NullSimulation class):

  • compute_timestep: return the timestep based on the solver’s specific needs (through method_compute_timestep()) and timestepping parameters in the driver
  • dovis: performs visualization of the current solution
  • evolve: advances the system of equations through a single timestep
  • finalize: any final clean-ups, printing of analysis hints.
  • finished: return True if we’ve met the stopping criteria for a simulation
  • initialize: sets up the grid and solution variables
  • method_compute_timestep: returns the timestep for evolving the system
  • preevolve: does any initialization to the fluid state that is necessary before the main evolution. Not every solver will need something here.
  • read_extras: read in any solver-specific data from a stored output file
  • write: write the state of the simulation to an HDF5 file
  • write_extras: any solver-specific writing

Each problem setup needs only provide an init_data() function that fills the data in the patch object.

Running

All the solvers are run through the pyro.py script. This takes 3 arguments: the solver name, the problem setup to run with that solver (this is defined in the solver’s problems/ sub-directory), and the inputs file (again, usually from the solver’s problems/ directory).

For example, to run the Sedov problem with the compressible solver we would do:

./pyro.py compressible sedov inputs.sedov

This knows to look for inputs.sedov in compressible/problems/ (alternately, you can specify the full path for the inputs file).

To run the smooth Gaussian advection problem with the advection solver, we would do:

./pyro.py advection smooth inputs.smooth

Any runtime parameter can also be specified on the command line, after the inputs file. For example, to disable runtime visualization for the above run, we could do:

./pyro.py advection smooth inputs.smooth vis.dovis=0

Note

Quite often, the slowest part of the runtime is the visualization, so disabling vis as shown above can dramatically speed up the execution. You can always plot the results after the fact using the plot.py script, as discussed in Analysis routines.

Runtime options

The behavior of the main driver, the solver, and the problem setup can be controlled by runtime parameters specified in the inputs file (or via the command line). Runtime parameters are grouped into sections, with the heading of that section enclosed in [ .. ]. The list of parameters are stored in three places:

  • the pyro/_defaults file
  • the solver’s _defaults file
  • problem’s _defaults file (named _problem-name.defaults in the solver’s problem/ sub-directory).

These three files are parsed at runtime to define the list of valid parameters. The inputs file is read next and used to override the default value of any of these previously defined parameters. Additionally, any parameter can be specified at the end of the commandline, and these will be used to override the defaults. The collection of runtime parameters is stored in a RuntimeParameters object.

The runparams.py module in util/ controls access to the runtime parameters. You can setup the runtime parameters, parse an inputs file, and access the value of a parameter (hydro.cfl in this example) as:

rp = RuntimeParameters()
rp.load_params("inputs.test")
...
cfl = rp.get_param("hydro.cfl")

When pyro is run, the file inputs.auto is output containing the full list of runtime parameters, their value for the simulation, and the comment that was associated with them from the _defaults files. This is a useful way to see what parameters are in play for a given simulation.

All solvers use the following parameters:

[driver]
max_steps the maximum number of steps in the simulation
tmax the simulation time to evolve to
init_tstep_factor the amount by which to shrink the first timestep. This lets the code ramp up to the CFL timestep slowly
max_dt_change the maximum factor by which the timestep can increase from one step to the next
[io]
basename the descriptive prefix to use for output files
dt_out the interval in simulation time between writing output files
n_out the number of timesteps between writing output files
[vis]
dovis enable (1) or disable (0) runtime visualization
store_images if 1, write out PNG files as we do the runtime visualization
n_out the number of timesteps between writing output files
[mesh]
xmin the physical coordinate of the lower x face of the domain
xmax the physical coordinate of the upper x face of the domain
ymin the physical coordinate of the lower y face of the domain
ymax the physical coordinate of the upper y face of the domain
xlboundary the physical description for the type of boundary at the lower x face of the domain
xrboundary the physical description for the type of boundary at the upper x face of the domain
ylboundary the physical description for the type of boundary at the lower y face of the domain
yrboundary the physical description for the type of boundary at the upper y face of the domain
nx the number zones in the x-direction
ny the number zones in the y-direction

Working with output

Utilities

Several simply utilities exist to operate on output files

  • compare.py: this script takes two plot files and compares them zone-by-zone and reports the differences. This is useful for testing, to see if code changes affect the solution. Many problems have stored benchmarks in their solver’s tests directory. For example, to compare the current results for the incompressible shear problem to the stored benchmark, we would do:

    ./compare.py shear_128_0216.pyro incompressible/tests/shear_128_0216.pyro
    

    Differences on the order of machine precision may arise because of optimizations and compiler differences across platforms. Students should familiarize themselves with the details of how computers store numbers (floating point). An excellent read is What every computer scientist should know about floating-point arithmetic by D. Goldberg.

  • plot.py: this script uses the solver’s dovis() routine to plot an output file. For example, to plot the data in the file shear_128_0216.pyro from the incompressible shear problem, you would do:

    ./plot.py -o image.png shear_128_0216.pyro
    

    where the -o option allows you to specify the output file name.

Reading and plotting manually

pyro data can be read using the patch.read method. The following sequence (done in a python session) reads in stored data (from the compressible Sedov problem) and plots data falling on a line in the x direction through the y-center of the domain (note: this will include the ghost cells).

import matplotlib.pyplot as plt
import util.io as io
sim = io.read("sedov_unsplit_0000.h5")
dens = sim.cc_data.get_var("density")
plt.plot(dens.g.x, dens[:,dens.g.ny//2])
plt.show()
_images/manual_plot.png

Note: this includes the ghost cells, by default, seen as the small regions of zeros on the left and right.

Adding a problem

The easiest way to add a problem is to copy an existing problem setup in the solver you wish to use (in its problems/ sub-directory). Three different files will need to be copied (created):

  • problem.py: this is the main initialization routine. The function init_data() is called at runtime by the Simulation object’s initialize() method. Two arguments are passed in, the simulation’s CellCenterData2d object and the RuntimeParameters object. The job of init_data() is to fill all of the variables defined in the CellCenterData2d object.
  • _problem.defaults: this contains the runtime parameters and their defaults for your problem. They should be placed in a block with the heading [problem] (where problem is your problem’s name). Anything listed here will be available through the RuntimeParameters object at runtime.
  • inputs.problem: this is the inputs file that is used at runtime to set the parameters for your problem. Any of the general parameters (like the grid size, boundary conditions, etc.) as well as the problem-specific parameters can be set here. Once the problem is defined, you need to add the problem name to the __all__ list in the __init__.py file in the problems/ sub-directory. This lets python know about the problem.

Mesh overview

All solvers are based on a finite-volume/cell-centered discretization. The basic theory of such methods is discussed in Notes on the numerical methods.

Note

The core data structure that holds data on the grid is CellCenterData2d. This does not distinguish between cell-centered data and cell-averages. This is fine for methods that are second-order accurate, but for higher-order methods, the FV2d class has methods for converting between the two data centerings.

mesh.patch implementation and use

We import the basic mesh functionality as:

import mesh.patch as patch
import mesh.fv as fv
import mesh.boundary as bnd
import mesh.array_indexer as ai

There are several main objects in the patch class that we interact with:

  • patch.Grid2d: this is the main grid object. It is basically a container that holds the number of zones in each coordinate direction, the domain extrema, and the coordinates of the zones themselves (both at the edges and center).
  • patch.CellCenterData2d: this is the main data object—it holds cell-centered data on a grid. To build a CellCenterData2d object you need to pass in the Grid2d object that defines the mesh. The CellCenterData2d object then allocates storage for the unknowns that live on the grid. This class also provides methods to fill boundary conditions, retrieve the data in different fashions, and read and write the object from/to disk.
  • fv.FV2d: this is a special class derived from patch.CellCenterData2d that implements some extra functions needed to convert between cell-center data and averages with fourth-order accuracy.
  • bnd.BC: This is simply a container that holds the names of the boundary conditions on each edge of the domain.
  • ai.ArrayIndexer: This is a class that subclasses the NumPy ndarray and makes the data in the array know about the details of the grid it is defined on. In particular, it knows which cells are valid and which are the ghost cells, and it has methods to do the \(a_{i+1,j}\) operations that are common in difference methods.
  • integration.RKIntegrator: This class implements Runge-Kutta integration in time by managing a hierarchy of grids at different time-levels. A Butcher tableau provides the weights and evaluation points for the different stages that make up the integration.

The procedure for setting up a grid and the data that lives on it is as follows:

myg = patch.Grid2d(16, 32, xmax=1.0, ymax=2.0)

This creates the 2-d grid object myg with 16 zones in the x-direction and 32 zones in the y-direction. It also specifies the physical coordinate of the rightmost edge in x and y.

mydata = patch.CellCenterData2d(myg)

bc = bnd.BC(xlb="periodic", xrb="periodic", ylb="reflect-even", yrb="outflow")

mydata.register_var("a", bc)
mydata.create()

This creates the cell-centered data object, mydata, that lives on the grid we just built above. Next we create a boundary condition object, specifying the type of boundary conditions for each edge of the domain, and finally use this to register a variable, a that lives on the grid. Once we call the create() method, the storage for the variables is allocated and we can no longer add variables to the grid. Note that each variable needs to specify a BC—this allows us to do different actions for each variable (for example, some may do even reflection while others may do odd reflection).

Jupyter notebook

A Jupyter notebook that illustrates some of the basics of working with the grid is provided as mesh-examples.ipynb. This will demonstrate, for example, how to use the ArrayIndexer methods to construct differences.

Tests

The actual filling of the boundary conditions is done by the fill_BC() method. The script bc_demo.py tests the various types of boundary conditions by initializing a small grid with sequential data, filling the BCs, and printing out the results.

Mesh examples

this notebook illustrates the basic ways of interacting with the pyro2 mesh module. We create some data that lives on a grid and show how to fill the ghost cells. The pretty_print() function shows us that they work as expected.

In [1]:
from __future__ import print_function
import numpy as np
import mesh.boundary as bnd
import mesh.patch as patch
import matplotlib.pyplot as plt
%matplotlib inline

# for unit testing, we want to ensure the same random numbers
np.random.seed(100)

Setup a Grid with Variables

There are a few core classes that we deal with when creating a grid with associated variables:

  • Grid2d : this holds the size of the grid (in zones) and the physical coordinate information, including coordinates of cell edges and centers
  • BC : this is a container class that simply holds the type of boundary condition on each domain edge.
  • ArrayIndexer : this is an array of data along with methods that know how to access it with different offsets into the data that usually arise in stencils (like {i+1, j})
  • CellCenterData2d : this holds the data that lives on a grid. Each variable that is part of this class has its own boundary condition type.

We start by creating a Grid2d object with 4 x 6 cells and 2 ghost cells

In [2]:
g = patch.Grid2d(4, 6, ng=2)
print(g)
2-d grid: nx = 4, ny = 6, ng = 2
In [3]:
help(g)
Help on Grid2d in module mesh.patch object:

class Grid2d(builtins.object)
 |  the 2-d grid class.  The grid object will contain the coordinate
 |  information (at various centerings).
 |
 |  A basic (1-d) representation of the layout is::
 |
 |     |     |      |     X     |     |      |     |     X     |      |     |
 |     +--*--+- // -+--*--X--*--+--*--+- // -+--*--+--*--X--*--+- // -+--*--+
 |        0          ng-1    ng   ng+1         ... ng+nx-1 ng+nx      2ng+nx-1
 |
 |                          ilo                      ihi
 |
 |     |<- ng guardcells->|<---- nx interior zones ----->|<- ng guardcells->|
 |
 |  The '*' marks the data locations.
 |
 |  Methods defined here:
 |
 |  __eq__(self, other)
 |      are two grids equivalent?
 |
 |  __init__(self, nx, ny, ng=1, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0)
 |      Create a Grid2d object.
 |
 |      The only data that we require is the number of points that
 |      make up the mesh in each direction.  Optionally we take the
 |      extrema of the domain (default is [0,1]x[0,1]) and number of
 |      ghost cells (default is 1).
 |
 |      Note that the Grid2d object only defines the discretization,
 |      it does not know about the boundary conditions, as these can
 |      vary depending on the variable.
 |
 |      Parameters
 |      ----------
 |      nx : int
 |          Number of zones in the x-direction
 |      ny : int
 |          Number of zones in the y-direction
 |      ng : int, optional
 |          Number of ghost cells
 |      xmin : float, optional
 |          Physical coordinate at the lower x boundary
 |      xmax : float, optional
 |          Physical coordinate at the upper x boundary
 |      ymin : float, optional
 |          Physical coordinate at the lower y boundary
 |      ymax : float, optional
 |          Physical coordinate at the upper y boundary
 |
 |  __str__(self)
 |      print out some basic information about the grid object
 |
 |  coarse_like(self, N)
 |      return a new grid object coarsened by a factor n, but with
 |      all the other properties the same
 |
 |  fine_like(self, N)
 |      return a new grid object finer by a factor n, but with
 |      all the other properties the same
 |
 |  norm(self, d)
 |      find the norm of the quantity d defined on the same grid, in the
 |      domain's valid region
 |
 |  scratch_array(self, nvar=1)
 |      return a standard numpy array dimensioned to have the size
 |      and number of ghostcells as the parent grid
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables (if defined)
 |
 |  __weakref__
 |      list of weak references to the object (if defined)
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __hash__ = None

Then create a dataset that lives on this grid and add a variable name. For each variable that lives on the grid, we need to define the boundary conditions – this is done through the BC object.

In [4]:
bc = bnd.BC(xlb="periodic", xrb="periodic", ylb="reflect", yrb="outflow")
print(bc)
BCs: -x: periodic  +x: periodic  -y: reflect-even  +y: outflow
In [5]:
d = patch.CellCenterData2d(g)
d.register_var("a", bc)
d.create()
print(d)
cc data: nx = 4, ny = 6, ng = 2
         nvars = 1
         variables:
               a: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: periodic     +x: periodic     -y: reflect-even +y: outflow

Working with the data

Now we fill the grid with random data. get_var() returns an ArrayIndexer object that has methods for accessing views into the data. Here we use a.v() to get the “valid” region, i.e. excluding ghost cells.

In [6]:
a = d.get_var("a")
a.v()[:,:] = np.random.rand(g.nx, g.ny)

when we pretty_print() the variable, we see the ghost cells colored red. Note that we just filled the interior above.

In [7]:
a.pretty_print()
         0         0         0         0         0         0         0         0
         0         0         0         0         0         0         0         0
         0         0   0.12157    0.2092   0.17194   0.33611         0         0
         0         0 0.0047189   0.89132   0.81168   0.81765         0         0
         0         0   0.84478   0.57509   0.97862   0.94003         0         0
         0         0   0.42452   0.13671    0.2197    0.4317         0         0
         0         0   0.27837   0.82585   0.10838   0.27407         0         0
         0         0    0.5434   0.67075   0.18533   0.81622         0         0
         0         0         0         0         0         0         0         0
         0         0         0         0         0         0         0         0

         ^ y
         |
         +---> x

pretty_print() can also take an argumet, specifying the format string to be used for the output.

In [8]:
a.pretty_print(fmt="%7.3g")
      0      0      0      0      0      0      0      0
      0      0      0      0      0      0      0      0
      0      0  0.122  0.209  0.172  0.336      0      0
      0      00.00472  0.891  0.812  0.818      0      0
      0      0  0.845  0.575  0.979   0.94      0      0
      0      0  0.425  0.137   0.22  0.432      0      0
      0      0  0.278  0.826  0.108  0.274      0      0
      0      0  0.543  0.671  0.185  0.816      0      0
      0      0      0      0      0      0      0      0
      0      0      0      0      0      0      0      0

         ^ y
         |
         +---> x

now fill the ghost cells – notice that the left and right are periodic, the upper is outflow, and the lower is reflect, as specified when we registered the data above.

In [9]:
d.fill_BC("a")
a.pretty_print()
   0.17194   0.33611   0.12157    0.2092   0.17194   0.33611   0.12157    0.2092
   0.17194   0.33611   0.12157    0.2092   0.17194   0.33611   0.12157    0.2092
   0.17194   0.33611   0.12157    0.2092   0.17194   0.33611   0.12157    0.2092
   0.81168   0.81765 0.0047189   0.89132   0.81168   0.81765 0.0047189   0.89132
   0.97862   0.94003   0.84478   0.57509   0.97862   0.94003   0.84478   0.57509
    0.2197    0.4317   0.42452   0.13671    0.2197    0.4317   0.42452   0.13671
   0.10838   0.27407   0.27837   0.82585   0.10838   0.27407   0.27837   0.82585
   0.18533   0.81622    0.5434   0.67075   0.18533   0.81622    0.5434   0.67075
   0.18533   0.81622    0.5434   0.67075   0.18533   0.81622    0.5434   0.67075
   0.10838   0.27407   0.27837   0.82585   0.10838   0.27407   0.27837   0.82585

         ^ y
         |
         +---> x

We can find the L2 norm of the data easily

In [10]:
a.norm()
Out[10]:
0.5749769043407793

and the min and max

In [11]:
print(a.min(), a.max())
0.004718856190972565 0.9786237847073697

ArrayIndexer

We we access the data, an ArrayIndexer object is returned. The ArrayIndexer sub-classes the NumPy ndarray, so it can do all of the methods that a NumPy array can, but in addition, we can use the ip(), jp(), or ipjp() methods to the ArrayIndexer object shift our view in the x, y, or x & y directions.

To make this clearer, we’ll change our data set to be nicely ordered numbers. We index the ArrayIndex the same way we would a NumPy array. The index space includes ghost cells, so the ilo and ihi attributes from the grid object are useful to index just the valid region. The .v() method is a shortcut that also gives a view into just the valid data.

Note: when we use one of the ip(), jp(), ipjp(), or v() methods, the result is a regular NumPy ndarray, not an ArrayIndexer object. This is because it only spans part of the domain (e.g., no ghost cells), and therefore cannot be associated with the Grid2d object that the ArrayIndexer is built from.

In [12]:
type(a)
Out[12]:
mesh.array_indexer.ArrayIndexer
In [13]:
type(a.v())
Out[13]:
numpy.ndarray
In [14]:
a[:,:] = np.arange(g.qx*g.qy).reshape(g.qx, g.qy)
In [15]:
a.pretty_print()
         9        19        29        39        49        59        69        79
         8        18        28        38        48        58        68        78
         7        17        27        37        47        57        67        77
         6        16        26        36        46        56        66        76
         5        15        25        35        45        55        65        75
         4        14        24        34        44        54        64        74
         3        13        23        33        43        53        63        73
         2        12        22        32        42        52        62        72
         1        11        21        31        41        51        61        71
         0        10        20        30        40        50        60        70

         ^ y
         |
         +---> x

We index our arrays as {i,j}, so x (indexed by i) is the row and y (indexed by j) is the column in the NumPy array. Note that python arrays are stored in row-major order, which means that all of the entries in the same row are adjacent in memory. This means that when we simply print out the ndarray, we see constant-x horizontally, which is the transpose of what we are used to.

In [16]:
a.v()
Out[16]:
array([[ 22.,  23.,  24.,  25.,  26.,  27.],
       [ 32.,  33.,  34.,  35.,  36.,  37.],
       [ 42.,  43.,  44.,  45.,  46.,  47.],
       [ 52.,  53.,  54.,  55.,  56.,  57.]])

We can offset our view into the array by one in x – this would be like {i+1, j} when we loop over data. The ip() method is used here, and takes an argument which is the (positive) shift in the x (i) direction. So here’s a shift by 1

In [17]:
a.ip(-1, buf=1)
Out[17]:
array([[  1.,   2.,   3.,   4.,   5.,   6.,   7.,   8.],
       [ 11.,  12.,  13.,  14.,  15.,  16.,  17.,  18.],
       [ 21.,  22.,  23.,  24.,  25.,  26.,  27.,  28.],
       [ 31.,  32.,  33.,  34.,  35.,  36.,  37.,  38.],
       [ 41.,  42.,  43.,  44.,  45.,  46.,  47.,  48.],
       [ 51.,  52.,  53.,  54.,  55.,  56.,  57.,  58.]])

A shifted view is necessarily smaller than the original array, and relies on ghost cells to bring new data into view. Because of this, the underlying data is no longer the same size as the original data, so we return it as an ndarray (which is actually just a view into the data in the ArrayIndexer object, so no copy is made.

To see that it is simply a view, lets shift and edit the data

In [18]:
d = a.ip(1)
d[1,1] = 0.0
a.pretty_print()
         9        19        29        39        49        59        69        79
         8        18        28        38        48        58        68        78
         7        17        27        37        47        57        67        77
         6        16        26        36        46        56        66        76
         5        15        25        35        45        55        65        75
         4        14        24        34        44        54        64        74
         3        13        23        33         0        53        63        73
         2        12        22        32        42        52        62        72
         1        11        21        31        41        51        61        71
         0        10        20        30        40        50        60        70

         ^ y
         |
         +---> x

Here, since d was really a view into \(a_{i+1,j}\), and we accessed element (1,1) into that view (with 0,0 as the origin), we were really accessing the element (2,1) in the valid region

Differencing

ArrayIndexer objects are easy to use to construct differences, like those that appear in a stencil for a finite-difference, without having to explicitly loop over the elements of the array.

Here’s we’ll create a new dataset that is initialized with a sine function

In [19]:
g = patch.Grid2d(8, 8, ng=2)
d = patch.CellCenterData2d(g)
bc = bnd.BC(xlb="periodic", xrb="periodic", ylb="periodic", yrb="periodic")
d.register_var("a", bc)
d.create()

a = d.get_var("a")
a[:,:] = np.sin(2.0*np.pi*a.g.x2d)
d.fill_BC("a")

Our grid object can provide us with a scratch array (an ArrayIndexer object) define on the same grid

In [20]:
b = g.scratch_array()
type(b)
Out[20]:
mesh.array_indexer.ArrayIndexer

We can then fill the data in this array with differenced data from our original array – since b has a separate data region in memory, its elements are independent of a. We do need to make sure that we have the same number of elements on the left and right of the =. Since by default, ip() will return a view with the same size as the valid region, we can use .v() on the left to accept the differences.

Here we compute a centered-difference approximation to the first derivative

In [21]:
b.v()[:,:] = (a.ip(1) - a.ip(-1))/(2.0*a.g.dx)
# normalization was 2.0*pi
b[:,:] /= 2.0*np.pi
In [22]:
plt.plot(g.x[g.ilo:g.ihi+1], a[g.ilo:g.ihi+1,a.g.jc])
plt.plot(g.x[g.ilo:g.ihi+1], b[g.ilo:g.ihi+1,b.g.jc])
print (a.g.dx)
0.125
_images/mesh-examples_44_1.png

Coarsening and prolonging

we can get a new ArrayIndexer object on a coarser grid for one of our variables

In [23]:
c = d.restrict("a")
In [24]:
c.pretty_print()
         0         0         0         0         0         0         0         0
         0         0         0         0         0         0         0         0
         0         0   0.65328   0.65328  -0.65328  -0.65328         0         0
         0         0   0.65328   0.65328  -0.65328  -0.65328         0         0
         0         0   0.65328   0.65328  -0.65328  -0.65328         0         0
         0         0   0.65328   0.65328  -0.65328  -0.65328         0         0
         0         0         0         0         0         0         0         0
         0         0         0         0         0         0         0         0

         ^ y
         |
         +---> x

or a finer grid

In [25]:
f = d.prolong("a")
In [26]:
f.pretty_print(fmt="%6.2g")
     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0
     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0  0.22  0.55  0.86  0.99  0.99  0.86  0.55  0.22 -0.22 -0.55 -0.86 -0.99 -0.99 -0.86 -0.55 -0.22     0     0
     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0
     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0     0

         ^ y
         |
         +---> x

Advection solvers

The linear advection equation:

\[a_t + u a_x + v a_y = 0\]

provides a good basis for understanding the methods used for compressible hydrodynamics. Chapter 4 of the notes summarizes the numerical methods for advection that we implement in pyro.

pyro has several solvers for linear advection:

  • advection implements the directionally unsplit corner transport upwind algorithm with piecewise linear reconstruction
  • advection_fv4 uses a fourth-order accurate finite-volume method with RK4 time integration
  • advection_rk uses a method of lines time-integration approach with piecewise linear spatial reconstruction for linear advection
  • advection_weno uses a WENO reconstruction and method of lines time-integration

The main parameters that affect this solver are:

[driver]
cfl the advective CFL number (what fraction of a zone can we cross in a single timestep)
[advection]
u the advective velocity in the x direction
v the advective velocity in the y direction
limiter what type of limiting to use in reconstructing the slopes. 0 means use an unlimited second-order centered difference. 1 is the MC limiter, and 2 is the 4th-order MC limiter
temporal_method the MOL integration method to use (RK2, TVD2, TVD3, RK4) (advection_rk only)

The main use for the advection solver is to understand how Godunov techniques work for hyperbolic problems. These same ideas will be used in the compressible and incompressible solvers. This video shows graphically how the basic advection algorithm works, consisting of reconstruction, evolution, and averaging steps:


Examples

smooth

The smooth problem initializes a Gaussian profile and advects it with \(u = v = 1\) through periodic boundaries for a period. The result is that the final state should be identical to the initial state—any disagreement is our numerical error. This is run as:

./pyro.py advection smooth inputs.smooth

By varying the resolution and comparing to the analytic solution, we can measure the convergence rate of the method. The smooth_error.py script in analysis/ will compare an output file to the analytic solution for this problem.

_images/smooth_converge.png

The points above are the L2-norm of the absolute error for the smooth advection problem after 1 period with CFL=0.8, for both the advection and advection_fv4 solvers. The dashed and dotted lines show ideal scaling. We see that we achieve nearly 2nd order convergence for the advection solver and 4th order convergence with the advection_fv4 solver. Departures from perfect scaling are likely due to the use of limiters.

tophat

The tophat problem initializes a circle in the center of the domain with value 1, and 0 outside. This has very steep jumps, and the limiters will kick in strongly here.

Exercises

The best way to learn these methods is to play with them yourself. The exercises below are suggestions for explorations and features to add to the advection solver.

Explorations

  • Test the convergence of the solver for a variety of initial conditions (tophat hat will differ from the smooth case because of limiting). Test with limiting on and off, and also test with the slopes set to 0 (this will reduce it down to a piecewise constant reconstruction method).
  • Run without any limiting and look for oscillations and under and overshoots (does the advected quantity go negative in the tophat problem?)

Extensions

  • Implement a dimensionally split version of the advection algorithm. How does the solution compare between the unsplit and split versions? Look at the amount of overshoot and undershoot, for example.

  • Research the inviscid Burger’s equation—this looks like the advection equation, but now the quantity being advected is the velocity itself, so this is a non-linear equation. It is very straightforward to modify this solver to solve Burger’s equation (the main things that need to change are the Riemann solver and the fluxes, and the computation of the timestep).

    The neat thing about Burger’s equation is that it admits shocks and rarefactions, so some very interesting flow problems can be setup.

Compressible hydrodynamics solvers

The equations of compressible hydrodynamics take the form:

\[\begin{split}\frac{\partial \rho}{\partial t} + \nabla \cdot (\rho U) &= 0 \\ \frac{\partial (\rho U)}{\partial t} + \nabla \cdot (\rho U U) + \nabla p &= \rho g \\ \frac{\partial (\rho E)}{\partial t} + \nabla \cdot [(\rho E + p ) U] &= \rho U \cdot g\end{split}\]

with \(\rho E = \rho e + \frac{1}{2} \rho |U|^2\) and \(p = p(\rho, e)\).

pyro has several compressible solvers:

  • compressible is based on a directionally unsplit (the corner transport upwind algorithm) piecewise linear method for the Euler equations
  • compressible_rk uses a method of lines time-integration approach with piecewise linear spatial reconstruction for the Euler equations
  • compressible_fv4 uses a 4th order accurate method with RK4 time integration

The implementations here have flattening at shocks, artificial viscosity, a simple gamma-law equation of state, and (in some cases) a choice of Riemann solvers. Optional constant gravity in the vertical direction is allowed.

The main parameters that affect this solver are:

[driver]
cfl the advective CFL number (what fraction of a zone can we cross in a single timestep)
[compressible]
use_flattening do we flatten the profiles at shocks? (0=no, 1=yes)
z0 the parameters that affect the flattening algorithm
z1
delta
cvisc the coefficient for the artifical viscosity
limiter what type of limiting to use in reconstructing the slopes. 0 means use an unlimited second-order centered difference. 1 is the MC limiter, and 2 is the 4th-order MC limiter
riemann which Riemann solver do we use? “HLLE” for the HLLE solver, or “CGF” for the Colella, Glaz, and Ferguson solver
grav the gravitational acceleration (vertical/y direction)
temporal_method the MOL integration method to use (RK2, TVD2, TVD3, RK4) (compressible_rk and compressible_fv4 only)
[eos]
gamma the constant adiabatic index for the gas

Example problems

Note

The 4th-order accurate solver (compressible_fv4) requires that the initialization create cell-averages accurate to 4th-order. To allow for all the solvers to use the same problem setups, we assume that the initialization routines initialize cell-centers (which is fine for 2nd-order accuracy), and the preevolve() method will convert these to cell-averages automatically after initialization.

Note

All the compressible solvers share the same problems/ directory, which lives in compressible/problems/. For the other compressible solvers, we simply use a symbolic-link to this directory in the solver’s directory.

Sod

The Sod problem is a standard hydrodynamics problem. It is a one-dimensional shock tube (two states separated by an interface), that exhibits all three hydrodynamic waves: a shock, contact, and rarefaction. Furthermore, there are exact solutions for a gamma-law equation of state, so we can check our solution against these exact solutions. See Toro’s book for details on this problem and the exact Riemann solver.

Because it is one-dimensional, we run it in narrow domains in the x- or y-directions. It can be run as:

./pyro.py compressible sod inputs.sod.x
./pyro.py compressible sod inputs.sod.y

A simple script, sod_compare.py in analysis/ will read a pyro output file and plot the solution over the exact Sod solution. Below we see the result for a Sod run with 128 points in the x-direction, gamma = 1.4, and run until t = 0.2 s.

_images/sod_compare.png

We see excellent agreement for all quantities. The shock wave is very steep, as expected. The contact wave is smeared out over ~5 zones—this is discussed in the notes above, and can be improved in the PPM method with contact steepening.

Sedov

The Sedov blast wave problem is another standard test with an analytic solution (Sedov 1959). A lot of energy is point into a point in a uniform medium and a blast wave propagates outward. The Sedov problem is run as:

./pyro.py compressible sedov inputs.sedov

The video below shows the output from a 128 x 128 grid with the energy put in a radius of 0.0125 surrounding the center of the domain. A gamma-law EOS with gamma = 1.4 is used, and we run until 0.1


We see some grid effects because it is hard to initialize a small circular explosion on a rectangular grid. To compare to the analytic solution, we need to radially bin the data. Since this is a 2-d explosion, the physical geometry it represents is a cylindrical blast wave, so we compare to Sedov’s cylindrical solution. The radial binning is done with the sedov_compare.py script in analysis/

_images/sedov_compare.png

This shows good agreement with the analytic solution.

quad

The quad problem sets up different states in four regions of the domain and watches the complex interfaces that develop as shocks interact. This problem has appeared in several places (and a detailed investigation is online by Pawel Artymowicz). It is run as:

./pyro.py compressible quad inputs.quad
_images/quad.png

rt

The Rayleigh-Taylor problem puts a dense fluid over a lighter one and perturbs the interface with a sinusoidal velocity. Hydrostatic boundary conditions are used to ensure any initial pressure waves can escape the domain. It is run as:

./pyro.py compressible er inputs.rt

bubble

The bubble problem initializes a hot spot in a stratified domain and watches it buoyantly rise and roll up. This is run as:

./pyro.py compressible bubble inputs.bubble
_images/bubble.png

The shock at the top of the domain is because we cut off the stratified atmosphere at some low density and the resulting material above that rains down on our atmosphere. Also note the acoustic signal propagating outward from the bubble (visible in the U and e panels).

Exercises

Explorations

  • Measure the growth rate of the Rayleigh-Taylor instability for different wavenumbers.
  • There are multiple Riemann solvers in the compressible algorithm. Run the same problem with the different Riemann solvers and look at the differences. Toro’s text is a good book to help understand what is happening.
  • Run the problems with and without limiting—do you notice any overshoots?

Extensions

  • Limit on the characteristic variables instead of the primitive variables. What changes do you see? (the notes show how to implement this change.)
  • Add passively advected species to the solver.
  • Add an external heating term to the equations.
  • Add 2-d axisymmetric coordinates (r-z) to the solver. This is discussed in the notes. Run the Sedov problem with the explosion on the symmetric axis—now the solution will behave like the spherical sedov explosion instead of the cylindrical explosion.
  • Swap the piecewise linear reconstruction for piecewise parabolic (PPM). The notes and the Miller and Colella paper provide a good basis for this. Research the Roe Riemann solver and implement it in pyro.

Going further

The compressible algorithm presented here is essentially the single-grid hydrodynamics algorithm used in the Castro code—an adaptive mesh radiation hydrodynamics code developed at CCSE/LBNL. Castro is freely available for download.

A simple, pure Fortran, 1-d compressible hydrodynamics code that does piecewise constant, linear, or parabolic (PPM) reconstruction is also available. See the hydro1d page.

Compressible solver comparisons

We run various problems run with the different compressible solvers in pyro (standard Riemann, Runge-Kutta, fourth order).

Kelvin-Helmholtz

The McNally Kelvin-Helmholtz problem sets up a heavier fluid moving in the negative x-direction sandwiched between regions of lighter fluid moving in the positive x-direction.

The image below shows the KH problem initialized with McNally’s test. It ran on a 128 x 128 grid, with gamma = 1.7, and ran until t = 2.0. This is run with:

./pyro.py compressible kh inputs.kh kh.vbulk=0
./pyro.py compressible_rk kh inputs.kh kh.vbulk=0
./pyro.py compressible_fv4 kh inputs.kh kh.vbulk=0
_images/kh.png

We vary the velocity in the positive y-direction (vbulk) to see how effective the solvers are at preserving the initial shape.

Sedov

The Sedov problem ran on a 128 x 128 grid, with gamma = 1.4, and until t = 0.1, which can be run as:

./pyro.py compressible sedov inputs.sedov
./pyro.py compressible_rk sedov inputs.sedov
./pyro.py compressible_fv4 sedov inputs.sedov
_images/sedov.png _images/sedov_rk.png _images/sedov_fv4.png

Quad

The quad problem ran on a 256 x 256 grid until t = 0.8, which can be run as:

./pyro.py compressible quad inputs.quad
./pyro.py compressible_rk quad inputs.quad
./pyro.py compressible_fv4 quad inputs.quad
_images/quad1.png _images/quad_rk.png _images/quad_fv4.png

Bubble

The bubble problem ran on a 128 x 256 grid until t = 3.0, which can be run as:

./pyro.py compressible bubble inputs.bubble
./pyro.py compressible_rk bubble inputs.bubble
./pyro.py compressible_fv4 bubble inputs.bubble
_images/bubble1.png _images/bubble_rk.png _images/bubble_fv4.png

Rayleigh-Taylor

The Rayleigh-Taylor problem ran on a 64 x 192 grid until t = 3.0, which can be run as:

./pyro.py compressible rt inputs.rt
./pyro.py compressible_rk rt inputs.rt
./pyro.py compressible_fv4 rt inputs.rt
_images/rt.png _images/rt_rk.png _images/rt_fv4.png

Multigrid solvers

pyro solves elliptic problems (like Laplace’s equation or Poisson’s equation) through multigrid. This accelerates the convergence of simple relaxation by moving the solution down and up through a series of grids. Chapter 9 of the `pdf notes<http://bender.astro.sunysb.edu/hydro_by_example/CompHydroTutorial.pdf>`_ gives an introduction to solving elliptic equations, including multigrid.

There are three solvers:

  • The core solver, provided in the class MG.CellCenterMG2d solves constant-coefficient Helmholtz problems of the form \((\alpha - \beta \nabla^2) \phi = f\)

  • The class variable_coeff_MG.VarCoeffCCMG2d solves variable coefficient Poisson problems of the form \(\nabla \cdot (\eta \nabla \phi ) = f\). This class inherits the core functionality from MG.CellCenterMG2d.

  • The class general_MG.GeneralMG2d solves a general elliptic equation of the form \(\alpha \phi + \nabla \cdot ( \beta \nabla \phi) + \gamma \cdot \nabla \phi = f\). This class inherits the core functionality from MG.CellCenterMG2d.

    This solver is the only one to support inhomogeneous boundary conditions.

We simply use V-cycles in our implementation, and restrict ourselves to square grids with zoning a power of 2.

The multigrid solver is not controlled through pyro.py since there is no time-dependence in pure elliptic problems. Instead, there are a few scripts in the multigrid/ subdirectory that demonstrate its use.

Examples

multigrid test

A basic multigrid test is run as:

./mg_test_simple.py

The mg_test_simple.py script solves a Poisson equation with a known analytic solution. This particular example comes from the text A Multigrid Tutorial, 2nd Ed., by Briggs. The example is:

\[u_{xx} + u_{yy} = -2 \left [(1-6x^2)y^2(1-y^2) + (1-6y^2)x^2(1-x^2)\right ]\]

on \([0,1] \times [0,1]\) with \(u = 0\) on the boundary.

The solution to this is shown below.

_images/mg_test.png

Since this has a known analytic solution:

\[u(x,y) = (x^2 - x^4)(y^4 - y^2)\]

We can assess the convergence of our solver by running at a variety of resolutions and computing the norm of the error with respect to the analytic solution. This is shown below:

_images/mg_converge.png

The dotted line is 2nd order convergence, which we match perfectly.

The movie below shows the smoothing at each level to realize this solution:


projection

Another example (examples/multigrid/project_periodic.py) uses multigrid to extract the divergence free part of a velocity field. This is run as:

./project-periodic.py

Given a vector field, \(U\), we can decompose it into a divergence free part, \(U_d\), and the gradient of a scalar, \(\phi\):

\[U = U_d + \nabla \phi\]

We can project out the divergence free part by taking the divergence, leading to an elliptic equation:

\[\nabla^2 \phi = \nabla \cdot U\]

The project-periodic.py script starts with a divergence free velocity field, adds to it the gradient of a scalar, and then projects it to recover the divergence free part. The error can found by comparing the original velocity field to the recovered field. The results are shown below:

_images/project.png

Left is the original u velocity, middle is the modified field after adding the gradient of the scalar, and right is the recovered field.

Exercises

Explorations

  • Try doing just smoothing, no multigrid. Show that it still converges second order if you use enough iterations, but that the amount of time needed to get a solution is much greater.

Extensions

  • Implement inhomogeneous dirichlet boundary conditions
  • Add a different bottom solver to the multigrid algorithm
  • Make the multigrid solver work for non-square domains

Multigrid examples

In [1]:
%matplotlib inline
import matplotlib.pyplot as plt
In [2]:
from __future__ import print_function

import numpy as np

import mesh.boundary as bnd
import mesh.patch as patch
import multigrid.MG as MG

Constant-coefficent Poisson equation

We want to solve

\[\phi_{xx} + \phi_{yy} = -2[(1-6x^2)y^2(1-y^2) + (1-6y^2)x^2(1-x^2)]\]

on

\[[0,1]\times [0,1]\]

with homogeneous Dirichlet boundary conditions (this example comes from “A Multigrid Tutorial”).

This has the analytic solution

\[u(x,y) = (x^2 - x^4)(y^4 - y^2)\]

We start by setting up a multigrid object–this needs to know the number of zones our problem is defined on

In [3]:
nx = ny = 256
mg = MG.CellCenterMG2d(nx, ny,
                       xl_BC_type="dirichlet", xr_BC_type="dirichlet",
                       yl_BC_type="dirichlet", yr_BC_type="dirichlet", verbose=1)
cc data: nx = 2, ny = 2, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 4, ny = 4, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 8, ny = 8, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 16, ny = 16, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 32, ny = 32, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 64, ny = 64, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 128, ny = 128, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

cc data: nx = 256, ny = 256, ng = 1
         nvars = 3
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet

Next, we initialize the RHS. To make life easier, the CellCenterMG2d object has the coordinates of the solution grid (including ghost cells) as mg.x2d and mg.y2d (these are two-dimensional arrays).

In [4]:
def rhs(x, y):
    return -2.0*((1.0-6.0*x**2)*y**2*(1.0-y**2) + (1.0-6.0*y**2)*x**2*(1.0-x**2))

mg.init_RHS(rhs(mg.x2d, mg.y2d))
Source norm =  1.09751581367

The last setup step is to initialize the solution–this is the starting point for the solve. Usually we just want to start with all zeros, so we use the init_zeros() method

In [5]:
mg.init_zeros()

we can now solve – there are actually two different techniques we can do here. We can just do pure smoothing on the solution grid using mg.smooth(mg.nlevels-1, N), where N is the number of smoothing iterations. To get the solution N will need to be large and this will take a long time.

Multigrid accelerates the smoothing. We can do a V-cycle multigrid solution using mg.solve()

In [6]:
mg.solve()
source norm =  1.09751581367
<<< beginning V-cycle (cycle 1) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.097515813669473
  after G-S, residual L2: 1.502308451578657

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.0616243965458263
  after G-S, residual L2: 1.4321452257629033

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.011366277976364
  after G-S, residual L2: 1.281872470375375

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.903531158162907
  after G-S, residual L2: 0.9607576999783505

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.6736112182020367
  after G-S, residual L2: 0.4439774050299674

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.30721142286171554
  after G-S, residual L2: 0.0727215591269748

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.04841813458618458
  after G-S, residual L2: 3.9610700301811246e-05

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 3.925006722484123e-05
  after G-S, residual L2: 1.0370099820862674e-09

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.07010129273961899
  after G-S, residual L2: 0.0008815704830693547

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.4307377377402105
  after G-S, residual L2: 0.007174899576794818

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.911086486792154
  after G-S, residual L2: 0.01618756602227813

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.1945438349788615
  after G-S, residual L2: 0.022021327892004925

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.313456560108626
  after G-S, residual L2: 0.02518650395173617

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.3618314516335004
  after G-S, residual L2: 0.026870007568672097

cycle 1: relative err = 0.999999999999964, residual err = 0.02448256984911586

<<< beginning V-cycle (cycle 2) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 0.026870007568672097
  after G-S, residual L2: 0.025790216249923552

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.018218080204017304
  after G-S, residual L2: 0.023654310121915274

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.01669077991582338
  after G-S, residual L2: 0.01977335201785163

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.013922595404814862
  after G-S, residual L2: 0.013577568890182053

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.009518306167970536
  after G-S, residual L2: 0.006115159484497302

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.004244630812032651
  after G-S, residual L2: 0.0010674120586864006

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.0007108144252738053
  after G-S, residual L2: 5.818246254772703e-07

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 5.765281065294632e-07
  after G-S, residual L2: 1.5231212503339452e-11

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.0010291471590693868
  after G-S, residual L2: 1.2950948742201083e-05

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.006239446983842889
  after G-S, residual L2: 0.00010483463130232172

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.014573363314854
  after G-S, residual L2: 0.00026233988398787004

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.021564270263952755
  after G-S, residual L2: 0.0003944827058086955

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.02579092712136628
  after G-S, residual L2: 0.00048636495715121916

  level: 7, grid: 256 x 256
  before G-S, residual L2: 0.028051324215592862
  after G-S, residual L2: 0.0005440874957950154

cycle 2: relative err = 13.739483825281054, residual err = 0.0004957445615074047

<<< beginning V-cycle (cycle 3) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 0.0005440874957950154
  after G-S, residual L2: 0.0005095844930046698

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.0003597879816772893
  after G-S, residual L2: 0.00044648485218937167

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.0003147892995472901
  after G-S, residual L2: 0.0003492541721056348

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.0002457276904804801
  after G-S, residual L2: 0.00022232862524233384

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.0001558932199490972
  after G-S, residual L2: 9.511093023364078e-05

  level: 2, grid: 8 x 8
  before G-S, residual L2: 6.616899520585456e-05
  after G-S, residual L2: 1.711006102346096e-05

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.139522901981679e-05
  after G-S, residual L2: 9.33004809910226e-09

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 9.245125097272049e-09
  after G-S, residual L2: 2.442311694447821e-13

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.64991725637487e-05
  after G-S, residual L2: 2.0771258971860784e-07

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.00010097720436460624
  after G-S, residual L2: 1.7241727900979902e-06

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.0002575410544503153
  after G-S, residual L2: 4.766282851613449e-06

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.00041133882050328275
  after G-S, residual L2: 7.600616845344458e-06

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.0005232809692242086
  after G-S, residual L2: 9.860758095018993e-06

  level: 7, grid: 256 x 256
  before G-S, residual L2: 0.0005945070122423073
  after G-S, residual L2: 1.1466134915427874e-05

cycle 3: relative err = 34.347638624909216, residual err = 1.0447352805871284e-05

<<< beginning V-cycle (cycle 4) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.1466134915427874e-05
  after G-S, residual L2: 1.054466722279011e-05

  level: 6, grid: 128 x 128
  before G-S, residual L2: 7.442814693866286e-06
  after G-S, residual L2: 8.955050475722099e-06

  level: 5, grid: 64 x 64
  before G-S, residual L2: 6.311313968968047e-06
  after G-S, residual L2: 6.734553609148436e-06

  level: 4, grid: 32 x 32
  before G-S, residual L2: 4.737984987500691e-06
  after G-S, residual L2: 4.091799997658277e-06

  level: 3, grid: 16 x 16
  before G-S, residual L2: 2.871028473858937e-06
  after G-S, residual L2: 1.6319551993366253e-06

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.1372178077508109e-06
  after G-S, residual L2: 2.961040430099916e-07

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.9721864323458624e-07
  after G-S, residual L2: 1.61503943872384e-10

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.6003411195777404e-10
  after G-S, residual L2: 4.2274326344473505e-15

  level: 2, grid: 8 x 8
  before G-S, residual L2: 2.855691101825338e-07
  after G-S, residual L2: 3.5961118754371857e-09

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.7893831203170535e-06
  after G-S, residual L2: 3.1136282101831173e-08

  level: 4, grid: 32 x 32
  before G-S, residual L2: 4.97129807196115e-06
  after G-S, residual L2: 9.544819739422644e-08

  level: 5, grid: 64 x 64
  before G-S, residual L2: 8.281644276572538e-06
  after G-S, residual L2: 1.56637783149839e-07

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.0888850082357996e-05
  after G-S, residual L2: 2.0777271327080248e-07

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.2717522622400765e-05
  after G-S, residual L2: 2.464531349025277e-07

cycle 4: relative err = 0.17409776671446628, residual err = 2.24555429482631e-07

<<< beginning V-cycle (cycle 5) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 2.464531349025277e-07
  after G-S, residual L2: 2.2491138140311698e-07

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.5874562191875262e-07
  after G-S, residual L2: 1.886249099391391e-07

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.3294481979637655e-07
  after G-S, residual L2: 1.397710191717015e-07

  level: 4, grid: 32 x 32
  before G-S, residual L2: 9.836928907527788e-08
  after G-S, residual L2: 8.269030961692836e-08

  level: 3, grid: 16 x 16
  before G-S, residual L2: 5.8062531341283565e-08
  after G-S, residual L2: 3.034725896415429e-08

  level: 2, grid: 8 x 8
  before G-S, residual L2: 2.116912379336852e-08
  after G-S, residual L2: 5.467519592468213e-09

  level: 1, grid: 4 x 4
  before G-S, residual L2: 3.6418116003284676e-09
  after G-S, residual L2: 2.982625229812215e-12

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 2.955484162036181e-12
  after G-S, residual L2: 7.806739482450516e-17

  level: 2, grid: 8 x 8
  before G-S, residual L2: 5.273610709946236e-09
  after G-S, residual L2: 6.642323465658688e-11

  level: 3, grid: 16 x 16
  before G-S, residual L2: 3.4146989205844565e-08
  after G-S, residual L2: 6.052228076583688e-10

  level: 4, grid: 32 x 32
  before G-S, residual L2: 1.031248597196911e-07
  after G-S, residual L2: 2.0541497445308587e-09

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.7585349306604133e-07
  after G-S, residual L2: 3.421022608879089e-09

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.3383756442516674e-07
  after G-S, residual L2: 4.552170797983864e-09

  level: 7, grid: 256 x 256
  before G-S, residual L2: 2.7592842790687426e-07
  after G-S, residual L2: 5.41488950707315e-09

cycle 5: relative err = 0.005391244339065405, residual err = 4.933769007818501e-09

<<< beginning V-cycle (cycle 6) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 5.41488950707315e-09
  after G-S, residual L2: 4.948141362729419e-09

  level: 6, grid: 128 x 128
  before G-S, residual L2: 3.4929583962703016e-09
  after G-S, residual L2: 4.154445183511443e-09

  level: 5, grid: 64 x 64
  before G-S, residual L2: 2.9288841397931397e-09
  after G-S, residual L2: 3.074779198797186e-09

  level: 4, grid: 32 x 32
  before G-S, residual L2: 2.164991235492634e-09
  after G-S, residual L2: 1.788028730183651e-09

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.2562223343389894e-09
  after G-S, residual L2: 6.021983813990021e-10

  level: 2, grid: 8 x 8
  before G-S, residual L2: 4.2028073688787063e-10
  after G-S, residual L2: 1.0655724637281067e-10

  level: 1, grid: 4 x 4
  before G-S, residual L2: 7.097871736854444e-11
  after G-S, residual L2: 5.813506543301849e-14

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 5.760611936011378e-14
  after G-S, residual L2: 1.521555112430923e-18

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.027891920456506e-10
  after G-S, residual L2: 1.294879454701896e-12

  level: 3, grid: 16 x 16
  before G-S, residual L2: 6.914011940773812e-10
  after G-S, residual L2: 1.2453691230551983e-11

  level: 4, grid: 32 x 32
  before G-S, residual L2: 2.2570491487662195e-09
  after G-S, residual L2: 4.639035392364569e-11

  level: 5, grid: 64 x 64
  before G-S, residual L2: 3.908967396962745e-09
  after G-S, residual L2: 7.803740782474827e-11

  level: 6, grid: 128 x 128
  before G-S, residual L2: 5.196394306272565e-09
  after G-S, residual L2: 1.033274523100204e-10

  level: 7, grid: 256 x 256
  before G-S, residual L2: 6.117636729623554e-09
  after G-S, residual L2: 1.2199402602477584e-10

cycle 6: relative err = 7.51413991329132e-05, residual err = 1.111546863428753e-10

<<< beginning V-cycle (cycle 7) >>>

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.2199402602477584e-10
  after G-S, residual L2: 1.121992266879251e-10

  level: 6, grid: 128 x 128
  before G-S, residual L2: 7.921861122082639e-11
  after G-S, residual L2: 9.493449600138316e-11

  level: 5, grid: 64 x 64
  before G-S, residual L2: 6.694993398453784e-11
  after G-S, residual L2: 7.050995614737483e-11

  level: 4, grid: 32 x 32
  before G-S, residual L2: 4.9666563586565975e-11
  after G-S, residual L2: 4.045094776680348e-11

  level: 3, grid: 16 x 16
  before G-S, residual L2: 2.843147343834713e-11
  after G-S, residual L2: 1.2576313722677801e-11

  level: 2, grid: 8 x 8
  before G-S, residual L2: 8.777954081387978e-12
  after G-S, residual L2: 2.170559196862902e-12

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.445876195415056e-12
  after G-S, residual L2: 1.1842925278593641e-15

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.1735184729034125e-15
  after G-S, residual L2: 3.0994757710835167e-20

  level: 2, grid: 8 x 8
  before G-S, residual L2: 2.094012660676073e-12
  after G-S, residual L2: 2.6382579574150587e-14

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.466147487151147e-11
  after G-S, residual L2: 2.6760553592700965e-13

  level: 4, grid: 32 x 32
  before G-S, residual L2: 5.130705216489902e-11
  after G-S, residual L2: 1.0810419626613159e-12

  level: 5, grid: 64 x 64
  before G-S, residual L2: 9.001551103280705e-11
  after G-S, residual L2: 1.8342879121275396e-12

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.1914921193827463e-10
  after G-S, residual L2: 2.4124327865487605e-12

  level: 7, grid: 256 x 256
  before G-S, residual L2: 1.3907209384461257e-10
  after G-S, residual L2: 2.8429898342353533e-12

cycle 7: relative err = 7.062255558417692e-07, residual err = 2.590386214782638e-12

We can access the solution on the finest grid using get_solution()

In [7]:
phi = mg.get_solution()
In [8]:
plt.imshow(np.transpose(phi.v()), origin="lower")
Out[8]:
<matplotlib.image.AxesImage at 0x7f7c47a0cc50>
_images/multigrid-examples_15_1.png

we can also get the gradient of the solution

In [9]:
gx, gy = mg.get_solution_gradient()
In [10]:
plt.subplot(121)
plt.imshow(np.transpose(gx.v()), origin="lower")
plt.subplot(122)
plt.imshow(np.transpose(gy.v()), origin="lower")
Out[10]:
<matplotlib.image.AxesImage at 0x7f7c47a345f8>
_images/multigrid-examples_18_1.png

General linear elliptic equation

The GeneralMG2d class implements support for a general elliptic equation of the form:

\[\alpha \phi + \nabla \cdot (\beta \nabla \phi) + \gamma \cdot \nabla \phi = f\]

with inhomogeneous boundary condtions.

It subclasses the CellCenterMG2d class, and the basic interface is the same

We will solve the above with

\begin{align} \alpha &= 10 \\ \beta &= xy + 1 \\ \gamma &= \hat{x} + \hat{y} \end{align}

and

\begin{align} f = &-\frac{\pi}{2}(x + 1)\sin\left(\frac{\pi y}{2}\right) \cos\left(\frac{\pi x}{2}\right ) \\ &-\frac{\pi}{2}(y + 1)\sin\left(\frac{\pi x}{2}\right) \cos\left(\frac{\pi y}{2}\right ) \\ &+\left(\frac{-\pi^2 (xy+1)}{2} + 10\right) \cos\left(\frac{\pi x}{2}\right) \cos\left(\frac{\pi y}{2}\right) \end{align}

on \([0, 1] \times [0,1]\) with boundary conditions:

\begin{align} \phi(x=0) &= \cos(\pi y/2) \\ \phi(x=1) &= 0 \\ \phi(y=0) &= \cos(\pi x/2) \\ \phi(y=1) &= 0 \end{align}

This has the exact solution:

\[\phi = \cos(\pi x/2) \cos(\pi y/2)\]
In [11]:
import multigrid.general_MG as gMG

For reference, we’ll define a function providing the analytic solution

In [12]:
def true(x,y):
    return np.cos(np.pi*x/2.0)*np.cos(np.pi*y/2.0)

Now the coefficents–note that since \(\gamma\) is a vector, we have a different function for each component

In [13]:
def alpha(x,y):
    return 10.0*np.ones_like(x)

def beta(x,y):
    return x*y + 1.0

def gamma_x(x,y):
    return np.ones_like(x)

def gamma_y(x,y):
    return np.ones_like(x)

and the righthand side function

In [14]:
def f(x,y):
    return -0.5*np.pi*(x + 1.0)*np.sin(np.pi*y/2.0)*np.cos(np.pi*x/2.0) - \
            0.5*np.pi*(y + 1.0)*np.sin(np.pi*x/2.0)*np.cos(np.pi*y/2.0) + \
            (-np.pi**2*(x*y+1.0)/2.0 + 10.0)*np.cos(np.pi*x/2.0)*np.cos(np.pi*y/2.0)

Our inhomogeneous boundary conditions require a function that can be evaluated on the boundary to give the value

In [15]:
def xl_func(y):
    return np.cos(np.pi*y/2.0)

def yl_func(x):
    return np.cos(np.pi*x/2.0)

Now we can setup our grid object and the coefficients, which are stored as a CellCenter2d object. Note, the coefficients do not need to have the same boundary conditions as \(\phi\) (and for real problems, they may not). The one that matters the most is \(\beta\), since that will need to be averaged to the edges of the domain, so the boundary conditions on the coefficients are important.

Here we use Neumann boundary conditions

In [16]:
import mesh.patch as patch

nx = ny = 128

g = patch.Grid2d(nx, ny, ng=1)
d = patch.CellCenterData2d(g)

bc_c = bnd.BC(xlb="neumann", xrb="neumann",
              ylb="neumann", yrb="neumann")

d.register_var("alpha", bc_c)
d.register_var("beta", bc_c)
d.register_var("gamma_x", bc_c)
d.register_var("gamma_y", bc_c)
d.create()

a = d.get_var("alpha")
a[:,:] = alpha(g.x2d, g.y2d)

b = d.get_var("beta")
b[:,:] = beta(g.x2d, g.y2d)

gx = d.get_var("gamma_x")
gx[:,:] = gamma_x(g.x2d, g.y2d)

gy = d.get_var("gamma_y")
gy[:,:] = gamma_y(g.x2d, g.y2d)

Now we can setup the multigrid object

In [17]:
a = gMG.GeneralMG2d(nx, ny,
                    xl_BC_type="dirichlet", yl_BC_type="dirichlet",
                    xr_BC_type="dirichlet", yr_BC_type="dirichlet",
                    xl_BC=xl_func,
                    yl_BC=yl_func,
                    coeffs=d,
                    verbose=1, vis=0, true_function=true)
cc data: nx = 2, ny = 2, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 4, ny = 4, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 8, ny = 8, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 16, ny = 16, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 32, ny = 32, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 64, ny = 64, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

cc data: nx = 128, ny = 128, ng = 1
         nvars = 7
         variables:
               v: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               f: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
               r: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: dirichlet    +x: dirichlet    -y: dirichlet    +y: dirichlet
           alpha: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
            beta: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_x: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann
         gamma_y: min:    0.0000000000    max:    0.0000000000
                  BCs: -x: neumann      +x: neumann      -y: neumann      +y: neumann

just as before, we specify the righthand side and initialize the solution

In [18]:
a.init_zeros()
a.init_RHS(f(a.x2d, a.y2d))
Source norm =  1.77518149234

and we can solve it

In [19]:
a.solve(rtol=1.e-10)
source norm =  1.77518149234
<<< beginning V-cycle (cycle 1) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.775181492337501
  after G-S, residual L2: 188.9332667507471

  level: 5, grid: 64 x 64
  before G-S, residual L2: 129.93801550392874
  after G-S, residual L2: 56.28708770794368

  level: 4, grid: 32 x 32
  before G-S, residual L2: 38.88692621665778
  after G-S, residual L2: 18.722754099081875

  level: 3, grid: 16 x 16
  before G-S, residual L2: 12.92606814051491
  after G-S, residual L2: 6.741858401611561

  level: 2, grid: 8 x 8
  before G-S, residual L2: 4.646478379380238
  after G-S, residual L2: 2.065126154146587

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.3745334259197384
  after G-S, residual L2: 0.02244519721859215

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.031252520872477096
  after G-S, residual L2: 8.232822131685586e-05

  level: 2, grid: 8 x 8
  before G-S, residual L2: 2.8059768631102893
  after G-S, residual L2: 0.07481536016730024

  level: 3, grid: 16 x 16
  before G-S, residual L2: 8.772402436595382
  after G-S, residual L2: 0.24361942694526875

  level: 4, grid: 32 x 32
  before G-S, residual L2: 19.591011324351037
  after G-S, residual L2: 0.5448263647958976

  level: 5, grid: 64 x 64
  before G-S, residual L2: 50.4641088994847
  after G-S, residual L2: 1.3597629173942398

  level: 6, grid: 128 x 128
  before G-S, residual L2: 160.2131163846867
  after G-S, residual L2: 4.125142056231141

cycle 1: relative err = 0.9999999999999981, residual err = 2.3237860883730193

<<< beginning V-cycle (cycle 2) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 4.125142056231141
  after G-S, residual L2: 2.4247311846143957

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.6915411385849393
  after G-S, residual L2: 1.0486241094402862

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.7283416353571861
  after G-S, residual L2: 0.45548181093652995

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.3165327512850198
  after G-S, residual L2: 0.22128563126748008

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.15332496186655512
  after G-S, residual L2: 0.0747196881784426

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.04974939187294444
  after G-S, residual L2: 0.0008133572860410457

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.0011325179143730458
  after G-S, residual L2: 2.98337783917788e-06

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.10152627387884022
  after G-S, residual L2: 0.0027007047002410374

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.29814672415595245
  after G-S, residual L2: 0.00819910795226899

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.5218848114624619
  after G-S, residual L2: 0.014956130961989498

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.9910630869231989
  after G-S, residual L2: 0.028422939317571984

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.044187745817752
  after G-S, residual L2: 0.058293826018797935

cycle 2: relative err = 0.036315310129800826, residual err = 0.032838234439926776

<<< beginning V-cycle (cycle 3) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.058293826018797935
  after G-S, residual L2: 0.0417201187072595

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.029246699093099564
  after G-S, residual L2: 0.023356326397591495

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.016306296792818056
  after G-S, residual L2: 0.012906629461195234

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.009011110787953703
  after G-S, residual L2: 0.007315262938908486

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.005081499522859323
  after G-S, residual L2: 0.002562526517155576

  level: 1, grid: 4 x 4
  before G-S, residual L2: 0.0017064130732665692
  after G-S, residual L2: 2.7912387046731474e-05

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 3.886526925433118e-05
  after G-S, residual L2: 1.0238217009484441e-07

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.0034819145217789937
  after G-S, residual L2: 9.252096659805176e-05

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.01006499034870321
  after G-S, residual L2: 0.0002744054418255884

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.016032310448838724
  after G-S, residual L2: 0.0004558226543272663

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.024303743880186898
  after G-S, residual L2: 0.0007098551729201239

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.037775318915862
  after G-S, residual L2: 0.0011035122819927912

cycle 3: relative err = 0.0012532978372415335, residual err = 0.0006216334987470617

<<< beginning V-cycle (cycle 4) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.0011035122819927912
  after G-S, residual L2: 0.0008898317346917108

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.0006257398720776081
  after G-S, residual L2: 0.000607740119084607

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.00042604165447901086
  after G-S, residual L2: 0.00039767401825608673

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.0002784624522907369
  after G-S, residual L2: 0.00024268300992319052

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.0001688184030119159
  after G-S, residual L2: 8.63435239999583e-05

  level: 1, grid: 4 x 4
  before G-S, residual L2: 5.750132804390505e-05
  after G-S, residual L2: 9.407985171344554e-07

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.3099714803222558e-06
  after G-S, residual L2: 3.450833950914012e-09

  level: 2, grid: 8 x 8
  before G-S, residual L2: 0.00011732421042687768
  after G-S, residual L2: 3.1157531467636086e-06

  level: 3, grid: 16 x 16
  before G-S, residual L2: 0.00033850867119400885
  after G-S, residual L2: 9.17760188796962e-06

  level: 4, grid: 32 x 32
  before G-S, residual L2: 0.0005249527904418192
  after G-S, residual L2: 1.4651643230958405e-05

  level: 5, grid: 64 x 64
  before G-S, residual L2: 0.0007080871923387015
  after G-S, residual L2: 2.0290645679943462e-05

  level: 6, grid: 128 x 128
  before G-S, residual L2: 0.0009185166830535544
  after G-S, residual L2: 2.6570300453995103e-05

cycle 4: relative err = 4.2574662963457396e-05, residual err = 1.4967652923762853e-05

<<< beginning V-cycle (cycle 5) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.6570300453995103e-05
  after G-S, residual L2: 2.3098223923757352e-05

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.6274857395354832e-05
  after G-S, residual L2: 1.7906142642175535e-05

  level: 4, grid: 32 x 32
  before G-S, residual L2: 1.258588239896169e-05
  after G-S, residual L2: 1.2880701433730278e-05

  level: 3, grid: 16 x 16
  before G-S, residual L2: 9.035061892671461e-06
  after G-S, residual L2: 8.10300318788889e-06

  level: 2, grid: 8 x 8
  before G-S, residual L2: 5.641504287378599e-06
  after G-S, residual L2: 2.9012129063955126e-06

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.932169517574082e-06
  after G-S, residual L2: 3.161675601835735e-08

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 4.4023320992879136e-08
  after G-S, residual L2: 1.1596974313938014e-10

  level: 2, grid: 8 x 8
  before G-S, residual L2: 3.9422658747144435e-06
  after G-S, residual L2: 1.0466257645445924e-07

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.1405869020431955e-05
  after G-S, residual L2: 3.0819546585464564e-07

  level: 4, grid: 32 x 32
  before G-S, residual L2: 1.7696025211842327e-05
  after G-S, residual L2: 4.853326074858634e-07

  level: 5, grid: 64 x 64
  before G-S, residual L2: 2.281722184794443e-05
  after G-S, residual L2: 6.339093026629609e-07

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.7204506586512792e-05
  after G-S, residual L2: 7.61736677407384e-07

cycle 5: relative err = 1.4372233555992132e-06, residual err = 4.2910354839513e-07

<<< beginning V-cycle (cycle 6) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 7.61736677407384e-07
  after G-S, residual L2: 6.887955287148536e-07

  level: 5, grid: 64 x 64
  before G-S, residual L2: 4.858303580829294e-07
  after G-S, residual L2: 5.698844682533653e-07

  level: 4, grid: 32 x 32
  before G-S, residual L2: 4.011448592273346e-07
  after G-S, residual L2: 4.2887305175998083e-07

  level: 3, grid: 16 x 16
  before G-S, residual L2: 3.011320287970724e-07
  after G-S, residual L2: 2.7229135972437344e-07

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.8967555884605451e-07
  after G-S, residual L2: 9.770491553515245e-08

  level: 1, grid: 4 x 4
  before G-S, residual L2: 6.507167357899105e-08
  after G-S, residual L2: 1.0648579116334552e-09

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.4827137294363792e-09
  after G-S, residual L2: 3.9058805523605475e-12

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.3276705475319977e-07
  after G-S, residual L2: 3.524245793876337e-09

  level: 3, grid: 16 x 16
  before G-S, residual L2: 3.8563144896921417e-07
  after G-S, residual L2: 1.0398885077513769e-08

  level: 4, grid: 32 x 32
  before G-S, residual L2: 6.038836850187365e-07
  after G-S, residual L2: 1.6338312481157817e-08

  level: 5, grid: 64 x 64
  before G-S, residual L2: 7.682416346530921e-07
  after G-S, residual L2: 2.0772116210685317e-08

  level: 6, grid: 128 x 128
  before G-S, residual L2: 8.865086230602598e-07
  after G-S, residual L2: 2.401923227919822e-08

cycle 6: relative err = 4.8492598977484135e-08, residual err = 1.353057835656594e-08

<<< beginning V-cycle (cycle 7) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.401923227919822e-08
  after G-S, residual L2: 2.2125290070425652e-08

  level: 5, grid: 64 x 64
  before G-S, residual L2: 1.5613809613835955e-08
  after G-S, residual L2: 1.8869606239963252e-08

  level: 4, grid: 32 x 32
  before G-S, residual L2: 1.3292687837677291e-08
  after G-S, residual L2: 1.4485742520315527e-08

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.0177212111802273e-08
  after G-S, residual L2: 9.198083791538658e-09

  level: 2, grid: 8 x 8
  before G-S, residual L2: 6.409467335640698e-09
  after G-S, residual L2: 3.3018379633629456e-09

  level: 1, grid: 4 x 4
  before G-S, residual L2: 2.1990607567876347e-09
  after G-S, residual L2: 3.598750197454369e-11

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 5.010919630110133e-11
  after G-S, residual L2: 1.3200151156453123e-13

  level: 2, grid: 8 x 8
  before G-S, residual L2: 4.48679228107323e-09
  after G-S, residual L2: 1.1908945622999935e-10

  level: 3, grid: 16 x 16
  before G-S, residual L2: 1.3081162779667808e-08
  after G-S, residual L2: 3.522982496836639e-10

  level: 4, grid: 32 x 32
  before G-S, residual L2: 2.0705037621548675e-08
  after G-S, residual L2: 5.546643639307605e-10

  level: 5, grid: 64 x 64
  before G-S, residual L2: 2.6280822057541362e-08
  after G-S, residual L2: 6.964954384251476e-10

  level: 6, grid: 128 x 128
  before G-S, residual L2: 2.994449911367404e-08
  after G-S, residual L2: 7.914383325620475e-10

cycle 7: relative err = 1.6392150533299687e-09, residual err = 4.4583516444840087e-10

<<< beginning V-cycle (cycle 8) >>>

  level: 6, grid: 128 x 128
  before G-S, residual L2: 7.914383325620475e-10
  after G-S, residual L2: 7.355629304356289e-10

  level: 5, grid: 64 x 64
  before G-S, residual L2: 5.19218220597571e-10
  after G-S, residual L2: 6.364663261794707e-10

  level: 4, grid: 32 x 32
  before G-S, residual L2: 4.485504928535875e-10
  after G-S, residual L2: 4.928237246176745e-10

  level: 3, grid: 16 x 16
  before G-S, residual L2: 3.4637122000064977e-10
  after G-S, residual L2: 3.1194162913950586e-10

  level: 2, grid: 8 x 8
  before G-S, residual L2: 2.174181615639314e-10
  after G-S, residual L2: 1.1194514367241423e-10

  level: 1, grid: 4 x 4
  before G-S, residual L2: 7.455734323986808e-11
  after G-S, residual L2: 1.2201499216239134e-12

  bottom solve:
  level: 0, grid: 2 x 2

  level: 1, grid: 4 x 4
  before G-S, residual L2: 1.6989436916357301e-12
  after G-S, residual L2: 4.475487188247986e-15

  level: 2, grid: 8 x 8
  before G-S, residual L2: 1.521214490284944e-10
  after G-S, residual L2: 4.037434677870943e-12

  level: 3, grid: 16 x 16
  before G-S, residual L2: 4.4491498629640967e-10
  after G-S, residual L2: 1.197248120085576e-11

  level: 4, grid: 32 x 32
  before G-S, residual L2: 7.109792371905777e-10
  after G-S, residual L2: 1.8912335700376235e-11

  level: 5, grid: 64 x 64
  before G-S, residual L2: 9.034017109357381e-10
  after G-S, residual L2: 2.3606466325271617e-11

  level: 6, grid: 128 x 128
  before G-S, residual L2: 1.0238349148814258e-09
  after G-S, residual L2: 2.678477889744364e-11

cycle 8: relative err = 5.555107077431201e-11, residual err = 1.5088473495842003e-11

We can compare to the true solution

In [20]:
v = a.get_solution()
b = true(a.x2d, a.y2d)
e = v - b

The norm of the error is

In [21]:
e.norm()
Out[21]:
1.6719344048744095e-05

Diffusion

pyro solves the constant-conductivity diffusion equation:

\[\frac{\partial \phi}{\partial t} = k \nabla^2 \phi\]

This is done implicitly using multigrid, using the solver diffusion.

The diffusion equation is discretized using Crank-Nicolson differencing (this makes the diffusion operator time-centered) and the implicit discretization forms a Helmholtz equation solved by the pyro multigrid class. The main parameters that affect this solver are:

[driver]
cfl the “CFL” number for the diffusion equation. Since we are doing an implicit discretization, this is the multiple of the explicit timestep to take.
[diffusion]
k the conductivity (constant)

Examples

gaussian

The gaussian problem initializes a strongly peaked Gaussian centered in the domain. The analytic solution for this shows that the profile remains a Gaussian, with a changing width and peak. This allows us to compare our solver to the analytic solution. This is run as:

./pyro.py diffusion gaussian inputs.gaussian
_images/gauss_diffusion.png

The above figure shows the scalar field after diffusing significantly from its initial strongly peaked state. We can compare to the analytic solution by making radial profiles of the scalar. The plot below shows the numerical solution (red points) overplotted on the analytic solution (solid curves) for several different times. The y-axis is restricted in range to bring out the detail at later times.

_images/gauss_diffusion_compare.png

Exercises

The best way to learn these methods is to play with them yourself. The exercises below are suggestions for explorations and features to add to the advection solver.

Explorations

  • Test the convergence of the solver by varying the resolution and comparing to the analytic solution.
  • How does the solution error change as the CFL number is increased well above 1?
  • Setup some other profiles and experiment with different boundary conditions.

Extensions

  • Switch from Crank-Nicolson (2nd order in time) to backward’s Euler (1st order in time) and compare the solution and convergence. This should only require changing the source term and coefficents used in setting up the multigrid solve. It does not require changes to the multigrid solver itself.
  • Implement a non-constant coefficient diffusion solver—note: this will require improving the multigrid solver.

Incompressible hydrodynamics solver

pyro’s incompressible solver solves:

\[\begin{split}\frac{\partial U}{\partial t} + U \cdot \nabla U + \nabla p &= 0 \\ \nabla \cdot U &= 0\end{split}\]

The algorithm combines the Godunov/advection features used in the advection and compressible solver together with multigrid to enforce the divergence constraint on the velocities.

Here we implement a cell-centered approximate projection method for solving the incompressible equations. At the moment, only periodic BCs are supported.

Examples

shear

The shear problem initializes a shear layer in a domain with doubly-periodic boundaries and looks at the development of two vortices as the shear layer rolls up. This problem was explored in a number of papers, for example, Bell, Colella, & Glaz (1989) and Martin & Colella (2000). This is run as:

./pyro.py incompressible shear inputs.shear
_images/shear.png

The vorticity panel (lower left) is what is usually shown in papers. Note that the velocity divergence is not zero—this is because we are using an approximate projection.

convergence

The convergence test initializes a simple velocity field on a periodic unit square with known analytic solution. By evolving at a variety of resolutions and comparing to the analytic solution, we can measure the convergence rate of the algorithm. The particular set of initial conditions is from Minion (1996). Limiting can be disabled by adding incompressible.limiter=0 to the run command. The basic set of tests shown below are run as:

./pyro.py incompressible converge inputs.converge.32 vis.dovis=0
./pyro.py incompressible converge inputs.converge.64 vis.dovis=0
./pyro.py incompressible converge inputs.converge.128 vis.dovis=0

The error is measured by comparing with the analytic solution using the routine incomp_converge_error.py in analysis/.

_images/incomp_converge.png

The dashed line is second order convergence. We see almost second order behavior with the limiters enabled and slightly better than second order with no limiting.

Exercises

Explorations

  • Disable the MAC projection and run the converge problem—is the method still 2nd order?
  • Disable all projections—does the solution still even try to preserve \(\nabla \cdot U = 0\)?
  • Experiment with what is projected. Try projecting \(U_t\) to see if that makes a difference.

Extensions

  • Switch the final projection from a cell-centered approximate projection to a nodal projection. This will require writing a new multigrid solver that operates on nodal data.
  • Add viscosity to the system. This will require doing 2 parabolic solves (one for each velocity component). These solves will look like the diffusion operation, and will update the provisional velocity field.
  • Switch to a variable density system. This will require adding a mass continuity equation that is advected and switching the projections to a variable-coeffient form (since ρ now enters).

Going further

The incompressible algorithm presented here is a simplified version of the projection methods used in the Maestro low Mach number hydrodynamics code. Maestro can do variable-density incompressible, anelastic, and low Mach number stratified flows in stellar (and terrestrial) environments in close hydrostatic equilibrium.

Low Mach number hydrodynamics solver

pyro’s low Mach hydrodynamics solver is designed for atmospheric flows. It captures the effects of stratification on a fluid element by enforcing a divergence constraint on the velocity field. The governing equations are:

\[\begin{split}\frac{\partial \rho}{\partial t} + \nabla \cdot (\rho U) &= 0 \\ \frac{\partial U}{\partial t} + U \cdot \nabla U + \frac{\beta_0}{\rho} \nabla \left ( \frac{p'}{\beta_0} \right ) &= \frac{\rho'}{\rho} g \\ \nabla \cdot (\beta_0 U) = 0\end{split}\]

with \(\nabla p_0 = \rho_0 g\) and \(\beta_0 = p_0^{1/\gamma}\).

As with the incompressible solver, we implement a cell-centered approximate projection method.

Examples

bubble

The bubble problem places a buoyant bubble in a stratified atmosphere and watches the development of the roll-up due to shear as it rises. This is run as:

./pyro.py lm_atm bubble inputs.bubble

Shallow water solvers

The (augmented) shallow water equations take the form:

\[\begin{split}\frac{\partial h}{\partial t} + \nabla \cdot (h U) &= 0 \\ \frac{\partial (h U)}{\partial t} + \nabla \cdot (h U U) + \frac{1}{2}g\nabla h^2 &= 0 \\ \frac{\partial (h \psi)}{\partial t} + \nabla \cdot (h U \psi) &= 0\end{split}\]

with \(h\) is the fluid height, \(U\) the fluid velocity, \(g\) the gravitational acceleration and \(\psi = \psi(x, t)\) represents some passive scalar.

The implementation here has flattening at shocks and a choice of Riemann solvers.

The main parameters that affect this solver are:

[driver]
cfl the advective CFL number (what fraction of a zone can we cross in a single timestep)
[swe]
use_flattening do we flatten the profiles at shocks? (0=no, 1=yes)
z0 the parameters that affect the flattening algorithm
z1
delta
limiter what type of limiting to use in reconstructing the slopes. 0 means use an unlimited second-order centered difference. 1 is the MC limiter, and 2 is the 4th-order MC limiter
riemann which Riemann solver do we use? “HLLC” for the HLLC solver, or “Roe” for the Roe fix solver
grav the gravitational acceleration (must be > 0)

Example problems

dam

The dam break problem is a standard hydrodynamics problem, analagous to the Sod shock tube problem in compressible hydrodynamics. It considers a one-multidimensional problem of two regions of fluid at different heights, initially separated by a dam. The problem then models the evolution of the system when this dam is removed. As for the Sod problem, there exists an exact solution for the dam break probem, so we can check our solution against the exact solutions. See Toro’s shallow water equations book for details on this problem and the exact Riemann solver.

Because it is one-dimensional, we run it in narrow domains in the x- or y-directions. It can be run as:

./pyro.py swe dam inputs.dam.x
./pyro.py swe dam inputs.dam.y

A simple script, dam_compare.py in analysis/ will read a pyro output file and plot the solution over the exact dam break solution (as given by Stoker (1958) and Wu, Huang & Zheng (1999)). Below we see the result for a dam break run with 128 points in the x-direction, and run until t = 0.3 s.

_images/dam_compare.png

We see excellent agreement for all quantities. The shock wave is very steep, as expected. For this problem, the Roe-fix solver performs slightly better than the HLLC solver, with less smearing at the shock and head/tail of the rarefaction.

quad

The quad problem sets up different states in four regions of the domain and watches the complex interfaces that develop as shocks interact. This problem has appeared in several places (and a detailed investigation is online by Pawel Artymowicz). It is run as:

./pyro.py swe quad inputs.quad

kh

The Kelvin-Helmholtz problem models three layers of fluid: two at the top and bottom of the domain travelling in one direction, one in the central part of the domain travelling in the opposite direction. At the interface of the layers, shearing produces the characteristic Kelvin-Helmholtz instabilities, just as is seen in the standard compressible problem. It is run as:

./pyro.py swe kh inputs.kh

Exercises

Explorations

  • There are multiple Riemann solvers in the swe algorithm. Run the same problem with the different Riemann solvers and look at the differences. Toro’s shallow water text is a good book to help understand what is happening.
  • Run the problems with and without limiting—do you notice any overshoots?

Extensions

  • Limit on the characteristic variables instead of the primitive variables. What changes do you see? (the notes show how to implement this change.)
  • Add a source term to model a non-flat sea floor (bathymetry).

Analysis routines

In addition to the main pyro program, there are many analysis tools that we describe here. Note: some problems write a report at the end of the simulation specifying the analysis routines that can be used with their data.

  • compare.py: this takes two simulation output files as input and compares zone-by-zone for exact agreement. This is used as part of the regression testing.

    usage: ./compare.py file1 file2

  • plot.py: this takes an output file as input and plots the data using the solver’s dovis method. It deduces the solver from the attributes stored in the HDF5 file.

    usage: ./plot.py file

  • analysis/

    • convergence.py: this compares two files with different resolutions (one a factor of 2 finer than the other). It coarsens the finer data and then computes the norm of the difference. This is used to test the convergence of solvers.

    • dam_compare.py: this takes an output file from the shallow water dam break problem and plots a slice through the domain together with the analytic solution (calculated in the script).

      usage: ./dam_compare.py file

    • gauss_diffusion_compare.py: this is for the diffusion solver’s Gaussian diffusion problem. It takes a sequence of output files as arguments, computes the angle-average, and the plots the resulting points over the analytic solution for comparison with the exact result.

      usage: ./gauss_diffusion_compare.py file*

    • incomp_converge_error.py: this is for the incompressible solver’s converge problem. This takes a single output file as input and compares the velocity field to the analytic solution, reporting the L2 norm of the error.

      usage: ./incomp_converge_error.py file

    • plotvar.py: this takes a single output file and a variable name and plots the data for that variable.

      usage: ./plotvar.py file variable

    • sedov_compare.py: this takes an output file from the compressible Sedov problem, computes the angle-average profile of the solution and plots it together with the analytic data (read in from cylindrical-sedov.out).

      usage: ./sedov_compare.py file

    • smooth_error.py: this takes an output file from the advection solver’s smooth problem and compares to the analytic solution, outputing the L2 norm of the error.

      usage: ./smooth_error.py file

    • sod_compare.py: this takes an output file from the compressible Sod problem and plots a slice through the domain over the analytic solution (read in from sod-exact.out).

      usage: ./sod_compare.py file

Testing

There are two types of testing implemented in pyro: unit tests and regression tests. Both of these are driven by the test.py script in the root directory.

Unit tests

pyro implements unit tests using py.test. These can be run via:

./test.py -u

Regression tests

The main driver, pyro.py has the ability to create benchmarks and compare output to stored benchmarks at the end of a simulation. Benchmark output is stored in each solver’s tests/ directory. When testing, we compare zone-by-zone for each variable to see if we agree exactly. If there is any disagreement, this means that we’ve made a change to the code that we need to understand (if may be a bug or may be a fix or optimization).

We can compare to the stored benchmarks simply by running:

./test.py

Note

When running on a new machine, it is possible that roundoff-level differences may mean that we do not pass the regression tests. In this case, one would need to create a new set of benchmarks for that machine and use those for future tests.

Getting help

Mailing list

There is a public mailing list for discussing pyro. Visit the pyro-code@googlegroups.com and subscribe. You can then send questions to that list.

Acknowledgments

The original pyro code was written in 2003-4 to help understand this methods for myself. It was originally written using the Numeric array package and handwritten C extensions for the compute-intensive kernels. It was ported to numarray when that replaced Numeric, and continued to use C extensions. This version “pyro2” was resurrected beginning in 2012 and rewritten for numpy using f2py, and brought up to date.

You are free to use this code and notes in your classes. Please credit Michael Zingale (SBU). Please send me a note describing how you use it, so I can keep track of it (and help justify the development effort).

If you use pyro in a publication, please cite it using this bibtex citation:

@ARTICLE{pyro:2014,
   author = {{Zingale}, M.},
    title = "{pyro: A teaching code for computational astrophysical hydrodynamics}",
  journal = {Astronomy and Computing},
archivePrefix = "arXiv",
   eprint = {1306.6883},
 primaryClass = "astro-ph.IM",
 keywords = {Hydrodynamics, Methods: numerical},
     year = 2014,
    month = oct,
   volume = 6,
    pages = {52--62},
      doi = {10.1016/j.ascom.2014.07.003},
   adsurl = {http://adsabs.harvard.edu/abs/2014A%26C.....6...52Z},
  adsnote = {Provided by the SAO/NASA Astrophysics Data System}
}

pyro benefited from numerous useful discussions with Ann Almgren, John Bell, and Andy Nonaka.

pyro2

advection package

The pyro advection solver. This implements a second-order, unsplit method for linear advection based on the Colella 1990 paper.

Subpackages

advection.problems package
Submodules
advection.problems.smooth module
advection.problems.smooth.finalize()[source]

print out any information to the user at the end of the run

advection.problems.smooth.init_data(my_data, rp)[source]

initialize the smooth advection problem

advection.problems.test module
advection.problems.test.finalize()[source]

print out any information to the user at the end of the run

advection.problems.test.init_data(my_data, rp)[source]

an init routine for unit testing

advection.problems.tophat module
advection.problems.tophat.finalize()[source]

print out any information to the user at the end of the run

advection.problems.tophat.init_data(myd, rp)[source]

initialize the tophat advection problem

Submodules

advection.advective_fluxes module

advection.advective_fluxes.unsplit_fluxes(my_data, rp, dt, scalar_name)[source]

Construct the fluxes through the interfaces for the linear advection equation:

\[a_t + u a_x + v a_y = 0\]

We use a second-order (piecewise linear) unsplit Godunov method (following Colella 1990).

In the pure advection case, there is no Riemann problem we need to solve – we just simply do upwinding. So there is only one ‘state’ at each interface, and the zone the information comes from depends on the sign of the velocity.

Our convection is that the fluxes are going to be defined on the left edge of the computational zones:

 |             |             |             |
 |             |             |             |
-+------+------+------+------+------+------+--
 |     i-1     |      i      |     i+1     |

          a_l,i  a_r,i   a_l,i+1

a_r,i and a_l,i+1 are computed using the information in zone i,j.

Parameters:
my_data : CellCenterData2d object

The data object containing the grid and advective scalar that we are advecting.

rp : RuntimeParameters object

The runtime parameters for the simulation

dt : float

The timestep we are advancing through.

scalar_name : str

The name of the variable contained in my_data that we are advecting

Returns:
out : ndarray, ndarray

The fluxes on the x- and y-interfaces

advection.simulation module

class advection.simulation.Simulation(solver_name, problem_name, rp, timers=None, data_class=<class 'mesh.patch.CellCenterData2d'>)[source]

Bases: simulation_null.NullSimulation

dovis()[source]

Do runtime visualization.

evolve()[source]

Evolve the linear advection equation through one timestep. We only consider the “density” variable in the CellCenterData2d object that is part of the Simulation.

initialize()[source]

Initialize the grid and variables for advection and set the initial conditions for the chosen problem.

method_compute_timestep()[source]

Compute the advective timestep (CFL) constraint. We use the driver.cfl parameter to control what fraction of the CFL step we actually take.

advection_fv4 package

Subpackages

advection_fv4.problems package
Submodules
advection_fv4.problems.smooth module

Submodules

advection_fv4.fluxes module

advection_fv4.setup module

advection_fv4.simulation module

advection_rk package

The pyro method-of-lines advection solver. This uses a piecewise linear reconstruction in space together with a Runge-Kutta integration for time.

Subpackages

Submodules

advection_rk.fluxes module

advection_rk.fluxes.fluxes(my_data, rp, dt)[source]

Construct the fluxes through the interfaces for the linear advection equation:

\[a_t + u a_x + v a_y = 0\]

We use a second-order (piecewise linear) Godunov method to construct the interface states, using Runge-Kutta integration. These are one-dimensional predictions to the interface, relying on the coupling in transverse directions through the intermediate stages of the Runge-Kutta integrator.

In the pure advection case, there is no Riemann problem we need to solve – we just simply do upwinding. So there is only one ‘state’ at each interface, and the zone the information comes from depends on the sign of the velocity.

Our convection is that the fluxes are going to be defined on the left edge of the computational zones:

 |             |             |             |
 |             |             |             |
-+------+------+------+------+------+------+--
 |     i-1     |      i      |     i+1     |

          a_l,i  a_r,i   a_l,i+1

a_r,i and a_l,i+1 are computed using the information in zone i,j.

Parameters:
my_data : CellCenterData2d object

The data object containing the grid and advective scalar that we are advecting.

rp : RuntimeParameters object

The runtime parameters for the simulation

dt : float

The timestep we are advancing through.

scalar_name : str

The name of the variable contained in my_data that we are advecting

Returns:
out : ndarray, ndarray

The fluxes on the x- and y-interfaces

advection_rk.simulation module

class advection_rk.simulation.Simulation(solver_name, problem_name, rp, timers=None, data_class=<class 'mesh.patch.CellCenterData2d'>)[source]

Bases: advection.simulation.Simulation

evolve()[source]

Evolve the linear advection equation through one timestep. We only consider the “density” variable in the CellCenterData2d object that is part of the Simulation.

method_compute_timestep()[source]

Compute the advective timestep (CFL) constraint. We use the driver.cfl parameter to control what fraction of the CFL step we actually take.

substep(myd)[source]

take a single substep in the RK timestepping starting with the conservative state defined as part of myd

advection_weno package

The pyro advection solver. This implements a finite difference Lax-Friedrichs flux split method with WENO reconstruction based on Shu’s review from 1998 (https://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/19980007543.pdf) although the notation more follows Gerolymos et al (https://doi.org/10.1016/j.jcp.2009.07.039).

Most of the code is taken from advection_rk and toy-conslaw.

The general flow of the solver when invoked through pyro.py is:

create grid

initial conditions

main loop

fill ghost cells

compute dt

compute fluxes

conservative update

output

Subpackages

Submodules

advection_weno.fluxes module

advection_weno.fluxes.fluxes(my_data, rp, dt)[source]

Construct the fluxes through the interfaces for the linear advection equation:

a + u a + v a = 0
t x y

We use a high-order flux split WENO method to construct the interface fluxes. No Riemann problems are solved. The Lax-Friedrichs flux split will probably make it diffusive; the lack of a transverse solver also will not help.

Parameters:
my_data : CellCenterData2d object

The data object containing the grid and advective scalar that we are advecting.

rp : RuntimeParameters object

The runtime parameters for the simulation

dt : float

The timestep we are advancing through.

scalar_name : str

The name of the variable contained in my_data that we are advecting

Returns:
out : ndarray, ndarray

The fluxes on the x- and y-interfaces

advection_weno.fluxes.fvs(q, order, u, alpha)[source]

Perform Flux-Vector-Split (LF) finite differencing using WENO in 1d.

Parameters:
q : np array

input data with at least order+1 ghost zones

order : int

WENO order (k)

u : float

Advection velocity in this direction

alpha : float

Maximum characteristic speed

Returns:
f : np array

flux

advection_weno.simulation module

class advection_weno.simulation.Simulation(solver_name, problem_name, rp, timers=None, data_class=<class 'mesh.patch.CellCenterData2d'>)[source]

Bases: advection.simulation.Simulation

evolve()[source]

Evolve the linear advection equation through one timestep. We only consider the “density” variable in the CellCenterData2d object that is part of the Simulation.

method_compute_timestep()[source]

Compute the advective timestep (CFL) constraint. We use the driver.cfl parameter to control what fraction of the CFL step we actually take.

substep(myd)[source]

take a single substep in the RK timestepping starting with the conservative state defined as part of myd

compare module

compare.compare(data1, data2)[source]

given two CellCenterData2d objects, compare the data, zone-by-zone and output any errors

compressible package

Subpackages

compressible.problems package
Submodules
compressible.problems.acoustic_pulse module
compressible.problems.advect module
compressible.problems.bubble module
compressible.problems.hse module
compressible.problems.kh module
compressible.problems.quad module
compressible.problems.rt module
compressible.problems.sedov module
compressible.problems.sod module
compressible.problems.test module

Submodules

compressible.BC module

compressible.derives module

compressible.eos module

compressible.setup module

compressible.simulation module

compressible.unsplit_fluxes module

compressible_fv4 package

Subpackages

compressible_fv4.problems package
Submodules
compressible_fv4.problems.acoustic_pulse module

Submodules

compressible_fv4.fluxes module

compressible_fv4.simulation module

compressible_react package

Subpackages

compressible_react.problems package
Submodules
compressible_react.problems.flame module
compressible_react.problems.rt module

Submodules

compressible_react.simulation module

compressible_rk package

Subpackages

Submodules

compressible_rk.fluxes module

compressible_rk.simulation module

compressible_sdc package

Subpackages

Submodules

compressible_sdc.simulation module

diffusion package

The pyro diffusion solver. This implements second-order implicit diffusion using Crank-Nicolson time-differencing. The resulting system is solved using multigrid.

The general flow is:

  • compute the RHS given the current state
  • set up the MG
  • solve the system using MG for updated phi

The timestep is computed as:

CFL* 0.5*dt/dx**2

Subpackages

diffusion.problems package
Submodules
diffusion.problems.gaussian module
diffusion.problems.gaussian.finalize()[source]

print out any information to the user at the end of the run

diffusion.problems.gaussian.init_data(my_data, rp)[source]

initialize the Gaussian diffusion problem

diffusion.problems.gaussian.phi_analytic(dist, t, t_0, k, phi_1, phi_2)[source]

the analytic solution to the Gaussian diffusion problem

diffusion.problems.test module
diffusion.problems.test.finalize()[source]

print out any information to the user at the end of the run

diffusion.problems.test.init_data(my_data, rp)[source]

an init routine for unit testing

Submodules

diffusion.simulation module

A simulation of diffusion

class diffusion.simulation.Simulation(solver_name, problem_name, rp, timers=None, data_class=<class 'mesh.patch.CellCenterData2d'>)[source]

Bases: simulation_null.NullSimulation

A simulation of diffusion

dovis()[source]

Do runtime visualization.

evolve()[source]

Diffusion through dt using C-N implicit solve with multigrid

initialize()[source]

Initialize the grid and variables for diffusion and set the initial conditions for the chosen problem.

method_compute_timestep()[source]

The diffusion timestep() function computes the timestep using the explicit timestep constraint as the starting point. We then multiply by the CFL number to get the timestep. Since we are doing an implicit discretization, we do not require CFL < 1.

examples package

Subpackages

examples.multigrid package
Submodules
examples.multigrid.mg_test_general_alphabeta_only module

Test the general MG solver with a variable coeffcient Helmholtz problem. This ensures we didn’t screw up the base functionality here.

Here we solve:

alpha phi + div . ( beta grad phi ) = f

with:

alpha = 1.0
beta = 2.0 + cos(2.0*pi*x)*cos(2.0*pi*y)

f = (-16.0*pi**2*cos(2*pi*x)*cos(2*pi*y) - 16.0*pi**2 + 1.0)*sin(2*pi*x)*sin(2*pi*y)

This has the exact solution:

phi = sin(2.0*pi*x)*sin(2.0*pi*y)

on [0,1] x [0,1]

We use Dirichlet BCs on phi. For beta, we do not have to impose the same BCs, since that may represent a different physical quantity. Here we take beta to have Neumann BCs. (Dirichlet BCs for beta will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_general_alphabeta_only.alpha(x, y)[source]
examples.multigrid.mg_test_general_alphabeta_only.beta(x, y)[source]
examples.multigrid.mg_test_general_alphabeta_only.f(x, y)[source]
examples.multigrid.mg_test_general_alphabeta_only.gamma_x(x, y)[source]
examples.multigrid.mg_test_general_alphabeta_only.gamma_y(x, y)[source]
examples.multigrid.mg_test_general_alphabeta_only.test_general_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the general MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_general_alphabeta_only.true(x, y)[source]
examples.multigrid.mg_test_general_beta_only module

Test the general MG solver with a variable coeffcient Poisson problem (in essence, we are making this solver act like the variable_coefficient_MG.py solver). This ensures we didn’t screw up the base functionality here.

Here we solve:

div . ( beta grad phi ) = f

with:

beta = 2.0 + cos(2.0*pi*x)*cos(2.0*pi*y)

f = -16.0*pi**2*(cos(2*pi*x)*cos(2*pi*y) + 1)*sin(2*pi*x)*sin(2*pi*y)

This has the exact solution:

phi = sin(2.0*pi*x)*sin(2.0*pi*y)

on [0,1] x [0,1]

We use Dirichlet BCs on phi. For beta, we do not have to impose the same BCs, since that may represent a different physical quantity. Here we take beta to have Neumann BCs. (Dirichlet BCs for beta will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_general_beta_only.alpha(x, y)[source]
examples.multigrid.mg_test_general_beta_only.beta(x, y)[source]
examples.multigrid.mg_test_general_beta_only.f(x, y)[source]
examples.multigrid.mg_test_general_beta_only.gamma_x(x, y)[source]
examples.multigrid.mg_test_general_beta_only.gamma_y(x, y)[source]
examples.multigrid.mg_test_general_beta_only.test_general_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the general MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_general_beta_only.true(x, y)[source]
examples.multigrid.mg_test_general_constant module

Test the general MG solver with a CONSTANT coefficient problem – the same one from the multigrid class test. This ensures we didn’t screw up the base functionality here.

We solve:

u_xx + u_yy = -2[(1-6x**2)y**2(1-y**2) + (1-6y**2)x**2(1-x**2)]
u = 0 on the boundary

this is the example from page 64 of the book A Multigrid Tutorial, 2nd Ed.

The analytic solution is u(x,y) = (x**2 - x**4)(y**4 - y**2)

examples.multigrid.mg_test_general_constant.alpha(x, y)[source]
examples.multigrid.mg_test_general_constant.beta(x, y)[source]
examples.multigrid.mg_test_general_constant.f(x, y)[source]
examples.multigrid.mg_test_general_constant.gamma_x(x, y)[source]
examples.multigrid.mg_test_general_constant.gamma_y(x, y)[source]
examples.multigrid.mg_test_general_constant.test_general_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the general MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_general_constant.true(x, y)[source]
examples.multigrid.mg_test_general_dirichlet module

Test the general MG solver with Dirichlet boundary conditions.

Here we solve:

alpha phi + div{beta grad phi} + gamma . grad phi = f

with:

alpha = 1.0
beta = cos(2*pi*x)*cos(2*pi*y) + 2.0
gamma_x = sin(2*pi*x)
gamma_y = sin(2*pi*y)

f = (-16.0*pi**2*cos(2*pi*x)*cos(2*pi*y) + 2.0*pi*cos(2*pi*x) +
      2.0*pi*cos(2*pi*y) - 16.0*pi**2 + 1.0)*sin(2*pi*x)*sin(2*pi*y)

This has the exact solution:

phi = sin(2.0*pi*x)*sin(2.0*pi*y)

on [0,1] x [0,1]

We use Dirichlet BCs on phi.

For the coefficients we do not have to impose the same BCs, since that may represent a different physical quantity. beta is the one that really matters since it must be brought to the edges. Here we take beta to have Neumann BCs. (Dirichlet BCs for beta will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_general_dirichlet.alpha(x, y)[source]
examples.multigrid.mg_test_general_dirichlet.beta(x, y)[source]
examples.multigrid.mg_test_general_dirichlet.f(x, y)[source]
examples.multigrid.mg_test_general_dirichlet.gamma_x(x, y)[source]
examples.multigrid.mg_test_general_dirichlet.gamma_y(x, y)[source]
examples.multigrid.mg_test_general_dirichlet.test_general_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the general MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_general_dirichlet.true(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous module
Test the general MG solver with inhomogeneous Dirichlet
boundary conditions.

Here we solve:

alpha phi + div{beta grad phi} + gamma . grad phi = f

with:

alpha = 10.0
beta = x*y + 1  (note: x*y alone doesn't work)
gamma_x = 1
gamma_y = 1

f =  -(pi/2)*(x + 1)*sin(pi*y/2)*cos(pi*x/2)
    - (pi/2)*(y + 1)*sin(pi*x/2)*cos(pi*y/2) +
    (-pi**2*(x*y+1)/2 + 10)*cos(pi*x/2)*cos(pi*y/2)

This has the exact solution:

phi = cos(pi*x/2)*cos(pi*y/2)

on [0,1] x [0,1], with Dirichlet boundary conditions:

phi(x=0) = cos(pi*y/2)
phi(x=1) = 0
phi(y=0) = cos(pi*x/2)
phi(y=1) = 0

For the coefficients we do not have to impose the same BCs, since that may represent a different physical quantity. beta is the one that really matters since it must be brought to the edges. Here we take beta to have Neumann BCs. (Dirichlet BCs for beta will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_general_inhomogeneous.alpha(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.beta(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.f(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.gamma_x(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.gamma_y(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.test_general_poisson_inhomogeneous(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the general MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_general_inhomogeneous.true(x, y)[source]
examples.multigrid.mg_test_general_inhomogeneous.xl_func(y)[source]
examples.multigrid.mg_test_general_inhomogeneous.yl_func(x)[source]
examples.multigrid.mg_test_simple module

an example of using the multigrid class to solve Laplace’s equation. Here, we solve:

u_xx + u_yy = -2[(1-6x**2)y**2(1-y**2) + (1-6y**2)x**2(1-x**2)]
u = 0 on the boundary

this is the example from page 64 of the book A Multigrid Tutorial, 2nd Ed.

The analytic solution is u(x,y) = (x**2 - x**4)(y**4 - y**2)

examples.multigrid.mg_test_simple.f(x, y)[source]
examples.multigrid.mg_test_simple.test_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]
examples.multigrid.mg_test_simple.true(x, y)[source]
examples.multigrid.mg_test_vc_constant module

Test the variable coefficient MG solver with a CONSTANT coefficient problem – the same one from the multigrid class test. This ensures we didn’t screw up the base functionality here.

We solve:

u_xx + u_yy = -2[(1-6x**2)y**2(1-y**2) + (1-6y**2)x**2(1-x**2)]
u = 0 on the boundary

this is the example from page 64 of the book A Multigrid Tutorial, 2nd Ed.

The analytic solution is u(x,y) = (x**2 - x**4)(y**4 - y**2)

examples.multigrid.mg_test_vc_constant.alpha(x, y)[source]
examples.multigrid.mg_test_vc_constant.f(x, y)[source]
examples.multigrid.mg_test_vc_constant.test_vc_constant(N)[source]
examples.multigrid.mg_test_vc_constant.true(x, y)[source]
examples.multigrid.mg_test_vc_dirichlet module

Test the variable-coefficient MG solver with Dirichlet boundary conditions.

Here we solve:

div . ( alpha grad phi ) = f

with:

alpha = 2.0 + cos(2.0*pi*x)*cos(2.0*pi*y)

f = -16.0*pi**2*(cos(2*pi*x)*cos(2*pi*y) + 1)*sin(2*pi*x)*sin(2*pi*y)

This has the exact solution:

phi = sin(2.0*pi*x)*sin(2.0*pi*y)

on [0,1] x [0,1]

We use Dirichlet BCs on phi. For alpha, we do not have to impose the same BCs, since that may represent a different physical quantity. Here we take alpha to have Neumann BCs. (Dirichlet BCs for alpha will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_vc_dirichlet.alpha(x, y)[source]
examples.multigrid.mg_test_vc_dirichlet.f(x, y)[source]
examples.multigrid.mg_test_vc_dirichlet.test_vc_poisson_dirichlet(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the variable-coefficient MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_vc_dirichlet.true(x, y)[source]
examples.multigrid.mg_test_vc_periodic module

Test the variable-coefficient MG solver with periodic data.

Here we solve:

div . ( alpha grad phi ) = f

with:

alpha = 2.0 + cos(2.0*pi*x)*cos(2.0*pi*y)

f = -16.0*pi**2*(cos(2*pi*x)*cos(2*pi*y) + 1)*sin(2*pi*x)*sin(2*pi*y)

This has the exact solution:

phi = sin(2.0*pi*x)*sin(2.0*pi*y)

on [0,1] x [0,1]

We use Dirichlet BCs on phi. For alpha, we do not have to impose the same BCs, since that may represent a different physical quantity. Here we take alpha to have Neumann BCs. (Dirichlet BCs for alpha will force it to 0 on the boundary, which is not correct here)

examples.multigrid.mg_test_vc_periodic.alpha(x, y)[source]
examples.multigrid.mg_test_vc_periodic.f(x, y)[source]
examples.multigrid.mg_test_vc_periodic.test_vc_poisson_periodic(N, store_bench=False, comp_bench=False, make_plot=False, verbose=1)[source]

test the variable-coefficient MG solver. The return value here is the error compared to the exact solution, UNLESS comp_bench=True, in which case the return value is the error compared to the stored benchmark

examples.multigrid.mg_test_vc_periodic.true(x, y)[source]
examples.multigrid.mg_vis module

an example of using the multigrid class to solve Laplace’s equation. Here, we solve:

u_xx + u_yy = -2[(1-6x**2)y**2(1-y**2) + (1-6y**2)x**2(1-x**2)]
u = 0 on the boundary

this is the example from page 64 of the book A Multigrid Tutorial, 2nd Ed.

The analytic solution is u(x,y) = (x**2 - x**4)(y**4 - y**2)

examples.multigrid.mg_vis.doit(nx, ny)[source]
examples.multigrid.mg_vis.f(x, y)[source]
examples.multigrid.mg_vis.true(x, y)[source]
examples.multigrid.project_periodic module

test of a cell-centered, centered-difference approximate projection.

initialize the velocity field to be divergence free and then add to it the gradient of a scalar (whose normal component vanishes on the boundaries). The projection should recover the original divergence- free velocity field.

The test velocity field comes from Almgen, Bell, and Szymczak 1996.

This makes use of the multigrid solver with periodic boundary conditions.

One of the things that this test demonstrates is that the initial projection may not be able to completely remove the divergence free part, so subsequent projections may be necessary. In this example, we add a very strong gradient component.

The total number of projections to perform is given by nproj. Each projection uses the divergence of the velocity field from the previous iteration as its source term.

Note: the output file created stores the original field, the poluted field, and the recovered field.

examples.multigrid.project_periodic.doit(nx, ny)[source]

manage the entire projection

examples.multigrid.prolong_restrict_demo module
examples.multigrid.prolong_restrict_demo.doit()[source]

incompressible package

Subpackages

incompressible.problems package
Submodules
incompressible.problems.converge module
incompressible.problems.shear module

Submodules

incompressible.setup module

incompressible.simulation module

lm_atm package

Subpackages

lm_atm.problems package
Submodules
lm_atm.problems.bubble module

Submodules

lm_atm.setup module

lm_atm.simulation module

mesh package

This is the general mesh module for pyro. It implements everything necessary to work with finite-volume data.

Submodules

mesh.array_indexer module

An array class that has methods supporting the type of stencil operations we see in finite-difference methods, like i+1, i-1, etc.

class mesh.array_indexer.ArrayIndexer[source]

Bases: numpy.ndarray

a class that wraps the data region of a single array (d) and allows us to easily do array operations like d[i+1,j] using the ip() method.

copy()[source]

make a copy of the array, defined on the same grid

fill_ghost(n=0, bc=None)[source]

Fill the boundary conditions. This operates on a single component, n. We do periodic, reflect-even, reflect-odd, and outflow

We need a BC object to tell us what BC type on each boundary.

ip(shift, buf=0, n=0, s=1)[source]

return a view of the data shifted by shift in the x direction. By default the view is the same size as the valid region, but the buf can specify how many ghost cells on each side to include. The component is n and s is the stride

ip_jp(ishift, jshift, buf=0, n=0, s=1)[source]

return a view of the data shifted by ishift in the x direction and jshift in the y direction. By default the view is the same size as the valid region, but the buf can specify how many ghost cells on each side to include. The component is n and s is the stride

is_asymmetric(nodal=False, tol=1e-14)[source]

return True is the data is left-right asymmetric (to the tolerance tol)—e.g, the sign flips. For node-centered data, set nodal=True

is_symmetric(nodal=False, tol=1e-14, asymmetric=False)[source]

return True is the data is left-right symmetric (to the tolerance tol) For node-centered data, set nodal=True

jp(shift, buf=0, n=0, s=1)[source]

return a view of the data shifted by shift in the y direction. By default the view is the same size as the valid region, but the buf can specify how many ghost cells on each side to include. The component is n and s is the stride

lap(n=0, buf=0)[source]

return the 5-point Laplacian

norm(n=0)[source]

find the norm of the quantity (index n) defined on the same grid, in the domain’s valid region

pretty_print(n=0, fmt=None, show_ghost=True)[source]

Print out a small dataset to the screen with the ghost cells a different color, to make things stand out

v(buf=0, n=0, s=1)[source]

return a view of the valid data region for component n, with stride s, and a buffer of ghost cells given by buf

mesh.boundary module

Methods to manage boundary conditions

class mesh.boundary.BC(xlb='outflow', xrb='outflow', ylb='outflow', yrb='outflow', xl_func=None, xr_func=None, yl_func=None, yr_func=None, grid=None, odd_reflect_dir='')[source]

Bases: object

Boundary condition container – hold the BCs on each boundary for a single variable.

For Neumann and Dirichlet BCs, a function callback can be stored for inhomogeous BCs. This function should provide the value on the physical boundary (not cell center). This is evaluated on the relevant edge when the __init__ routine is called. For this reason, you need to pass in a grid object. Note: this only ensures that the first ghost cells is consistent with the BC value.

class mesh.boundary.BCProp(xl_prop, xr_prop, yl_prop, yr_prop)[source]

Bases: object

A simple container to hold properties of the boundary conditions.

mesh.boundary.bc_is_solid(bc)[source]

return a container class indicating which boundaries are solid walls

mesh.boundary.define_bc(bc_type, function, is_solid=False)[source]

use this to extend the types of boundary conditions supported on a solver-by-solver basis. Here we pass in the reference to a function that can be called with the data that needs to be filled. is_solid indicates whether it should be interpreted as a solid wall (no flux through the BC)”

mesh.fv module

This implements support for 4th-order accurate finite-volume data by adding support for converting between cell averages and centers.

class mesh.fv.FV2d(grid, dtype=<class 'numpy.float64'>)[source]

Bases: mesh.patch.CellCenterData2d

this is a finite-volume grid. We expect the data to represent cell-averages, and do operations to 4th order. This assumes dx = dy

from_centers(name)[source]

treat the stored data as if it lives at cell-centers and convert it to an average

to_centers(name)[source]

convert variable name from an average to cell-centers

mesh.integration module

A generic Runge-Kutta type integrator for integrating CellCenterData2d. We support a generic Butcher tableau for explicit the Runge-Kutta update:

0   |
c_2 | a_21
c_3 | a_31 a_32
:   |  :        .
:   |  :          .
c_s | a_s1 a_s2 ... a_s,s-1
----+---------------------------
    | b_1  b_2  ... b_{s-1}  b_s

the update is:

y_{n+1} = y_n + dt sum_{i=1}^s {b_i k_i}

and the s increment is:

k_s = f(t + c_s dt, y_n + dt (a_s1 k1 + a_s2 k2 + ... + a_s,s-1 k_{s-1})
class mesh.integration.RKIntegrator(t, dt, method='RK4')[source]

Bases: object

the integration class for CellCenterData2d, supporting RK integration

compute_final_update()[source]

this constructs the final t + dt update, overwriting the inital data

get_stage_start(istage)[source]

get the starting conditions (a CellCenterData2d object) for stage istage

nstages()[source]

return the number of stages

set_start(start)[source]

store the starting conditions (should be a CellCenterData2d object)

store_increment(istage, k_stage)[source]

store the increment for stage istage – this should not have a dt weighting

mesh.patch module

The patch module defines the classes necessary to describe finite-volume data and the grid that it lives on.

Typical usage:

  • create the grid:

    grid = Grid2d(nx, ny)
    
  • create the data that lives on that grid:

    data = CellCenterData2d(grid)
    
    bc = BC(xlb="reflect", xrb="reflect",
           ylb="outflow", yrb="outflow")
    data.register_var("density", bc)
    ...
    
    data.create()
    
  • initialize some data:

    dens = data.get_var("density")
    dens[:, :] = ...
    
  • fill the ghost cells:

    data.fill_BC("density")
    
class mesh.patch.CellCenterData2d(grid, dtype=<class 'numpy.float64'>)[source]

Bases: object

A class to define cell-centered data that lives on a grid. A CellCenterData2d object is built in a multi-step process before it can be used.

  • Create the object. We pass in a grid object to describe where the data lives:

    my_data = patch.CellCenterData2d(myGrid)
    
  • Register any variables that we expect to live on this patch. Here BC describes the boundary conditions for that variable:

    my_data.register_var('density', BC)
    my_data.register_var('x-momentum', BC)
    ...
    
  • Register any auxillary data – these are any parameters that are needed to interpret the data outside of the simulation (for example, the gamma for the equation of state):

    my_data.set_aux(keyword, value)
    
  • Finish the initialization of the patch:

    my_data.create()
    

This last step actually allocates the storage for the state variables. Once this is done, the patch is considered to be locked. New variables cannot be added.

add_derived(func)[source]

Register a function to compute derived variable

Parameters:
func : function

A function to call to derive the variable. This function should take two arguments, a CellCenterData2d object and a string variable name (or list of variables)

create()[source]

Called after all the variables are registered and allocates the storage for the state data.

fill_BC(name)[source]

Fill the boundary conditions. This operates on a single state variable at a time, to allow for maximum flexibility.

We do periodic, reflect-even, reflect-odd, and outflow

Each variable name has a corresponding BC stored in the CellCenterData2d object – we refer to this to figure out the action to take at each boundary.

Parameters:
name : str

The name of the variable for which to fill the BCs.

fill_BC_all()[source]

Fill boundary conditions on all variables.

get_aux(keyword)[source]

Get the auxillary data associated with keyword

Parameters:
keyword : str

The name of the auxillary data to access

Returns:
out : variable type

The value corresponding to the keyword

get_var(name)[source]

Return a data array for the variable described by name. Stored variables will be checked first, and then any derived variables will be checked.

For a stored variable, changes made to this are automatically reflected in the CellCenterData2d object.

Parameters:
name : str

The name of the variable to access

Returns:
out : ndarray

The array of data corresponding to the variable name

get_var_by_index(n)[source]

Return a data array for the variable with index n in the data array. Any changes made to this are automatically reflected in the CellCenterData2d object.

Parameters:
n : int

The index of the variable to access

Returns:
out : ndarray

The array of data corresponding to the index

get_vars()[source]

Return the entire data array. Any changes made to this are automatically reflected in the CellCenterData2d object.

Returns:
out : ndarray

The array of data

max(name, ng=0)[source]

return the maximum of the variable name in the domain’s valid region

min(name, ng=0)[source]

return the minimum of the variable name in the domain’s valid region

pretty_print(var, fmt=None)[source]

print out the contents of the data array with pretty formatting indicating where ghost cells are.

prolong(varname)[source]

Prolong the data in the current (coarse) grid to a finer (factor of 2 finer) grid. Return an array with the resulting data (and same number of ghostcells). Only the data for the variable varname will be operated upon.

We will reconstruct the data in the zone from the zone-averaged variables using the same limited slopes as in the advection routine. Getting a good multidimensional reconstruction polynomial is hard – we want it to be bilinear and monotonic – we settle for having each slope be independently monotonic:

          (x)         (y)
f(x,y) = m    x/dx + m    y/dy + <f>

where the m’s are the limited differences in each direction. When averaged over the parent cell, this reproduces <f>.

Each zone’s reconstrution will be averaged over 4 children:

+-----------+     +-----+-----+
|           |     |     |     |
|           |     |  3  |  4  |
|    <f>    | --> +-----+-----+
|           |     |     |     |
|           |     |  1  |  2  |
+-----------+     +-----+-----+

We will fill each of the finer resolution zones by filling all the 1’s together, using a stride 2 into the fine array. Then the 2’s and …, this allows us to operate in a vector fashion. All operations will use the same slopes for their respective parents.

register_var(name, bc)[source]

Register a variable with CellCenterData2d object.

Parameters:
name : str

The variable name

bc : BC object

The boundary conditions that describe the actions to take for this variable at the physical domain boundaries.

restrict(varname, N=2)[source]

Restrict the variable varname to a coarser grid (factor of 2 coarser) and return an array with the resulting data (and same number of ghostcells)

set_aux(keyword, value)[source]

Set any auxillary (scalar) data. This data is simply carried along with the CellCenterData2d object

Parameters:
keyword : str

The name of the datum

value : any time

The value to associate with the keyword

write(filename)[source]

create an output file in HDF5 format and write out our data and grid.

write_data(f)[source]

write the data out to an hdf5 file – here, f is an h5py File pbject

zero(name)[source]

Zero out the data array associated with variable name.

Parameters:
name : str

The name of the variable to zero

class mesh.patch.Grid2d(nx, ny, ng=1, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0)[source]

Bases: object

the 2-d grid class. The grid object will contain the coordinate information (at various centerings).

A basic (1-d) representation of the layout is:

|     |      |     X     |     |      |     |     X     |      |     |
+--*--+- // -+--*--X--*--+--*--+- // -+--*--+--*--X--*--+- // -+--*--+
   0          ng-1    ng   ng+1         ... ng+nx-1 ng+nx      2ng+nx-1

                     ilo                      ihi

|<- ng guardcells->|<---- nx interior zones ----->|<- ng guardcells->|

The ‘*’ marks the data locations.

coarse_like(N)[source]

return a new grid object coarsened by a factor n, but with all the other properties the same

fine_like(N)[source]

return a new grid object finer by a factor n, but with all the other properties the same

norm(d)[source]

find the norm of the quantity d defined on the same grid, in the domain’s valid region

scratch_array(nvar=1)[source]

return a standard numpy array dimensioned to have the size and number of ghostcells as the parent grid

mesh.patch.cell_center_data_clone(old)[source]

Create a new CellCenterData2d object that is a copy of an existing one

Parameters:
old : CellCenterData2d object

The CellCenterData2d object we wish to copy

mesh.patch.do_demo()[source]

show examples of the patch methods / classes

mesh.reconstruction module

Support for computing limited differences needed in reconstruction of slopes in constructing interface states.

mesh.reconstruction.flatten(myg, q, idir, ivars, rp)[source]

compute the 1-d flattening coefficients

mesh.reconstruction.flatten_multid(myg, q, xi_x, xi_y, ivars)[source]

compute the multidimensional flattening coefficient

mesh.reconstruction.limit(data, myg, idir, limiter)[source]

a single driver that calls the different limiters based on the value of the limiter input variable.

mesh.reconstruction.limit2(a, myg, idir)[source]

2nd order monotonized central difference limiter

mesh.reconstruction.limit4(a, myg, idir)[source]

4th order monotonized central difference limiter

mesh.reconstruction.nolimit(a, myg, idir)[source]

just a centered difference without any limiting

mesh.reconstruction.well_balance(q, myg, limiter, iv, grav)[source]

subtract off the hydrostatic pressure before limiting. Note, this only considers the y direction.

mesh.reconstruction.weno(q, order)[source]

Perform WENO reconstruction

Parameters:
q : np array

input data with 3 ghost zones

order : int

WENO order (k)

Returns:
q_plus, q_minus : np array

data reconstructed to the right / left respectively

mesh.reconstruction.weno_upwind(q, order)[source]

Perform upwinded (left biased) WENO reconstruction

Parameters:
q : np array

input data

order : int

WENO order (k)

Returns:
q_plus : np array

data reconstructed to the right

multigrid package

This is the pyro multigrid solver. THere are several versions.

MG implements a second-order discretization of a constant-coefficient Helmholtz equation:

(alpha - beta L) phi = f

variable_coeff_MG implements a variable-coefficient Poisson equation:

div { eta grad phi } = f

general_MG implements a more general elliptic equation:

alpha phi + div { beta grad phi } + gamma . grad phi = f

All use pure V-cycles to solve elliptic problems

Submodules

multigrid.MG module

The multigrid module provides a framework for solving elliptic problems. A multigrid object is just a list of grids, from the finest mesh down (by factors of two) to a single interior zone (each grid has the same number of guardcells).

The main multigrid class is setup to solve a constant-coefficient Helmholtz equation:

(alpha - beta L) phi = f

where L is the Laplacian and alpha and beta are constants. If alpha = 0 and beta = -1, then this is the Poisson equation.

We support Dirichlet or Neumann BCs, or a periodic domain.

The general usage is as follows:

a = multigrid.CellCenterMG2d(nx, ny, verbose=1, alpha=alpha, beta=beta)

this creates the multigrid object a, with a finest grid of nx by ny zones and the default boundary condition types. alpha and beta are the coefficients of the Helmholtz equation. Setting verbose = 1 causing debugging information to be output, so you can see the residual errors in each of the V-cycles.

Initialization is done as:

a.init_zeros()

this initializes the solution vector with zeros (this is not necessary if you just created the multigrid object, but it can be used to reset the solution between runs on the same object).

Next:

a.init_RHS(zeros((nx, ny), numpy.float64))

this initializes the RHS on the finest grid to 0 (Laplace’s equation). Any RHS can be set by passing through an array of (nx, ny) values here.

Then to solve, you just do:

a.solve(rtol = 1.e-10)

where rtol is the desired tolerance (residual norm / source norm)

to access the final solution, use the get_solution method:

v = a.get_solution()

For convenience, the grid information on the solution level is available as attributes to the class,

a.ilo, a.ihi, a.jlo, a.jhi are the indices bounding the interior of the solution array (i.e. excluding the ghost cells).

a.x and a.y are the coordinate arrays a.dx and a.dy are the grid spacings

class multigrid.MG.CellCenterMG2d(nx, ny, ng=1, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, xl_BC_type='dirichlet', xr_BC_type='dirichlet', yl_BC_type='dirichlet', yr_BC_type='dirichlet', xl_BC=None, xr_BC=None, yl_BC=None, yr_BC=None, alpha=0.0, beta=-1.0, nsmooth=10, nsmooth_bottom=50, verbose=0, aux_field=None, aux_bc=None, true_function=None, vis=0, vis_title='')[source]

Bases: object

The main multigrid class for cell-centered data.

We require that nx = ny be a power of 2 and dx = dy, for simplicity

get_solution(grid=None)[source]

Return the solution after doing the MG solve

If a grid object is passed in, then the solution is put on that grid – not the passed in grid must have the same dx and dy

Returns:
out : ndarray
get_solution_gradient(grid=None)[source]

Return the gradient of the solution after doing the MG solve. The x- and y-components are returned in separate arrays.

If a grid object is passed in, then the gradient is computed on that grid. Note: the passed-in grid must have the same dx, dy

Returns:
out : ndarray, ndarray
get_solution_object()[source]

Return the full solution data object at the finest resolution after doing the MG solve

Returns:
out : CellCenterData2d object
grid_info(level, indent=0)[source]

Report simple grid information

init_RHS(data)[source]

Initialize the right hand side, f, of the Helmholtz equation (alpha - beta L) phi = f

Parameters:
data : ndarray

An array (of the same size as the finest MG level) with the values to initialize the solution to the elliptic problem.

init_solution(data)[source]

Initialize the solution to the elliptic problem by passing in a value for all defined zones

Parameters:
data : ndarray

An array (of the same size as the finest MG level) with the values to initialize the solution to the elliptic problem.

init_zeros()[source]

Set the initial solution to zero

smooth(level, nsmooth)[source]

Use red-black Gauss-Seidel iterations to smooth the solution at a given level. This is used at each stage of the V-cycle (up and down) in the MG solution, but it can also be called directly to solve the elliptic problem (although it will take many more iterations).

Parameters:
level : int

The level in the MG hierarchy to smooth the solution

nsmooth : int

The number of r-b Gauss-Seidel smoothing iterations to perform

solve(rtol=1e-11)[source]

The main driver for the multigrid solution of the Helmholtz equation. This controls the V-cycles, smoothing at each step of the way and uses simple smoothing at the coarsest level to perform the bottom solve.

Parameters:
rtol : float

The relative tolerance (residual norm / source norm) to solve to. Note that if the source norm is 0 (e.g. the righthand side of our equation is 0), then we just use the norm of the residual.

v_cycle(level)[source]

Perform a V-cycle for a single 2-level solve. This is applied recursively do V-cycle through the entire hierarchy.

multigrid.edge_coeffs module

class multigrid.edge_coeffs.EdgeCoeffs(g, eta, empty=False)[source]

Bases: object

a simple container class to hold edge-centered coefficients and restrict them to coarse levels

restrict()[source]

restrict the edge values to a coarser grid. Return a new EdgeCoeffs object

multigrid.general_MG module

This multigrid solver is build from multigrid/MG.py and implements a more general solver for an equation of the form:

alpha phi + div { beta grad phi } + gamma . grad phi = f

where alpha, beta, and gamma are defined on the same grid as phi. These should all come in as cell-centered quantities. The solver will put beta on edges. Note that gamma is a vector here, with x- and y-components.

A cell-centered discretization for phi is used throughout.

class multigrid.general_MG.GeneralMG2d(nx, ny, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, xl_BC_type='dirichlet', xr_BC_type='dirichlet', yl_BC_type='dirichlet', yr_BC_type='dirichlet', xl_BC=None, xr_BC=None, yl_BC=None, yr_BC=None, nsmooth=10, nsmooth_bottom=50, verbose=0, coeffs=None, true_function=None, vis=0, vis_title='')[source]

Bases: multigrid.MG.CellCenterMG2d

this is a multigrid solver that supports our general elliptic equation.

we need to accept a coefficient CellCenterData2d object with fields defined for alpha, beta, gamma_x, and gamma_y on the fine level.

We then restrict this data through the MG hierarchy (and average beta to the edges).

we need a new compute_residual() and smooth() function, that understands these coeffs.

smooth(level, nsmooth)[source]

Use red-black Gauss-Seidel iterations to smooth the solution at a given level. This is used at each stage of the V-cycle (up and down) in the MG solution, but it can also be called directly to solve the elliptic problem (although it will take many more iterations).

Parameters:
level : int

The level in the MG hierarchy to smooth the solution

nsmooth : int

The number of r-b Gauss-Seidel smoothing iterations to perform

multigrid.variable_coeff_MG module

This multigrid solver is build from multigrid/MG.py and implements a variable coefficient solver for an equation of the form:

div { eta grad phi } = f

where eta is defined on the same grid as phi.

A cell-centered discretization is used throughout.

class multigrid.variable_coeff_MG.VarCoeffCCMG2d(nx, ny, xmin=0.0, xmax=1.0, ymin=0.0, ymax=1.0, xl_BC_type='dirichlet', xr_BC_type='dirichlet', yl_BC_type='dirichlet', yr_BC_type='dirichlet', nsmooth=10, nsmooth_bottom=50, verbose=0, coeffs=None, coeffs_bc=None, true_function=None, vis=0, vis_title='')[source]

Bases: multigrid.MG.CellCenterMG2d

this is a multigrid solver that supports variable coefficients

we need to accept a coefficient array, coeffs, defined at each level. We can do this at the fine level and restrict it down the MG grids once.

we need a new compute_residual() and smooth() function, that understands coeffs.

smooth(level, nsmooth)[source]

Use red-black Gauss-Seidel iterations to smooth the solution at a given level. This is used at each stage of the V-cycle (up and down) in the MG solution, but it can also be called directly to solve the elliptic problem (although it will take many more iterations).

Parameters:
level : int

The level in the MG hierarchy to smooth the solution

nsmooth : int

The number of r-b Gauss-Seidel smoothing iterations to perform

plot module

plot.get_args()[source]
plot.makeplot(plotfile_name, outfile, width, height)[source]

plot the data in a plotfile using the solver’s vis() method

pyro module

pyro.doit(solver_name, problem_name, param_file, other_commands=None, comp_bench=False, reset_bench_on_fail=False, make_bench=False)[source]

The main driver to run pyro

pyro.parse_and_run()[source]

Parse the runtime parameters and run a pyro instance

simulation_null module

class simulation_null.NullSimulation(solver_name, problem_name, rp, timers=None, data_class=<class 'mesh.patch.CellCenterData2d'>)[source]

Bases: object

compute_timestep()[source]

a generic wrapper for computing the timestep that respects the driver parameters on timestepping

do_output()[source]

is it time to output?

dovis()[source]
evolve()[source]
finalize()[source]

Do any final clean-ups for the simulation and call the problem’s finalize() method.

finished()[source]

is the simulation finished based on time or the number of steps

initialize()[source]
method_compute_timestep()[source]

the method-specific timestep code

preevolve()[source]

Do any necessary evolution before the main evolve loop. This is not needed for advection

read_extras(f)[source]

read in any simulation-specific data from an h5py file object f

write(filename)[source]

Output the state of the simulation to an HDF5 file for plotting

write_extras(f)[source]

write out any extra simulation-specific stuff

simulation_null.bc_setup(rp)[source]
simulation_null.grid_setup(rp, ng=1)[source]

swe package

Subpackages

swe.problems package
Submodules
swe.problems.acoustic_pulse module
swe.problems.advect module
swe.problems.dam module
swe.problems.kh module
swe.problems.quad module
swe.problems.test module

Submodules

swe.derives module

swe.simulation module

swe.unsplit_fluxes module

test module

class test.PyroTest(solver, problem, inputs, options)[source]

Bases: object

test.do_tests(build, out_file, do_standalone=True, do_main=True, reset_fails=False, store_all_benchmarks=False, single=None, solver=None)[source]

util package

This module provides utility functions for pyro

Submodules

util.io module

This manages the reading of the HDF5 output files for pyro.

util.io.read(filename)[source]

read an HDF5 file and recreate the simulation object that holds the data and state of the simulation.

util.io.read_bcs(f)[source]

read in the boundary condition record from the HDF5 file

util.msg module

support output in highlighted colors

util.msg.bold(string)[source]

Output a string in a bold weight

util.msg.fail(string)[source]

Output a string to the terminal and abort if we are running non-interactively. The string is colored red to indicate a failure

util.msg.success(string)[source]

Output a string to the terminal colored green to indicate success

util.msg.warning(string)[source]

Output a string to the terminal colored orange to indicate a warning

util.plot_tools module

Some basic support routines for configuring the plots during runtime visualization

util.plot_tools.setup_axes(myg, num)[source]

create a grid of axes whose layout depends on the aspect ratio of the domain

util.profile module

A very simple profiling class, to use to determine where most of the time is spent in a code. This supports nested timers and outputs a report at the end.

Warning: At present, no enforcement is done to ensure proper nesting.

class util.profile.Timer(name, stack_count=0)[source]

Bases: object

A single timer – this simply stores the accumulated time for a single named region

begin()[source]

Start timing

end()[source]

Stop timing. This does not destroy the timer, it simply stops it from counting time.

class util.profile.TimerCollection[source]

Bases: object

A timer collection—this manages the timers and has methods to start and stop them. Nesting of timers is tracked so we can pretty print the profiling information.

To define a timer:

tc = TimerCollection()
a = tc.timer('my timer')

This will add ‘my timer’ to the list of Timers managed by the TimerCollection. Subsequent calls to timer() will return the same Timer object.

To start the timer:

a.begin()

and to end it:

a.end()

For best results, the block of code timed should be large enough to offset the overhead of the timer class method calls.

tc.report() prints out a summary of the timing.

report()[source]

Generate a timing summary report

timer(name)[source]

Create a timer with the given name. If one with that name already exists, then we return that timer.

Parameters:
name : str

Name of the timer

Returns:
out : Timer object

A timer object corresponding to the name.

util.runparams module

basic syntax of the parameter file is:

# simple parameter file

[driver]
nsteps = 100         ; comment
max_time = 0.25

[riemann]
tol = 1.e-10
max_iter = 10

[io]
basename = myfile_

The recommended way to use this is for the code to have a master list of parameters and their defaults (e.g. _defaults), and then the user can override these defaults at runtime through an inputs file. These two files have the same format.

The calling sequence would then be:

rp = RuntimeParameters()
rp.load_params("_defaults")
rp.load_params("inputs")

The parser will determine what datatype the parameter is (string, integer, float), and store it in a RuntimeParameters object. If a parameter that already exists is encountered a second time (e.g., there is a default value in _defaults and the user specifies a new value in inputs), then the second instance replaces the first.

Runtime parameters can then be accessed via any module through the get_param method:

tol = rp.get_param('riemann.tol')

If the optional flag no_new=1 is set, then the load_params function will not define any new parameters, but only overwrite existing ones. This is useful for reading in an inputs file that overrides previously read default values.

class util.runparams.RuntimeParameters[source]

Bases: object

command_line_params(cmd_strings)[source]

finds dictionary pairs from a string that came from the commandline. Stores the parameters in only if they already exist.

we expect things in the string in the form:
[“sec.opt=value”, “sec.opt=value”]

with each opt an element in the list

Parameters:
cmd_strings : list

The list of strings containing runtime parameter pairs

get_param(key)[source]

returns the value of the runtime parameter corresponding to the input key

load_params(pfile, no_new=0)[source]

Reads line from file and makes dictionary pairs from the data to store.

Parameters:
file : str

The name of the file to parse

no_new : int, optional

If no_new = 1, then we don’t add any new paramters to the dictionary of runtime parameters, but instead just override the values of existing ones.

print_all_params()[source]

Print out all runtime parameters and their values

print_paramfile()[source]

Create a file, inputs.auto, that has the structure of a pyro inputs file, with all known parameters and values

print_unused_params()[source]

Print out the list of parameters that were defined by never used

write_params(f)[source]

Write the runtime parameters to an HDF5 file. Here, f is the h5py file object

util.runparams.is_float(string)[source]

is the given string a float?

util.runparams.is_int(string)[source]

is the given string an interger?

Indices and tables