Project

General

Profile

Conditional configuration

Conditional parameters and their constructors

Situations can arise where some configuration parameters are conditionally required, based on the value of previously declared FHiCL parameters. Consider the following configuration:

pset : {
   shape: @nil
   halfLengths: [1,2,3] # if 'shape: box'
   radius: 5            # if 'shape: sphere'
}

Clearly, halfLengths and radius have a well-defined meaning only in the context of a box and sphere, respectively. To be able to signify such a configuration via fhiclcpp parameters, the following constructor is used (e.g.):

explicit Atom(Name&&, Comment&, std::function<bool()> maybeUse);

Such three-argument constructors exist for all fhiclcpp types. The condition checking is done by invoking the std::function<bool()> maybeUse object, or configuration predicate, which the user provides. Unlike other constructors, the Comment&& argument is required, forcing the user to provide a meaningful description of when such a parameter will actually be used. This is for the benefit of all users who may want a printout of a given allowed configuration.

Configuration predicates

As shown above, the configuration predicate is an std::function<bool()> object supplied to the constructor of a fhiclcpp parameter. There are two ways to provide the predicate:

  1. by providing a lambda closure, or
  2. by passing a non-static member function pointer to a fhicl::use_if or fhicl::use_unless function.

Option 1 may be preferred when the predicate that is supplied is simple, whereas option 2 may be desired if more complex conditions are required. In both cases, the return value of the predicate function must be bool, and the function can receive no arguments.

// 1. Lambda option
[this](){ 
  ... 
  return <boolean expression>; 
}

// 2.  Function pointer option, where non-static member function has the signature
bool someFunction() const { 
  ... 
  return <boolean expression>;
};

Providing a lambda predicate

For simple conditions, providing a lambda closure is the simplest approach. Using the configuration from above, the fhiclcpp declarations could be

struct Config {
  Atom<string> shape { Name("shape") };

  Sequence<double, 3u> hls { Name("halfLengths"), 
                             Comment("Use if 'shape' is 'box'."),
                             [this](){ return shape() == "box"; } };

  Atom<double> radius { Name("radius"), 
                        Comment("Use if 'shape' is 'sphere'."),
                        [this](){ return shape() == "sphere"; } };
};

Providing a function pointer

Another alternative to the above is to use pointers to non-static member functions of Config to provide the predicate:

struct Config {
  Atom<string> shape { Name("shape") };

  bool shapeIsBox()    const { return shape() == "box"; }
  bool shapeIsSphere() const { return shape() == "sphere"; }

  Sequence<double, 3u> hls { Name("halfLengths"), 
                             Comment("Use if 'shape' is 'box'."),
                             use_if(this, &Config::shapeIsBox) };

  Atom<double> radius { Name("radius"), 
                        Comment("Use if 'shape' is 'sphere'."),
                        use_if(this, &Config::shapeIsSphere) };
};

In the above the fhicl::use_if creates the std::function<bool()> object that is used by the validation system. The complement fhicl::use_unless can also be used when needed. When using either of the fhicl::use_* helpers, the function pointer ('&Config::someFunction') must be to a non-static member function that is const-qualified, as in the above shapeIs* functions.

Passing the this pointer

In either case above (using a lambda, or passing a member-function pointer with fhicl::use_*), it is imperative that the this pointer is provided. For lambdas, the this pointer is captured in the capture list ([this]), and for the fhicl::use_* helpers, the this pointer is passed as the first argument.

Configuration description for conditional parameters

Using either of the fhiclcpp parameter declarations above, the printed allowed description is:

   pset: {

      shape: <string>

   ┌──────────────────────────────
   │  # Use if 'shape' is 'box'.
   │
   │  halfLengths: [
   │     <double>,
   │     <double>,
   │     <double>
   │  ]
   └──────────────────────────────

   ┌──────────────────────────────
   │  # Use if 'shape' is 'sphere'.
   │
   │  radius: <double>
   └──────────────────────────────
   }

The utility or requiring the Comment argument for a conditional parameter is apparent.

Which parameters can I access in the predicates?

The order in which the predicates are invoked is the same in which the fhiclcpp parameters are declared and initialized, but they do not occur at the same time. Specifically, the order is:

  1. fhiclcpp parameters (comprising the fhiclcpp schema) are initialized in declaration order, per standard C++ behavior
  2. the ParameterSet object is passed to the fhiclcpp schema
  3. each FHiCL parameter is validated (this is when the predicate is invoked) with its corresponding fhiclcpp schema parameter, in declaration order
  4. if the validation is successful, the fhiclcpp parameter is seeded with the value from the FHiCL parameter

Since the validation is done in declaration/initialization order, the only parameters that should be used in a predicate are those that are declared earlier than where the predicate is referenced/provided by the given fhiclcpp parameter. See the below examples.

Example showing correct usage

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

  Atom<int> num3 { Name("num3"), 
                   Comment("Use if 'predicate' returns 'true'."),
                   use_if(this, &Config::predicate) }; // 'predicate' can use 'num1' and 'num2'

  Atom<int> num4 { Name("num4"), 7 };

  bool predicate() const { return num2() == 7; }
}

Example showing incorrect usage

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

  Atom<int> num3 { Name("num3"), 
                   Comment("Use if 'predicate' returns 'true'."),
                   use_if(this, &Config::predicate) }; // 'predicate' can use 'num1' and 'num2'

  Atom<int> num4 { Name("num4"), 7 };

  bool predicate() const { return num4() == 7; }
}

The only change between this example and the previous is that the num2() call in predicate has been changed to num4(). Since the predicate is invoked during num3's validation step, and num4 has not yet been validated, and therefore num4's value has not been updated by that supplied by the FHiCL file, the predicate will always return true because num4's default value is 7. Note that the placement of the 'predicate' definition in the struct definition is irrelevant.

Approximating predicates that can receive arguments

For various reasons, a configuration predicate cannot receive any arguments. However, a predicate that receives arguments can be approximated by the following pattern:

struct Config {
  Atom<string> shape { Name("shape") };

  bool shapeIs(std::string const& s) const { return shape() == s; }

  Sequence<double, 3u> hls { Name("halfLengths"), 
                             Comment("Use if 'shape' is 'box'."),
                             [this](){ return shapeIs("box"); } };

  Atom<double> radius { Name("radius"), 
                        Comment("Use if 'shape' is 'sphere'."),
                        [this](){ return shapeIs("sphere"); } };
};

Although, in this case, such a change only marginally improves the maintenance of the code, the benefits could be more drastic in more complicated configurations.