Octree
This page documents the internal data structure and the init_octree → split_domain → build → update pipeline that octree drives.
Data layout
A built tree is an instance of Octree. Its fields are:
| Field | Type | Meaning |
|---|---|---|
id | Pair{Int,Int} | Unique tree id; first is the owning worker, second is a local counter. |
units | Vector{Units} or nothing | Internal unit system; nothing for unitless. |
config | OctreeConfig | Build parameters (BoxSize, Periodic, …). |
data | StructArray / Vector | The particles the tree was built over. |
pids | Vector{Int} | Worker pids the tree lives on. |
domain | DomainData | Per-worker domain decomposition. |
treenodes | Vector{OctreeNode} | The actual octree. |
mutable | OctreeInfoMutable | extent, NumTotal, NumLocal, NTreenodes, nextfreenode, last. |
sendbuffer / recvbuffer | Dict{Int,Any} | Buffers used by send_buffer during the domain exchange. |
OctreeNode is an immutable struct:
| Field | Type | Meaning |
|---|---|---|
ID | Int | Node id (1 = root). |
Father | Int | Parent id (0 for the root). |
DaughterID | MVector{8,Int} | The 8 children; 0 means "no child here". |
Center | PVector | Geometric center. |
SideLength | Quantity / Float64 | Side length. |
Mass | Quantity / Float64 | Subtree mass. |
MassCenter | PVector | Mass-weighted center. |
MaxSoft | Quantity / Float64 | Largest smoothing length in the subtree. |
IsAssigned | Bool | true on a leaf that owns a particle. |
ParticleID | Int | Index of the assigned particle (0 if none / many). |
NextNode | Int | Sibling / leaf traversal pointer. |
Sibling | Int | Pointer to next node in a depth-first walk. |
BitFlag | Int | Worker-locality flags. |
The non-MVector fields are scalars; because OctreeNode is immutable, mutation is done through Setfield.jl (used internally) and BangBang.jl's setproperties!! / setproperty!!.
The build pipeline
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ init_octree │ → │ split_domain │ → │ build │ → │ update │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
on every pid Peano-decompose Recurse, place Mass / Center /
(config + e + the work into particles into NextNode / Sibling
units) NTopLeaves leaves + fill pseudo
leavesStep 1 — init_octree
PhysicalTrees.init_octree — Functioninit_octree(data, units, config::OctreeConfig, pids::Array{Int64,1})Pre-process data (find the global extent, compute the per-worker DomainFac), and create an empty Octree on every pid in pids. Returns the master-side tree.
This is the first step of the octree build pipeline. Most users should not call this directly; use octree instead.
Computes the global extent, expands it by config.ExtentMargin, computes the per-worker DomainFac, and registers an empty Octree in the local PhysicalTrees.registry. All workers are reached via Distributed.remotecall_eval so the master never directly owns the master's tree when the master is not in pids.
Step 2 — split_domain
Each worker:
- Sorts its local
databypeanokey. - Builds a top tree of
TopNodes describing the local per-leaf particle counts. - Walks the top tree downward (
split_topnode_local_kernel) until every leaf hasCount <= NumTotal / (TopnodeFactor * NWorkers²). - Walks the global top tree to assign each leaf a worker (
find_split). - Ships the particles / peano keys to the assigned worker (
fill_domain_buffer,send_buffer,clear_domain_buffer).
After this stage every worker owns exactly the particles that the top-tree DomainTask assigned to it, in Peano-sorted order.
Step 3 — build
init_treenodes— allocates the root treenode and recursively carves the empty top tree into the local treenode array (create_empty_treenodes).insert_data— walks each particle down the local top tree and writes it into the deepest unoccupied leaf (assign_data_to_tree_leaf). When two particles end up in the same leaf,assign_new_tree_leafis invoked to split it.insert_data_pseudo— attaches a pseudo leaf to each remote top-node leaf. The pseudo leaf stores the aggregated mass and mass center of the remote subtree, so the local tree can resolve gravity / SPH contributions from remote particles.
Step 4 — update
update_local_datawalks the local tree depth-first to computeMass,MassCenter,MaxSoft,NextNodeandSiblingfor every node.fill_pseudo_bufferserializes the local top-tree leaf moments.update_pseudo_datamerges the pseudo-leaf moments into the local top tree, re-aggregatingMass/MassCenter/MaxSoftfrom root to leaves.flag_local_treenodesmarks the top-level / local-partition subtrees with bit flags so search routines can short-circuit leaves that contain no relevant data.
Periodic boundary conditions
When config.Periodic == true the search routines apply a minimum-image convention via ngb_periodic_diff. The relevant differences are:
check_incube_periodicreplacescheck_incubeinsidengb_treefind_variable/ngb_treefind_pairsso a particle whose periodic image falls inside the search cube is included.ngb_periodic_diffmaps a raw coordinate difference into the half-open range(-boxHalf, boxHalf].BoxSizemust match the underlying physical box — the tree'smutable.extent.SideLengthis set byconfig.ExtentMarginand can be much larger than the physical box.
Side-length maintenance
After particle positions drift (SPH integration, gravity kicks, …), the tree's SideLength may no longer enclose its assigned particle or its children. Call update_node_len:
Missing docstring for update_node_len. Check Documenter's build log for details.
This is the Julia equivalent of Gadget-2's force_update_node_len_local. It performs two sweeps:
- Leaf pass — for every treenode with a non-zero
ParticleID, ensureSideLength >= 2 * max(|Pos - Center|). - Internal-node pass (high → low index) — for every non-leaf, ensure
SideLength >= 2 * (max over children of |Center_child - Center_parent| + SideLength_child / 2).
For a full rebuild (e.g. after re-gridding), use rebuild:
Missing docstring for rebuild. Check Documenter's build log for details.
rebuild runs global_extent → split_domain → build → update against the existing tree.data. Use it when the data geometry has changed substantially (e.g. after a load-balancing step or a redistribute); use update_node_len for the common case of small per-step drift.
Distributing and unregistering
Missing docstring for unregister. Check Documenter's build log for details.
unregister(tree) removes tree.id from each worker's PhysicalTrees.registry and from the master. After unregister, accessing the old id raises a KeyError.
To wipe all trees at once (e.g. when tearing down a long-lived session), use clear(workers()) (the lowercase clear is PhysicalTrees.clear; it's not exported to avoid clashing with Base.clear).
Tree serialization and debugging
summary returns a human-readable string of every top-level field:
io = IOBuffer()
show(io, tree)
String(take!(io))Base.show(::IO, ::OctreeNode) prints a single line per node (id, parent, daughters, mass, mass-center, …). These are used heavily by the test suite as well as by AstroPlot.plot_tree.
Field-level invariants
After a successful octree(...) call, the following must hold:
length(treenodes) == config.TreeAllocSectionor the array has been grown to accommodate the tree depth.- For every non-leaf node
n,n.SideLengthdivides inton.DaughterID's children'sSideLength* 2 (geometric halving). - For every assigned leaf
n,data[n.ParticleID].Poslies inside the cube[n.Center ± n.SideLength/2]³. tree.mutable.NumTotal == length(data);tree.mutable.NumLocalis the number of particles on this worker.
Violations of these invariants mean update_node_len (or rebuild) should be called before the next search.