Basics of Writing a Property Type
A property type of type T
is ultimately a class that inherits from
pluginplay::PropertyType<T>
. The property type defines the API for computing
the associated property. This API has two parts: the positional input arguments
and the returned value (multiple returns are possible, but rarely needed).
Declaring A Property Type
Like declaring a module, declaring a property type involves a lot of
boilerplate. For this reason we have provided macros to faciliate the process.
To declare our ElectricField
property type the relevant code is:
#pragma once
#include <pluginplay/pluginplay.hpp>
namespace pluginplay_examples {
DECLARE_PROPERTY_TYPE(ElectricField);
}
Unlike modules, property types are usually defined in the header file (this is because these macros hide a significant amount of template meta-programming). Thus it is best practice to declare one property type per header file as opposed to modules where it is not uncommon to declare several modules in one header file.
Defining a Property Type
Warning
Property types use, and are used in, a lot of template meta-programming. In an attempt to make life easy for developers we have hidden all of this from the user. As long as you follow the pattern laid out in this subsection you can remain ignorant to how we did this for you. If you want to deviate from this pattern you likely will have to do quite a bit of template meta-programming on your own.
Defining the Input Positional Arguments
Defining a property type involves defining two functions: inputs_()
and
results_()
. Respectively these functions establish the positional input
arguments (and their types) for computing the property and how the property is
represented in code. Again we have provided macros to take care of the
boilerplate.
The definition of the inputs_()
function looks like:
PROPERTY_TYPE_INPUTS(ElectricField) {
// Type of an std::vector filled with PointCharge instances
using point_charge_vector = std::vector<PointCharge>;
auto rv = pluginplay::declare_input()
.add_field<const Point&>("r")
.template add_field<const point_charge_vector&>("charges");
rv.at("r").set_description(
"The point where the electric field will be evaluated.");
rv.at("charges").set_description(
"The set of point charges giving rise to the electric field.");
return rv;
}
As mentioned at the beginning of this chapter we are only concerning ourselves
with the scenario in which we want the value of the electric field at a single
point and we further assume that the electric field entirely arises from a
series of point charges. We thus need two inputs: where we are evaluating the
electric field, and the set of point charges. In these tutorials we are modeling
vectors, like positions, via the Point
class so our first field is of type
Point
. Point charges are being modeled via the PointCharge
class and we
opt to have our second argument be an std::vector<PointCharge>
instance. In
both cases we only need to read the value of the inputs and we opt to take the
inputs via constant references.
Note
Inputs in PluginPlay can not be used for returns and must be either by value or by constant reference.
After agreeing on the types we declare the fields. This involves calling
pluginplay::delare_input()
and then chaining to the result two calls to
add_field
. add_field
takes one type parameter (the type of the field)
and a string which will be used as the name of the field. You can think of:
auto rv = pluginplay::declare_input()
.add_field<const Point&>("r")
.template add_field<const point_charge_vector&>("charges");
as declaring a function ElectricField
with the signature
ElectricField(const Point& r, const std::vector<PointCharge>& charges)
(the
type of the return will be set via the results_()
function).
Note
The need for the template
keyword in the second (and all subsequent)
add_field
call is a quirk of the C++ language and can not be avoided.
The input fields we just declared are actually stored in an object with a
(potentially) nasty type (we let the compiler worry about the type by using
auto
).
Once we have declared all inputs we can set the meta-data associated with those inputs (in this case we set their descriptions; a full list of available meta-data is located at (TODO: Add link)). For this all to work right it is important to declare the fields first and then set the meta-data. The meta-data is used in the automatically generated documentation.
Defining the Result
The definition of the results_()
function looks very similar to
inputs_()
and follows the same rules except that we use
pluginplay::declare_result
instead of pluginplay::declare_inputs
. The
definition is:
PROPERTY_TYPE_RESULTS(ElectricField) {
return pluginplay::declare_result().add_field<Point>("electric field at r");
}
The electric field at a point is simply a vector and we have been modeling
vectors with the Point
class. Thus we establish that the return of our
ElectricField
property type is a Point
instance. Since most properties
are a single object (i.e., the electric field, the force, etc.) property
types typically only have a single return values. However, like the inputs we
could change multiple add_field
calls together to create additional return
values (the actual return would be an std::tuple
with those values since
strictly speaking C++ does not support multiple return types; the distinction is
immaterial since C++17 where structured bindings allow us to effectively have
multiple returns). Akin to the inputs, we also could have assigned a more
detailed description to the return value if we wanted to; for better or worse,
we decided that the tag "electric field at r"
was descriptive enough and
forwent a more detailed description.
Summary
Generally speaking for both the inputs and results follow this pattern when defining the function:
call
pluginplay::declare_input()
/pluginplay::declare_result()
as appropriatechain to that call a call to
add_field<T>(std::string)
to create a field that is of typeT
and is tagged with the provided string.repeat the previous step for each additional field (making sure to include the
template
keyword before subsequentadd_field
calls; this is a quirk of the C++ language).Assign the result of the aforementioned calls to an object whose type is deduced by the compiler via the
auto
keyword (trust us, you don’t want to see it/work it out).Working with the aforementioned object continuing setting meta-data such as descriptions and default values.
Return the object
The full declaration and definition of the ElectricField
property type is
shown below:
/*
* Copyright 2022 NWChemEx-Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "point_charge.hpp"
#include <pluginplay/pluginplay.hpp>
namespace pluginplay_examples {
DECLARE_PROPERTY_TYPE(ElectricField);
PROPERTY_TYPE_INPUTS(ElectricField) {
// Type of an std::vector filled with PointCharge instances
using point_charge_vector = std::vector<PointCharge>;
auto rv = pluginplay::declare_input()
.add_field<const Point&>("r")
.template add_field<const point_charge_vector&>("charges");
rv.at("r").set_description(
"The point where the electric field will be evaluated.");
rv.at("charges").set_description(
"The set of point charges giving rise to the electric field.");
return rv;
}
PROPERTY_TYPE_RESULTS(ElectricField) {
return pluginplay::declare_result().add_field<Point>("electric field at r");
}
} // namespace pluginplay_examples