MAD Cycloidal Actuator

New Method (Updated 2023-09-30)

# generate_cycloidal_profile.py
import numpy as np
import matplotlib.pyplot as plt
import ezdxf


def generateCycloidalGearProfile(r_housing=36, r_roller=4, N=16, eccentricity=2):
    theta = np.arange(0, 2*np.pi, 0.001)

    psi = - np.arctan(np.sin((1 - N) * theta) / (r_housing / (eccentricity * N) - np.cos((1 - N)*theta)))

    x = r_housing * np.cos(theta) - r_roller * np.cos(theta - psi) - eccentricity * np.cos(N * theta)
    y = -r_housing * np.sin(theta) + r_roller * np.sin(theta - psi) + eccentricity * np.sin(N * theta)

    return np.concatenate(([x], [y]), axis=0)


def writeDXF(filename, fit_points, ref_circle_radius=10):
    doc = ezdxf.new("R2010") # create a new DXF drawing in R2010 fromat 
    msp = doc.modelspace()

    fit_points = fit_points.T.tolist()

    start_point = fit_points[0]

    # close the loop
    fit_points.append(start_point)

    spline = msp.add_spline(fit_points)

    spline.set_closed([(start_point[0], start_point[1], 0)])

    print(spline.dxf.n_fit_points)

    circle = msp.add_circle((0, 0), radius=ref_circle_radius)
    
    doc.saveas(filename)

# 5010

fit_points = generateCycloidalGearProfile(
    r_housing=29.,
    r_roller=2.5,
    N=16.,
    eccentricity=1.25
    )
'''
#fit_points_ = generateCycloidalGearProfile(
#    r_housing=29.,
#    r_roller=3.,
#    N=16.,
#    eccentricity=1.25
#    )

'''
# M6C12
'''
fit_points = generateCycloidalGearProfile(
    r_housing=36,
    r_roller=4,
    N=16,
    eccentricity=2
    )
'''
print(fit_points.shape)

x, y = fit_points
#x_, y_ = fit_points_
plt.plot(x, y)
#plt.plot(x_, y_)
plt.show()

writeDXF("cycloidal_profile.dxf", fit_points, ref_circle_radius=31)

RI-60 Parameters

Housing Radius

29

mm

Housing Roller Diameter

5

mm

Central Hole Diameter

18

mm

Side Hole Diameter

13

mm

Eccentricity

1.25

mm

Spacing Radius of Side Hole

17

mm

RI-70 Parameters

Housing Radius

36

mm

Housing Roller Diameter

8

mm

Central Hole Diameter

18

mm

Side Hole Diameter

13

mm

Eccentricity

2

mm

Spacing Radius of Side Hole

20

mm

Old Method

Go in an active sketch plane

Run the following script:


import math

import adsk.core, adsk.fusion, traceback


def generateCycloidalOutline(n_planet_teeth, n_housing_teeth, e, pin_to_center_distance):
    intersectsItself = False
    lastMaxAngle = -math.pi / 2
    
    origin = adsk.core.Point3D.create(0, 0, 0)
    halfAngle = 2 * math.pi / n_planet_teeth / 2
    N = 101

    points = adsk.core.ObjectCollection.create()

    for ip in range(N + 1):
        angle = (2 * math.pi / n_planet_teeth) / N * ip
        x = pin_to_center_distance * math.cos(angle) + e * math.cos(n_housing_teeth * angle)# - e
        y = pin_to_center_distance * math.sin(angle) + e * math.sin(n_housing_teeth * angle)

        p = adsk.core.Point3D.create(x, y, 0)
        points.add(p)

        #currentAngle = Angle.getToPoint(origin, p)
        currentAngle = math.atan2(p.y - origin.y, p.x - origin.x)

        # if not intersectsItself and currentAngle < lastMaxAngle:
        #     intersectsItself = True

        # if intersectsItself and currentAngle <= halfAngle:
        #     break

        lastMaxAngle = currentAngle

    # if intersectsItself:
    #     upperPoints = [adsk.core.Point3D.create(p.x, -p.y, 0) for p in points]
    #     upperPoints = [getRotated(p, Matrix.getForRotation(2 * halfAngle, origin)) for p in upperPoints]
    #     return [points, upperPoints]

    return points


