Project

General

Profile

General design considerations

The benefits of modularity

The modularity of the art framework provides many benefits, some of which include:

  • Framework programs are composed by writing a configuration file, rather than by compiling and linking code. Therefore, defining and modifying what your framework program does is easier.
  • Users can build arbitrarily many modules without any inter-module dependence. A change in one module does not necessitate compiling others.
  • With the exception of a few functions, the art framework does not care about the structure of a module--its design is entirely in the hands of the user.
  • The modular mindset gives users the opportunity to consider what behavior a particular module should implement, creating better separation of concerns.

Such modularity applies to framework modules (EDProducers, etc.), input sources, services, tools, and other kinds of plugins, all of which are dynamically loaded using art's plugin system.

Good design for flexible framework programs

A recommended software design practice is to separate code into units, each of which handle a specific aspect of a given algorithm or concept. For example, a 2000-line function, which may implement a particular track-finding algorithm, can be very cumbersome to understand and maintain. Breaking such a large function into smaller pieces localizes maintenance, errors, and improves readability and comprehension. The modularity of the art framework supports this concept, providing additional benefits:

Suppose a reconstruction workflow can be described in three steps:

  1. Read raw data from the input file.
  2. Transform the raw data into a set of calibrated objects.
  3. Reduce the calibrated objects into a collection that represents detector hits.

A natural way to implement this workflow in art is to use the RootInput source for reading the raw data, and then create two modules: one that transforms the raw data into the calibrated objects, and one that creates the detector-hit collection from the calibrated objects.

A configuration that represents this workflow could be:

source: {
  module_type: RootInput
}

physics: {
  producers: {
    calib: {
      module_type: CalibratedData
    }

    hits: {
      module_type: HitsFromCalibratedData
      calibDataLabel: "calib" 
    }
  }
  t1: [calib, hits]
}

where the calibDataLabel parameter is necessary for retrieving the calibrated data object that was produced by the calib module instance. In this way, any changes in (e.g.) the CalibratedData module type source code will not necessitate rebuilding the HitsFromCalibratedData source code.

The framework program is controlled entirely by what the user specifies in the configuration file and at the command line--i.e. modules provide the ability to modify what the framework does without having to change framework code.

Guideline 1: If you need to handle events, subruns, or runs,
(a) Break up what you want to do into individual identifiable tasks, then
(b) Provide a module to do each one of those tasks

Good design for flexible framework modules

Now that you have decided to create a module for a particular task, you must decide how that task should be accomplished. If it is a small task, then perhaps only a simple function is required to accomplish it. A more-involved task, however, should be broken down or factorized into subtasks as much as reasonable.

Factorize your code

To be completed.

Factorizing your code is a way of making your module itself modular.

Configurability

In addition to framework programs being configurable at run-time, modules themselves are also configurable at run-time so that users can influence the behavior of a module without having to recompile it whenever a change in behavior is needed. Each module (and, in general, most other plugins), receive a const reference to a fhicl::ParameterSet object, which is a C++ representation of the configuration for that module. For example, a minimal implementation of the HitsFromCalibratedData module type might look like:

class HitsFromCalibratedData : public art::EDProducer {
public:

  explicit HitsFromCalibratedData(fhicl::ParameterSet const& ps) :
    calibDataTag_{ps.get<art::InputTag>("calibDataLabel")}
  {
    produces<Hits>();
  }

  void produce(art::Event& e) 
  {
    auto const& calibData = e.getValidHandle<CalibratedData>(calibDataTag_);
    auto hits = std::make_unique<Hits>();
    // ... fill hits and then put them into event
    e.put(std::move(hits));
  }

};

In the constructor, the calibDataTag_ is initialized with the value corresponding to the FHiCL parameter "calibDataLabel", which is "calib". Through this interface, the product that is retrieved from the event can be changed at run-time insofar as the C++ type is the same.

This configurability can be extended to which selecting a given function for a subtask. For example, consider the following implementation:

class HitsFromCalibratedData : public art::EDProducer {
public:

