WangY18 / NEPath

toolpath planning for additive manufacturing and CNC milling
Boost Software License 1.0
24 stars 8 forks source link
additive-manufacturing cnc-milling contour-parallel cp iqop non-equidistant-toolpath optimization-based-toolpath path-connecting path-planning path-planning-algorithm raster smooth-path toolpath-planning underfill zigzag

NEPath

A Classical Toolpath and Optimization-Based Non-Equidistant Toolpath Planning Library (In C++)

License

The NEPath library plans toolpaths for [additive manufacturing (AM, 3D printing)](3D printing - Wikipedia) and CNC milling. Toolpath planning is to generate some 1D toolpaths to filling given 2D slices. The NEPath library is able to plan the following toolpaths:

Among them, the IQOP is proposed by Wang et al., with a journal article, i.e.,

Wang Y, Hu C, Wang Z, et al. Optimization-based non-equidistant toolpath planning for robotic additive manufacturing with non-underfill orientation[J]. Robotics and Computer-Integrated Manufacturing, 2023, 84: 102599.

or in BiBTeX:

@article{wang2023optimization,
  title={Optimization-based non-equidistant toolpath planning for robotic additive manufacturing with non-underfill orientation},
  author={Wang, Yunan and Hu, Chuxiong and Wang, Ze and Lin, Shize and Zhao, Ziyan and Zhao, Wenxiang and Hu, Kehui and Huang, Zhongyi and Zhu, Yu and Lu, Zhigang},
  journal={Robotics and Computer-Integrated Manufacturing},
  volume={84},
  pages={102599},
  year={2023},
  publisher={Elsevier}
}

Complier

C++17

Statement and Dependence

About Citing

If you need to use the NEPath project, please cite "Wang Y, Hu C, Wang Z, et al. Optimization-based non-equidistant toolpath planning for robotic additive manufacturing with non-underfill orientation[J]. Robotics and Computer-Integrated Manufacturing, 2023, 84: 102599."

Introduction to IQOP

Framework

IQOP is an optimization-based non-equidistant toolpath planning method for AM and CNC milling. IQOP tries to optimize the smoothness and material cost of the child toolpath from a parent toolpath. IQOP has the following advantages:

gallery

Figure. Some demos of IQOP.

different_object_functions

Figure. Toolpaths generated by different object functions.

different_weight

Figure. Toolpaths generated by different weighting coefficient.

More details of IQOP would be provided after the article is published.

Optimization Problem of IQOP

The toolpaths can be planned by offsetting non-equidistantly. The offsetting distances $(\deltai){i=1}^n$ can be seen as optimization variables. $\delta_i$ is the offsetting distance at $(x_i,y_i)$.

Underfill

Figure. Optimization variables.

Given $l$, the optimization problem for generating $\tilde{l}$ can be written as:

optimization_problem

In our paper Optimization-Based Non-Equidistant Toolpath Planning for Robotic Additive Manufacturing with Non-Underfill Orientation, the above optimization problem is convexified, and the problem of self-intersection is solved. The above method can be applied for slices with arbitrary shapes and topological structures.

API

NEPath-master/path.h

NEPath-master/NEPathPlanner.h

The package NEPathPlanner.h include the key class of NEPath, i.e., NEPathPlanner. All operations on toolpath planning is based on NEPathPlanner. The API of NEPathPlanner is as follows:

NEPath-master/Curve.h

Curve.h has some fundamental methods on geometry.

Examples

Toolpath Generation

IQOP (Isoperimetric-Quotient-Optimal Toolpath, Wang Y et al., 2023)

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.1 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    NonEquidistantOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.alpha = 0.5; // the scale of minimum distance
    opts.dot_delta = 1.0; // the upper bound of \dot{delta_i}
    opts.ddot_delta = 0.1; // the upper bound of \ddot{delta_i}

    opts.optimize_Q = true; // the isoperimetric quotient is in the objective function
    opts.optimize_S = false; // the area is not in the objective function
    opts.optimize_L = false; // the length is not in the objective function
    opts.lambda_Q = 1.0; // the weighting coefficient of the isoperimetric quotient

    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;

    paths IQOP_paths = planner.IQOP(opts, true); // all IQOP paths
    cout << "There are " << IQOP_paths.size() << " continuous toolpaths in total." << endl;
    return 0;
}

