Project

General

Profile

Auxiliary classes


TupleAs - converting from FHiCL sequences to nearly arbitrary C++ types

The fhicl::TupleAs class is available so that FHiCL sequences can be directly converted to an object of class type as specified by the user. The basic syntax is

struct Config {
    TupleAs< DesiredType( required_ctor_arguments ) > dt { Name("name") };
};

where the specified required_ctor_types describes a sequence in FHiCL whose elements must be convertible to the listed types in required_ctor_types.

For example, consider the FHiCL file:

config : {
  offset : [124., 5.6, 9.7 ] # in mm
}

The intention is for the numbers in the sequence above to be converted to a class of type CLHEP::Hep3Vector. The CLHEP::Hep3Vector has the constructor

namespace CLHEP {

  class Hep3Vector {
  public:

    Hep3Vector( double x, double y, double z );
    // ...

  };

}

Without using the TupleAs class, one must convert the three numbers to a CLHEP::Hep3Vector object "by hand":

struct Config {
    Sequence<double,3> offset { Name("offset") };
};
Table<Config> config { Name("config") };

int main()
{
  auto tmp = config().offset(); // ===> std::array<double,3>

  CLHEP::Hep3Vector const offset { tmp[0], tmp[1], tmp[2] };
}

This same conversion operation can be performed more cleanly, and more descriptively by:

struct Config {
    TupleAs< CLHEP::Hep3Vector(double, double, double) > offset { Name("offset") };
};
Table<Config> config { Name("config") };

int main()
{
  auto offset = config().offset(); // ===> CLHEP::HepLorentzVector
}

With this facility, any object of a desired type can be obtained directly through fhiclcpp as long as the types required by the constructor are decodable from the FHiCL representation to the requested C++ type. Most basic C++ types are decodable; if you have concerns about a certain type, please contact .

Aliases for template arguments

Although descriptive, the CLHEP::Hep3Vector(double, double, double) template argument could be considered a bit lengthy. In such cases, an alias can be used:

using Hep3Vector_t = CLHEP::Hep3Vector(double, double, double);

struct Config {

    TupleAs< CLHEP::Hep3Vector(double, double, double) > offset1 { Name("offset1") };
    TupleAs< Hep3Vector_t > offset2 { Name("offset2") };

};

Table<Config> config { Name("config") };

The variables offset1 and offset2 have identical types. Note that using in this context is not a using declaration or directive, but it is an alias, which is equivalent to a typedef. It is therefore safe to use it in a header file.

Allowed constructors

The supported constructors are similar to those of other fhiclcpp types:

// TupleAs<T(ARGS...)>
explicit TupleAs(Name&& name);
explicit TupleAs(Name&& name, Comment&& comment);
explicit TupleAs(Name&& name, Comment&& comment, std::function<bool()> maybeUse);

// c'tors supporting default values
explicit TupleAs(Name&& name, T const& t);
explicit TupleAs(Name&& name, Comment&& comment, T const& t);
explicit TupleAs(Name&& name, Comment&& comment, std::function<bool()> maybeUse, T const& t);

Default values

If a default value is specified, it must be of the desired conversion type. For example:

namespace {
  using namespace CLHEP;

  using Hep2Vector_t = Hep2Vector(double, double);

  Hep2Vector const center {1,1};

  std::vector<Hep2Vector> const corners { 
    Hep2Vector{0,0},
    Hep2Vector{2,0},
    Hep2Vector{2,2},
    Hep2Vector{0,2}
  };
}

struct Config {
    TupleAs< Hep2Vector_t > boxCenter { Name("boxCenter"), center };
    Sequence< TupleAs<Hep2Vector_t>, 4u > boxCorners { Name("boxCorners"), corners };
};

Notice that the defaults provided to the TupleAs and Sequence<TupleAs> objects above are entirely independent of fhiclcpp.

Configuration description

Whenever the allowed configuration is printed for TupleAs objects, the target type is explicitly mentioned in the comment. For example, if the above configuration only had boxCenter, with no default, the printout would be:

pset: {

   # N.B. The following sequence is converted to type:
   #         'CLHEP::Hep2Vector'

   boxCenter: [
      <double>,
      <double>
   ]

}

If a default is provided (as in the above configuration), the system uses the operator<< overload, if available, for the target type (in this case, CLHEP::Hep2Vector):

