This repository contains code that parallels what students need to create for the BYU Scientific Computing Course's semester-long project. The problem, of course, is different, but imitating this code should serve most students well. Some sacrifices to good style have been made for ease of understanding by students unfamiliar with modern C++.
Make sure to clone the repository with submodules:
git clone --recursive [email protected]:BYUHPC/sci-comp-course-example-cxx.git
Alternately, you can initialize and update submodules after cloning:
git submodule update --init
To build and test, set SRCDIR
to the directory containing CMakeLists.txt
, navigate to a clean build directory, and run:
cmake "$SRCDIR"
cmake --build . --parallel
ctest
The binaries initial
, mountaindiff
and mountainsolve_*
will be built.
mountaindiff
is used to check whether two binary mountain range files are similar enough to be considered identical; run mountaindiff --help
for its usage.
The other binaries mirror those that will be built for the C++ phases of the project:
Corresponding phase | Binary | Source files |
---|---|---|
Phase 1 | initial |
initial, MountainRange |
Phase 2 | mountainsolve_serial * |
MountainRange |
Phase 3 | mountainsolve_openmp |
MountainRange |
Phase 6 | mountainsolve_thread |
MountainRangeThreaded |
Phase 7 | mountainsolve_mpi * |
MountainRangeMPI |
Phase 8 | mountainsolve_gpu * |
MountainRangeGPU |
In addition to the source files listed above, each binary uses the base class MountainRange, and each mountainsolve_*
uses binary_io and mountainsolve.
* mountainsolve_serial
uses identical code to mountainsolve_openmp
, but is compiled without OpenMP--part of the beauty of OpenMP. mountainsolve_mpi
is only built if an MPI compiler is found. mountainsolve_gpu
is only built if the compiler is Nvidia's HPC SDK. On our supercomputer, you can access an MPI compiler with module load gcc openmpi mpl
, and Nvidia's HPC SDK with module load nvhpc
.
Each generated mountainsolve_*
has a help message explaining its usage; use <binary-name> --help
to print it.
initial.jl
contains example code for phase 9; it can be run with julia src/initial.jl
and runs on the same mountain range as initial.cpp
. Mountains.jl
is a Julia package with similar functionality to the C++ code.
This example code simulates a crude approximation of mountain building with a Neumann boundary condition in one dimension:
As with the problem this one parallels, simplifications are made to reduce code complexity. The state is fully determined by:
t
: the amount of time that the simulation has progressed. Given a time stepdt
, this is the amount of iterations multiplied bydt
, which like in the parallel problem is fixed at 0.01.r
: an array in which each cell represents the uplift rate at the corresponding point.h
: an array in which each cell represents the current height of the correspondint point.g
: an array in which each cell represents the current growth rate of the corresponding point.
This state will hence be referred to as a mountain range.
To step the i
th cell of a mountain range from time t
to time t+dt
the following algorithm is used:
Here's how one step of dt
for the whole mountain range might look in Julia:
function step!(h, g, r, dt)
# Update h
for i in eachindex(r, h, g)
h[i] += dt*g[i]
end
# Update g
for i in firstindex(h)+1:lastindex(h)-1
L = (h[i-1]+h[i+1])/2 - h[i]
g[i] = r[i]-h[i]^3+L
end
# Enforce boundary condition
g[begin] = g[begin+1]
g[end] = g[end-1]
end
We discretize the derivative of the mountain range's "steepness" at cell
$$\dot{s}i = \frac{\left( h{i+1} - h_{i-1} \right)\left( g_{i+1} - g_{i-1} \right)}{2 n}$$
...where h
and g
arrays.
The simulation stops when the sum of the steepness derivative over the whole mountain range falls below zero--i.e. when the range is at its "steepest."
The steepness derivative of a mountain range could be calculated in Julia thus:
function dsteepness(h, g)
ds = 0
for i in firstindex(h)+1:lastindex(h)-1
ds += (h[i+1]-h[i-1]) * (g[i+1]-g[i-1]) / 2
end
return ds/(length(h)/2)
end
The purpose of this code is to, given an initial state, update that state until the steepness derivative drops below the machine eps of a 64-bit float (i.e. solve the state). Given initial state given by uplift rate r
, time step dt
, simulation time t0
, height h0
, and growth rate g0
, and the functions step!
and dsteepness
defined above, here is a Julia function that would solve the state:
function solve(t0, h0, g0, r, dt)
# Initialize
t = t0
h, g = deepcopy.(h0, g0)
# Solve
while dsteepness(h, g) > eps(Float64)
step!(h, g, r, dt)
t += dt
end
# Return updated state as a tuple
return h, g, t
end
initial
simply creates a mountain range, solves it, and prints the resulting simulation time. mountainsolve_*
read files containing binary mountain ranges, solve them, and write the solved mountain ranges in the same format. There are several sample input and output files in the samples directory that are meant to be used for validation; as an example, one could check that mountainsolve_thread
works as expected with four threads thus:
SOLVER_NUM_THREADS=4 build-dir/mountainsolve_thread samples/small-1D-in.mr /tmp/my-small-1D-out.mr
build-dir/mountaindiff samples/small-1D-out.mr /tmp/my-small-1D-out.mr \
&& echo Success \
|| echo Failure
Mountain range data files contain binary data sufficient to represent the state of the simulation. Here is the order and format of the elements in a mountain range file:
Member | Format | |
---|---|---|
1 | Number of dimensions (always 1 in our case) | 64-bit unsigned integer |
2 | Number of elements in each array (n ) |
64-bit unsigned interger |
3 | Simulation time | 64-bit float |
4 | Uplift rate array | n 64-bit floats |
5 | Height array | n 64-bit floats |
The data is tightly packed--there are no gaps between elements.
See the write
function in src/MountainRange.hpp
for an example of how to write in binary.
As with the project problem, you don't need to understand the following, but it's included for context.
The differential equation governing the evolution of the simulation is:
...where
Again like the project problem, this can be generalized to an arbitrary number of dimensions, but we stick to one dimension here for simplicity.
The derivative of h
at the edges is simulated as zero by setting the first and last cells of g
to the value of their immediate neighbors after updating g
's interior.
We define the steepness to be the average squared 2-norm of the gradient of the height across the mountain range:
...where
The derivative of the steepness with respect to time is:
The simulation stops when eps(Float64)
--i.e. when the mountain range is at its "steepest."
The gradient is discretized as:
...which in one dimension becomes:
The derivative of the two-norm of the gradient of
...where each of
...where