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 cargo:
sudo apt install cargo cargo build
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
doubles, 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 Rust implementation):
fn probability(energy_state: f64, energy_neighbor: f64, temperature: f64) -> f64 {
if temperature < 0.0 {
if energy_neighbor < energy_state { 1.0 } else { 0.0 }
} else if temperature >= INFINITY {
1.0
} else {
((energy_state - energy_neighbor) / temperature).exp()
}
}-
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.
-
Apply the temperature function and just repeat.
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. - low_high; 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).
- initial_thickness; the initial thickness of the surface. Every element in the
thicknessesarray 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. - initial_num_points; Number of nodes in the outer and inner polygons. Higher numbers mean smoother-looking simulations, but also slower ones.
- 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.
- node_addition_threshold; the distance between nodes under which nodes will be added to the system.
- node_deletion_threshold; the distance between nodes under which nodes will be deleted from the system - meaning they'll be merged into one.