pset: {

   # N.B. The following sequence is converted to type:
   #         'CLHEP::Hep2Vector'
   #      with a default value of:
   #         (1, 1)

   boxCenter: [
      <double>,
      <double>
   ]

}

If a default is provided (as in the above configuration), and no operator<< is available for the target type, the following message is printed instead:

pset: {

   # N.B. The following sequence is converted to type:
   #         'somenamespace::MyVector'
   #      A default value is present, but it cannot be
   #      printed out since no 'operator<<' overload has
   #      been provided for the above type.        

   boxCenter: [
      <double>,
      <double>
   ]

}

OptionalTupleAs

An Optional variant of TupleAs is provided so that a given sequence to be converted to the specified type is not required to be present. The interface is similar to the Optional* template interfaces elsewhere:

# Parameter declaration
struct Config {
  OptionalTupleAs<CLHEP::Hep2Vector(double,double)> maybeVec { Name("maybeVec") };
};
# Parameter retrieval
Table<Config> t { Name("<table_name>") };
// t is filled somehow
CLHEP::Hep2Vector pos;
if (t().maybeVec(pos))
  cout << "maybeVec exists: " << pos << '\n';

The TableFragment - accommodating flat FHiCL configurations

Consider the following fhiclcpp configuration and its use:

namespace {

  struct A {
    Atom<int>    a1 {...};
    Atom<double> a2 {...};
  };

  struct B {
    Atom<string> b1 {...};
    Atom<string> b2 {...};
  };

}

with the top-level fhiclcpp configuration:

namespace {

  struct Config {
    Table<A>   a { Name("a") };
    Table<B>   b { Name("b") };
    Atom<bool> c { Name("c") };
  };

  Table<Config> config { Name("config") };

}

Good configuration design

With this supported configuration, the constructor of SomeClass can look like:

SomeClass( Table<Config> const & config )
: a_( config().a() ) // ===> A
, b_( config().b() ) // ===> B
, c_( config().c() ) // ===> bool
{}

where a_ and b_ are objects initialized with types A and B respectively. The following FHiCL configuration is then expected:

config : {

  a : { a1 : <value>   a2 : <value> }

  b : { b1 : <value>   b2 : <value> }

  c : <value>

}

Accommodating legacy code

Although we recommend the configuration design as presented in the previous section (namely, specifying the parameters of one type (e.g. A) or algorithm in its own nested FHiCL table), there are situations where the expected configuration has been flattened:

config : {

  a1 : <value>   
  a2 : <value>

  b1 : <value>   
  b2 : <value>

  c  : <value>

}

In such cases, the TableFragment class can be used. By switching the top-level fhiclcpp configuration to:

namespace {

  struct Config {
    TableFragment<A> a;
    TableFragment<B> b;
    Atom<bool> c { Name("c") };
  };

  Table<Config> config { Name("config") };

}

the SomeClass constructor is still able to call individual objects of types A, B, and bool, but it expects the flattened configuration above, with no sub-nesting of FHiCL tables.

TableFragment accessors

The TableFragment has one public function that can be called: operator(). For example, consider again the configuration:

namespace {

  struct Config {
    TableFragment<A> a;
    TableFragment<B> b;
    Atom<bool> c { Name("c") };
  };

}

To retrieve the struct object of type A, the following syntax is used:

void f() {

  Table<Config> config { Name("config") };
  auto afrag = config().a;   // ===> fhicl::TableFragment<A>
  auto a1    = afrag();      // ===> A
  auto a2    = config().a(); // ===> A

}

This enables a consistent syntax with the fhiclcpp types.

TableFragment restrictions

There are several restrictions for a TableFragment:

  • The TableFragment constructor receives no arguments
  • The provided template argument must be of class or struct type.
  • There is no OptionalTableFragment by design.

Bottomline: A flattened FHiCL design should be avoided if possible--the TableFragment class template will exist primarily to support legacy code where a flattened design was used.


DelegatedParameter - deferring validation

Suppose an experiment has a Track class that has the following shape:

class Track {
public:

  struct Config {
    Atom<std::string> detector { Name("detector"), "InnerTracker" };
    Table<AlgorithmDetails::Config> details { Name("details") };
  };

  Track(Config const& c);

private:
  std::string detector_;
  AlgorithmDetails details_;
  // ...
};

