Examples
Tutorial
// SPDX-FileCopyrightText: 2026 Jan Kleinert <jan.kleinert@dlr.de>
// SPDX-FileCopyrightText: 2026 Martin Siggel <martin.siggel@dlr.de>
//
// SPDX-License-Identifier: Apache-2.0
#include <iostream>
#include <parametric/core.hpp>
#include <parametric/operators.hpp>
#include <cmath>
double divide(double v1, double v2)
{
std::cout << "Divide result / k\n";
return v1 / v2;
}
parametric::param<double> p_pow(const parametric::param<double>& base, double exponent)
{
return parametric::eval([exponent](double b) {
return pow(b, exponent);
}, base);
}
int main()
{
auto k = parametric::new_param(3.);
auto j = parametric::new_param(4.);
// This is an example to compute j*j using a lambda expression
auto j_sqr = parametric::eval([](double v) {
std::cout << "Computing j_sqr\n";
return v*v;
}, j);
// Alternatively, we can also use the operator approach
auto result = k * k + j_sqr;
std::cout << "Until here, nothing has been computed!" << std::endl;
std::cout << " Result: " << result << std::endl;
std::cout << " Result: " << result << std::endl;
std::cout << "Change k=2" << std::endl;
k = 2;
std::cout << " Result: " << result << std::endl;
std::cout << "Change j=6" << std::endl;
j = 6;
std::cout << " Result: " << result << std::endl;
// And now lets use a user defined function
result = parametric::eval(divide, result, k);
std::cout << " Result: " << result << std::endl;
result = p_pow(j, 2);
std::cout << result << std::endl;
j = 2;
std::cout << result << std::endl;
return 0;
}
Custom compute nodes
// SPDX-FileCopyrightText: 2026 Jan Kleinert <jan.kleinert@dlr.de>
// SPDX-FileCopyrightText: 2026 Martin Siggel <martin.siggel@dlr.de>
//
// SPDX-License-Identifier: Apache-2.0
#include <iostream>
#include <parametric/core.hpp>
// Lets define a custom compute node that
// simply computes a division of two values
class DivComputer : public parametric::ComputeNode<DivComputer, parametric::Results<double>, parametric::Arguments<double, double>>
{
public:
// Overriding eval does the actual computation
void eval() const override
{
if (auto r = res<0>(); r)
r->set_value(arg<0>().value() / arg<1>().value());
}
};
int main()
{
// just create to values
auto v1 = parametric::new_param("v1", 10.0);
auto v2 = parametric::new_param("v2", 2.0);
// now we are creating the compute node and getting the result parameter
auto result = parametric::compute<DivComputer>(v1, v2);
std::cout << "Expected result: 5" << std::endl;
std::cout << "Actual result: " << result << std::endl;
v2 = 4.0;
std::cout << "Expected result: 2.5" << std::endl;
std::cout << "Actual result: " << result << std::endl;
}
Parametric structures / compositions
// SPDX-FileCopyrightText: 2026 Jan Kleinert <jan.kleinert@dlr.de>
// SPDX-FileCopyrightText: 2026 Martin Siggel <martin.siggel@dlr.de>
//
// SPDX-License-Identifier: Apache-2.0
#include <iostream>
#include <parametric/core.hpp>
// This is a simple structure composed of multiple parameters
struct AStruct
{
parametric::param<int> a{"a", 10};
parametric::param<int> b{"b", 100};
};
// As a best practice, create a helper function to build parametric
// versions of the structure.
// new_parametric_struct ensures, that the changes of a and b
// are propagated also into the structure itself
template<>
parametric::param<AStruct> parametric::new_param(const AStruct& s)
{
return parametric::new_parametric_struct(s, s.a, s.b);
}
int main()
{
auto structSum = [](const AStruct& s) -> int {
return s.a.value() + s.b.value();
};
AStruct s;
auto parametricS = parametric::new_param(s);
auto sum = parametric::eval(structSum, parametricS);
std::cout << "Initial sum: " << sum.value() << std::endl; // 110
// change one parameter
s.b = 1000;
std::cout << "Sum after changing b: " <<sum.value() << std::endl; // 1010
}
A parametric CAD example
This example requires OCE (OpenCASCADE Community Edition) to build.
// SPDX-FileCopyrightText: 2026 Jan Kleinert <jan.kleinert@dlr.de>
// SPDX-FileCopyrightText: 2026 Martin Siggel <martin.siggel@dlr.de>
//
// SPDX-License-Identifier: Apache-2.0
#include <parametric/core.hpp>
#include <TopoDS_Shape.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepTools.hxx>
#include <BRepAlgoAPI_Cut.hxx>
#include <iostream>
class CylinderParms
{
public:
CylinderParms(double w, double h)
: _w(w), _h(h) {}
void SetW(double w) {_w = w;}
void SetH(double h) {_h = h;}
double W() const {return _w;}
double H() const {return _h;}
private:
double _w, _h;
};
// create a box, just with plain old doubles
TopoDS_Shape makeBox (double a, double b, double c)
{
std::cout << " Compute box" << std::endl;
return BRepPrimAPI_MakeBox(gp_Pnt(-a/2., -b/2., .0), a, b, c).Shape();
}
int main()
{
// Define the cylinder parameters
parametric::param<CylinderParms> cyl_parms(CylinderParms(3, 10), "p1");
// Create a cylinder, use custom parametrization object, we can use a lambda
parametric::param<TopoDS_Shape> cyl = parametric::eval([](const CylinderParms& parms) {
std::cout << " Compute cylinder" << std::endl;
return BRepPrimAPI_MakeCylinder(parms.W()/2., parms.H()).Shape();
}, cyl_parms);
// Define the shape parameters of the box
auto box_w = parametric::new_param(10., "w");
auto box_l = parametric::new_param(10., "l");
// optionally, we can also omit the id
auto box_h = parametric::new_param(10.);
// Of course, we also can use auto as a return type
// Instead of using a lambda, lets use a normal function
auto box = parametric::eval(makeBox, box_w, box_l, box_h);
// define boolean cut free function
auto boolean_cut = [](const TopoDS_Shape& s1, const TopoDS_Shape& s2) {
std::cout << " Compute boolean cut" << std::endl;
return BRepAlgoAPI_Cut(s1, s2).Shape();
};
// Perform the cut. The result is a hole in the cylinder.
auto result = parametric::eval(boolean_cut, box, cyl);
std::cout << "Writing result" << std::endl;
BRepTools::Write(result, "result1.brep");
std::cout << "change width of cylinder -> cylinder must be recomputed" << std::endl;
cyl_parms.change_value().SetW(8);
BRepTools::Write(result, "result2.brep");
std::cout << "change width of box -> box must be recomputed" << std::endl;
box_w = 20.;
BRepTools::Write(result, "result3.brep");
std::cout << "nothing has changed so no recomputation is required" << std::endl;
BRepTools::Write(result, "result4.brep");
}
Advanced: Adding serialization
With parametric, you can build a feature tree that tracks the dependencies through any calculation. parametric allows you to serialize the dependency tree to a string. Using this feature, you could come up with a file format for your application and serialize the dependency tree to a file.
Consider the following conceptualized code, that defines a wrapper type
MyDouble around a double and defines four binary operations +,-,*,/
for it. We want to be able to serialize the dependency tree resulting from
any chain of calculations that uses this custom type and the four binary
operations.
struct MyDouble {
double value;
};
MyDouble operator+(MyDouble const& l, MyDouble const& r)
{
return {l.value + r.value};
}
MyDouble operator-(MyDouble const& l, MyDouble const& r)
{
return {l.value - r.value};
}
MyDouble operator*(MyDouble const& l, MyDouble const& r)
{
return {l.value * r.value};
}
MyDouble operator/(MyDouble const& l, MyDouble const& r)
{
return {l.value / r.value};
}
We start out by defining a new custom compute node so that we can use parametric withour struct and our operations.
#include <parametric/core.hpp>
enum struct BinaryOp {
plus,
minus,
mult,
div
};
class MyBinaryOperation : public parametric::ComputeNode<MyBinaryOperation,
parametric::Results<MyDouble>,
parametric::Arguments<MyDouble, MyDouble>>
{
public:
MyBinaryOperation(std::string const& id,
BinaryOp op
)
: operation(op)
{
this->set_id(id);
}
void eval() const override
{
switch (operation) {
case BinaryOp::plus:
if (auto r = res<0>(); r)
r->set_value(arg<0>().value() + arg<1>().value());
return;
case BinaryOp::minus:
if (auto r = res<0>(); r)
r->set_value(arg<0>().value() - arg<1>().value());
return;
case BinaryOp::mult:
if (auto r = res<0>(); r)
r->set_value(arg<0>().value() * arg<1>().value());
return;
case BinaryOp::div:
if (auto r = res<0>(); r)
r->set_value(arg<0>().value() / arg<1>().value());
return;
default:
throw std::logic_error("Not implemented\n");
}
}
void post_connect() const override
{
if (auto r = res<0>(); r)
r->set_id(id());
}
private:
BinaryOp operation;
};
parametric::param<MyDouble> my_eval(const char* id,
BinaryOp op,
parametric::param<MyDouble> const& left,
parametric::param<MyDouble> const& right
)
{
auto ptr = std::make_shared<MyBinaryOperation>(id, op);
return parametric::compute(ptr, left, right);
}
A dependecy tree in parametric consists of alternating parametric::ComputeNodes
and parametric::impl::param_holder<T>s, which both are derived
from parametric::DAGNode.
parametric::DAGNode has a virtual
serialize method returning a string. The overridden
parametric::impl::param_holder<T>::serialize will return an empty string
for dependent parameters and will delegate to the templated function
template <typename T>
std::string parametric::serialize(T const&);
for root parameters. So to be able to serialize any kind of node in our tree, we must
override the virtual
serializemethod of our custom compute node.specialize
std::string parametric::serialize<T>for the types we intend to use as root parameters.
Lets begin by overriding the virtual serialize method of our custom
compute node.
class MyBinaryOperation : public parametric::ComputeNode
{
public:
// ...
std::string serialize() const override
{
std::string out = arg<0>().id();
switch (operation) {
default:
case BinaryOp::plus:
out += " + ";
break;
case BinaryOp::minus:
out += " - ";
break;
case BinaryOp::mult:
out += " * ";
break;
case BinaryOp::div:
out += " / ";
break;
throw std::logic_error("Not implemented\n");
}
out += arg<1>().id();
return out;
}
// ...
}
Next, let us specialize parametric::serialize for MyDouble:
namespace parametric {
template <>
std::string serialize(MyDouble const& md){
return std::to_string(md.value);
}
}
Now we can serialize individual nodes of our dependency tree, but not the
tree itself. parametric provides a convenience class to help us with this task,
the parametric::Serializer. Given a parametric::impl::param_holder<T>,
it will navigate the dependency tree and store the serialized strings of all
nodes in two stacks, one for the root parameters and one for the compute nodes.
We will add a function to unwind these two stacks to get the serialized strings
in topological order:
std::string serialize(parametric::param<MyDouble> const& p){
parametric::Serializer serializer(*(p.node_pointer()));
std::string out;
auto& params = serializer.parameter_stack();
while (!params.empty()) {
auto& e = params.top();
out += e.id + " = " + e.serialized + "\n";
params.pop();
}
auto& compute_nodes = serializer.compute_node_stack();
while (!compute_nodes.empty()) {
auto& e = compute_nodes.top();
out += e.id + " = " + e.serialized + "\n";
compute_nodes.pop();
}
return out;
}
Finally, we can test our serialization framework:
int main()
{
auto a = parametric::new_param(MyDouble{0.5}, "a");
auto b = parametric::new_param(MyDouble{0.1}, "b");
auto c = my_eval("c", BinaryOp::plus, a, b);
auto d = my_eval("d", BinaryOp::div, c, b);
std::cout<<serialize(d);
return 0;
}
The output will look like this:
b = 0.100000
a = 0.500000
c = a + b
d = c / b
This example can be extended by using a thirdparty serialization framework like Boost::serialize, jsoncpp etc or by writing the functionality to deserialize a string to a dependency tree by hand.