Merger Tree Structure in the Output¶
Each row of the datasets in the Outputs/Outputn/nodeData group of a Galacticus output file describes a single node—a dark matter halo, or a subhalo, together with the galaxy that it contains—at the time of that output. The datasets whose names end in “Index” describe how those rows are related to each other: which node hosts which, which node a halo will merge into, and which nodes belong to the same merger tree.
The one thing to know before using them is that the meaning of parentIndex, siblingIndex, and satelliteIndex depends on whether the node is an isolated halo or a subhalo. Galacticus uses a single set of links to represent both the progenitor structure of the tree (which halo descends into which) and the substructure hierarchy (which subhalo lives inside which halo), so the same dataset carries two different meanings depending on the row. The nodeIsIsolated dataset tells you which case you are looking at. This page explains the conventions, and gives recipes for the operations that most analyses need.
The datasets described here are produced by the nodePropertyExtractorNodeIndices property extractor, which is included in the output by default. The additional, unambiguously-named indices described in Reference: all index datasets must be requested explicitly.
Isolated halos and subhalos¶
A node is an isolated halo if it is not contained within any other halo. It is a subhalo (equivalently, a satellite) if it has fallen into, and now orbits within, another halo. The distinction is recorded in nodeIsIsolated, which is \(1\) for an isolated halo and \(0\) for a subhalo. Galaxies in isolated halos are usually referred to as central galaxies, and those in subhalos as satellite galaxies.
Subhalos may themselves contain subhalos: a node whose nodeIsIsolated is \(0\) can still have a non-negative satelliteIndex. Such sub-subhalos arise when a halo which already contained substructure falls into a larger halo.
Links between isolated halos¶
For isolated halos the links describe the progenitor structure of the merger tree—which halo grows into which, as time advances.
---
config:
block:
padding: 40
---
block-beta
columns 8
t2["t₂"] space:4 n1["1"] space:2
space:8
t1["t₁"] space:2 n2["2"] space:3 n3["3"]
space:8
t0["t₀"] n4["4"] space:3 n5["5"] space:2
n4 -- "parentIndex" --> n2
n5 -- "parentIndex" --> n2
n4 -- "siblingIndex" --> n5
n2 -- "parentIndex" --> n1
n3 -- "parentIndex" --> n1
n2 -- "siblingIndex" --> n3
style t0 fill:none,stroke:none
style t1 fill:none,stroke:none
style t2 fill:none,stroke:none
Fig. 1 Links between isolated halos. Time increases upward: halos \(4\) and \(5\) at time \(t_0\) merge to form halo \(2\) at \(t_1\), which merges with halo \(3\) to form halo \(1\) at \(t_2\). Each arrow points in the direction of the index which labels it. Every link which exists here is drawn: halos \(5\) and \(3\) have siblingIndex \(=-1\), since each is the last in its list, and halo \(1\) has parentIndex \(=-1\), since it is at the base of the tree. Note that this is the structure of the tree as it was built, spanning several timesteps—the nodes present in any one output all exist at a single time (see Caveats).¶
parentIndexis the halo into which this halo grows at the next timestep of the merger tree. Where several halos share the same parent (halos \(4\) and \(5\) in Fig. 1) they are merging with each other, and the parent is the halo which results. A halo at the base of the tree has no parent, and so hasparentIndex\(=-1\).siblingIndexis the next halo in the list of halos sharing that same parent, or \(-1\) for the last halo in the list. FollowingsiblingIndexfrom any halo therefore enumerates the remaining halos with which it is merging. (Note that the output contains no dataset naming the first halo in that list, so the list can be entered only from a halo already known to be in it—unlike the list of subhalos below, which is entered viasatelliteIndex.)
Links between a halo and its subhalos¶
For subhalos the same three datasets describe the substructure hierarchy within a single output instead.
---
config:
block:
padding: 40
---
block-beta
columns 6
space:2 h10["10"] space:3
space:6
space s11("11") space:2 s12("12") space
space:6
space s13("13") space:4
h10 -- "satelliteIndex" --> s11
s12 -- "parentIndex" --> h10
s11 -- "siblingIndex" --> s12
s11 -- "satelliteIndex" --> s13
Fig. 2 Links between a halo and its subhalos, all at a single output time. Halo \(10\) hosts subhalos \(11\) and \(12\) (drawn with rounded corners), and subhalo \(11\) in turn hosts the sub-subhalo \(13\). As in Fig. 1, each arrow points in the direction of the index which labels it and is drawn only once—subhalo \(11\) also has parentIndex \(=10\), and sub-subhalo \(13\) has parentIndex \(=11\).¶
parentIndexis the halo which hosts this subhalo—not a descendant.siblingIndexis the next subhalo in the same host’s list of subhalos, or \(-1\) for the last subhalo in that list.satelliteIndexis the first subhalo of this node, or \(-1\) if it has none. It has the same meaning for isolated halos and for subhalos, and is the entry point to the list whichsiblingIndexthen walks.
The nodes of Fig. 2 appear in the output as:
|
|
|
|
|
|---|---|---|---|---|
10 |
1 |
-1 |
-1 |
11 |
11 |
0 |
10 |
12 |
13 |
12 |
0 |
10 |
-1 |
-1 |
13 |
0 |
11 |
-1 |
-1 |
(the parentIndex of \(-1\) for halo \(10\) assumes that it is at the base of its merger tree; otherwise it would be the index of the halo into which halo \(10\) will grow.)
The two pictures are connected: a subhalo is simply a halo whose branch has already joined another branch. In Fig. 1, when halos \(4\) and \(5\) merge at \(t_1\), halo \(4\)—the primary progenitor—is promoted to become halo \(2\), while halo \(5\) becomes a subhalo of it, and from that point on halo \(5\) is linked into the satellite list of halo \(2\), exactly as in Fig. 2.
How to read a row¶
|
|
|
|---|---|---|
|
The halo into which this halo grows at the next timestep of the merger tree (\(-1\) at the base of the tree). |
The halo which hosts this subhalo. |
|
The next halo sharing the same parent, i.e. the next halo with which this halo is merging (\(-1\) if there is none). |
The next subhalo in the same host (\(-1\) if there is none). |
|
The first subhalo of this halo (\(-1\) if it has none). |
The first sub-subhalo of this subhalo (\(-1\) if it has none). |
Reference: all index datasets¶
Only nodeIndex, parentIndex, siblingIndex, satelliteIndex, and nodeIsIsolated are output by default. The remaining datasets below are produced by property extractors which must be added to the nodePropertyExtractor parameter to appear in the output—their names are unambiguous, so they are a good choice if you would rather not have to test nodeIsIsolated in your analysis.
Dataset |
Extractor |
Meaning |
|---|---|---|
|
Identifies the node. Unique within a merger tree, but not between trees—see Caveats. |
|
|
Descendant or host, depending on |
|
|
Next progenitor of the same parent, or next subhalo of the same host, depending on |
|
|
First subhalo of this node (\(-1\) if it has none). |
|
|
\(1\) for an isolated halo, \(0\) for a subhalo. |
|
|
The host of this subhalo. For an isolated halo the node’s own |
|
|
The node containing this galaxy at the next output time (its forward descendant). At the final output, or if the galaxy survives to the next output, the node’s own index is returned—this extractor never returns \(-1\). This is the dataset to use to trace a galaxy from one output to the next. |
|
|
The node at the base of the merger tree into which this node ultimately merges. |
|
|
The index of the merger tree containing this node. Note that the enclosing |
|
|
The host halo at the time this node most recently became a subhalo. Requires the |
|
|
The earliest progenitor along this node’s branch. Requires the |
Recipes¶
The examples below use h5py, and all begin by selecting the nodes belonging to a single merger tree. Because nodeIndex is unique only within a tree, index look-ups must always be done tree-by-tree: the mergerTreeIndex, mergerTreeStartIndex, and mergerTreeCount datasets in each output group give, for each tree, its index and the range of rows in the nodeData datasets which belong to it (see mergerTree datasets for the full structure of the output file).
import h5py
import numpy as np
model = h5py.File('galacticus.hdf5','r')
output = model['Outputs/Output1']
nodes = output['nodeData']
# Select the rows belonging to the first merger tree in this output.
treeFirst = 0
start = output['mergerTreeStartIndex'][treeFirst]
count = output['mergerTreeCount' ][treeFirst]
rows = slice(start,start+count)
nodeIndex = nodes['nodeIndex' ][rows]
parentIndex = nodes['parentIndex' ][rows]
siblingIndex = nodes['siblingIndex' ][rows]
satelliteIndex = nodes['satelliteIndex'][rows]
isolated = nodes['nodeIsIsolated'][rows]
# Build a look-up from node index to row number within this tree.
row = { index: i for i, index in enumerate(nodeIndex) }
Find the central galaxy and all of its subhalos¶
Isolated halos are those with nodeIsIsolated \(=1\). To enumerate the subhalos of one of them, start from its satelliteIndex and follow siblingIndex until reaching \(-1\):
def subhalosOf(index):
"""Return the indices of all direct subhalos of the node with the given index."""
subhalos = []
subhalo = satelliteIndex[row[index]]
while subhalo != -1:
subhalos.append(subhalo)
subhalo = siblingIndex[row[subhalo]]
return subhalos
central = nodeIndex[np.nonzero(isolated == 1)[0][0]]
print(central, subhalosOf(central))
This finds only the direct subhalos. To include sub-subhalos, apply the same function recursively to each subhalo found—or add the nodePropertyExtractorIndicesHost extractor with topLevel set to true, and simply select all nodes whose hostIndex equals the index of the central (which selects the central itself too, since an isolated halo is its own host).
Note that a merger tree may contain more than one isolated halo at a given output time—any branch which has not yet merged with another is isolated. If you want the most massive one, select on basicMass rather than assuming the first row.
Enumerate the progenitors of a halo¶
The same siblingIndex walk applies to isolated halos, where it enumerates the halos which are merging together. Given any halo, the halos which follow it in its parent’s list of progenitors are:
def siblingsOf(index):
"""Return the indices of the halos which follow the given halo in its parent's list of progenitors."""
siblings = []
sibling = siblingIndex[row[index]]
while sibling != -1:
siblings.append(sibling)
sibling = siblingIndex[row[sibling]]
return siblings
Trace a galaxy from one output to the next¶
parentIndex is not the right tool for this: for an isolated halo it names a node at the next timestep of the merger tree, which will generally not correspond to any output time. Instead, add the nodePropertyExtractorDescendants extractor to the output:
<nodePropertyExtractor value="multi">
<nodePropertyExtractor value="nodeIndices"/>
<nodePropertyExtractor value="descendants"/>
</nodePropertyExtractor>
and match the descendantIndex of each node in one output to the nodeIndex of a node in the next output (within the same merger tree). Note that for merger trees read from file this requires that the presetMergerNodes and presetMergerTimes parameters both be set to true, so that Galacticus knows which node each galaxy will merge with, and when. Alternatively, include the indexShift node operator:
<nodeOperator value="indexShift"/>
which changes the convention used for nodeIndex itself so that a galaxy keeps the same index throughout its evolution—see the description of nodePropertyExtractorNodeIndices for the two conventions.
Caveats¶
Node indices are unique only within a merger tree. Two nodes in different trees may share the same
nodeIndex. Always restrict index look-ups to a single tree, usingmergerTreeIndex,mergerTreeStartIndex, andmergerTreeCount, or pairnodeIndexwithmergerTreeIndexto form a globally unique identifier.An isolated halo’s
parentIndexpoints into the merger tree, not into the next output. It is the halo into which this halo grows at the next timestep of the tree, and that timestep will generally fall between two output times, so no row with thatnodeIndexneed exist in any output. UsedescendantIndexto move between outputs. By contrast, an isolated halo’ssiblingIndex, and all of the links between a subhalo and its host, generally do refer to nodes which are present in the same output: those nodes are being evolved concurrently.A node’s
nodeIndexchanges when it is promoted. By defaultnodeIndexis the index of the node in the original merger tree, so when a galaxy’s halo is promoted into its parent at a tree timestep the index changes, even though the galaxy is the same object. TheindexShiftnode operator changes this behavior; both conventions are illustrated in the description ofnodePropertyExtractorNodeIndices.The \(-1\) sentinel is not used consistently between extractors.
parentIndex,siblingIndex, andsatelliteIndexare \(-1\) when the corresponding link does not exist, buthostIndexreports the node’s own index for an isolated halo, anddescendantIndexreports the node’s own index when the galaxy survives to the next output. This is deliberate:hostIndexfollows the convention used for the same-named dataset in merger tree files (see Merger Tree File Format), in which a halo which is not a subhalo is its own host.The name
descendantIndexmeans something different in merger tree input files. In a merger tree file read by Galacticus it is the descendant at the next timestep of that file; in the output it is the descendant at the next output time. See Merger Tree File Format.If you are working with the
treeNodeobjects themselves—in Fortran, or through the Python interface—rather than with the output datasets, the same structure is exposed through theparent,firstChild,sibling, andfirstSatellitepointers. Their traversal is described in Traversing a Merger Tree.