Project

General

Profile

Art Module Design Guide

Prelude

This design guide provides "best practice" guidelines for writing art modules. The goal of the design guide is to help in the production of code that is flexible (and thus able to be generalized, shared, and re-used) and testable (and thus more likely to be correct). Both of these goals are achieved through modularity, or separation of concerns: having different software artifacts (classes, functions, or template) responsible for different purposes.

Use the artmod utility to create a skeleton for your module

artmod is a tool, provided by the cetpkgsupport package, that should be used to generate a skeleton for a new art module. artmod has substantial internal documentation, available through artmod --help.

Put both the class declaration and the class definition in a single compilation unit

One art module is never allowed to invoke another art module. Thus there is no reason to have a class header file for an art module. Class header files are written to declare a class, so that all source code files (formally, compilation units) that use a specific class agree on the data members and function members of the class. Since no code ever directly invokes the functions of a module (the art framework invokes them only through the base classes' virtual functions), only the class declaration (where the functions are implemented) needs to see the class declaration.

If you use artmod, you will get this automatically.

Use art::InputTag to identify your modules inputs

The class art::InputTag allows the specification of module name, product instance name, and even process name, and comes along with an easy means of initialization from a std::string and thus from a fhicl::ParameterSet. Best practice is to have a data member of type art::InputTag for each input you retrieve from the art::Event. Initialize them in the initialization list of your module's constructor:

class X : public art::EDProducer
{
   ... public interface omitted
  private:
    art::InputTag tag_1_;
}

X::X(fhicl::ParameterSet const& ps) :
  tag_1_(ps.get<std::string>("tag_1")),
  ... other initialization omitted
{ }

Of course, your data member should have a meaningful name, rather than tag_1_, which is a generic name just for this example.

Use the canonical pattern for produce or filter

The canonical patterns for produce or filter are similar; the only difference is that filter returns either true or false. We show the canonical form for produce.

The "canonical form" has a few different versions, depending on the degree of complexity of the algorithms being invoked. Use the simplest form that works for your task.

In all cases, the canonical form for produce has the following "steps":

  1. Obtain the inputs needed, using art::Event::getValidHandle, using the form that accepts an art::InputTag
  2. Invoke an algorithm that does the real work, passing through a const reference to the algorithm input, and whatever additional parameters are needed to configure the algorithm

Note that we do not pass the art::Event, nor the art::ValidHandle, to the algorithm; we only pass in the result of dereferencing the art::ValidHandle. This allows the algorithm to remain as free as possible from entaglement with features of the framework, and makes it easier to write unit tests for the algorithm.

For algorithms that do not need state other than configuration parameters

In the simplest case, the algorithm is a function with no state, but which may take some configuration parameters. In this case, the algorithm can be implemented in a function that accepts however many arguments are needed. The parameters of the algorithm, if they are more than a few in number, should be put into a struct that is held as a data member of the module, and passed by const reference to the algorithm, as shown below:

void X::produce(art::Event& e)
{
   // Obtain inputs
   auto h = e.getValidHandle<SomeProd>(tag_1_);
   // Call the algorithm that does the work, and which returns the
   // newly-made product via std::unique_ptr. Put the result directly
   // into the art::Event
   ev.put(make_interesting_product(*h, params));
}

For algorithms that require persistent state

For an algorithm that requires state beyond configuration parameters (perhaps some state that needs to be modified for each new Run or Subrun), use a class to encapsulate the algorithm. The class instance should be kept as a data member of the module. Its state can be updated whenever necessary. As much state as possible should be immutable. The canonical form of produce in this case can look similar to that above; however, the line that invokes make_interesting_product is then a call to a member function of the algorithm class. If the member function is a functional call operator (operator()), the produce function can be exactly the same as that above.

Physics algorithms should be implemented in external libraries

Putting the physics algorithms (e.g., make_interesting_product in the example above) into a separate library provides two important benefits:
  1. It allows the algorithm to be shared between several modules
  2. It makes it possible to test the algorithm outside of the art framework

Testing of a stand-alone algorithm implementation outside of the art framework is simpler than testing a module that implements that algorithm because it does not require the production of modules to create the correct input for the module to be tested, nor the production of modules to test the output of the module to be tested.

Note that stand-alone testing of algorithms is important, but is not sufficient. Integration testing of complete modules, or even sets of modules, is also necessary. The stand-alone testing of algorithms allows for fine-grained testing of algorithms, especially in "corner cases", which augments but does not replace other testing.