This project is a renderer, using nanogui, of a particular Simulated Annealing algorithm that calculates the folding of the cerebellum (yes, the actual part of the brain) in 2 dimensions (which can lead to valid extrapolations since the cerebellum folds uniformly across dimensions) based on an energy function that is an estimate of a surface's Free Energy.
- Install git:
sudo apt install git
- Install cmake:
sudo apt install cmake
- Install OpenGL headers:
sudo apt install libgl1-mesa-dev
- Install this package for some reason:
sudo apt install xorg-dev
- Clone recursively:
git clone https://github.com/amurices/sa-surface-2d.git --recursive
- cd into nanogui’s folder:
cd sa-surface-2d/nanogui
- make a build directory:
mkdir build
- change directory into it:
cd build
- use CMake to write build files to this directory by doing:
cmake ..
(in Unix, .. means “the directory above”. So if your current path is /usr/bin/local, doing cd .. will take you to /usr/bin. CMake takes a directory as a parameter to find a CMakeLists.txt file, which in this case, is the one directly above it) - run
make
to compile the generated code - Go back to sa-surface-2d’s directory:
cd ../..
- Finally, do the same we did for nanogui; first create a build directory:
mkdir build
- cd into that folder:
cd build
- use CMake to write build files to it:
cmake ..
- finally, run
make
There, just 14 steps lol. Now you can run the program by being in the build directory and just doing ./sa_surface_2d
If make
complains at the end, try replacing the dylib
in CMakeLists.txt with so
:
${CMAKE_SOURCE_DIR}/nanogui/build/libnanogui.dylib -> ${CMAKE_SOURCE_DIR}/nanogui/build/libnanogui.so
This is due to the fact that building with cmake on Mac generates .dylib by default, while Linux generates .so. Functionally they’re the same thing: libraries, which is just a collection of compiled code and symbols that map on to that code (which is why we have to include the nanogui headers; we have to tell our code which parts of the compiled nanogui library we’re going to use).
The model is based on Two polygons with the same amount of sides, one internal and one external. It follows the
Simulated Annealing algorithm by iteratively performing a step function, called Optimizer::stepSimulatedAnnealing
. Before
describing what an iteration of this instance of the SA algorithm does, here are a few definitions:
- A surface is represented by a mere polygon. It is essentially a circular graph.
- A thick surface is represented by two surfaces and one thickness vector. The latter is an array of
double
s, wherethicknesses[n]
represents the distance betweenouterSurfaceNodes[n]
andinnerSurfaceNodes[n]
; each node in a surface is therefore identified by a number in the range[0..amountOfPoints-1]
.
The function works as follows:
- First, find a neighbor of the current state. Here, a state is just the current configuration of the thick surface under optimization. A state's neighbor is defined by choosing a random node on the state's outer surface and altering it in a random direction. More details on how this alteration can be performed later.
- Find the energy of both the neighbor and the current state. The energy function is defined as follows (pseudocode):
let whiteMatter = area(innerSurface)
grayMatter = area(outerSurface) - area(innerSurface)
grayMatterStretch = absolute(g0 - grayMatter) // a0 is initial (relaxed) state's gray matter area; Must be non-negative
in
am * whiteMatter^ap + dm * (grayMatter + 1)^dp // where `am`, `ap`, `dm` and `dp` are *given* constants defined by an input file
- Find the probability of moving into a new state. The probability function is defined as follows (actual C++ implementation):
double Optimizer::findProbability(double eS, double eN, double t){
if (eN < eS)
return 1;
else
return exp((eS - eN) / t); // t being 0 doesn't break this, interestingly
}
-
If the neighbor contains no intersections between the set of edges comprising the inner and outer polygons, consider it the new state with the probability found in 3.
-
Currently, don't decrement temperature and just repeat. Decrementing temperature by just subtracting an absolute value from it was giving weird results.
As noted in the description above, altering a node is an operation that can be configured by an input file or via the nanogui-powered front end GUI. The parameters are:
- Smoothness; Arguably the most important parameter, it's more easily explained visually. The drawing below showcases what happens with a randomly selected node in a given iteration. A node's neighbors are pushed in the same direction it was, with decreasing intensity based on how far from it they are. Smoothness is an integer number that determines how many nodes will be "smoothed"; so the example in the picture shows a simulation with smoothness set to 2. lower smoothness values tend to lead to more "gyrified" but more jagged and "spiky" simulations.
- forceOffsetRange; Defines the numerical range in the XY plane a node can be pushed in. Example: if set to 0.066, the maximum alteration to a node's coordinates is (0.033, 0.033) and the minimum (-0.033, -0.033).
- thickness; the initial thickness of the surface. Every element in the
thicknesses
array described above is set to this value in the beginning of the simulation. The number means multiplication of radius, so a thick surface with thickness 1.0 has no inner surface area. - points; Number of nodes in the outer and inner polygons. Higher numbers mean smoother-looking simulations, but also slower ones.
- diffPow, diffMul, areaPow and areaMul; the variables described in the energy function's pseudocode above. They are dimensionless and are all set to 1.0 by default.
- multiProb; the probability, after selecting a random node, that another random node will be selected to be altered in the same iteration. So if this is set to 0.5, the probability that 3 random nodes will be picked in the same iteration to be altered and considered part of the same neighbor is 1 * 0.5 * 0.5 (1 first because at least one node is always selected). Set to 0 by default.
- tempProb; unused.
- Last but not least, compression; An important parameter that describes how the surface stretches as it's pulled and pushed. If a node is pulled away from its inner correspondent (the first node altered is always the outer one), then its thickness is multiplied by this value. If it's pushed towards its inner correspondent, it is divided by this value.