IQOP

Figure. IQOP toolpath minimizing Q.

IQSOP

Figure. IQOP toolpath minimizing Q+1.0S.

IQSOP

Figure. IQOP toolpath minimizing L.

CP (Contour-Parallel)

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    ContourParallelOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;

    paths CP_paths = planner.CP(opts); // all CP paths
    cout << "There are " << CP_paths.size() << " continuous toolpaths in total." << endl;
    for (int i = 0; i < CP_paths.size(); ++i) {
        // CP_paths[i] is the i-th continuous toolpath
        cout << "Toopath " << i << " has " << CP_paths[i].length << " waypoints." << endl;
    }

    return 0;
}

CP

Figure. CP toolpath.

Zigzag

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Set the contour
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }
    planner.set_contour(contour);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    DirectParallelOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.angle = pi / 3.0; // the angle of Zigzag toolpaths, unit: rad

    paths zigzag_paths = planner.Zigzag(opts); // all zigzag paths
    cout << "There are " << zigzag_paths.size() << " continuous toolpaths in total." << endl;
    for (int i = 0; i < zigzag_paths.size(); ++i) {
        // zigzag_paths[i] is the i-th continuous toolpath
        cout << "Toopath " << i << " has " << zigzag_paths[i].length << " waypoints." << endl;
    }

    return 0;
}

zigzag

Figure. Zigzag toolpath.

Raster

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Set the contour
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }
    planner.set_contour(contour);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    DirectParallelOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.angle = - pi / 3.0; // the angle of raster toolpaths, unit: rad

    paths raster_paths = planner.Raster(opts); // all raster paths
    cout << "There are " << raster_paths.size() << " continuous toolpaths in total." << endl;

    return 0;
}

raster

Figure. Raster toolpath.

Toolpath Connection

IQOP connected by CFS

CP can be connected by CFS in the same way.

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.1 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    NonEquidistantOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.alpha = 0.5; // the scale of minimum distance
    opts.dot_delta = 1.0; // the upper bound of \dot{delta_i}
    opts.ddot_delta = 0.1; // the upper bound of \ddot{delta_i}

    opts.optimize_Q = true; // the isoperimetric quotient is in the objective function
    opts.optimize_S = false; // the area is not in the objective function
    opts.optimize_L = false; // the length is not in the objective function
    opts.lambda_Q = 1.0; // the weighting coefficient of the isoperimetric quotient

    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;
    opts.connect = cfs; // select cfs as the connecting method

    paths IQOP_paths = planner.IQOP(opts, true); // IQOP with CFS
    return 0;
}

iqop_cfs

Figure. IQOP connected by CFS.

IQOP connected by DFS

CP can be connected by DFS in the same way.

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.1 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    NonEquidistantOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.alpha = 0.5; // the scale of minimum distance
    opts.dot_delta = 1.0; // the upper bound of \dot{delta_i}
    opts.ddot_delta = 0.1; // the upper bound of \ddot{delta_i}

    opts.optimize_Q = true; // the isoperimetric quotient is in the objective function
    opts.optimize_S = false; // the area is not in the objective function
    opts.optimize_L = false; // the length is not in the objective function
    opts.lambda_Q = 1.0; // the weighting coefficient of the isoperimetric quotient

    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;
    opts.connect = dfs; // select dfs as the connecting method

    paths IQOP_paths = planner.IQOP(opts, true); // IQOP with DFS
    return 0;
}

iqop_dfs

Figure. IQOP connected by DFS.

