OpenPFC / A small extension point
Let configuration choose an initial condition
Keep the mathematics in C++. Put the choice and its parameters in the input file.
Download runnable lesson (.zip)
Includes the model, inputs, required headers and a short run guide.
The pattern lesson passes a callable directly to a field. A configurable application can instead create a FieldModifier from a name. This refactoring of 10_ui_register_ic uses OpenPFC’s actual catalog rather than maintaining an unrelated map of functions.
Describe one new field
Our extension fills an affine function,
\[ u(\mathbf x)=b+a_xx+a_yy+a_zz. \tag{1}\]
class AffineFill : public pfc::FieldModifier {
double offset_;
pfc::Real3 slope_;
public:
AffineFill(double offset, pfc::Real3 slope) : offset_(offset), slope_(slope) {}
void apply(pfc::field::FieldOutput<double> field, const pfc::Domain &domain,
const pfc::Box3i &box, double) override {
pfc::field::apply(field, domain, box, [&](const pfc::Real3 &x) {
return offset_ + slope_[0] * x[0] + slope_[1] * x[1] + slope_[2] * x[2];
});
}
};The modifier receives a mutable host view, the physical domain and the owned box. pfc::field::apply performs the coordinate traversal. This keeps the formula independent of both the local array layout and the application’s configuration format.
Register the constructor, then apply its result
auto catalog = pfc::ui::make_builtin_field_modifier_catalog();
catalog.register_modifier("affine", [](const nlohmann::json ¶meters) {
const double offset = parameters.at("offset").get<double>();
const auto slope = parameters.at("slope").get<pfc::Real3>();
if (!std::isfinite(offset) ||
!std::ranges::all_of(slope, [](double v) { return std::isfinite(v); }))
throw std::invalid_argument("Affine parameters must be finite");
return std::make_unique<AffineFill>(offset, slope);
});
const auto specification =
config.value("initial_condition", nlohmann::json{{"type", "affine"},
{"offset", 1},
{"slope", {.2, -.1, .05}}});
auto modifier = catalog.create_modifier(
specification.at("type").get<std::string>(), specification);
pfc::apply_field_modifier(*modifier, field, 0);The catalog starts with built-in initial conditions and adds affine. Its factory validates the supplied parameters and constructs the modifier. An unknown name reports the registered choices. Every MPI process builds its own catalog and applies its modifier to its own field.
This catalog is an ordinary local object. It does not mutate the process-wide default catalog, and the modifier’s mathematics does not depend on registration. A larger JSON-driven session can perform the same wiring for named simulation fields.
Make a change without recompiling the physics
openpfc run model.cpp input.json --source-root .. --launcher=noneChange offset or slope in initial_condition. As a second experiment, replace the whole object with {"type":"constant","n0":0.4}. That selects a built-in modifier through the same catalog. Try an unknown type and read the diagnostic instead of guessing what was selected.
The program writes the actual owned field to results/fields_rank_*.csv. Both the affine and constant cases are checked globally on one and two ranks. The default x/y interval is \([-4,4)\), so this affine profile is not generally continuous across a periodic seam; this lesson fills and inspects it without evolving a periodic PDE.
Keep the model, input and shared helper. Continue with a native configuration-driven session.