Writing a Driver
Note
Drivers are a planned feature. The APIs on this page are planned APIs that are not yet implemented, and the code is not yet tested. See Designing Drivers for the design.
A driver is written like a module (see Module Development). Its
constructor declares, on top of what a module’s constructor declares, how the
driver configures the modules below it. This page writes ForceDriver,
which computes a force with “Classical Force” and lets the user choose
whether the electric field is screened.
Declaring the Driver
PluginPlay provides the macro DECLARE_DRIVER(T), the driver counterpart of
DECLARE_MODULE(T). It declares a class T that derives from
DriverBase, which in turn derives from ModuleBase:
#pragma once
#include <pluginplay/pluginplay.hpp>
namespace pluginplay_examples {
DECLARE_DRIVER(ForceDriver);
} // namespace pluginplay_examples
Note
Declaring a driver is not required if the driver is written in Python. In
Python, drivers derive from pluginplay.DriverBase.
The Driver’s Settings
The constructor is started with DRIVER_CTOR(T). As for a module, it sets
the property type the driver satisfies, and the driver’s inputs and
submodules. ForceDriver satisfies Force, so that it can be used
anywhere a force module can. Its “force” submodule is the module that does the
work, and its two inputs are the settings the user can change:
DRIVER_CTOR(ForceDriver) {
description(driver_desc);
satisfies_property_type<force_type>();
add_submodule<force_type>("force").set_description(
"The module that computes the force");
add_input<bool>("screen")
.set_description("Should the electric field be screened?")
.set_default(false);
add_input<double>("threshold")
.set_description("maximum ||r-r_i|| for contributing to electric field")
.set_default(std::numeric_limits<double>::max());
}
class ForceDriver(pp.DriverBase):
def __init__(self):
pp.DriverBase.__init__(self)
self.description("Classical force with an optionally screened field")
self.satisfies_property_type(ppe.Force())
self.add_submodule(ppe.Force(), "force").set_description(
"The module that computes the force"
)
self.add_input("screen").set_default(False)
self.add_input("threshold").set_default(inf)
The rest of the constructor says how those settings reach the modules below
the driver. A driver has two ways to do that: wiring and conditional wiring.
Each names the modules it configures by their keys in the ModuleManager.
Wiring
In a driver’s constructor, add_input and add_submodule return
wrappers that, besides the usual setters, have
set_target(module, target). It gives the input or submodule target of
the module module the value of the driver’s input or submodule, whatever
the driver’s other settings are. A driver that let the user pick the electric
field directly would declare:
add_submodule<ElectricField>("electric field")
.set_default("Coulomb's Law")
.set_target("Classical Force", "electric field");
self.add_submodule(ppe.ElectricField(), "electric field").set_default(
"Coulomb's Law"
).set_target("Classical Force", "electric field")
set_default on a submodule names the module used if the user binds none.
set_target can be called more than once, to wire one input or submodule to
several modules. set_required_target does the same, but makes it an error
for the setting to have no effect (see Using a Driver).
ForceDriver does not use set_target: its “threshold” only matters when
the screened electric field is used, so it is wired in a branch of a chain
instead.
Conditional Wiring
Sometimes one setting changes several things, picks between fixed values, or
decides whether another setting matters. when(input, value) starts a
chain of branches. The first branch applies if the driver’s input input
equals value. otherwise(input, value) starts another branch with its
own condition, and otherwise() starts a branch that applies if no other
does. Only the first branch whose condition holds is applied. A branch can
hold:
fixed values, added with
set_input(module, input, value)andset_submod(module, submod, choice); andinputs and submodules of the driver, wired with
forward_input(module, input, driver_input)andforward_submod(module, submod, driver_submod), which work likeset_target, but only when the branch is taken.
ForceDriver picks the electric field with its “screen” input, and passes
its “threshold” on only when the screened field is used:
// Use the screened electric field, and its threshold, if "screen"
// is true
when("screen", true)
.set_submod("Classical Force", "electric field",
"Coulomb's Law with screening")
.forward_input("Coulomb's Law with screening", "threshold",
"threshold")
.otherwise()
.set_submod("Classical Force", "electric field", "Coulomb's Law");
# Use the screened electric field, and its threshold, if "screen"
# is True
self.when("screen", True).set_submod(
"Classical Force", "electric field", "Coulomb's Law with screening"
).forward_input(
"Coulomb's Law with screening", "threshold", "threshold"
).otherwise().set_submod(
"Classical Force", "electric field", "Coulomb's Law"
)
Because “threshold” is forwarded only in the “screen” branch, it is ignored quietly when “screen” is false, unless the user changed it, in which case the user is warned that it has no effect (see Using a Driver).
A chain only sets fixed values and forwards the driver’s settings. A choice that needs more logic than that is better written as its own module or driver.
One Setting per Target
Within a driver, each input or submodule of a module below it is set by one
set_target call, or by one chain, whose branches may all set or forward to
it. Setting it in two places is an error, so a driver never has to
decide which of its own settings wins. Values that a user sets on the driver
(see Using a Driver) are applied after the driver’s settings, so they
always win.
Other Settings
Two more settings are not used by ForceDriver:
wire_memoization(on, except)turns memoization on or off for every module below the driver, except the modules whose keys are inexcept.A branch of a chain can be marked
.required(), asset_required_targetmarks a wired input or submodule. A required setting that would have no effect (see Using a Driver) is an error instead of a warning.
The Run Member
The run member is defined with MODULE_RUN(T), as for a module. By the time
it is called, the driver’s settings have been applied, so ForceDriver only
needs to call its “force” submodule:
MODULE_RUN(ForceDriver) {
const auto& [q, m, a, charges] = force_type::unwrap_inputs(inputs);
auto F = submods.at("force").run_as<force_type>(q, m, a, charges);
auto rv = results();
return force_type::wrap_results(rv, F);
}
def run_(self, inputs, submods):
pt = ppe.Force()
[q, m, a, charges] = pt.unwrap_inputs(inputs)
F = submods["force"].run_as(pt, q, m, a, charges)
return pt.wrap_results(self.results(), F)