Advanced Module Implementation

The previous page covered the basics of writing a module, which is fine for many applications. For more complicated algorithms however, there are additional considerations. That is what this page focuses on.

Module-Specific Inputs

Continuing from our electric field example, the factor of: \(||\vec{r} - \vec{r}_i||^2\) means that point charges which are far from the evaluation point, \(\vec{r}\), minimally contributes to the electric field (assuming all point charges have approximately the same charge). Consequently, we can reduce the computational prefactor slightly by only considering point charges “near” \(\vec{r}\). What constitutes “near” will in general be situational (i.e., depends on magnitudes of point charges involved, target precision, etc.). The point of all this is that for computational reasons, we would like to add a threshold paramter to our electric field module.

For simplicity we will implement a new module ScreenedCoulombsLaw rather than modifying CoulombsLaw. Relative to our CoulombsLaw module, the new parameter slightly modifies the constructor and the run_ member functions. With the new input the constructor becomes:

MODULE_CTOR(ScreenedCoulombsLaw) {
    description(module_desc);
    satisfies_property_type<prop_type>();

    add_input<double>("threshold")
      .set_description("maximum ||r-r_i|| for contributing to electric field")
      .set_default(std::numeric_limits<double>::max());
}

Specifically we declare a new double-precision input called “threshold”, with a brief description and a default value set to the maximum double value (i.e., no screening will occur by default). The relevant change to the run_ member is:

MODULE_RUN(ScreenedCoulombsLaw) {
    const auto& [r, charges] = prop_type::unwrap_inputs(inputs);
    auto thresh              = inputs.at("threshold").value<double>();

    // This will be the value of the electric field
    Point E{0.0, 0.0, 0.0};
    auto rv = results();
    return prop_type::wrap_results(rv, E);
}

Here the point to note is that our module is responsible for manually unwrapping the new input parameter. It will NOT automatically be unwrapped by the property type.

Note

Module-specific inputs work best for parameters that are typically set in advance versus parameters that would typically be passed as an input.

Satisfying Multiple Property Types

Sometimes an algorithm will compute more than one property. Reasons why this may occur include performance (the two quantities may share many common intermediates) and wrapping code not originally designed for PluginPlay (for example calling a library which has a single API that computes many properties). This poses no problem as PluginPlay allows modules to satisfy multiple property types.

If a module satisfies \(n\) property types, the inputs to the module are the union of the inputs to the \(n\) property types as well as any module-specific inputs that the module declares. Similarly the returns of the module are the union of the returns of the \(n\) property types as well as any module-specific results that the module returns.

TODO: Add examples

Lambda Modules

Lambda modules are one-off modules which satisfy a single property type. They can be used anywhere the property type is needed. Unlike a normal module which requires defining a class and metadata, lambda modules can be written inline in turn allowing the lambda module to use runtime state in its defintion. Lambda functions are generated by wrapping a function (either a free function or a lambda function).

TODO: Add examples

Facade Modules

Facade modules are modules which ignore the arguments they are given and return a specified value. They are a simplified version of a lambda module which can be created by providing a single value (instead of a function like a lambda module, or a class like a normal module).