-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathinvolute_gear.py
262 lines (218 loc) · 12 KB
/
involute_gear.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import numpy as np
from math import *
from mathutils import *
import sys
from svgwrite.path import Path
from svgwrite import mm, Drawing
import ezdxf
class DimensionException(Exception):
pass
class InvoluteGear:
def __init__(self, module=1, teeth=30, pressure_angle_deg=20, fillet=0, backlash=0,
max_steps=100, arc_step_size=0.1, reduction_tolerance_deg=0, dedendum_factor=1.157, addendum_factor=1.0, ring=False):
'''
Construct an involute gear, ready for generation using one of the generation methods.
:param module: The 'module' of the gear. (Diameter / Teeth)
:param teeth: How many teeth in the desired gear.
:param pressure_angle_deg: The pressure angle of the gear in DEGREES.
:param fillet: The radius of the fillet connecting a tooth to the root circle. NOT WORKING in ring gear.
:param backlash: The circumfrential play between teeth, if meshed with another gear of the same backlash held stationary
:param max_steps: Maximum steps allowed to generate the involute profile. Higher is more accurate.
:param arc_step_size: The step size used for generating arcs.
:param ring: True if this is a ring (internal) gear, otherwise False.
'''
pressure_angle = radians(pressure_angle_deg)
self.reduction_tolerance = radians(reduction_tolerance_deg)
self.module = module
self.teeth = teeth
self.pressure_angle = pressure_angle
# Addendum is the height above the pitch circle that the tooth extends to
self.addendum = addendum_factor * module
# Dedendum is the depth below the pitch circle the root extends to. 1.157 is a std value allowing for clearance.
self.dedendum = dedendum_factor * module
# If the gear is a ring gear, then the clearance needs to be on the other side
if ring:
temp = self.addendum
self.addendum = self.dedendum
self.dedendum = temp
# The radius of the pitch circle
self.pitch_radius = (module * teeth) / 2
# The radius of the base circle, used to generate the involute curve
self.base_radius = cos(pressure_angle) * self.pitch_radius
# The radius of the gear's extremities
self.outer_radius = self.pitch_radius + self.addendum
# The radius of the gaps between the teeth
self.root_radius = self.pitch_radius - self.dedendum
# The radius of the fillet circle connecting the tooth to the root circle
self.fillet_radius = fillet if not ring else 0
# The angular width of a tooth and a gap. 360 degrees divided by the number of teeth
self.theta_tooth_and_gap = pi * 2 / teeth
# Converting the circumfrential backlash into an angle
angular_backlash = (backlash / 2 / self.pitch_radius)
# The angular width of the tooth at the pitch circle minus backlash, not taking the involute into account
self.theta_tooth = self.theta_tooth_and_gap / 2 + (-angular_backlash if not ring else angular_backlash)
# Where the involute profile intersects the pitch circle, found on iteration.
self.theta_pitch_intersect = None
# The angular width of the full tooth, at the root circle
self.theta_full_tooth = None
self.max_steps = max_steps
self.arc_step_size = arc_step_size
'''
Reduces a line of many points to less points depending on the allowed angle tolerance
'''
def reduce_polyline(self, polyline):
vertices = [[],[]]
last_vertex = [polyline[0][0], polyline[1][0]]
# Look through all vertices except start and end vertex
# Calculate by how much the lines before and after the vertex
# deviate from a straight path.
# If the deviation angle exceeds the specification, store it
for vertex_idx in range(1, len(polyline[0])-1):
next_slope = np.arctan2( polyline[1][vertex_idx+1] - polyline[1][vertex_idx+0],
polyline[0][vertex_idx+1] - polyline[0][vertex_idx+0] )
prev_slope = np.arctan2( polyline[1][vertex_idx-0] - last_vertex[1],
polyline[0][vertex_idx-0] - last_vertex[0] )
deviation_angle = abs(prev_slope - next_slope)
if (deviation_angle > self.reduction_tolerance):
vertices[0] += [polyline[0][vertex_idx]]
vertices[1] += [polyline[1][vertex_idx]]
last_vertex = [polyline[0][vertex_idx], polyline[1][vertex_idx]]
# Return vertices along with first and last point of the original polyline
return np.array([
np.concatenate([ [polyline[0][0]], vertices[0], [polyline[0][-1]] ]),
np.concatenate([ [polyline[1][0]], vertices[1], [polyline[1][-1]] ])
])
def generate_half_tooth(self):
'''
Generate half an involute profile, ready to be mirrored in order to create one symmetrical involute tooth
:return: A numpy array, of the format [[x1, x2, ... , xn], [y1, y2, ... , yn]]
'''
# Theta is the angle around the circle, however PHI is simply a parameter for iteratively building the involute
phis = np.linspace(0, pi, self.max_steps)
points = []
reached_limit = False
self.theta_pitch_intersect = None
for phi in phis:
x = (self.base_radius * cos(phi)) + (phi * self.base_radius * sin(phi))
y = (self.base_radius * sin(phi)) - (phi * self.base_radius * cos(phi))
point = (x, y)
dist, theta = cart_to_polar(point)
if self.theta_pitch_intersect is None and dist >= self.pitch_radius:
self.theta_pitch_intersect = theta
self.theta_full_tooth = self.theta_pitch_intersect * 2 + self.theta_tooth
elif self.theta_pitch_intersect is not None and theta >= self.theta_full_tooth / 2:
reached_limit = True
break
if dist >= self.outer_radius:
points.append(polar_to_cart((self.outer_radius, theta)))
elif dist <= self.root_radius:
points.append(polar_to_cart((self.root_radius, theta)))
else:
points.append((x,y))
if not reached_limit:
raise Exception("Couldn't complete tooth profile.")
return np.transpose(points)
def generate_half_root(self):
'''
Generate half of the gap between teeth, for the first tooth
:return: A numpy array, of the format [[x1, x2, ... , xn], [y1, y2, ... , yn]]
'''
root_arc_length = (self.theta_tooth_and_gap - self.theta_full_tooth) * self.root_radius
points_root = []
for theta in np.arange(self.theta_full_tooth, self.theta_tooth_and_gap/2 + self.theta_full_tooth/2, self.arc_step_size / self.root_radius):
# The current circumfrential position we are in the root arc, starting from 0
arc_position = (theta - self.theta_full_tooth) * self.root_radius
# If we are in the extemities of the root arc (defined by fillet_radius), then we are in a fillet
in_fillet = min((root_arc_length - arc_position), arc_position) < self.fillet_radius
r = self.root_radius
if in_fillet:
# Add a circular profile onto the normal root radius to form the fillet.
# High near the edges, small towards the centre
# The min() function handles the situation where the fillet size is massive and overlaps itself
circle_pos = min(arc_position, (root_arc_length - arc_position))
r = r + (self.fillet_radius - sqrt(pow(self.fillet_radius, 2) - pow(self.fillet_radius - circle_pos, 2)))
points_root.append(polar_to_cart((r, theta)))
return np.transpose(points_root)
def generate_roots(self):
'''
Generate both roots on either side of the first tooth
:return: A numpy array, of the format [ [[x01, x02, ... , x0n], [y01, y02, ... , y0n]], [[x11, x12, ... , x1n], [y11, y12, ... , y1n]] ]
'''
self.half_root = self.generate_half_root()
self.half_root = np.dot(rotation_matrix(-self.theta_full_tooth / 2), self.half_root)
points_second_half = np.dot(flip_matrix(False, True), self.half_root)
points_second_half = np.flip(points_second_half, 1)
self.roots = [points_second_half, self.half_root]
# Generate a second set of point-reduced root
self.half_root_reduced = self.reduce_polyline(self.half_root)
points_second_half = np.dot(flip_matrix(False, True), self.half_root_reduced)
points_second_half = np.flip(points_second_half, 1)
self.roots_reduced = [points_second_half, self.half_root_reduced]
return self.roots_reduced
def generate_tooth(self):
'''
Generate only one involute tooth, without an accompanying tooth gap
:return: A numpy array, of the format [[x1, x2, ... , xn], [y1, y2, ... , yn]]
'''
self.half_tooth = self.generate_half_tooth()
self.half_tooth = np.dot(rotation_matrix(-self.theta_full_tooth / 2), self.half_tooth)
points_second_half = np.dot(flip_matrix(False, True), self.half_tooth)
points_second_half = np.flip(points_second_half, 1)
self.tooth = np.concatenate((self.half_tooth, points_second_half), axis=1)
# Generate a second set of point-reduced teeth
self.half_tooth_reduced = self.reduce_polyline(self.half_tooth)
points_second_half = np.dot(flip_matrix(False, True), self.half_tooth_reduced)
points_second_half = np.flip(points_second_half, 1)
self.tooth_reduced = np.concatenate((self.half_tooth_reduced, points_second_half), axis=1)
return self.tooth_reduced
def generate_tooth_and_gap(self):
'''
Generate only one tooth and one root profile, ready to be duplicated by rotating around the gear center
:return: A numpy array, of the format [[x1, x2, ... , xn], [y1, y2, ... , yn]]
'''
points_tooth = self.generate_tooth()
points_roots = self.generate_roots()
self.tooth_and_gap = np.concatenate((points_roots[0], points_tooth, points_roots[1]), axis=1)
return self.tooth_and_gap
def generate_gear(self):
'''
Generate the gear profile, and return a sequence of co-ordinates representing the outline of the gear
:return: A numpy array, of the format [[x1, x2, ... , xn], [y1, y2, ... , yn]]
'''
points_tooth_and_gap = self.generate_tooth_and_gap()
points_teeth = [np.dot(rotation_matrix(self.theta_tooth_and_gap * n), points_tooth_and_gap) for n in range(self.teeth)]
points_gear = np.concatenate(points_teeth, axis=1)
return points_gear
def get_point_list(self):
'''
Generate the gear profile, and return a sequence of co-ordinates representing the outline of the gear
:return: A numpy array, of the format [[x1, y2], [x2, y2], ... , [xn, yn]]
'''
gear = self.generate_gear()
return np.transpose(gear)
def get_svg(self, unit=mm):
'''
Generate an SVG Drawing based of the generated gear profile.
:param unit: None or a unit within the 'svgwrite' module, such as svgwrite.mm, svgwrite.cm
:return: An svgwrite.Drawing object populated only with the gear path.
'''
points = self.get_point_list()
width, height = np.ptp(points, axis=0)
left, top = np.min(points, axis=0)
size = (width*unit, height*unit) if unit is not None else (width,height)
dwg = Drawing(size=size, viewBox='{} {} {} {}'.format(left,top,width,height))
p = Path('M')
p.push(points)
p.push('Z')
dwg.add(p)
return dwg
def get_dxf(self):
points = self.get_point_list()
doc = ezdxf.new('R2010')
doc.header['$MEASUREMENT'] = 1
doc.header['$INSUNITS'] = 4
msp = doc.modelspace()
msp.add_lwpolyline(points, dxfattribs = {'closed': True})
return doc
def error_out(s, *args):
sys.stderr.write(s + "\n")