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
/usr/local/lib/python3.8/site-packages/setuptools_scm/git.py:68: UserWarning: "/github/workspace" is shallow and may cause errors
  warnings.warn('"{}" is shallow and may cause errors'.format(wd.path))

Let’s define a few example containers with some dummy fields in them:

[2]:
class SubContainer(Container):
    junk = Field("nothing","Some junk")
    value = Field(0.0, "some value", unit=u.deg)

class EventContainer(Container):
    event_id = Field(-1,"event id number")
    tels_with_data = Field([], "list of telescopes with data")
    sub = Field(SubContainer(), "stuff")  # a sub-container in the hierarchy

    # for dicts of sub-containers, use Map instead
    # of a dict() as the default value to support serialization
    tel = Field(Map(), "telescopes")

Basic features

[3]:
ev = EventContainer()

Check that default values are automatically filled in

[4]:
print(ev.event_id)
print(ev.tel.keys())
print(ev.tel)
-1
dict_keys([])
Map(None, {})

print the json representation

[5]:
print(ev)
{'event_id': -1,
 'sub': {'junk': 'nothing', 'value': 0.0},
 'tel': {},
 'tels_with_data': []}

values can be set as normal for a class:

[6]:
ev.event_id = 100
ev.event_id
[6]:
100
[7]:
ev.as_dict()  # by default only shows the bare items, not sub-containers (See later)
[7]:
{'event_id': 100,
 'tels_with_data': [],
 'sub': __main__.SubContainer:
                           junk: Some junk
                          value: some value [deg],
 'tel': Map(None, {})}
[8]:
ev.as_dict(recursive=True)
[8]:
{'event_id': 100,
 'tels_with_data': [],
 'sub': {'junk': 'nothing', 'value': 0.0},
 'tel': {}}

Now, let’s define a sub-container that we can add per telescope:

[9]:
class TelContainer(Container):
    tel_id = Field(-1, "telescope ID number")
    image = Field(np.zeros(10), "camera pixel data")


and we can add a few of these to the parent container inside the tel dict:

[10]:
ev.tel[10] = TelContainer()
ev.tel[5] = TelContainer()
ev.tel[42] = TelContainer()
[11]:
ev.tel
[11]:
Map(None,
    {10: __main__.TelContainer:
                             tel_id: telescope ID number
                              image: camera pixel data,
     5: __main__.TelContainer:
                             tel_id: telescope ID number
                              image: camera pixel data,
     42: __main__.TelContainer:
                             tel_id: telescope ID number
                              image: camera pixel data})

Converion to dictionaries

[12]:
ev.as_dict()
[12]:
{'event_id': 100,
 'tels_with_data': [],
 'sub': __main__.SubContainer:
                           junk: Some junk
                          value: some value [deg],
 'tel': Map(None,
     {10: __main__.TelContainer:
                              tel_id: telescope ID number
                               image: camera pixel data,
      5: __main__.TelContainer:
                              tel_id: telescope ID number
                               image: camera pixel data,
      42: __main__.TelContainer:
                              tel_id: telescope ID number
                               image: camera pixel data})}
[13]:
ev.as_dict(recursive=True, flatten=False)
[13]:
{'event_id': 100,
 'tels_with_data': [],
 'sub': {'junk': 'nothing', 'value': 0.0},
 'tel': {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.])}}}

for serialization to a table, we can even flatten the output into a single set of columns

[14]:
ev.as_dict(recursive=True, flatten=True)
[14]:
{'event_id': 100,
 'tels_with_data': [],
 'sub_junk': 'nothing',
 'sub_value': 0.0,
 'tel_10': {'tel_id': -1,
  'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
 'tel_5': {'tel_id': -1,
  'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])},
 'tel_42': {'tel_id': -1,
  'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])}}

Setting and clearing values

[15]:
ev.tel[5].image[:] = 9
print(ev)
{'event_id': 100,
 'sub': {'junk': 'nothing', 'value': 0.0},
 'tel': {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},
         42: {'image': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]),
              'tel_id': -1}},
 'tels_with_data': []}
[16]:
ev.reset()
ev.as_dict(recursive=True, flatten=True)
[16]:
{'event_id': -1, 'tels_with_data': [], 'sub_junk': 'nothing', 'sub_value': 0.0}

look at a pre-defined Container

[17]:
from ctapipe.containers import SimulatedShowerContainer
[18]:
shower = SimulatedShowerContainer()
[19]:
shower
[19]:
ctapipe.containers.SimulatedShowerContainer:
                        energy: Simulated Energy [TeV]
                           alt: Simulated altitude [deg]
                            az: Simulated azimuth [deg]
                        core_x: Simulated core position (x) [m]
                        core_y: Simulated core position (y) [m]
                   h_first_int: Height of first interaction [m]
                         x_max: Simulated Xmax value [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.