LArSoft

Logo

Software for Liquid Argon time projection chambers

View My GitHub Profile

Full examples are available in larexamples repositories, extensively commented. You can also access that from LArSoft Doxygen pages .

Guidelines on writing (and using) services in LArSoft

A LArSoft service is a class, with a single instance managed by the framework, that performs an operation. A service is used by LArSoft algorithms and art modules.

In the context of the art framework, a service is implemented as a class with the following requirements:

  1. special macros are used to declare and then define factory functions and other things specific to art
  2. an implementation file name that follows a pattern like MyService_service.cc
  3. a constructor is available with a specific signature, like
    
          MyService(fhicl::ParameterSet const&, art::ActivityRegistry&);
    

The best practice is to make a service provider (see below) that is as independent as possible from the framework and to use the art service class to provide the interface between the service provider and the framework. In other words,

A specimen of this protocol can be seen in Geometry service:

To write services from scratch, one can start with the examples in larexample repository.

The Geometry service is actually not quite following all LArSoft prescriptions, for legacy reasons.
In particular, geo::Geometry service doesn’t follow the standard name pattern (that would have it geo::GeometryService) and, more important, is actually able to provide directly all the geometry functionality since it inherits the interface from geo::GeometryCore. This is not the recommended implementation.

To get to the functionality, a user needs to ask the framework about the service, and about the provider to the service:


    art::ServiceHandle<geo::Geometry> GeoHandler;
    geo::Geometry const&amp; GeoService = *GeoHandler;
    geo::GeometryCore const* geom = GeoService->provider();

or, more compactly,


    geo::GeometryCore const* geom = art::ServiceHandle<geo::Geometry>()->provider();

LArSoft provides a utility function providerFrom() in larcore/CoreUtils/ServiceUtils.h to make this even more compact:


    geo::GeometryCore const* geom = lar::providerFrom<geo::Geometry>();

Both the forms can be made even more compact, at the expense of readability, by replacing the provider class name with auto: geo::GeometryCore const* geom becomes auto const*: faster to write, but then one has to figure out where to find the documentation of the interface (hint: start from the service documentation, and a pointer will lead you to the provider).

Models for writing services

The plain art service is like described above: any class with a public constructor with a specific signature and a special art macro.
In LArSoft, we may need more flexibility than just a single service with a single implementation:

The following paragraphs describe the three combinations of features on top of the plain art service.

Service interface with many implementations (e.g., experiment-specific)

This model allows different ways to implement the same service. This is an art feature, and nothing of this is specific to LArSoft.
At run time, a single implementation will be chosen by art depending on the service configuration (from the FHiCL configuration file).

The service interface is a (possibly abstract1) class that describes all the service is expected to be able to do. For example2:


    #include "larcorealg/CoreUtils/UncopiableAndUnmovableClass.h"

    class DetectorProperties: private lar::UncopiableAndUnmovableClass {
        public:
      // classes with virtual methods are required a virtual destructor
      virtual ~DetectorProperties() = default;

      /// Return the electric field in the TPC, in kV/cm; field is assumed the same in all TPCs
      virtual float Efield() const = 0;

      /// Return the temperature of the argon in the TPC, in kelvin
      virtual float Temperature() const = 0;

    }; // class DetectorProperties

    DECLARE_ART_SERVICE_INTERFACE(DetectorProperties, LEGACY)

For a complete example, see lar::example::ShowerCalibrationGalore, or geo::ExptGeoHelperInterface in larcore (Redmine link to larcore/Geometry/ExptGeoHelperInterface.h ).

An module or algorithm can use this service by:


    art::ServiceHandle<DetectorProperties> detProp;
    float temperature = detProp->Temperature();

or pick the class directly with


    DetectorProperties const* detProp = &amp;(*art::ServiceHandle<DetectorProperties>());
    float temperature = detProp->Temperature();

Note that in both cases the service class is dependent on the framework (at very least via the art service macros).

The user will have to choose which implementation of the service to make available in the job.
The job configuration will include something like:

services: {
  DetectorProperties: # this is the name of the service interface
  {
     service_provider: DetectorPropertiesStandard # this is the name of the implementation chosen
     Efield:       0.6                            # the remaining configuration is implementation-dependent
     Temperature: 83 
  } # DetectorProperties 
} # services

or another one, like:

services: {
  DetectorProperties: # this is the name of the service interface
  {
     service_provider: DetectorPropertiesFromDB            # this is the name of the implementation chosen
     DBServer: "https://database.experiment.org/conditions" # the remaining configuration is implementation-dependent
  } # DetectorProperties 
} # services