In this case, the Track object configuration corresponds to a given detector (as represented by an std::string), and it also contains the configuration for track-reconstruction algorithm details, the configuration of which is gathered from a separate class, which might look like this:

class AlgorithmDetails {
public:

  struct Config {
    Atom<int> verbosity { Name("verbosity"); }
  };

  AlgorithmDetails(Config const& c)
    : verbosity_{c.verbosity()}
  {}

  // ...

private:
  int verbosity_;  
};

The constructor for Track would then look like:

Track::Track(Track::Config const& c) 
  : detector_{c.detector()}
  , details_{c.details()}
{}

Note the absence of the '()' call after 'c', since the constructor argument is of type Track::Config and not a fhicl::Table. This arrangement works well because all of the Track configuration is supported using the fhiclcpp types system. The allowed configuration would then be:

<table_name>: {

   detector: "InnerTracker"  # default

   details: {
      verbosity: <int>
   }
}

However, suppose the AlgorithmDetails class did not yet support the configuration validation and description system. The AlgorithmDetails constructor might then look like:

AlgorithmDetails::AlgorithmDetails(ParameterSet const& pset)
  : verbosity_{pset.get<int>("verbosity")}
{}

In this situation, there is a mismatch between Track parameters that are validated (i.e. detector_), and those that are not (i.e. details_). To be able to support this mismatched situation, the DelegatedParameter class has been introduced. Its purpose is to declare that a parameter with a given name must be present, but the structure of the parameter's value is undefined, and the underlying class can decide to interrogate it. For example:

class AwkwardlyConfiguredTrack {
public:

  struct Config {
    Atom<std::string> detector { Name("detector"), "InnerTracker" };
    DelegatedParameter details { Name("details") };
  };

  Track(Config const& c);

private:
  std::string detector_;
  AlgorithmDetails details_;
  // ...
};

whose constructor looks like:

AwkwardlyConfiguredTrack::AwkwardlyConfiguredTrack(Config const& c)
  : detector_{c.detector()}
  , details_{c.details.get<ParameterSet>()}
{}

The AwkwardlyConfiguredTrack::Config struct implies a supported configuration of:

<table_name>: {

   detector: "InnerTracker"  # default

   details: # ... unspecified shape

}

Important notes

The DelegatedParameter class is not a template. All type specification takes place at the parameter retrieval stage:

DelegatedParameter dp { Name("myParam") };
// dp is prepared somehow
auto const delegatedParameterValue = dp.get<int>();

Note that the above is very similar to:

ParameterSet pset;
// pset is prepared somehow
auto const delegatedParameterValue = pset.get<int>("myParam");

The difference is that the parameter to retrieve (myParam) is declared ahead of time (in the DelegatedParameter constructor) so that only the retrieved type must be specified (int).

OptionalDelegatedParameter

An Optional variant of the DelegatedParameter exists, which is analogous to the ParameterSet::get_if_present semantic. Consider this example:

OptionalDelegatedParameter odp {"maybeParam"};
// odp is filled somehow
int maybeParamValue {};
if (odp.get_if_present(maybeParamValue))
  std::cout << "maybeParam is present with value: " << maybeParamValue << '\n';

Note again the similarity to the ParameterSet retrieval functions--ParameterSet::get_if_present in this case:

ParameterSet pset;
// pset is filled somehow
int maybeParamValue {};
if (pset.get_if_present("maybeParam", maybeParamValue))
  std::cout << "maybeParam is present with value: " << maybeParamValue << '\n';

Explicitly specifying a type (e.g. odp.get<int>(maybeParamValue)) is not necessary since the type of maybeParamValue can be inferred.

Allowed constructors

// #include "fhiclcpp/types/DelegatedParameter.h" 
explicit DelegatedParameter(Name&& name);
explicit DelegatedParameter(Name&& name, Comment&& comment);
explicit DelegatedParameter(Name&& name, Comment&& comment, std::function<bool()> maybeUse);

// #include "fhiclcpp/types/OptionalDelegatedParameter.h" 
explicit OptionalDelegatedParameter(Name&& name);
explicit OptionalDelegatedParameter(Name&& name, Comment&& comment);
explicit OptionalDelegatedParameter(Name&& name, Comment&& comment, std::function<bool()> maybeUse);

N.B. Default values are not supported for the (Optional)DelegatedParameter class.