Problem setups

Simulations are configured by an object called a setup. The setup specifies initial conditions, a mesh, boundary conditions, and a solver class, among several other optional things. Setups can be specific test problems used for code validation, or they can encompass whole classes of research problems, with many internal parameters. The degrees of freedom within a setup are called model parameters, and these have default values which define a fiducial model, but they can also be overridden from command line input or other sources of configuration.

Minimal example

Here is a minimal setup, which defines a 1D relativistic hydrodynamics problem consisting of a traveling density wave:

from math import sin, pi
from sailfish.setup_base import SetupBase
from sailfish.mesh import PlanarCartesianMesh

class DensityWave(SetupBase):
    """
    A sinusoidal variation of density propagating to the right.
    """
    def primitive(self, t, x, p):
        p[0] = 1.0 + 0.1 * sin(2.0 * pi * x) # gas density
        p[1] = 0.5 # velocity
        p[2] = 1.0 # gas pressure

    def mesh(self, resolution):
        return PlanarCartesianMesh(0.0, 1.0, resolution)

    @property
    def solver(self):
        return "srhd_1d"

    @property
    def boundary_condition(self):
        return "periodic"

This setup uses the srhd_1d solver (relativistic 1D hydrodynamics) with cartesian coordinates, on a domain from 0.0 to 1.0. The boundary condition is periodic, and the hydrodynamic primitive data so the density has a sinusoidal variation, and the velocity is uniform.

Required methods

Custom setup classes must inherit the SetupBase base class. The setup base class provides a lot of functionality in the form of methods that have trivial default implementations, but can be overridden if needed. The four methods implemented in the example above are required for the setup to be instantiated (they are abstract methods in the base class).

The primitive method takes as arguments the time, a coordinate position, and a slice of a primitive variable array representing a single zone. The result is written to this slice rather than returned from the function for efficiency reasons. Generally, the primitive method is only used at the start of a new simulation, so the time will typically be t=0.0. However, in the case of an inflow boundary condition, the solver will also call the setup’s primitive method in the guard zones of the mesh each time boundary conditions need to be applied. In other words, the primitive method always serves as an initial condition, but it can also provide a time-dependent Dirichlet boundary condition. The number and meaning of primitive variables depends on the system being solved (the system is in turn a property of the solver class, which is identified by the solver property).

The object returned by the mesh method describes the physical extent of the simulation domain, as well as how it is discretized and what kind of coordinates are used (i.e. cartesian or spherical). The mesh method accepts a resolution argument, the value of which comes from the command line option --resolution | -n, or the resolution member of the DriverArgs class. It should be interpreted sensibly in the context of your setup to create a mesh (for example no logical issues would arise if you were to ignore this argument altogether, but don’t do that). For example if your setup uses a LogSphericalMesh, it makes sense to interpret the resolution as the number of zones per decade in radius. If it’s a 2D problem in spherical coordinates, it could mean the number of polar zones, or in a 3D problem it could be the number of zones on each side of the domain.

The boundary_condition method returns a string to identify the type of boundary condition to be applied. The mesh and boundary condition objects must be compatible with (supported by) the solver, otherwise the solver will throw an exception when it’s constructed.

Choosing a setup

The first argument to the sailfish command line tool is the name of a setup class, converted to dash-case, i.e. you could run the setup above in a default configuration by running sailfish density-wave. The command line tool will print the setup documentation to the terminal if invoked as sailfish density-wave --describe. It’s good practice to give your setup class an accurate doc string.

Setup sub-classes are automatically discovered when sailfish is imported or run from the command line. However to be found, the module containing your class must be imported before driver.simulate is called. If you add a new source file to the setups module, you must import that module in the setups/__init__.py file for the setup class(es) in it to be discovered.

Model parameters

A setup can have internal degrees of freedom to be configured at runtime, which are referred to as model parameters. To add a model parameter to a setup, just define it as a class variable using the param constructor:

from sailfish.setup_base import SetupBase, param

class DensityWave(SetupBase):
    wavenumber = param(1, "integer wavenumber of the sinusoid")
    velocity = param(0.0, "speed of the wave")

    # ...

The two positional arguments to param are a default value (from which the parameter type is inferred), and a help message. An optional keyword argument mutable=True can be supplied to indicate that a parameter can be changed in a restarted run from its initial value. For the model parameters that only influence the initial condition, it doesn’t make sense to make them mutable.

Model parameters are passed to the setup class from the command line as key-value pairs like this: sailfish density-wave --model amplitude=0.5 wavenumber=2.

Optional methods