An implementation class will have a declaration like:


    class DetectorPropertiesStandard: public DetectorProperties {
        public:

      DetectorPropertiesStandard(fhicl::ParameterSet const&amp;, art::ActivityRegistry&amp;);

      /// Return the electric field in the TPC, in kV/cm; field is assumed the same in all TPCs
      virtual float Efield() const override { return fEfield; }

      /// Return the temperature of the argon in the TPC, in kelvin
      virtual float Temperature() const override { return fTemperature; }


        private:
      float fEfield;      ///< electric field [kV/cm]
      float fTemperature; ///< argon temperature [K]

    }; // class DetectorPropertiesStandard

    DECLARE_ART_SERVICE_INTERFACE_IMPL(DetectorPropertiesStandard, DetectorProperties, LEGACY)

Again, this is a standard art facility.

Service factorization model

This is a simple scheme of LArSoft services in factorization model:

The art service class is expected to have:

These requests allow LArSoft to provide a simple function to obtain the provider of any service, lar::providerFrom(). Regardless, the service should provide some method to obtain the provider that the users can use directly.

An example of service provider may be:


    /// Configuration parameter documentation goes here
    class DetectorProperties {
        public:
      /// Constructor: reads the configuration from a parameter set
      DetectorProperties(fhicl::ParameterSet const&amp; pset)
        : fEfield(pset.get<float>("Efield"))
        , fTemperature(pset.get<float>("Temperature"))
        {}

      /// Return the electric field as configured [kV/cm]
      float Efield() const { return fEfield; }

      /// Return the argon temperature as configured [K]
      float Temperature() const { return fTemperature; }

        private:
      float fEfield;      ///< value of electric field [kV/cm]
      float fTemperature; ///< value of argon temperature [K]

    }; // class DetectorProperties

This class has no dependency on the framework (although it does depend on some non-standard library, the FHiCL library). It can be instantiated in a simple unit test with no knowledge of any framework.

To be able to use this service provider as a art service, an additional class is required:


    class DetectorPropertiesService {
        public:
      using provider_type = DetectorProperties; ///< type of the service provider

      // Standard art service constructor
      DetectorPropertiesService(fhicl::ParameterSet const&amp;, art::ActivityRegistry&amp;);

      /// Return a pointer to a (constant) detector properties provider
      provider_type const* provider() const { return fProvider.get(); }

        private:
      std::unique_ptr<DetectorProperties> fProvider; ///< owned provider

    }; // class DetectorPropertiesService

    DECLARE_ART_SERVICE(DetectorPropertiesService, LEGACY)

and a possible constructor implementation may be:



    DetectorPropertiesService::DetectorPropertiesService
      (fhicl::ParameterSet const&amp; pset, art::ActivityRegistry&amp;)
      : fProvider(new DetectorProperties(pset))
    {
    }

    DEFINE_ART_SERVICE(DetectorPropertiesService)

An example of this factorization can be seen in the geometry service.
In this example, the service contains a instance of the provider. This is the recommended pattern.
The Geometry service instead inherits, rather than containing, the provider. This is an implementation forced by backward compatibility requirement and is not the recommended method, since it strongly couples provider and service (for example, they are forced to have the same life time).

Service interface factorization (e.g., experiment-specific, framework-independent service implementations)

The factorization model can be extended to service interfaces (which are summarized above).
The way to achieve that is by writing an additional, special configuration parameter for the service:


    services: {
      IService: {
        service_provider: "ImplAService"

        # ... on with the rest of the service configuration

      } # IService
    } # services

The factorization model applies to both the interface and to all the implementations: each of them will be split into a service and a provider.
For the interface side, both service and provider are abstract classes:

For the implementation(s) side, each service is pretty much the same as the normal services, except that:

Therefore, note that users do not interact at all (and don’t even know the existence of) the implementation classes: outside of the implementations themselves, the name of the service implementation appears only in the configuration file, and the name of the provider implementation does not appear at all. In fact, anything added in the implementations that is not already present in the interface will be inaccessible to the user.

A fully developed example of this scheme is the actual implementation of DetectorProperties service (and of DetectorClocks and LArProperties as well).
The service providers are contained in lardataalg/DetectorInfo, while the art services are in lardata/DetectorInfoService:

The interface classes (of provider and service) do not need to have an implementation file (in case of DetectorProperties, they don’t).

Note: %{font-family: monospace}ShowerCalibrationGalore in larexamples is also a fully developed and thoroughly documented example of this pattern.

Prescriptions for the use of LArSoft services

In the factorization development model, user code typically lives in an algorithm class that is interfaced to the framework by whatever the framework provides for the job (in art, that is a module).
The algorithm is prescribed to be portable and with minimal dependencies: it should not usSe a framework service, because that will require the framework to be present. Therefore, the algorithm code has to use providers directly.

