Helix Contouring
Forum rules
Be nice to others! Respect the FreeCAD code of conduct!
Be nice to others! Respect the FreeCAD code of conduct!
-
- Posts: 74
- Joined: Tue Oct 23, 2018 3:35 pm
Helix Contouring
I found that the normal Contour Path operation was damaging my HSS tool when cutting aluminium, even with a stepdown of 0.2mm. Those little plunges at the start of each layer sounded pretty bad, even with a very slow vertical feedrate.
So I got into the guts and added a helix contour feature onto this, by hacking an extra function call into the code of _buildPathArea() in class ObjectOp of the Mod/Path/PathScripts/PathAreaOp.py module.
This code (attached) identifies the distinct layers from the path object, checks they are all exactly the same (as expected), and then interpolates the Z-values between the layers along the XY path length.
I've been able to cut a whole series of parts without busting a tool, so it's a really good feature to have. In fact, it if were a standard option, the helix operation would be redundant as it would simply be a contour of a cylindrical pocket.
My implementation (attached) is absolutely not the way to do it. It came as a consequence of going deep into the guts of the software.
The function _buildPathArea() is the last point of Python sanity before the code enters C++ craziness through the fromShapes() function in Mod/Path/App/AppPath.py
The function fromShapes() in turn calls Area::toPath() in Mod/Path/App/Area.cpp which is a mass of code so complicated that there was no way to retrofit something as trivial as this interpolated Z ramping into it.
I have no idea why this function is in C++ anyway, since it only requires a single contour to be extracted, and then all the path generation and linking can be done by repeating the shape several times and setting the Z-value.
For example, another way of linking is to ramp down to the next level over a short distance, in which case the start-end point of the contour advances around the shape. This isn't a problem, and is quite efficient, and a lot better than the ramp dressup that needs to place a horrible zigzag in the connection, because it doesn't know how to advance the start point.
So I got into the guts and added a helix contour feature onto this, by hacking an extra function call into the code of _buildPathArea() in class ObjectOp of the Mod/Path/PathScripts/PathAreaOp.py module.
This code (attached) identifies the distinct layers from the path object, checks they are all exactly the same (as expected), and then interpolates the Z-values between the layers along the XY path length.
I've been able to cut a whole series of parts without busting a tool, so it's a really good feature to have. In fact, it if were a standard option, the helix operation would be redundant as it would simply be a contour of a cylindrical pocket.
My implementation (attached) is absolutely not the way to do it. It came as a consequence of going deep into the guts of the software.
The function _buildPathArea() is the last point of Python sanity before the code enters C++ craziness through the fromShapes() function in Mod/Path/App/AppPath.py
The function fromShapes() in turn calls Area::toPath() in Mod/Path/App/Area.cpp which is a mass of code so complicated that there was no way to retrofit something as trivial as this interpolated Z ramping into it.
I have no idea why this function is in C++ anyway, since it only requires a single contour to be extracted, and then all the path generation and linking can be done by repeating the shape several times and setting the Z-value.
For example, another way of linking is to ramp down to the next level over a short distance, in which case the start-end point of the contour advances around the shape. This isn't a problem, and is quite efficient, and a lot better than the ramp dressup that needs to place a horrible zigzag in the connection, because it doesn't know how to advance the start point.
- Attachments
-
- PathAreaOp_HelixingContour.py
- (4.84 KiB) Downloaded 66 times
-
- contour_helix.png (22.84 KiB) Viewed 2748 times
Re: Helix Contouring
Nice addition Julian!
Josh
Josh
Re: Helix Contouring
I agree that I do not like the plunge from z-layer to z-layer, especially since the default x-y location always seems to be in some relatively critical area of the shape. I often see a small gouge at the z-down location.
On the other hand I have tried the continuous ramp (g-code by hand) and it also has a potentially significant problem. When proceeding as shown in the image above, climb milling while spiraling down in a clockwise direction (viewed from the top), the trailing edge of the cutter end tries to force the cutter into the workpiece. In my experience, milling stainless steel, the surface finish ended up rougher than when keeping a constant z-level.
If I had a multi-100K$ machine perhaps I would not see that effect.
So while the code may be fine, this is not a universal solution for contouring.
Gene
On the other hand I have tried the continuous ramp (g-code by hand) and it also has a potentially significant problem. When proceeding as shown in the image above, climb milling while spiraling down in a clockwise direction (viewed from the top), the trailing edge of the cutter end tries to force the cutter into the workpiece. In my experience, milling stainless steel, the surface finish ended up rougher than when keeping a constant z-level.
If I had a multi-100K$ machine perhaps I would not see that effect.
So while the code may be fine, this is not a universal solution for contouring.
Gene
Re: Helix Contouring
Why not using the ramp dressup?
HTH
Thorsten
HTH
Thorsten
- Attachments
-
- Screenshot_20190104_225607.png (47.44 KiB) Viewed 2698 times
-
- Posts: 74
- Joined: Tue Oct 23, 2018 3:35 pm
Re: Helix Contouring
@schnebeck That would have saved me a lot of time. I didn't see that there was a helixing "method" on the ramp-dressup hidden among the Property-values. All I got to see was this awful zig-zag effect going down. I will need to investigate its code, after my entanglement with all the other stuff.
@GeneFC there is climb milling or doing it as semi-finishing, then doing a finishing pass. cutting steel is always pretty tricky. What machine to you have that is strong enough to do it? The stepovers must be microscopic.
Also, there are ways to spread out the ramping down by real distance from start relative to a long ruler, rather than distance along the curved line.
Also, the ramp can be shorter than the whole length, and be done on a straight section that is less problematic.
This helixing can only apply to constant contour shapes. If you have general slicing, then linking between them is not just a change in Z and becomes a general problem of smooth linking between 3axis finishing passes. Sometimes this is done by little loop backs away from the surface (which particularly applies when doing finish machining).
@GeneFC there is climb milling or doing it as semi-finishing, then doing a finishing pass. cutting steel is always pretty tricky. What machine to you have that is strong enough to do it? The stepovers must be microscopic.
Also, there are ways to spread out the ramping down by real distance from start relative to a long ruler, rather than distance along the curved line.
Also, the ramp can be shorter than the whole length, and be done on a straight section that is less problematic.
This helixing can only apply to constant contour shapes. If you have general slicing, then linking between them is not just a change in Z and becomes a general problem of smooth linking between 3axis finishing passes. Sometimes this is done by little loop backs away from the surface (which particularly applies when doing finish machining).
Re: Helix Contouring
Not sure where you are going with this, but I have various strategies depending on the part size and the amount to be cut. Typically a balance between side cut and end cut. And of course the capability of the machine is critical. I always use climb milling when possible.JulianTodd wrote: ↑Fri Jan 11, 2019 12:47 pm @GeneFC there is climb milling or doing it as semi-finishing, then doing a finishing pass. cutting steel is always pretty tricky. What machine to you have that is strong enough to do it? The stepovers must be microscopic.
I am not at all opposed to using helix strategies: I use them in some cases. I just want to be clear that I do not see that as the ultimate (and perhaps the only supported) strategy for the future.
Gene
-
- Posts: 74
- Joined: Tue Oct 23, 2018 3:35 pm
Re: Helix Contouring
The code is in PathScripts/PathDressupRampEntry.py and goes through several stages.
Firstly, PathScripts.PathGeom.wireForPath() converts the Path.Toolpath object (which is a sequence of nodes) into a Part.Wire (which is a sequence of edges).
This is a notable transformation, because having the endpoints of the component of the path available in the one object makes coding a lot easier than when you have to iterate back one step in a larger list to obtain a complete description of a motion. This cost hugely outweighs the cost of carrying what appears to be redundant information. (The problem is made worse by the inclusion of non-geometric commands in Toolpath, requiring you to have to iterate an arbitrary distance back to in search of the start-point information you want!)
Then we have ObjectDressup.generateHelix() which has two nested loops.
The outer loop reidentifies the rapid edges by checking them against the list of rapid edges -- to overcome the fact that the property of rapidness cannot be encoded into the wire's list of edges.
The inner loop scans forwards for matching edge point, using the PathGeom.pointsCoincide() function that depends on a hidden hard-coded Tolerance value (a trick that hides bugs and makes them utterly fiendish to fix in the long term), before calling outedges.extend(self.createHelix(rampedges, p0, p1))
This is followed by an extremely sneaky trick in line 259 of "if not PathGeom.isRoughly(p1.z, minZ): i=j" that causes the bottom layer to be duplicated without any ramping.
Everything depends on the createRampEdge() function, which has two constructive functions: Part.makeLine(startPoint, endPoint) and Part.Arc(startPoint, arcMid, endPoint).toShape().
There is no recognition here that an Arc in 3D is not the same as a Helix oriented in Z, even though they can both be defined by 3 points.
These objects are now converted back to Path.Commands (with the whole problem of self-contained edges vs sequential incomplete nodes) in cmdsForEdge()
In my case, these unaligned Arcs -- which should have been helixes -- get linearized into lots of little segments.
Firstly, PathScripts.PathGeom.wireForPath() converts the Path.Toolpath object (which is a sequence of nodes) into a Part.Wire (which is a sequence of edges).
This is a notable transformation, because having the endpoints of the component of the path available in the one object makes coding a lot easier than when you have to iterate back one step in a larger list to obtain a complete description of a motion. This cost hugely outweighs the cost of carrying what appears to be redundant information. (The problem is made worse by the inclusion of non-geometric commands in Toolpath, requiring you to have to iterate an arbitrary distance back to in search of the start-point information you want!)
Then we have ObjectDressup.generateHelix() which has two nested loops.
The outer loop reidentifies the rapid edges by checking them against the list of rapid edges -- to overcome the fact that the property of rapidness cannot be encoded into the wire's list of edges.
The inner loop scans forwards for matching edge point, using the PathGeom.pointsCoincide() function that depends on a hidden hard-coded Tolerance value (a trick that hides bugs and makes them utterly fiendish to fix in the long term), before calling outedges.extend(self.createHelix(rampedges, p0, p1))
This is followed by an extremely sneaky trick in line 259 of "if not PathGeom.isRoughly(p1.z, minZ): i=j" that causes the bottom layer to be duplicated without any ramping.
Everything depends on the createRampEdge() function, which has two constructive functions: Part.makeLine(startPoint, endPoint) and Part.Arc(startPoint, arcMid, endPoint).toShape().
There is no recognition here that an Arc in 3D is not the same as a Helix oriented in Z, even though they can both be defined by 3 points.
These objects are now converted back to Path.Commands (with the whole problem of self-contained edges vs sequential incomplete nodes) in cmdsForEdge()
In my case, these unaligned Arcs -- which should have been helixes -- get linearized into lots of little segments.
-
- Posts: 74
- Joined: Tue Oct 23, 2018 3:35 pm
Re: Helix Contouring
Okay, this is pretty bad.
I've got access to the Part.Edge records used in the ramping by inserting a couple of lines into Mod/Path/PathScripts/PathGeom.py:
and then using the Python console:
This confirms the ramp function has fit a 3D arc -- ie not aligned in the XY plane or with rad=5 -- between the three endpoints of what should be a helix. The "useHelixForBSpline" doesn't work, because those three endpoints make this non-aligned Arc in createRampEdge() instead of a BSpline -- which I guess used to happen, whereupon it was extremely hackily reinterpreted back into a helix by recalculating the circle from the endpoints after dropping the Z-components (using the convenience xy() function).
There is a lot of this kind-of recoverable information that can be sneakily lifted from geometric shapes to make functions work on what are otherwise inexplicit inputs. However, it very quickly results in extremely brittle code and fiendish bugs that fit the very definition of technical debt.
There are bugs where there's been a genuine mistake, which can be fixed because the design is sound. And then there are bugs where a badly thought out shortcut has been taken, and the code is irreparable. https://en.wikipedia.org/wiki/Technical_debt
Computational geometry (like that used in CADCAM software) is rife with opportunities for this sort of thing.
The cmdsForEdge() function then goes on to split this arc up into straight line segments based on a hard-coded parameter called segm=50.
This causes the path piece (the arc) to be subdivided into a number of pieces defined by the formula:
This calculation has no justification in terms of tolerance whatsoever. If I have an arc 50mm long with radius of 313mm then the deflection will be 1mm and so that segments=1, which means no subdivision and a line fitting tolerance to the curve of 1mm. I can get any number I like by choosing different arcs.
While this weird segm (aka Segmentation Factor) value is hard-coded in the ramp dressup, it is available to the the Path_Dressup: https://www.freecadweb.org/wiki/Path_DressupTag
Basically, things are far more complicated in this implementation than they need to be. The process of converting a Path to a Part.Wire of Part.Edges causes a lot more problems than it solves, and is a lot worse than simply considering the contour as a simple list of pairs or triples of Vectors representing line segments and arcs and working on it as that.
If I had my way, I'd ban the use of these Part objects from inner workings of every Path algorithm in the system as the cause of so much sorrow.
I've got access to the Part.Edge records used in the ramping by inserting a couple of lines into Mod/Path/PathScripts/PathGeom.py:
Code: Select all
Dedges = [ ]
def cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 500):
Dedges.append(edge)
....
Code: Select all
>>> from PathScripts import PathGeom
>>> e = PathGeom.Dedges[2]
>>> e.Curve
Circle (Radius : 5.17446, Position : (9.87664, 0.123365, 9.75), Direction : (0.140132, 0.140132, -0.980166))
There is a lot of this kind-of recoverable information that can be sneakily lifted from geometric shapes to make functions work on what are otherwise inexplicit inputs. However, it very quickly results in extremely brittle code and fiendish bugs that fit the very definition of technical debt.
There are bugs where there's been a genuine mistake, which can be fixed because the design is sound. And then there are bugs where a badly thought out shortcut has been taken, and the code is irreparable. https://en.wikipedia.org/wiki/Technical_debt
Computational geometry (like that used in CADCAM software) is rife with opportunities for this sort of thing.
The cmdsForEdge() function then goes on to split this arc up into straight line segments based on a hard-coded parameter called segm=50.
This causes the path piece (the arc) to be subdivided into a number of pieces defined by the formula:
Code: Select all
segments = int(math.ceil((deviation / eStraight.Length) * segm))
While this weird segm (aka Segmentation Factor) value is hard-coded in the ramp dressup, it is available to the the Path_Dressup: https://www.freecadweb.org/wiki/Path_DressupTag
Basically, things are far more complicated in this implementation than they need to be. The process of converting a Path to a Part.Wire of Part.Edges causes a lot more problems than it solves, and is a lot worse than simply considering the contour as a simple list of pairs or triples of Vectors representing line segments and arcs and working on it as that.
If I had my way, I'd ban the use of these Part objects from inner workings of every Path algorithm in the system as the cause of so much sorrow.
Re: Helix Contouring
Thank you Julian for your comments. I have written this code and I am sure this is the longest documentation anyone has ever written about it, including myself.
This was written originally for my own need of generating the ramp entries for cutting with tools which are not able to plunge straight down. The full helical "method" was added later after someone requested it here. But like you saw, it's not written by someone who knows too much about Path, 3D geometry or even programming to be honest. Your comments were very eye-opening. In fact, it took me quite a while to understand the difference between 3D arc and helix. It's scary that this thing has been in here for maybe two years, and no-one has noticed anything fishy about the generated paths, even I think this ramp dressup has seen quite a lot of use.
I agree with all of your points, especially the horribleness of the hard-coded values of tolerances and such. But that is the nightmare/beauty of open source software. I considered it good enough for my needs back then and many agreed, so there has been no need to push it better, but anyone who disagrees is able to do so. I'd love to see this get better, at least get that actual helix bug go away, but I am stretched both by time and competence I'm afraid..
This was written originally for my own need of generating the ramp entries for cutting with tools which are not able to plunge straight down. The full helical "method" was added later after someone requested it here. But like you saw, it's not written by someone who knows too much about Path, 3D geometry or even programming to be honest. Your comments were very eye-opening. In fact, it took me quite a while to understand the difference between 3D arc and helix. It's scary that this thing has been in here for maybe two years, and no-one has noticed anything fishy about the generated paths, even I think this ramp dressup has seen quite a lot of use.
I agree with all of your points, especially the horribleness of the hard-coded values of tolerances and such. But that is the nightmare/beauty of open source software. I considered it good enough for my needs back then and many agreed, so there has been no need to push it better, but anyone who disagrees is able to do so. I'd love to see this get better, at least get that actual helix bug go away, but I am stretched both by time and competence I'm afraid..
-
- Posts: 74
- Joined: Tue Oct 23, 2018 3:35 pm
Re: Helix Contouring
@roivai The code you have written is quite advanced. It was much more difficult to write than doing it a simpler way. It is best owned by the person who wrote it. If you want, we can see what we can do with it together.
I see two mistakes which has made it difficult.
The first mistake is using the PathGeom.wireForPath() function, which creates the list of Part.Edge objects. These objects don't help you. They are missing the israpid property -- causing quite a difficult work-around for you -- while not adding anything very useful.
Sure, you use the edge.Length, edge.splitEdgeAt() and edge.valueAt() functions, but there are only two simple types of geometry (line and arc) and it is easily implement these functions directly. Using the Part library is not worth it.
To keep it really simple, you could take the list of Path.Command objects as the input and instead use objects that are basically the pair [Vector, Path.Command], where the Vector is the previous point. This takes away the dependency on the gigantic solid modelling kernel library, and means we can streamline these calculations later on.
The second mistake is do everything at the same time in a single outer loop.
For example, at the start of generateRamps() you have "for edge in edges", and then deeper in you use a while-loop to look forwards for complete closed contours while creating ramps as you go along.
Don't nest the code like this. There is plenty of time and memory space. It is much better to do the processing in multiple passes in separate functions.
In my code, the function ConvertToContourHelix() begins:
Then for each layer i, the helix was applied like this:
Hopefully, the first block of code above would be common to every other 2D contour path dressup, because they all need to do the same topological identification before they apply the dressup function on each layer.
If I was doing ramps, rather than helixes, I would add another pass (function) choose the split points where the ramp enters and exits.
It would then not be hard to add the further feature to allow the ramp to advance around the contour with each layer. If each of these features is completely separated out, not mixed in with a complicated nest of a nest of a loop, then they don't add to the complexity.
When you do everything in one loop, each feature doubles the complexity. If you do it in separate passes, then you can add features without adding complexity.
My code above would be even simpler if ContourLayerIndexes() returned complete copies of lists of Path.Command objects, rather than a list of indexes into the starting list, because then each pass or step would be more isolated from anything it doesn't need to know. We should do it that way if we rewrite it.
So, the things we might get from doing something along these lines are:
(1) fix a helixing bug that is hardly visible,
(2) potentially reimplement/combine some of the other dressups (eg tag dressup),
(3) add new features, like ramp advancing, global reordering,
(4) make the path helix object redundant.
How would we decide how to do this? It mostly comes down to hacking a particular file in the freecad-build directory. I don't think I've understood the workflow where code is edited in the freecad-code repository and I need to execute the makefile for every iteration.
I've done most of my experimentation in Jupyter Notebooks with code like this: https://github.com/goatchurchprime/tran ... nest.ipynb
I see two mistakes which has made it difficult.
The first mistake is using the PathGeom.wireForPath() function, which creates the list of Part.Edge objects. These objects don't help you. They are missing the israpid property -- causing quite a difficult work-around for you -- while not adding anything very useful.
Sure, you use the edge.Length, edge.splitEdgeAt() and edge.valueAt() functions, but there are only two simple types of geometry (line and arc) and it is easily implement these functions directly. Using the Part library is not worth it.
To keep it really simple, you could take the list of Path.Command objects as the input and instead use objects that are basically the pair [Vector, Path.Command], where the Vector is the previous point. This takes away the dependency on the gigantic solid modelling kernel library, and means we can streamline these calculations later on.
The second mistake is do everything at the same time in a single outer loop.
For example, at the start of generateRamps() you have "for edge in edges", and then deeper in you use a while-loop to look forwards for complete closed contours while creating ramps as you go along.
Don't nest the code like this. There is plenty of time and memory space. It is much better to do the processing in multiple passes in separate functions.
In my code, the function ConvertToContourHelix() begins:
Code: Select all
cmds = list(pp.Commands) # extract the Path.Command objects
zlevelis = ContourLayerIndexes(cmds) # list of indexes at start of each layer
zlevelzs = [ cmds[zlevelis[i]].Parameters["Z"] for i in range(len(zlevelis)-1) ]
layerstartpt = ContourLayerStartpoint(cmds[:zlevelis[0]]) # start point of 0th layer (should check same for each other layer)
layer0 = cmds[zlevelis[0]:zlevelis[1]]
cumlenlayer0 = cmdcumlen(layer0) # cumulative length of each segment of 0th layer
Code: Select all
zi, zn = zlevelzs[i], zlevelzs[i+1]
ll = zlevelis[i+1] - zlevelis[i]
assert zlevelis[i+1] - zlevelis[i] == len(cumlenlayer0)
for j in range(len(cumlenlayer0)):
lam = cumlenlayer0[j]/cumlenlayer0[-1]
cmds[zlevelis[i]+j].__setattr__("Z", zi*(1-lam) + zn*lam)
If I was doing ramps, rather than helixes, I would add another pass (function) choose the split points where the ramp enters and exits.
It would then not be hard to add the further feature to allow the ramp to advance around the contour with each layer. If each of these features is completely separated out, not mixed in with a complicated nest of a nest of a loop, then they don't add to the complexity.
When you do everything in one loop, each feature doubles the complexity. If you do it in separate passes, then you can add features without adding complexity.
My code above would be even simpler if ContourLayerIndexes() returned complete copies of lists of Path.Command objects, rather than a list of indexes into the starting list, because then each pass or step would be more isolated from anything it doesn't need to know. We should do it that way if we rewrite it.
So, the things we might get from doing something along these lines are:
(1) fix a helixing bug that is hardly visible,
(2) potentially reimplement/combine some of the other dressups (eg tag dressup),
(3) add new features, like ramp advancing, global reordering,
(4) make the path helix object redundant.
How would we decide how to do this? It mostly comes down to hacking a particular file in the freecad-build directory. I don't think I've understood the workflow where code is edited in the freecad-code repository and I need to execute the makefile for every iteration.
I've done most of my experimentation in Jupyter Notebooks with code like this: https://github.com/goatchurchprime/tran ... nest.ipynb