def run(context):
    '''
    ## Etch M6C12 params
    reduction_ratio = 15
    eccentricity = 2
    pin_diameter = 8
    housing_inner_diameter = 72
    '''
    
    ## Etch 5010 params
    reduction_ratio = 15
    eccentricity = 1.5
    pin_diameter = 5
    housing_inner_diameter = 56




    n_planet_teeth = reduction_ratio
    n_housing_teeth = reduction_ratio + 1

    
    eccentricity *= 0.1
    pin_diameter *= 0.1
    housing_inner_diameter *= 0.1

    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        design = adsk.fusion.Design.cast(app.activeProduct)
        if not design:
            ui.messageBox('No active Fusion 360 design', 'No Design')
            return
        rootComp = design.rootComponent
        
        sketch = app.activeEditObject
        

        origin_point = adsk.core.Point3D.create(0, 0, 0)

        points = generateCycloidalOutline(
            n_planet_teeth=n_planet_teeth,
            n_housing_teeth=n_housing_teeth,
            e=eccentricity,
            pin_to_center_distance=housing_inner_diameter/2
            )

        cycloidSpline = sketch.sketchCurves.sketchFittedSplines.add(points)
        
        
        cycloidSplinecoll = adsk.core.ObjectCollection.create()
        
        cycloidSplinecoll.add(cycloidSpline)


        offsetSpline = sketch.offset(cycloidSplinecoll, adsk.core.Point3D.create(), pin_diameter / 2).item(0)
        cycloidSpline.deleteMe()
        
        #endSplinePoint = getRotated(offsetSpline.startSketchPoint.geometry, getForRotation(math.tau / n_planet_teeth, origin_point))
        p = offsetSpline.startSketchPoint.geometry.copy()

        mat = adsk.core.Matrix3D.create()

        mat.setToRotation(math.tau / n_planet_teeth, adsk.core.Vector3D.create(0, 0, 0.5), origin_point)

        p.transformBy(mat)

        endSplinePoint = p
        circularFeatures = [offsetSpline]

        # If profile will be probably open, make more accurate segment
        if not endSplinePoint.isEqualToByTolerance(offsetSpline.endSketchPoint.geometry, 1e-8):
            #line = Sketch.Draw.lines(sketch, [endSplinePoint, offsetSpline.endSketchPoint.geometry])[0]
            points = [endSplinePoint, offsetSpline.endSketchPoint.geometry]
            asConstruction = False
            close = False
            lines = []

            for ip in range(1, len(points)):
                line = sketch.sketchCurves.sketchLines.addByTwoPoints(points[ip - 1], points[ip])
                line.isConstruction = asConstruction
                lines.append(line)

            if close:
                line = sketch.sketchCurves.sketchLines.addByTwoPoints(points[len(points) - 1], points[0])
                line.isConstruction = asConstruction
                lines.append(line)

            circularFeatures.append(lines[0])
            

        #Sketch.Pattern.circular(sketch, circularFeatures, n_planet_teeth, origin=origin_point)   
        sketchObjectOrObjects = circularFeatures
        quantity = n_planet_teeth
        angle = 2 * math.pi
        origin = origin_point

        objects = adsk.core.ObjectCollection.create()
        for o in sketchObjectOrObjects:
            objects.add(o)

        #objects = getCollection(sketchObjectOrObjects)

        for step in range(quantity - 1):
            stepAngle = angle / quantity * (step + 1)
            
            transformMatrix = adsk.core.Matrix3D.create()

            transformMatrix.setToRotation(stepAngle, adsk.core.Vector3D.create(0, 0, 0.5), origin)
            sketch.copy(objects, transformMatrix, sketch)

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

Then we get the cycloidal gear profile

Pins and housing
════════════════
  rr     28.0000 mm
  dr      5.0000 mm
  zr          16

Planets(s)
══════════
  dc      8.0000 mm
  ds     13.0000 mm
   e      1.5000 mm
  df     48.0000 mm
  da     54.0000 mm
  tp      3.5000 mm
   N           2
  ap      0.5000 mm

Input
═════
  dc      8.0000 mm
   e      1.5000 mm
  tp      3.5000 mm
   N           2
  ap      0.5000 mm

Output
══════
  rb     16.0000 mm
  db     10.0000 mm
  zs           6
  to      1.0000 mm
  ao      0.5000 mm

To adapt to the motor we are using, we need to edit the model a bit.

Last updated