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}