Data Representations¶
An extracted tree is held as a TreeData.
It stores NumPy arrays, which every output format can be built from, and offers
a pandas.DataFrame view for analysis.
The NumPy View¶
data = read_tree(path, GateTree.HITS)
data.entry_count # 54956
data.branch_names # ('PDGEncoding', 'trackID', ...)
data.dtypes["edep"] # dtype('float32')
data.array_branches # {'volumeID': 10}
data["edep"] # numpy.ndarray of shape (54956,)
len(data) # same as entry_count
Two kinds of column exist:
scalar branches, one-dimensional arrays of shape
(entries,)fixed-width array branches, two-dimensional arrays of shape
(entries, width)
volumeID is the array branch GATE writes for hits; it carries the detector
identifier hierarchy, so it is kept as an array rather than flattened.
array_branches maps each array branch to its width, which is the quickest way
to tell the two kinds apart.
Selecting Branches¶
select returns a new instance sharing the same arrays:
reduced = data.select(["edep", "eventID"])
reduced.branch_names # ('edep', 'eventID')
The order is the one the caller asked for. A repeated name is kept once, at the
position of its first occurrence. An unknown name raises BranchNotFoundError.
Immutability And Memory¶
TreeData is frozen and its column mapping is read-only, so branches cannot be
added or replaced after construction.
The arrays themselves are referenced rather than copied. Building a TreeData,
or calling select, does not duplicate the data. The arrays stay writable, so
modifying one in place also changes what any other reference to it sees.
Equality is deliberately not defined. Comparing NumPy arrays with == yields
arrays rather than booleans, so a generated comparison would raise instead of
answering. Compare the fields that matter instead:
import numpy as np
np.array_equal(first["edep"], second["edep"])
The Data Frame View¶
frame = data.to_dataframe()
The result is a plain pandas.DataFrame, not a subclass, so every pandas
feature works on it unchanged.
A data frame holds scalar cells, so a fixed-width array branch is expanded into
one column per component, named <branch>_<index> with indices counted from
zero, placed where the original branch stood:
|
|
|---|---|
|
|
|
|
The expansion is one way. Reading such a frame back keeps the columns separate:
restored = TreeData.from_dataframe(GateTree.HITS, frame)
restored.array_branches # {} — volumeID_0 ... volumeID_9 are scalar branches
from_dataframe drops the index and rejects data frames with MultiIndex
columns or repeated column labels. A repeated label makes pandas return a data
frame rather than a series, which would silently turn two scalar branches into
one array branch.
Building Data Yourself¶
TreeData can be built directly, which is useful for tests and for writing
derived results:
import numpy as np
from opengate_gate_tree import GateTree, TreeData
data = TreeData(
GateTree.HITS,
{
"eventID": np.arange(3, dtype=np.int32),
"edep": np.array([0.1, 0.5, 1.2], dtype=np.float32),
},
)
Construction checks that branch names carry a value, that every column is a
NumPy array of one or two dimensions, and that all columns agree on the number
of entries. Anything else raises ValueError.
Values That Stand For Something¶
Some branches hold whole numbers that are not quantities: the four branches a
PositroniumSource writes say what a gamma was and where it came from. All
four stay integer columns; three of them — sourceType, decayType and
gammaType — have an enum class describing what their values mean, whose
members are those integers. The fourth, decayIndex, holds component numbers
whose meaning depends on how the source was configured, so it has none:
from opengate_gate_tree import GammaType
prompt = data["gammaType"] == GammaType.PROMPT
Nothing is converted, and nothing is copied: the comparison runs on the column as it was read. See PositroniumSource Data.
Picking Out Rows¶
Importing the package registers a gate namespace on pandas.Series and on
pandas.DataFrame, so a selection reads as a question about the data:
frame.gate.by_event(1, 5) # frame in, frame out
frame["edep"].gate.in_range(0.2, 0.511) # column in, column out
The frame stays a plain pandas.DataFrame throughout — the namespace is added
to the class, not to the object — and TreeData.from_dataframe reads a
selection back. See Filtering And Selecting.