  explicit HitsFromCalibratedData(fhicl::ParameterSet const& ps) :
    calibDataTag_{ps.get<art::InputTag>("calibDataLabel")},
    hitMakingMode_{ps.get<unsigned>("hitMakingAlgorithm")}
  {
    produces<Hits>();
  }

  void produce(art::Event& e) 
  {
    auto const& calibData = *e.getValidHandle<CalibratedData>(calibDataTag_);
    auto hits = std::make_unique<Hits>();
    switch(hitMakingMode_) {
    case 0: fillCalibratedHits(calibData, *hits); break;
    case 1: fillRawHits(*hits); break;
    case 2: fillGainCorrectedHits(calibData, *hits);
    }
    e.put(std::move(hits));
  }

};

where the fillCalibratedHits, fillRawHits, and fillGainCorrectedHits methods are responsible for filling the hit collection.

Guideline 2: For each module, make code that is modular.
(a) Write classes/functions that do things for a given subtask. (Not 2000-line function.)
(b) Provide configurability to select options among and within subtasks. By providing configurability, you provide the ability to alter a module's behavior without needing to modify the code.
(c) If a given subtask needs to be configurable, then provide configurability to choose between a specified number of functions.

When more flexibility is required

There are times when there are aspects of a module that cannot (or should not) be broken apart into separate modules, and for which the author of the code desires to allow users of the module to specify their own behavior. For example, suppose you have a module that is intended to create tracks from hits, and one of the intermediary steps is a hit-clustering subtask:

class TracksFromHits : public art::EDProducer {
public:

  explicit TracksFromHits(fhicl::ParameterSet const& ps) :
    hitsTag_{ps.get<art::InputTag>("hitsTag")},
  {
    produces<Tracks>();
  }

  void produce(art::Event& e) 
  {
    auto const& hits = *e.getValidHandle<Hits>(hitsTag_);
    auto const& clusters = clusterHits(hits);
    e.put(makeTracks(clusters));
  }

};

One might think that the clustering step should be done with a separate module, but it's possible that the clusters object is very large and should not be placed onto the event due to memory restrictions. If the implementation of the clustering algorithm needed to be adjusted because it was (e.g.) under development, then every time a user wanted to test a change in its implementation, a recompilation of the TracksFromHits module would be triggered. If the module that calls clusterHits(...) is small, then this may not be a significant issue. However, if the module is large, then such recompilation can be expensive in time, and also hamper productivity.

To avoid this problem, the art tool exists, which allows users to specify which library they want loaded for a module at run time so that the need to recompile is removed whenever a change in the clusterHits(...) implementation is needed. In other words, whereas the art module makes it possible to modify the behavior of a framework program without rebuilding the framework, the art tool makes it possible to modify the behavior of a module without rebuilding the module.

An example of how this could be accomplished in a module is:

class TracksFromHits : public art::EDProducer {
public:

  explicit TracksFromHits(fhicl::ParameterSet const& ps) :
    hitsTag_{ps.get<art::InputTag>("hitsTag")},
    clusteringAlgo_{art::make_tool<Clusters(Hits const&)>(ps.get<fhicl::ParameterSet>("clusteringAlgorithm"), "Clustering")}
  {
    produces<Tracks>();
  }

  void produce(art::Event& e) 
  {
    auto const& hits = *e.getValidHandle<Hits>(hitsTag_);
    auto const& clusters = clusteringAlgo_(hits);
    e.put(makeTracks(clusters));
  }

};

where the art::make_tool invocation loads the tool specified by the module configuration, and then returns a callable object that accepts a const reference to a Hits object, and returns a Clusters object. For more details on art tools, see the documentation here.

Guideline 3: One of your module's subtasks should be an art tool if
(a) The subtask to be done does not make sense outside of the context of the module, and
(b) The user needs to be able to extend what your module does without modifying the module code.

When to use art's other plugins

To be completed.

If nothing above fits

To be completed.

Get ahold of the art team!