Project

General

Profile

How to write an overlay class

In the context of an artdaq-based DAQ system, an "overlay class" is simply a class which "overlays" the artdaq::Fragment payload, providing an experiment-specific API so writers of art modules need not worry about the layout of bytes (e.g., where to find the Nth ADC count value). This section of the wiki could also have been titled "A tutorial on the artdaq::Fragment class", as one needs to understand the structure of artdaq::Fragment objects in order to write overlays. Info provided on the artdaq::Fragment here may also prove useful for writers of fragment generators. It will be useful when reading this section to refer to the interface provided by artdaq::Fragment, artdaq-core/artdaq-core/Data/Fragment.hh, as well as the interface to the ToyFragment overlay class, artdaq-core-demo/artdaq-core-demo/Overlays/ToyFragment.hh. Also potentially useful is the drawing below of the memory layout of a fragment generated by the ToySimulator consisting of N ADC counts; also shown are what addresses memory layout various artdaq::Fragment and demo::ToyFragment functions (described in more detail further down the document) can provide the user. Memory addresses in the drawing decrease down the page, i.e., the start of the fragment is at the bottom of the drawing.

The fundamental building block of an artdaq-based DAQ system: artdaq::Fragment

The artdaq::Fragment's header

In any artdaq-based DAQ system, the data provided by an experiment is stored within an object of the artdaq::Fragment class. The physical representation of the data in the class is contained within a vector called "vals_"; the datatype in the vector is called "RawDataType", a typedef of a 64-bit unsigned integer found in artdaq-core's artdaq-core/artdaq-core/Data/detail/RawFragmentHeader.hh file. While the choice of this datatype is meant to be as transparent as possible to the end user of the artdaq::Fragment class, it will reveal itself occasionally in a "padding" effect, described later. On top of this vector of data, the artdaq::Fragment class also provides some logical structure. The vals_ vector can be thought of as consisting of an artdaq "header", containing information about the fragment itself used by the artdaq-based system, followed by experiment-specific data, or "payload" (such as ADCs counts, experiment-specific headers, and optional user-defined metadata).

The artdaq::Fragment header is defined in RawFragmentHeader.hh; making up the first three RawDataTypes in the vals_ vector, it consists of two 64-bit unsigned integers containing multiple variables with one more 64-bit unsigned integer the first half of which can contain a timestamp, the second half of which is reserved for future use. This is organized via the following bitfield:


  RawDataType word_count          : 32; // number of RawDataTypes in this Fragment
  RawDataType version             : 16;
  RawDataType type                :  8;
  RawDataType metadata_word_count :  8;

  RawDataType sequence_id : 48;
  RawDataType fragment_id : 16;
  RawDataType timestamp   : 32;

  RawDataType unused1     : 16;
  RawDataType unused2     : 16;

Some of these variables - sequence_id, fragment_id, fragment_type and timestamp- can be set by the user; as can be seen in How to write a fragment generator, the way in which this is done in ToySimulator::getNext_() is by passing the desired values as arguments to the artdaq::Fragment::FragmentBytes() factory function. However, they can also be set with the standalone functions setUserType(), setSequenceID(), setFragmentID() and setTimestamp(). The sequence_id uniquely identifies an experiment's event, whether triggered or a timeslice, and the fragment_id uniquely identifies a subset of the full amount of data representing an event, typically corresponding to a physical subsection of a detector. For the meaning of "type", an examination of artdaq's Fragment.hh header will reveal that it defines a datatype called "type_t", an unsigned 8-bit integer whose value provides information on the type of information a fragment contains. Some of these values are reserved for system use; the range of these values are set in artdaq's RawFragmentHeader.hh file, run from 225-255, and can be employed to tell artdaq-based DAQ systems that a run has ended, etc. Values 1-224 can be assigned experiment-specific meanings by the user; e.g., in artdaq-demo "FragmentType.hh" associates type values with different kinds of CAEN boards. The timestamp, optional, can be used to provide information about the fragment useful for triggering purposes.

