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

An art service is a class that performs an operation, is configurable via FHiCL, and has a single instance that is managed by the art framework. To define a service class for use in art, the implementation must satisfy the following:

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

A service may be used within any art module in LArSoft.

In the context of LArSoft, the best practice is to create a “service provider” class (or simply “provider”) in addition to the service class itself with the following relationship between the two:

This separation between the provider (i.e., the desired functionality) and the service (i.e., the framework interface) is a core design principle of LArSoft – the separation of algorithm and framework code. Without this separation at the service level, it would be impossible to write framework-independent algorithms that required services.

Another best design practice is for the service to contain the provider either by reference or by value. For historical reasons, some services inherit from the provider class, though this design pattern is strongly discouraged. In either case, the service must implement a provider() method that returns a reference to the provider.

An example of this protocol can be seen in Geometry service:

(Note that the description of the geometry service above is intended to convey the essence of the design. The details are more complex.)

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

To get to the provider, a user first needs to ask the framework for the service instance, and then the service for the provider:


    art::ServiceHandle<geo::Geometry> GeoHandle;
    geo::Geometry const&; GeoService = *GeoHandle;
    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>();

Models for writing services

The service / provider model provides a high degree of configurability and customization through abstraction at both the service and provider levels. In the following, we provide examples of the two most common service design models, one without that abstraction, and one with. A third model, one that exploits abstraction at the service level, but that has no provider, is also present in a few legacy services, and so is shown at the end for illustrative purposes. This design does not comply with LArSoft service design guidelines, so should not be used for new services.

Basic service factorization model

The most simple scheme for LArSoft services design is the following:

As previously noted, the art service class is expected to have:

LArSoft offers a simple function to obtain the provider of any service that meets these requirements, lar::providerFrom(). Regardless, only the provider should be used directly within algorithm code. The service should live only in art modules and be used only to access the provider and manage art state transitions.

An example of a service provider may be:


    /// Configuration parameter documentation goes here
    class DetectorProperties {
        public:
      /// Constructor: reads the configuration from a parameter set
      DetectorProperties(fhicl::ParameterSet const&; 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 libraries, such as the FHiCL C++ interface library). It can be instantiated in a simple unit test with no knowledge of any framework.

The corresponding art service used to access the provider could then be the following:


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

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

      /// 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&; pset, art::ActivityRegistry&;)
      : fProvider(new DetectorProperties(pset))
    {
    }

    DEFINE_ART_SERVICE(DetectorPropertiesService)

In this example, the service contains a instance of the provider, the recommended pattern. Another example of this type of factorization can be found in the Geometry service. In that case, however, the service inherits from the provider, rather than containing it. This is an implementation forced by a backwards compatibility requirement and is not the recommended method, since it strongly couples provider and service. (They are forced to have the same life time, for instance.)

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

To provide context-depedent behavior for services, the art framework allows the creation of abstract service interface classes that define the user interface for the service, but with an implementation that is defined at run-time via the FHiCL configuration:


    services: {
      IService: {
        service_provider: "ImplAService"

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

      } # IService
    } # services

In this example, the calling code is written to the service interface class IService, while the run-time implementation, which inherits from the interface class, is determined by the value service_provider set in the FHiCL configuration for IService. (The “service_provider” in this case should not to be confused with the “provider” that is part of the LArSoft design rules for services. The former is specific to the FHiCL configuration and refers to the concrete, art service class that implements the service interface, while the latter is an art-independent class that implements the functionality the user needs in (art-independent) algorithm code.) The use of service interfaces for services in LArSoft allows the creation of experiment or detector-dependent implementations for the service.

As before, the functionality of the service in this model is split between an art service class that interfaces with art, and an art-independent provider class that provides the functionality needed by algorithms. The art-independent provider class in this case must also have an abstract base class. The picture at the interface side is then:

For the implementation side, each concrete service follows the basic design scheme, except that:

Note that when using this model, users do not interact at all (and don’t even know the existence of) the implementation classes. The name of the service implementation appears only in the configuration file, and the name of the provider implementation does not appear at all (unless defined as an art tool, a case that is not discusssed here). 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 the art-independent repository area 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: ShowerCalibrationGalore in larexamples is also a fully developed and thoroughly documented example of this pattern.

Non-standard service interface with no provider (legacy services only)

This model user an abstract art service interface class to define directly the functionality needed by the user. Though it allows for experiment or detector-dependent implementations, the design embeds the implementation of the needed functionality within framework interface code, and is therefore a pattern to be avoided. We show it here for illustrative purposes only, since some legacy services use this model. The main distinction from the basic service deisgn is simply that the provider interface is moved into the service class itself.

For example, a service interface class could be defined as follows:


    #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 = &;(*art::ServiceHandle<DetectorProperties>());
    float temperature = detProp->Temperature();

To choose the implementation of the service to make available in the job, the 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&;, art::ActivityRegistry&;);

      /// 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)

Note that in all such cases, the service class depends on the framework at the very least via the art service macros.

Again, this design pattern uses a standard art service feature, but is a non-standard LArSoft service design.

Prescriptions for the use of LArSoft services

In the LArSoft factorization model, user code typically lives in an algorithm class that is interfaced to the framework by whatever the framework provides for the purpose. (In art, that is a module).
The algorithm is designed to be portable with minimal dependencies. It should not use a framework service directly, because that will require the framework to be present. Therefore, the algorithm code may only 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&; 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&; pset)
        : pAlgo(new ns::MyAlgorithm(pset))
        {}

      virtual void analyze(art::Event const&;) 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&; pset, art::ActivityRegistry&;)
        : 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 Erica Snider.