Using Container classes¶
ctapipe.core.Container
is the base class for all event-wise data classes in ctapipe. It works like a object-relational mapper, in that it defines a set of Fields
along with their metadata (description, unit, default), which can be later translated automatially into an output table using a ctapipe.io.TableWriter
.
[1]:
from ctapipe.core import Container, Field, Map
import numpy as np
from astropy import units as u
from functools import partial
Let’s define a few example containers with some dummy fields in them:
[2]:
class SubContainer(Container):
junk = Field(-1, "Some junk")
value = Field(0.0, "some value", unit=u.deg)
class TelContainer(Container):
# defaults should match the other requirements, e.g. the defaults
# should have the correct unit. It most often also makes sense to use
# an invalid value marker like nan for floats or -1 for positive integers
# as default
tel_id = Field(-1, "telescope ID number")
# For mutable structures like lists, arrays or containers, use a `default_factory` function or class
# not an instance to assure each container gets a fresh instance and there is no hidden
# shared state between containers.
image = Field(default_factory=lambda: np.zeros(10), description="camera pixel data")
class EventContainer(Container):
event_id = Field(-1, "event id number")
tels_with_data = Field(
default_factory=list, description="list of telescopes with data"
)
sub = Field(
default_factory=SubContainer, description="stuff"
) # a sub-container in the hierarchy
# A Map is like a defaultdictionary with a specific container type as default.
# This can be used to e.g. store a container per telescope
# we use partial here to automatically get a function that creates a map with the correct container type
# as default
tel = Field(default_factory=partial(Map, TelContainer), description="telescopes")
Basic features¶
[3]:
ev = EventContainer()
Check that default values are automatically filled in
[4]:
print(ev.event_id)
print(ev.sub)
print(ev.tel)
print(ev.tel.keys())
# default dict access will create container:
print(ev.tel[1])
-1
{'junk': -1, 'value': 0.0}
Map(__main__.TelContainer, {})
dict_keys([])
{'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), 'tel_id': -1}
print the dict representation
[5]:
print(ev)
{'event_id': -1,
'sub': {'junk': -1, 'value': 0.0},
'tel': {1: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'tel_id': -1}},
'tels_with_data': []}
We also get docstrings “for free”
[6]:
?EventContainer
[7]:
?SubContainer
values can be set as normal for a class:
[8]:
ev.event_id = 100
ev.event_id
[8]:
100
[9]:
ev.as_dict() # by default only shows the bare items, not sub-containers (See later)
[9]:
{'event_id': 100,
'tels_with_data': [],
'sub': __main__.SubContainer:
junk: Some junk with default -1
value: some value with default 0.0 [deg],
'tel': Map(__main__.TelContainer, {1: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None})}
[10]:
ev.as_dict(recursive=True)
[10]:
{'event_id': 100,
'tels_with_data': [],
'sub': {'junk': -1, 'value': 0.0},
'tel': {1: {'tel_id': -1,
'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])}}}
and we can add a few of these to the parent container inside the tel dict:
[11]:
ev.tel[10] = TelContainer()
ev.tel[5] = TelContainer()
ev.tel[42] = TelContainer()
[12]:
# because we are using a default_factory to handle mutable defaults, the images are actually different:
ev.tel[42].image is ev.tel[32]
[12]:
False
Be careful to use the default_factory
mechanism for mutable fields, see this negative example:
[13]:
class DangerousContainer(Container):
image = Field(
np.zeros(10),
description="Attention!!!! Globally mutable shared state. Use default_factory instead",
)
c1 = DangerousContainer()
c2 = DangerousContainer()
c1.image[5] = 9999
print(c1.image)
print(c2.image)
print(c1.image is c2.image)
[ 0. 0. 0. 0. 0. 9999. 0. 0. 0. 0.]
[ 0. 0. 0. 0. 0. 9999. 0. 0. 0. 0.]
True
[14]:
ev.tel
[14]:
Map(__main__.TelContainer, {1: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 10: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 5: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 42: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 32: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None})
Converion to dictionaries¶
[15]:
ev.as_dict()
[15]:
{'event_id': 100,
'tels_with_data': [],
'sub': __main__.SubContainer:
junk: Some junk with default -1
value: some value with default 0.0 [deg],
'tel': Map(__main__.TelContainer, {1: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 10: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 5: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 42: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None, 32: __main__.TelContainer:
tel_id: telescope ID number with default -1
image: camera pixel data with default None})}
[16]:
ev.as_dict(recursive=True, flatten=False)
[16]:
{'event_id': 100,
'tels_with_data': [],
'sub': {'junk': -1, 'value': 0.0},
'tel': {1: {'tel_id': -1,
'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
10: {'tel_id': -1, 'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
5: {'tel_id': -1, 'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
42: {'tel_id': -1, 'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
32: {'tel_id': -1,
'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])}}}
for serialization to a table, we can even flatten the output into a single set of columns
[17]:
ev.as_dict(recursive=True, flatten=True)
[17]:
{'event_id': 100,
'tels_with_data': [],
'junk': -1,
'value': 0.0,
'tel_id': -1,
'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])}
Setting and clearing values¶
[18]:
ev.tel[5].image[:] = 9
print(ev)
{'event_id': 100,
'sub': {'junk': -1, 'value': 0.0},
'tel': {1: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'tel_id': -1},
5: {'image': array([9., 9., 9., 9., 9., 9., 9., 9., 9., 9.]),
'tel_id': -1},
10: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'tel_id': -1},
32: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'tel_id': -1},
42: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
'tel_id': -1}},
'tels_with_data': []}
[19]:
ev.reset()
ev.as_dict(recursive=True)
[19]:
{'event_id': -1,
'tels_with_data': [],
'sub': {'junk': -1, 'value': 0.0},
'tel': {}}
look at a pre-defined Container¶
[20]:
from ctapipe.containers import SimulatedShowerContainer
[21]:
?SimulatedShowerContainer
[22]:
shower = SimulatedShowerContainer()
shower
[22]:
ctapipe.containers.SimulatedShowerContainer:
energy: Simulated Energy with default nan TeV [TeV]
alt: Simulated altitude with default nan deg [deg]
az: Simulated azimuth with default nan deg [deg]
core_x: Simulated core position (x) with default nan m
[m]
core_y: Simulated core position (y) with default nan m
[m]
h_first_int: Height of first interaction with default nan m
[m]
x_max: Simulated Xmax value with default nan g / cm2 [g
/ cm2]
shower_primary_id: Simulated shower primary ID 0 (gamma),
1(e-),2(mu-), 100*A+Z for nucleons and
nuclei,negative for antimatter. with default
32767
Container prefixes¶
To store the same container in the same table in a file or give more information, containers support setting a custom prefix:
[23]:
c1 = SubContainer(junk=5, value=3, prefix="foo")
c2 = SubContainer(junk=10, value=9001, prefix="bar")
# create a common dict with data from both containers:
d = c1.as_dict(add_prefix=True)
d.update(c2.as_dict(add_prefix=True))
d
[23]:
{'foo_junk': 5, 'foo_value': 3, 'bar_junk': 10, 'bar_value': 9001}