A recommended pattern is to have an algorithm class with a method that receives and stores pointers to the required providers.
In the following example, that method is called Setup():


    namespace ns {

      /// Never forget plenty of documentation!!
      class MyAlgorithm {
          public:
        MyAlgorithm(fhicl::ParameterSet const&amp; pset);

        void Setup(pns1::NeededProvider1 const* pProv1, pns2::NeededProvider2 const* pProv2)
          {
            prov1 = pProv1;
            prov2 = pProv2;
            // maybe some check that they are not null...
          }

        // ...

          protected:
        pns1::NeededProvider1 const* prov1 = nullptr; ///< service provider 1
        pns2::NeededProvider2 const* prov2 = nullptr; ///< service provider 2

        // ...

      }; // class MyAlgorithm

    } // namespace ns

An art module using this algorithm would look like this:


    #include "larcore/CoreUtils/ServiceUtil.h" // lar::providerFrom()
    // ...

    class MyModule: public art::EDAnalyzer {
        public:
      MyModule(fhicl::ParameterSet const&amp; pset)
        : pAlgo(new ns::MyAlgorithm(pset))
        {}

      virtual void analyze(art::Event const&amp;) override
        {
          // make sure the algorithm is provided the services it needs
          pAlgo->Setup(
            lar::providerFrom<pns1::NeededService1>(),
            lar::providerFrom<pns2::NeededService2>()
            );

          // then give the algorithm the input from the event
          // (and set for example output histograms if needed)

          // then run the algorithm

          // then do something with the results

        } // analyze()

        private:
      std::unique_ptr<ns::MyAlgorithm> pAlgo; ///< instance of my algorithm

    }; // class MyModule

Naming conventions

We are currently endorsing the following naming convention:

Developing a new LArSoft service

This section will host excerpts of the new DetectorProperties service. Since the latter is not ready yet, this is a to-do.

Lazy provider instantiation

The framework service is required to return a working, fully configured provider as the result of a provider() call.
The service can delay the creation of the provider until then. For example:


    class MyLazyService {
        public:
      using provider_type = MyProvider;

      MyLazyService(fhicl::ParameterSet const&amp; pset, art::ActivityRegistry&amp;)
        : config(pset)
        {}

      /// Returns a (constant) pointer to the service provider
      MyProvider const* provider()
        {
          if (!prov) prov.reset(CreateProvider());
          return prov;
        }


        private:
      fhicl::ParameterSet config; ///< a copy of the service configuration

      std::unique_ptr<MyProvider> prov; ///< pointer to our provider

      /// Creates and returns new provider
      std::unique_ptr<MyProvider> CreateProvider() const
        {
          auto new_prov = std::make_unique<MyProvider>(config);

          new_prov->Setup(
            lar::providerFrom<NeededService>(),
            lar::providerFrom<RequiredService>()
            );
          return new_prov;
        } // CreateProvider()

    }; // class MyLazyService

    DEFINE_ART_SERVICE(MyLazyService)

Note that in this paradigm the provider could still get effectively unused.
For example, an algorithm might claim it requires MyProvider, and then never actually use it. Since the Setup() call of that algorithm takes a pointer to the provider, provider creation is forced when the algorithm is Setup() rather than when the provider is actually used.
In general, providers should consider to implement their own laziness. The right solution depends on the specific case.

Service dependencies

Service providers are not responsible for the other providers they depend on: they assume the providers are fully functional.
It is responsibility of the framework to ensure that the services are ready when needed, and they are around long until they are not needed any more. It is responsibility of each service to make sure that their own provider is ready when its pointer is requested (by provide() call), and to make sure to get, by the proper provider() calls to other services, all the needed providers.

Updating services and service dependencies

Services can (and, in LArSoft, often do) depend on other services. This dependency is propagated to the providers too.
Therefore the service must take care that the provider is informed of any change in the providers it depends from. In other words, when service B depends on service A, if the pointer to provider A can change during the job, it is on the service B owning provider B to give the latter the updated pointer to provider A.
For this, a Setup() method is recommended.
Although it is annoying to have the services do this, it is safe: doing it won’t cause harm, not doing it might.

Multi-threading support

This model has not been confronted with multi threading yet.
Once the supported framework, art, defines its multi threading policy, this model will be updated to cope with it.


For questions, contact Gianluca Petrillo.

  1. An abstract class, in C sense, is a class that has virtual methods it does not provide a definition of. Derived classes must provide such definitions. 

  2. Some LArSoft utilities enforce the recommendation of having service provider classes non-copiable and unmovable. The derivation from lar::UncopiableAndUnmovableClass achieves that goal.