-
Notifications
You must be signed in to change notification settings - Fork 306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Assembly support #440
Assembly support #440
Conversation
Codecov Report
@@ Coverage Diff @@
## master #440 +/- ##
==========================================
+ Coverage 93.61% 93.87% +0.25%
==========================================
Files 25 30 +5
Lines 5326 6984 +1658
Branches 554 807 +253
==========================================
+ Hits 4986 6556 +1570
- Misses 215 259 +44
- Partials 125 169 +44
Continue to review full report at Codecov.
|
Tagging @bernhard-42 on this since jupyter-cadquery supports lightweight assemblies, and it may be useful to compare notes. |
When I look at the above examples, assy3 = cq.Assembly(b3, loc=(0, 0, 5), name="THIRD", color='orange') and let the |
This is how I would visualize the example in jupyter-cadquery: From how it is built I would have seen a difference between SECOND/THIRD and 4th: SECOND and THIRD are assemblies itself, while 4th is a shape. I would have expected 4th to be on the same level as the shapes of TOP. assy = cq.Assembly(b1, loc=L(1, 1, 0), name="TOP")
assy2 = cq.Assembly(b2, loc=L(0, -3, 4), name="SECOND")
assy3 = cq.Assembly(b3, loc=L(0, 0, 5), name="THIRD", color=C('orange'))
assy.add(assy2, color=C("green"))
assy.add(assy3)
assy.add(b4, loc=L(0, 0, 5), name="4th",color=C("blue1")) assy = cq.Assembly(b1, loc=L(1, 1, 0), name="TOP")
assy2 = cq.Assembly(b2, loc=L(0, -3, 4), name="SECOND")
assy3 = cq.Assembly(b3, loc=L(0, 0, 5), name="THIRD", color=C('orange'))
assy4 = cq.Assembly(b4, loc=L(0, 0, 5), name="4th",color=C("blue1"))
assy.add(assy2, color=C("green"))
assy.add(assy3)
assy.add(assy4) lead to identical results, which I found a little bit confusing (btw. |
On a completely different topic, I think I have now stared for two hours into the one example above, commenting out constraints and visualising constraints and results - and I must admit, I don't understand it. Constraint based assembly is totally new to me. Is there any documentation around this? |
btw. converting from cadquery assemblies to jupyter-cadquery assemblies is straightforward (however, one needs to use the Assembly and Part classes from the occ version of juypter-cadquery - need to check how to properly integrate later) from jupyter_cadquery.occ import Part, Assembly
def rgb(assy):
def b(x):
return int(255*x)
if assy.color is None:
return "#aaa"
rgb = assy.color.wrapped.GetRGB()
return "#%02x%02x%02x" % (b(rgb.Red()), b(rgb.Green()), b(rgb.Blue()))
def convert(assy, loc=None):
loc = assy.loc if loc is None else loc * assy.loc
color = rgb(assy)
parent = [Part(shape.located(loc).wrapped, "object_%d" % (i), color=color) for i, shape in enumerate(assy.shapes)]
children = [convert(c, loc) for c in assy.children]
return Assembly(parent + children, assy.name) @adam-urbanczyk Does the convert function look right for you? |
Thanks @bernhard-42 , looks cool! You are right regarding the product of locations. The semantics of the Assembly class is essentially contained in this snippet: class Assembly(object):
loc: Location
name: str
color: Optional[Color]
metadata: Dict[str, Any]
obj: Union[Shape, Workplane, None]
parent: Optional["Assembly"]
children: List["Assembly"] So every child of an Regarding constraints, this is actually a standard feature of CAD software. I have not seen it yet in scripted CAD packages (not that there are so many). For now it is highly experimental, may break and change at any time kind of thing. So no docs at the moment. Convert looks good to me. |
Thanks, Adam |
* numerical jacobian by hand taking into account the sparsity pattern * better code structure * scaling of the dir constraint
I did my homework about constraint based assemblies looking at onshape and the Assembly3 FreeCad plugin. def ring(inner_radius, outer_radius, width):
ring = (cq.Workplane(origin=(0, 0, -width / 2))
.circle(outer_radius).circle(inner_radius)
.extrude(width)
)
return ring
tol = 0.05
ball_diam = 5
r1, r2, r3, r4 = 4, 6, 8, 10
r5 = (r3 + r2) / 2
inner_ring = ring(r1, r2, ball_diam)
outer_ring = ring(r3, r4, ball_diam)
torus = cq.CQ(cq.Solid.makeTorus(r5, ball_diam / 2 + tol))
ball = cq.Workplane().sphere(ball_diam / 2)
inner = inner_ring.cut(torus)
outer = outer_ring.cut(torus) For the issue I had with this, see #458 After having done all the steps below, I am not sure this is the best example for the assembly feature. However, you asked for some feedback on the API and so I thought to share some of my experiences:
Finally, I don't know whether the below approach using helper objects in assemblies is appropriate, but at least it worked. And I am happy to learn, how to avoid them if possible. The Bearing assembly For the way I wanted to create the constraints, I needed a way to create a constraint for the center of a shape and to def center(assy, name):
return cq.Vertex.makeVertex(*assy.objects[name].obj.val().Center().toTuple())
def query_all(assy, q):
name, kind, arg = q.split("@")
tmp = cq.Workplane()
obj = assy.objects[name].obj
if isinstance(obj, (cq.Workplane, cq.Shape)):
tmp.add(obj)
res = getattr(tmp, kind)(arg)
return res.objects if isinstance(res.val(), cq.Shape) else None Given this, I could create the assembly and add the constraints by additionally adding two construction objects:
def balls(i):
return "ball_%d" % i
number_balls = 5
# components
assy = cq.Assembly(outer, loc=L(0, 0, ball_diam/2), name="outer", color=C("orange"))
assy.add(inner, loc=L(20, 0, 0), name="inner", color=C("orange"))
for i in range(number_balls):
assy.add(ball, loc=L(6*i, 20, 0), name=balls(i), color=C("black"))
# helpers
assy.add(cq.Workplane().circle(1).extrude(1), loc=L(0,-20,0), name="_center")
assy.add(cq.Workplane().polygon(number_balls, 2*r5).extrude(ball_diam/2), loc=L(20,-20,0), name="_points")
# contraints
points = query_all(assy, "_points@vertices@>Z")
cs = [
("outer@faces@<Z", "_center@faces@>Z", "Plane"),
("inner@faces@<Z", "_center@faces@>Z", "Plane"),
("_points@faces@<Z", "_center@faces@>Z", "Plane"),
] + [("_points", points[i],
balls(i), center(assy, balls(i)),
"Point")
for i in range(number_balls)]
for c in cs:
assy.constrain(*c) This is the assembly before solving: And this is the result after solving: However, when I added a 6th ball, the optimzation algorithm did not finish after after 1000 iterations. So I increased the max iterations to 10000. It finally converged
but the result was not exact: |
Even for 5 balls, the result is not exact: The assembled center coordinates (after solving): loc = assy.loc
for i in range(number_balls):
o = assy.objects[balls(i)]
v = o.shapes[0].located(loc * o.loc).Center()
print("%7.4f, %7.4f, %7.4f" % (v.x, v.y, v.z))
# 6.9230, -0.9758, 2.5239
# 3.0724, 6.2966, 2.4826
# -5.0340, 4.8817, 2.4654
# -6.1935, -3.2651, 2.4959
# 1.1963, -6.8853, 2.5321 The coordinates of the polygon vertices (also after solving) ["%7.4f, %7.4f, %7.4f" % (v.X, v.Y, v.Z) for v in query_all(assy, "_points@vertices@>Z")]
# [' 7.0000, 0.0000, 2.5000',
# ' 2.1631, 6.6574, 2.5000',
# '-5.6631, 4.1145, 2.5000',
# '-5.6631, -4.1145, 2.5000',
# ' 2.1631, -6.6574, 2.5000'] I had expected that the "Point" constraint ensures that the points are colocated. |
Thanks for the suggestions regarding the API. I think we can add those in future iterations of this functionality. I changed the cost function slightly @bernhard-42 , seems to give better result now. Pleas pull and try. In general I use a numerical solver here so nothing is "exact". BTW: please share complete code examples so that I can reproduce next time, it took me a while to glue the snippets together. |
@adam-urbanczyk The results are much better now. The issue before the change was that the solver did not align the normals well enough. To compare I created a little metrics function. For the 6 ball bearing I get
with Before All code including the metrics function can be found in https://github.com/bernhard-42/jupyter-cadquery/blob/master/examples/cadquery-assemblies.ipynb |
Thanks @bernhard-42 . FYI I implemented parameter handling for |
Still need to add the pic, but I don't want touch the code. I think yo ucan start reviewing @jmwright . |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adam-urbanczyk I think this looks good. The warning about the ftol parameter is the only thing I see that might be introducing a bug or unexpected behavior. All the rest are minor things.
OK this is it, I think. You can merge @jmwright if everything looks good. Thanks to all for feedback and testing, thanks to @marcus7070 for the cool spindle model and to @bernhard-42 for the bearing assy that allowed me to improve the solver. |
Thanks for all the work you've put into this @adam-urbanczyk ! |
Congrats on this really really nice feature |
This PR will eventually resolve #276, #20
What's left
Exact jacobian, finite diff jacobian by hand