In Using USim to solve the Euler Equations, Solving Problems on Unstructured Meshes in USim, we saw how to solve the Euler and MHD equations for a range of problems using USim. The algorithms that we used to do this were created using a range of macros. USim simulations can also be created without the use of these macros, allowing the user much greater control over the simulation design process and enabling simulations with greater complexity. The next set of tutorials describes how to create USim simulations without the use of macros. It does so by making use of the examples in USimBase again, but this time we examine the actual input blocks generated from the macros. This is done in the Setup window by choosing Save And Process Setup and then clicking on the <exampleName>.in tab. In the .in tab all macros are expanded to produce input blocks.

We start this set of tutorials by presenting advanced concepts about USim that you should understand before creating and running USim simulations without using the macros described in earlier tutorials. Taking the time to examine these concepts before reading about the simulation process will make the simulation procedures simple to understand and tutorial lessons straightforward to follow.

## The Fluids Component¶

USim input files without macros look like XML files. Input files can contain nested blocks, each block can contain double, integer and string values and vectors (arrays) of these types. Remember: if you want to specify a double in the input file you must not omit the decimal point. Otherwise, the number will be interpreted as an integer. Similarly, if you want an array of doubles, the first element must have a decimal point. Hence, what follows are examples of a double scalar and a double vector:

value = 1.0
listOfNumbers = [1.0, 2.0, 3.0]

The simplest example of an input file given below. It basically does nothing but starts USim and quits:

# start and end times
tStart = 0.0
tEnd = 0.2
# number of frames to write
numFrames = 2
# initial time-step to use
initDt = 0.01
verbosity = debug
defaultParallelSync = false

# top level component
<Component fluids>
kind = updaterComponent
</Component>

All input files need a start and end time for the simulation, specified as tStart and tEnd. The numFrames variable tells USim how many frames of data to write. USim will always write the initial frame and then will write additional numFrames frames. Each frame is an HDF5 file named same as the input file with numbers appended to indicate frame number. For example, euler_0.h5, euler_1.h5 and euler_2.h5 will be created by the above simulation. More information on USim data output is given in Writing Out Data.

The initial time-step is specified in initDt. This can be set to whatever you want as USim will simply adjust this depending on the physics included in the simulation.

The option defaultParallelSync=false tells USim that the user will specify the parallel synchronization of the data in the simulation. defaultParallelSync is optional, but defaults to false. If defaultParallelSync=true then parallel data synchronization is done automatically. Automatic synchronization is less efficient than user defined synchronization, but avoids many of the pitfalls. If you are having problems with a parallel simulations, a simple check of your synchronization is to set defaultParallelSync=true.

In each simulation there must be exactly one top-level Component block. This block is called fluids. You can name it anything you want and this name will be reflected in the output HDF5 files. Many blocks take a special field called the kind field. This field tells USim what kind of Component (in this case) to create. For now, we will use the standard updaterComponent component kind.

## Defining the Simulation Grid¶

Every object in a USim simulation interacts with a grid in one, two or three dimensions, in either Cartesian or axisymmetric Cylindrical coordinates. Grids in USim can either be rectangular, body-fitted or unstructured. The simplest example is a one-dimensional rectangular grid, which here we name domain:

<Grid domain>
kind = cart1d
ghostLayers = 2
lower = [0.0]
upper = [1.0]
cells = [512]
</Grid>

This will create a $$512$$ zone, 1D Cartesian grid spanning the physical space $$(0.0, 1.0)$$. Instructions for creating multi-dimensional Cartesian grids, body-fitted grids and unstructured meshes can be found in other sections of this manual.

## Allocating Memory¶

Data can be allocated on the grid defined in Defining the Simulation Grid through the code block such as:

<DataStruct q>
kind = nodalArray
onGrid = domain
numComponents = 5
</DataStruct>

This block will create a nodalArray array called “q”. A nodal array is one that interacts with rectangular, body-fitted and unstructured meshes and is decomposed using MPI when run in parallel. Note we have to specify the name of the grid the data lives on using the onGrid field. The field numComponents tells USim that we wish to store 5 components in this array.

Now our input file should look like:

# start and end times
tStart = 0.0
tEnd = 0.2
# number of frames to write
numFrames = 2
# initial time-step to use
initDt = 0.01
verbosity = debug

# top level component
<Component fluids>
kind = updaterComponent

<Grid domain>
kind = cart1d
ghostLayers = 2
lower = [0.0]
upper = [1.0]
cells = [512]
</Grid>

<DataStruct q>
kind = nodalArray
onGrid = domain
numComponents = 5
</DataStruct>

</Component>

This input file can be executed within USimComposer.

## Setting Initial Conditions¶

Now that we have allocated an array we will initialize it. USim provides a block called Updater to perform action on data. There are many Updaters and we will use them extensively in constructing a simulation. Updaters are very powerful and can be used to create very complex simulations by combining them carefully. Think of an updater as a generalization of a subroutine or function.

Let’s create an updater, named “init” to initialize the “q” array. This updater will use a expressions to initialize all the components of “q”. As a simple example, we will use the initial condition for a one-dimensional Sod Shock tube, which consists of a left-state and a right-state separated by a discontinuity:

<Updater init>
kind = initialize1d
onGrid = domain
out = [q]

# initial condition to use
<Function func>
kind = exprFunc

# location of discontinuity
sloc = 0.5

# gas density in left state
rhol = 3.0
# gas pressure in left state
prl = 3.0
# gas density in right state
rhor = 1.0
# gas pressure in right state
prr = 1.0

# Adiabatic index, or ratio of specific heats
gasGamma = $GAMMA$

preExprs = [ \
"rho = if (x>sloc, rhor, rhol)", \
"pr = if (x>sloc, prr, prl)"]
exprs = ["rho", "0.0", "0.0", "0.0", "pr/(gasGamma-1)"]

</Function>

</Updater>

Each updater must have a kind field and an onGrid field. This updater is of kind “initialize1d” and runs on the grid “domain” that we created previously. Updaters usually have in and out fields that specify which datastructures are input and which are output. Other fields and blocks depend on the updater kind. In this updater we are using a Function block to specify a function for use in the initial conditions. The kind of function we are using is the “exprFunction” that uses expressions. This particular expression sets the following initial condition

$\begin{eqnarray} & \rho &= 3.0 \quad x<0.5\\ & \rho &= 1.0 \quad x>0.5\\ & P_g &= 3.0 \quad x<0.5 \\ & P_g &= 1.0 \quad x>0.5 \end{eqnarray}$

If we simply put the above block in the input file and run the simulation nothing will happen! I.e. the initial conditions will not be applied. The reason is that updaters do not run automatically. We have to specify when the updater will be run using UpdateStep blocks. To do this we add the following block in the input file:

updaters = [init]
syncVars = [q]

This tells USim that when the initStep updater-step is run to call the updater “init”. The variable syncVars specifies which variables should be synchronized AFTER the list of updaters is called. In this case the variable q is synchronized. If defaultParallelSync=false then syncVars can be dropped and USim will automatically synchronize all variables in the out list of the updaters.

The final step to have this updater-step run is to add a single UpdateSequence block:

startOnly = [initStep]
loop = []
writeOnly = []

Thats it! When we run the input file the “q” array will be initialized with the updater. Now our complete input file looks like:

# Adiabatic index, or ratio of specific heats
$GAMMA = 1.4 # start and end times tStart = 0.0 tEnd = 0.2 # number of frames to write numFrames = 2 # initial time-step to use initDt = 0.01 verbosity = debug # top level component <Component fluids> kind = updaterComponent <Grid domain> kind = cart1d numLayers = 2 lower = [0.0] upper = [1.0] cells = [100] isRadial = false </Grid> <DataStruct q> kind = nodalArray onGrid = domain numComponents = 5 </DataStruct> <Updater init> kind = initialize1d onGrid = domain out = [q] # initial condition to use <Function func> kind = exprFunc # location of discontinuity sloc = 0.5 # gas density in left state rhol = 3.0 # gas pressure in left state prl = 3.0 # gas density in right state rhor = 1.0 # gas pressure in right state prr = 1.0 # Adiabatic index, or ratio of specific heats gasGamma =$GAMMA\$

preExprs = [ \
"rho = if (x>sloc, rhor, rhol)", \
"pr = if (x>sloc, prr, prl)"]

exprs = ["rho", "0.0", "0.0", "0.0", "pr/(gasGamma-1)"]

</Function>

</Updater>

updaters = [init]

startOnly = [initStep]
loop = []
writeOnly = []

</Component>

## Writing Out Data¶

All data produced by USim is output using the HDF5 data file format. These output files are dumped as defined by the user; they can be written at the end of a simulation, for example, or every n steps to create a time series that can be used to see how a system evolves over time. Additionally, these output files can be used to restart a run and continue from a given point; for example, if a user has run a simulation for 1000 time steps and wishes to see how the simulation progresses if run for another 1000.

Execution of USim leads to the generation of the data from the simulation in the form of HDF5 .h5 files. There are separate executables for serial and parallel runs.

If we plot the q_4 variable in Composer we will be plotting the total energy and not the gas pressure. To compute the pressure we can add another array called pressure to store the pressure:

<DataStruct pressure>
kind = nodalArray
onGrid = domain
numComponents = 1
</DataStruct>

Next, we add an “combiner” updater to extract the pressure:

<Updater pressCalc>
kind = combiner1d

onGrid = domain
# input array
in = [q]

# ouput data-structures
out = [pressure]

indVars_q = ["rho", "rhou", "rhov", "rhow", "Er"]

gamma = GAMMA
preExprs = ["pr = (Er - 0.5*(rhou^2+rhov^2+rhow^2)/rho)*(gamma-1)"]
exprs = ["pr"]

</Updater>

In this updater we assign a name to each of the five components of “q” and use them in expressions to compute the pressure using the formula

$p = (\gamma-1)\left(E-0.5\rho(u^2+v^2+w^2)\right)$

where $$E$$ is the total fluid energy and $$(u,v,w)$$ are the fluid velocity components.

Finally, we need to call this updater in an updater-step and add it to the writeOnly list in the update-sequence block:

updaters = [pressCalc]