The user need not worry about setting the remaining variables in the artdaq::Fragment header: metadata_word_count and word_count, as these are calculated internally by the artdaq::Fragment class. The metadata_word_count is the number of RawDataTypes which describe the experiment's metadata (e.g., the trigger bits, the upstream DAQ board's serial number, etc.), and the word_count is the total number of RawDataTypes in the fragment ( 3 from the fragment header + metadata_word_count + the number which make up the experiment's data).

The artdaq::Fragment interface

The most important functions provided by artdaq::Fragment are its factory functions. Meant to be used in place of its (now-deprecated) constructors, there two versions of the factory function, both which will return a unique_ptr to the artdaq::Fragment instance they create. Be aware that the first argument refers to the number of bytes past the artdaq::Fragment header and user-defined metadata. If you're creating the artdaq::Fragment with foreknowledge of the sequence ID, metadata, etc., you want it to contain, use the following:

template <class T> 
static std::unique_ptr<Fragment> FragmentBytes(std::size_t payload_size_in_bytes, 
                                sequence_id_t sequence_id,
                                fragment_id_t fragment_id, type_t type, 
                                const T & metadata,
                                timestamp_t timestamp = Fragment::InvalidTimestamp) 

otherwise, there's a simpler version of the function:

static std::unique_ptr<Fragment> FragmentBytes(std::size_t nbytes)

...where in the second case, you'd need to use setUserType(), setSequenceID(), setFragmentID() and (if desired) setTimestamp(), after creating the fragment.

To provide a (contrived) example of use of the factory function, take a look at the following (and ignore the omission of the timestamp argument in the FragmentBytes signature given in the comment):


struct MetadataType {
  uint16_t board_id;
  uint32_t run_number;
};

MetadataType mtype;
mtype.board_id = 867;
mtype.run_number = 3509;

// Create the fragment, using the following factory function:

//   template <class T>
//   static std::unique_ptr<Fragment> FragmentBytes(std::size_t payload_size, sequence_id_t sequence_id,
//          fragment_id_t fragment_id, type_t type, const T & metadata);

std::unique_ptr<artdaq::Fragment> toyfrag = artdaq::Fragment::FragmentBytes( 100, 1000, 1, 1, mtype);

After this code snippet is run, toyfrag's header now represents that it comes from the event with sequence ID #1000, that it's fragment ID #1 in that event, and that it's type #1 (the first allowed user type, which should have some experiment-specific meaning here). Its metadata consists of a 16-bit unsigned integer and a 32-bit unsigned integer, representing the upstream board id and the run number. Also, through the first argument to the function, its vals_ vector gets expanded to hold an additional 100 bytes of payload beyond the metadata. Note that physically, artdaq::Fragment will "pad" these quantities as its internal representation of data is in 8-byte chunks of type RawDataType, so that although 6 bytes are requested for metadata this will be rounded up to 8 bytes, and the 100 bytes requested beyond the metadata will actually result in 104 bytes being allocated; hence the physical artdaq::Fragment object will take up 24 bytes (header) + 8 bytes (metadata) + 104 bytes (payload) = 136 bytes, rather than the 130 bytes one might assume. The timestamp, not explicitly provided as an argument, is implicitly set to its default value.

Other useful functions are as follows:


resizeBytes() -- resize the payload beyond the header and metadata to contain a desired number of bytes
sizeBytes() -- returns the number of bytes representing the entire fragment
dataSizeBytes() -- the number of bytes representing the payload beyond the header and the metadata (note that this value may not match up to the argument passed to resizeBytes() due to the padding described above)

hasMetadata() -- did the user fill the fragment with metadata via setMetadata() ?
setMetadata() -- if there's no metadata in the fragment, can add it with this function. Potentially costly memory movement can result.

dataBeginBytes() -- returns a pointer to the beginning of the section of the fragment beyond the header and metadata
dataEndBytes() -- returns a pointer to the end of the fragment
headerBeginBytes() -- returns a pointer to the beginning of the header, i.e., the beginning of the fragment

empty() -- determine if there's any data beyond the header and metadata. True if dataBeginBytes() == dataEndBytes()
isUserFragmentType() / isSystemFragmentType() -- determine whether the fragment type is user-defined or system-defined.


Note that where the artdaq::Fragment class has functions of the form stub Bytes(), it also has functions of the form stub() -- e.g., there's a function sizeBytes(), and a function size(). In this case, the functions of the form stub Bytes() should be used - as their name suggests, they work in units of bytes, while the functions of the form stub() work in units of RawDataType - so, e.g., sizeBytes() will tell you the size of fragment in bytes, while size() will tell you the size of the fragment in units of RawDataType. Generally, the functions of the form stub() are kept for backwards compatibility, and should be considered deprecated.

The demo::ToyFragment overlay class: using artdaq::Fragment in an experiment-specific manner

In order to learn how to manipulate the artdaq::Fragment's data in a manner useful to an experiment, it's instructive to look at the ToyFragment overlay class, defined in artdaq-core-demo/artdaq-core-demo/Overlays/ToyFragment.* . As it's fairly stripped down, when writing one's own experiment-specific classes it may be a good idea to copy them and use them as a starting point, adding additional functionality where needed.

There are some aspects of an experiment's data fragment which might be of interest that aren't covered by artdaq::Fragment. For example, it's useful for an experiment to represent its fundamental unit of data as an ADC count, the size of which of course will be unknown to the artdaq::Fragment class-- so for, example, one 64-bit RawDataType might be able to represent four 16-bit ADC counts. Also, it may be that there's information of interest concerning the fragment that's not covered by the artdaq::Fragment header or the metadata structure; a common example is a hardware-specific header which precedes the ADC counts sent by an upstream board. In artdaq-core-demo, the artdaq::Fragment overlay class demo::ToyFragment demonstrates how the data in a fragment can be organized in a real experiment. As can be seen from the ToyFragment.hh header, the demo::ToyFragment class contains within it the definition of a metadata struct:

   struct Metadata {

    typedef uint32_t data_t;

    uint32_t board_serial_number : 16;
    uint32_t num_adc_bits : 8;
    uint32_t unused : 8;                                         

    static size_t const size_words = 1ul; // Units of Metadata::data_t                     
  };

As well as a header struct :

   struct Header {
    typedef uint32_t data_t;

    typedef uint32_t event_size_t;
    typedef uint32_t run_number_t;

    uint32_t event_size : 28;
    uint32_t unused_1   :  4;

    run_number_t run_number : 32;

    static size_t const size_words = 2ul;   // Units of Header::data_t                     
  };

The ToyFragment::Metadata struct's bitfield contains hardware information (the serial # of the upstream hardware DAQ board from where the fragment came, as well as the number of bits composing an ADC count); the ToyFragment::Header struct's bitfield contains experiment information (the run number, the event number, and the size of the fragment, expressed by the event_size variable in units of the Header's data_t typedef ). The main functions for demo::ToyFragment are the following:

total_adc_values() : the # of ADCs in the payload
adc_value(): the value of a specific ADC
dataBeginADCs()/_dataEndADCs()_ : start and end of the ADC values, returned as a pointer the ToyFragment's ADC datatype, adc_t

hdr_event_size(), hdr_trigger_number(), hdr_size_words(): getter functions for the ToyFragment::Header struct

Creating your own generator and overlay classes

The source files for ToySimulator and ToyFragment can be copied and their class names changed to provide a useful starting point for creating your own simulator and overlay classes. The actual set of steps involved will be the following:

  • Copy the Toy* source code into new, appropriately-named files- both the ToyFragment* files in artdaq-core-demo's Overlays/ directory and the ToySimulator* files in artdaq-demo's Generators/ directory.
  • Search-and-replace the appropriate tokens in these copied files
  • Add the new fragment generator to the artdaq-demo/Generators/CMakeLists.txt file via the "simple_plugin" directive; simply cut-and-paste an example of this from earlier in the file and swap out the referenced fragment generator with MySimulator
  • Add the new fragment(s) to the enum list in artdaq-core-demo's FragmentType.hh and the "names" vector in FragmentType.cc . Also modify the "typeToADC" function and the constructor's "ftypes" vector in your fragment generator source file to reflect your new choice of fragment(s).
  • Recompile artdaq-core-demo and artdaq-demo
  • Test out your new fragment generator by copying the artdaq-demo's tools/fcl/driver.fcl code, editing it to use the new generator, and then calling the driver executable with it, "artdaqDriver -c <yournewfcl>". The simplest way to modify your copy of the driver.fcl code is the following:
    • Replace the FHiCL code specifying the use of the ToySimulator fragment generator, i.e.,
       generator: ToySimulator
         fragment_type: TOY2        # Make sure downstream modules know about this fragment_type!
         nADCcounts: 100
         random_seed: 6514
      

      with whatever code is appropriate to the fragment generator you've written
    • Edit the "fragment_type_map" sequence in the "source" block at the bottom of the file so it contains the number and name of the fragment(s) produced by your fragment generator, where the fragment definitions will be the ones you added to FragmentType.hh and FragmentType.cc .
    • Instead of using the ToyDump art module, use the FileDumperOutput module, i.e., replace
        a1: [ toyDump ]
      #  e1: [ out1 ]
        end_paths: [ a1 ]
      }
      

      with
      #  a1: [ toyDump ]
        e1: [ out1 ]
        end_paths: [ e1 ]
      
  • Consider writing your own art module designed to analyze the data produced by your fragment generator. The simplest thing you can do is copy an existing art module (e.g., artdaq-demo/ArtModules/ToyDump_module.cc), rename it, and edit it to work with the fragments your fragment generator produces; once you've done this, assuming for the sake of argument that the name of your art module source file is "MyNewAnalyzer_module.cc", add the line
     simple_plugin(MyNewAnalyzer "module")
    

    to the CMakeLists.txt file in the ArtModules directory and run "buildtool" to compile it in. Once you've done this, you can test it out by adding it to the analysis path used in the FHiCL document you pass to the driver executable. If you wish to write more sophisticated modules, consult the links provided in art modules.