SolidCode / SolidPython

A python frontend for solid modelling that compiles to OpenSCAD
1.1k stars 172 forks source link

extrude_along_path with scaling crashes: AttributeError: 'numpy.ndarray' object has no attribute 'x' #151

Closed tpchuckles closed 3 years ago

tpchuckles commented 4 years ago

consider the creation of path, profile, and scaling lists sensibly and quickly via numpy:

thetas=np.linspace(0,np.pi,N) path=list(zip(3*np.sin(thetas),3*np.cos(thetas),thetas)) profile=list(zip(np.sin(thetas),np.cos(thetas))) scalepts=list(np.linspace(1,.1,N))

vs building them element-by-element, without numpy:

path=[] ; profile=[] ; scalepts=[] for i in range(N): t=3.14/N*i path.append(Point3(3*math.sin(t),3*math.cos(t),t)) profile.append(Point3(math.sin(t),math.cos(t),0)) scalepts.append((N-i)/N)

the latter works, while the former yields error: "AttributeError: 'numpy.ndarray' object has no attribute 'x'", which is especially confusing since i carefully converted my numpy arrays into lists! even when faced with the error, the user has no idea what's wrong (printing these lists shows nothing abnormal; python prints floats and numpy floats identically)

I would think the former example (minus the need to cast them into lists) would be preferred stylistically, especially if the latter case were to be broken into 3 for loops depending on the situation. the former will certainly be faster if these lists are large.

etjones commented 4 years ago

I’m away from a computer right now, but I think this is an issue with expecting the profile path to be a list of tuples, lists, or euclid3.Point3s, but not expecting the np.ndarrays that your code uses. It’s reasonable and increasingly common to use numpy to create geometry, so I’ll take a look at adding this capacity. For the moment, i suspect (but haven’t verified) that the line:

profile = list(zip(map(math.sin, thetas), map(math.cos, thetas))) 

wouldn’t show the same error.

There’s one function, solid.utils.euclidify() that would would make your original (quite graceful, btw) code work if updated. I’ll see what would be required to make that fix.

On Jul 17, 2020, at 10:05 PM, tpchuckles notifications@github.com wrote:

 consider the following basic attempt at modelling a sheep's horn:

`from solid import #SolidPython https://github.com/SolidCode/SolidPython from solid.utils import import numpy as np

output="horn-SolidPython.scad"

thetas=np.linspace(0,np.pi,50)

path=list(zip(3np.sin(thetas),3np.cos(thetas),thetas))

profile=list(zip(np.sin(thetas),np.cos(thetas)))

scalepts=list(np.linspace(1.,.5,50))

print(scalepts)

extruded=extrude_along_path(shape_pts=profile,path_pts=path,scale_factors=scalepts)

openscadCode="$fn=100;" openscadCode=openscadCode+scad_render(extruded) outFile=open(output,"w") outFile.write(openscadCode) outFile.close()`

run this (using python3 at least) and you'll encounter the following error:

Traceback (most recent call last): File "horn.py", line 17, in extruded=extrude_along_path(shape_pts=profile,path_pts=path,scale_factors=scalepts) File "/home/athena/.local/lib/python3.8/site-packages/solid/utils.py", line 1143, in extrude_along_path this_loop = [Point3(v.x, v.y, v.z) for v in this_loop] File "/home/athena/.local/lib/python3.8/site-packages/solid/utils.py", line 1143, in this_loop = [Point3(v.x, v.y, v.z) for v in this_loop] AttributeError: 'numpy.ndarray' object has no attribute 'x'

it seems like the np objects we've created which we then attempt to scale dont have these x,y,zs:

if scale != 1.0: this_loop = [(scale * sh) for sh in shape_pts] # Convert this_loop back to points; scaling changes them to Vectors this_loop = [Point3(v.x, v.y, v.z) for v in this_loop]

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.

tpchuckles commented 4 years ago

you're correct that the first code incorrectly presents tuples rather than euclid3.Point3 objects, but even when that is corrected (for i in range(N): profile[i]=Point3(profile[i][0],profile[i][1],profile[i][2])...etc, or via euclidify (thanks for that, that's awesome)), it still fails.

they key to making it stop failing seemed to be in the building of scalepts list element-by-element. (I originally had posted a much different issue here, and since edited it, since this suggested the root of the issue was the numpy floats within the scalepts list), where the list elements are floats rather than numpy floats.

this code:

`1: path=euclidify(list(zip(3np.sin(thetas),3np.cos(thetas),thetas))) 2: profile=euclidify(list(zip(np.sin(thetas),np.cos(thetas)))) 3a: scalepts=list(np.linspace(1,.1,N))

3b: scalepts=list(map(float,np.linspace(1.,.1,N))) `

using one or the other of 3a/3b, this fails using line 3a, and works using line 3b

edit: as far as making extrude_along_path accept numpy arrays, it should be as "simple" as wrapping the args as follows: path=euclidify(list(map(list,list(path)))) ; profile=euclidify(list(map(list,list(profile)))) ; scalepts=list(map(float,scalepts)) (to convert a 2D numpy matrix to a 2D pure python list, you first need to cast each element of the outer level (map) and then cast the outer level itself, and then pass this into euclidify (which itself doesn't appear to accept numpy matrices. that might be a better place to make this change though))

etjones commented 4 years ago

Thanks, this is great info. I'll see what I can figure out

etjones commented 3 years ago

Sorry for delay on this (I just had a baby! Little busy this week.) Looks like this is another numpy instance. Numpy has some different multiplication rules defined than standard python. Multiplying a standard python numeric by a euclid3.Vector3 | euclid3.Point3 yields a Vector3. Multiplying a Numpy float64 by that same Vector3 yields a 3-tuple instead, which caused the problem you found.

I'll push a fix for this behavior shortly, and then I'll see if I can add some more general behavior to solid.utils.euclidify that will play more nicely with Numpy.

etjones commented 3 years ago

Pushed and released as V1.0.1

etjones commented 3 years ago

Also worth noting: although you could call euclidify() on all the numpy lines like path=list(zip(3*np.sin(thetas),3*np.cos(thetas),thetas)), you don't have to. Once the issue with scalepts was fixed, this just works.