Others

Tool compensate

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }
    planner.set_contour(contour);

    // Obtain the hole
    double x_hole[] = { -5,5,5,0,-5 };
    double y_hole[] = { -5,-5,5,0,5 };
    planner.addhole(x_hole, y_hole, 5);

    // Tool compensate
    ContourParallelOptions opts;
    opts.delta = -1.5; // the offset distance
    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;
    paths ps_toolcompensate = planner.tool_compensate(opts); // Tool compensate

    cout << "There are " << ps_toolcompensate.size() << " continuous toolpaths in total." << endl;
    for (int i = 0; i < ps_toolcompensate.size(); ++i) {
        // ps_toolcompensate[i] is the i-th continuous toolpath
        cout << "Toopath " << i << " has " << ps_toolcompensate[i].length << " waypoints." << endl;
    }

    return 0;
}

Tool compensate

Figure. Tool compensate.

Underfill

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    ContourParallelOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;

    paths CP_paths = planner.CP(opts); // all CP paths

    double delta_underfill = opts.delta; // the line width for underfill computation
    double reratio = 0.03; // resolution ratio for underfill computation

    UnderFillSolution ufs = Curve::UnderFill(contour, paths(), CP_paths, delta_underfill, reratio); // Obtain the results of underfill

    cout << "The underfill rate is " << ufs.underfillrate * 100 << "%." << endl;

    return 0;
}

Underfill

Figure. Underfill. The underfill rate is 1.2401% in this example.

Sharp corner

#include "NEPath-master/NEPathPlanner.h"
#include <iostream>
using namespace std;

int main() {
    NEPathPlanner planner;

    // Obtain the contour of the outer boundary of slices
    path contour;
    contour.length = 1000; // the number of waypoints
    contour.x = new double[contour.length](); // x-coordinate of waypoints
    contour.y = new double[contour.length](); // y-coordinate of waypoints
    const double pi = acos(-1.0); // pi == 3.1415926...
    for (int i = 0; i < contour.length; ++i) {
        double theta = 2.0 * pi * i / contour.length;
        double r = 15.0 * (1.0 + 0.15 * cos(10.0 * theta));
        contour.x[i] = r * cos(theta);
        contour.y[i] = r * sin(theta);
    }

    // The out boundary should be offset with half of the line width to obtain the outmost toolpath
    NEPathPlanner planner_toolcompensate;
    planner_toolcompensate.set_contour(contour);
    ContourParallelOptions opts_toolcompensate;
    opts_toolcompensate.delta = -1.0 * 0.5; // half of the line width of toolpaths
    opts_toolcompensate.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts_toolcompensate.washdis = 0.2;
    paths path_outmost = planner_toolcompensate.tool_compensate(opts_toolcompensate);

    planner.set_contour(path_outmost[0]);
    // or `planner.set_contour(contour.x, contour.y, contour.length)`

    // Set the toolpath parameters
    ContourParallelOptions opts;
    opts.delta = 1.0; // the line width of toolpaths
    opts.wash = true; // it is recommended to set opt.wash=true
    // if wash==true, then all toolpaths would have yniformly distributed waypoints, with a distance near opts.washdis
    opts.washdis = 0.2;

    paths CP_paths = planner.CP(opts); // all CP paths

    double radius = 1.0; // radius of the rolling circle
    double threshold = 0.3; // threshold of area on one side to determine a sharp corner

    // Obtain the results of underfill
    int num = 0;
    for (int i = 0; i < CP_paths.size(); ++i) {
        SharpTurnSolution sol = Curve::SharpTurn_Invariant(CP_paths[i], radius, threshold, true, 0.5);
        for (int j = 0; j < sol.length; ++j) {
            num += sol.SharpTurn[j];
        }
    }

    cout << "There exist " << num << " sharp corners." << endl;

    return 0;
}

Underfill

Figure. Sharp corners. There exist 44 sharp corners in this example.