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 serialize method 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.