| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from math import cos, sin, tan, pi, acos, asin, atan, sqrt, radians |
| | from math import comb as binom |
| |
|
| |
|
| | def CreateExternalGear( |
| | w, m, Z, phi, split=True, addCoeff=1.0, dedCoeff=1.25, filletCoeff=0.375, shiftCoeff=0.0 |
| | ): |
| | """ |
| | Create an external gear |
| | |
| | w is wire builder object (in which the gear will be constructed) |
| | m is the gear's module (pitch diameter divided by the number of teeth) |
| | Z is the number of teeth |
| | phi is the gear's pressure angle, in degrees |
| | addCoeff is the addendum coefficient (addendum normalized by module) |
| | dedCoeff is the dedendum coefficient (dedendum normalized by module) |
| | filletCoeff is the root fillet radius, normalized by the module. |
| | The default of 0.375 matches the hard-coded value (1.5 * 0.25) of the implementation |
| | up to v0.20. The ISO Rack specified 0.38, though. |
| | shiftCoeff is the profile shift coefficient (profile shift normalized by module) |
| | |
| | if split is True, each profile of a teeth will consist in 2 Bezier |
| | curves of degree 3, otherwise it will be made of one Bezier curve |
| | of degree 4 |
| | """ |
| | _create_involute_profile( |
| | wire_builder=w, |
| | module=m, |
| | number_of_teeth=Z, |
| | pressure_angle=radians(phi), |
| | split_involute=split, |
| | outer_height_coefficient=addCoeff, |
| | inner_height_coefficient=dedCoeff, |
| | inner_fillet_coefficient=filletCoeff, |
| | profile_shift_coefficient=shiftCoeff, |
| | ) |
| |
|
| |
|
| | def CreateInternalGear( |
| | w, m, Z, phi, split=True, addCoeff=0.6, dedCoeff=1.25, filletCoeff=0.375, shiftCoeff=0.0 |
| | ): |
| | """ |
| | Create an internal gear |
| | |
| | w is wire builder object (in which the gear will be constructed) |
| | m is the gear's module (pitch diameter divided by the number of teeth) |
| | Z is the number of teeth |
| | phi is the gear's pressure angle, in degrees |
| | addCoeff is the addendum coefficient (addendum normalized by module) |
| | The default of 0.6 comes from the "Handbook of Gear Design" by Gitin M. Maitra, |
| | with the goal to push the addendum circle beyond the base circle to avoid non-involute |
| | flanks on the tips. |
| | It in turn assumes, however, that the mating pinion uses a larger value of 1.25. |
| | And it's only required for a small number of teeth and/or a relatively large mating gear. |
| | Anyways, it's kept here as this was the hard-coded value of the implementation up to v0.20. |
| | dedCoeff is the dedendum coefficient (dedendum normalized by module) |
| | filletCoeff is the root fillet radius, normalized by the module. |
| | The default of 0.375 matches the hard-coded value (1.5 * 0.25) of the implementation |
| | up to v0.20. The ISO Rack specified 0.38, though. |
| | shiftCoeff is the profile shift coefficient (profile shift normalized by module) |
| | |
| | if split is True, each profile of a teeth will consist in 2 Bezier |
| | curves of degree 3, otherwise it will be made of one Bezier curve |
| | of degree 4 |
| | """ |
| | _create_involute_profile( |
| | wire_builder=w, |
| | module=m, |
| | number_of_teeth=Z, |
| | pressure_angle=radians(phi), |
| | split_involute=split, |
| | rotation=pi / Z, |
| | outer_height_coefficient=dedCoeff, |
| | inner_height_coefficient=addCoeff, |
| | outer_fillet_coefficient=filletCoeff, |
| | profile_shift_coefficient=shiftCoeff, |
| | ) |
| |
|
| |
|
| | def _create_involute_profile( |
| | wire_builder, |
| | module, |
| | number_of_teeth, |
| | pressure_angle=radians(20.0), |
| | split_involute=True, |
| | rotation=radians(0), |
| | outer_height_coefficient=1.0, |
| | inner_height_coefficient=1.0, |
| | outer_fillet_coefficient=0.0, |
| | inner_fillet_coefficient=0.0, |
| | profile_shift_coefficient=0.0, |
| | ): |
| | """ |
| | Create an involute gear profile in the given wire builder |
| | |
| | This method can be used to create external as well as internal gear and spline profiles. |
| | Thus this method does not use the terms "addednum" and "dedendum" or "tip" and "root", |
| | but refers to the elements below the reference circle (i.e. towards the center) as "inner", |
| | and those above the reference circle (i.e. away from the center) as "outer". |
| | |
| | For an external gear, outer_height is the addendum, inner_height is the dedendum, |
| | and inner_fillet is the root fillet. |
| | For an internal gear, inner_height is the addendum, outer_height is the dedendum, |
| | and outer_fillet is the root fillet. |
| | |
| | The "_coefficient" suffix denotes values normalized by the module. |
| | """ |
| |
|
| | profile_shift = profile_shift_coefficient * module |
| | outer_height = outer_height_coefficient * module + profile_shift |
| | inner_height = inner_height_coefficient * module - profile_shift |
| |
|
| | |
| | |
| | |
| | |
| | Rref = number_of_teeth * module / 2 |
| | Rb = Rref * cos(pressure_angle) |
| | Ro = Rref + outer_height |
| | Ri = Rref - inner_height |
| |
|
| | fi = inner_fillet_coefficient * module |
| | Rci = Ri + fi |
| | Rfi = Rci |
| |
|
| | fo = outer_fillet_coefficient * module |
| | Rco = Ro - fo |
| | Rfo = Ro |
| |
|
| | has_non_involute_flank = Rfi < Rb |
| | |
| | has_inner_fillet = fi > 0 |
| | has_outer_fillet = fo > 0 |
| |
|
| | if has_inner_fillet and not has_non_involute_flank: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | q = lambda r: (sqrt(r**2 - Rb**2) / Rb - asin((-(r**2) + fi**2 + Rci**2) / (2 * fi * Rci))) |
| | q_prime = lambda r: ( |
| | r / (sqrt(-(Rb**2) + r**2) * Rb) |
| | + r / (fi * Rci * sqrt(1 - 1 / 4 * (r**2 - fi**2 - Rci**2) ** 2 / (fi**2 * Rci**2))) |
| | ) |
| | Rfi = findRootNewton(q, q_prime, x_min=max(Rb, Ri), x_max=Rci) |
| |
|
| | if has_outer_fillet: |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | phi_corr = genInvolutePolar(Rb, Ro) + atan(fo / Ro) |
| | q = lambda r: ( |
| | sqrt(r**2 - Rb**2) / Rb - asin((r**2 - fo**2 - Rco**2) / (2 * fo * Rco)) - phi_corr |
| | ) |
| | q_prime = lambda r: ( |
| | r / (sqrt(-(Rb**2) + r**2) * Rb) |
| | - r / (fo * Rco * sqrt(1 - 1 / 4 * (r**2 - fo**2 - Rco**2) ** 2 / (fo**2 * Rco**2))) |
| | ) |
| | Rfo = findRootNewton(q, q_prime, x_min=max(Rb, Rco), x_max=Ro) |
| |
|
| | |
| | angular_pitch = 2 * pi / number_of_teeth |
| | base_to_ref = genInvolutePolar(Rb, Rref) |
| | ref_to_stop = genInvolutePolar(Rb, Rfo) - base_to_ref |
| | if has_non_involute_flank: |
| | start_to_ref = base_to_ref |
| | else: |
| | start_to_ref = base_to_ref - genInvolutePolar(Rb, Rfi) |
| |
|
| | inner_fillet_width = sqrt(fi**2 - (Rci - Rfi) ** 2) |
| | inner_fillet_angle = atan(inner_fillet_width / Rfi) |
| | outer_fillet_width = sqrt(fo**2 - (Rfo - Rco) ** 2) |
| | outer_fillet_angle = atan(outer_fillet_width / Rfo) |
| |
|
| | |
| | fe = 1 |
| | fs = 0.01 |
| | if not has_non_involute_flank: |
| | fs = (Rfi**2 - Rb**2) / (Rfo**2 - Rb**2) |
| |
|
| | if split_involute: |
| | |
| | fm = fs + (fe - fs) / 4 |
| | part1 = BezCoeffs(Rb, Rfo, 3, fs, fm) |
| | part2 = BezCoeffs(Rb, Rfo, 3, fm, fe) |
| | inv = part1 + part2[1:] |
| | else: |
| | inv = BezCoeffs(Rb, Rfo, 4, fs, fe) |
| |
|
| | |
| | enlargement_by_shift = profile_shift * tan(pressure_angle) / Rref |
| | tooth_thickness_half_angle = angular_pitch / 4 + enlargement_by_shift |
| | psi = tooth_thickness_half_angle |
| |
|
| | |
| | inv = [rotate(pt, -base_to_ref - psi) for pt in inv] |
| |
|
| | |
| | invR = [mirror(pt) for pt in inv] |
| |
|
| | |
| | |
| | |
| | inner_fillet = toCartesian(Rfi, -psi - start_to_ref) |
| | inner_fillet_back = mirror(inner_fillet) |
| | inner_circle_back = toCartesian(Ri, psi + start_to_ref + inner_fillet_angle) |
| | inner_circle_next = toCartesian(Ri, angular_pitch - psi - start_to_ref - inner_fillet_angle) |
| | inner_fillet_next = rotate(inner_fillet, angular_pitch) |
| | outer_fillet = toCartesian(Ro, -psi + ref_to_stop + outer_fillet_angle) |
| | outer_circle = mirror(outer_fillet) |
| |
|
| | |
| | thetas = [x * angular_pitch + rotation for x in range(number_of_teeth)] |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | if has_inner_fillet: |
| | wire_builder.move(rotate(inner_fillet_next, thetas[-1])) |
| | else: |
| | wire_builder.move(rotate(inner_circle_next, thetas[-1])) |
| |
|
| | for theta in thetas: |
| | wire_builder.theta = theta |
| |
|
| | if has_non_involute_flank: |
| | wire_builder.line(inv[0]) |
| |
|
| | |
| | if split_involute: |
| | wire_builder.curve(inv[1], inv[2], inv[3]) |
| | wire_builder.curve(inv[4], inv[5], inv[6]) |
| | else: |
| | wire_builder.curve(*inv[1:]) |
| |
|
| | |
| | if outer_circle[1] > outer_fillet[1]: |
| | if has_outer_fillet: |
| | wire_builder.arc(outer_fillet, fo, 1) |
| | wire_builder.arc(outer_circle, Ro, 1) |
| |
|
| | if has_outer_fillet: |
| | wire_builder.arc(invR[-1], fo, 1) |
| |
|
| | |
| | if split_involute: |
| | wire_builder.curve(invR[5], invR[4], invR[3]) |
| | wire_builder.curve(invR[2], invR[1], invR[0]) |
| | else: |
| | wire_builder.curve(*invR[-2::-1]) |
| |
|
| | if has_non_involute_flank: |
| | wire_builder.line(inner_fillet_back) |
| |
|
| | |
| | if inner_circle_next[1] > inner_circle_back[1]: |
| | if has_inner_fillet: |
| | wire_builder.arc(inner_circle_back, fi, 0) |
| | wire_builder.arc(inner_circle_next, Ri, 1) |
| |
|
| | if has_inner_fillet: |
| | wire_builder.arc(inner_fillet_next, fi, 0) |
| |
|
| | wire_builder.close() |
| |
|
| |
|
| | def genInvolutePolar(Rb, R): |
| | """return the involute angle as function of radius R. |
| | Rb = base circle radius |
| | """ |
| | return (sqrt(R * R - Rb * Rb) / Rb) - acos(Rb / R) |
| |
|
| |
|
| | def rotate(pt, rads): |
| | """rotate pt by rads radians about origin""" |
| | sinA = sin(rads) |
| | cosA = cos(rads) |
| | return (pt[0] * cosA - pt[1] * sinA, pt[0] * sinA + pt[1] * cosA) |
| |
|
| |
|
| | def mirror(pt): |
| | """mirror pt on the X axis, i.e. flip its Y""" |
| | return (pt[0], -pt[1]) |
| |
|
| |
|
| | def toCartesian(radius, angle): |
| | """convert polar coords to cartesian""" |
| | return (radius * cos(angle), radius * sin(angle)) |
| |
|
| |
|
| | def findRootNewton(f, f_prime, x_min, x_max): |
| | """Apply Newton's Method to find the root of f within x_min and x_max |
| | We assume that there is a root in that range and that f is strictly monotonic, |
| | i.e. we don't take precautions for overshooting beyond the input range. |
| | """ |
| | |
| | x = (x_min + x_max) / 2 |
| |
|
| | |
| | |
| | PRECISION_INTERSECTION = 1e-9 |
| |
|
| | |
| | for i in range(6): |
| | f_x = f(x) |
| | if abs(f_x) < PRECISION_INTERSECTION: |
| | return x |
| | x = x - f_x / f_prime(x) |
| |
|
| | raise RuntimeError(f"No convergence after {i+1} iterations.") |
| |
|
| |
|
| | def chebyExpnCoeffs(j, func): |
| | N = 50 |
| | c = 0 |
| | for k in range(1, N + 1): |
| | c += func(cos(pi * (k - 0.5) / N)) * cos(pi * j * (k - 0.5) / N) |
| | return 2 * c / N |
| |
|
| |
|
| | def chebyPolyCoeffs(p, func): |
| | coeffs = [0] * (p + 1) |
| | fnCoeff = [] |
| | T = [coeffs[:] for i in range(p + 1)] |
| | T[0][0] = 1 |
| | T[1][1] = 1 |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | for k in range(1, p): |
| | for j in range(len(T[k]) - 1): |
| | T[k + 1][j + 1] = 2 * T[k][j] |
| | for j in range(len(T[k - 1])): |
| | T[k + 1][j] -= T[k - 1][j] |
| |
|
| | |
| | |
| | for k in range(p + 1): |
| | fnCoeff.append(chebyExpnCoeffs(k, func)) |
| |
|
| | for k in range(p + 1): |
| | for pwr in range(p + 1): |
| | coeffs[pwr] += fnCoeff[k] * T[k][pwr] |
| |
|
| | coeffs[0] -= fnCoeff[0] / 2 |
| | return coeffs |
| |
|
| |
|
| | def bezCoeff(i, p, polyCoeffs): |
| | """generate the polynomial coeffs in one go""" |
| | return sum(binom(i, j) * polyCoeffs[j] / binom(p, j) for j in range(i + 1)) |
| |
|
| |
|
| | def BezCoeffs(baseRadius, limitRadius, order, fstart, fstop): |
| | """Approximates an involute using a Bezier-curve |
| | |
| | Parameters: |
| | baseRadius - the radius of base circle of the involute. |
| | This is where the involute starts, too. |
| | limitRadius - the radius of an outer circle, where the involute ends. |
| | order - the order of the Bezier curve to be fitted e.g. 3, 4, 5, ... |
| | fstart - fraction of distance along the involute to start the approximation. |
| | fstop - fraction of distance along the involute to stop the approximation. |
| | """ |
| | Rb = baseRadius |
| | Ra = limitRadius |
| | ta = sqrt(Ra * Ra - Rb * Rb) / Rb |
| | te = sqrt(fstop) * ta |
| | ts = sqrt(fstart) * ta |
| | p = order |
| |
|
| | def involuteXbez(t): |
| | "Equation of involute using the Bezier parameter t as variable" |
| | |
| | x = t * 2 - 1 |
| | |
| | theta = x * (te - ts) / 2 + (ts + te) / 2 |
| | return Rb * (cos(theta) + theta * sin(theta)) |
| |
|
| | def involuteYbez(t): |
| | "Equation of involute using the Bezier parameter t as variable" |
| | |
| | x = t * 2 - 1 |
| | |
| | theta = x * (te - ts) / 2 + (ts + te) / 2 |
| | return Rb * (sin(theta) - theta * cos(theta)) |
| |
|
| | |
| | bzCoeffs = [] |
| | polyCoeffsX = chebyPolyCoeffs(p, involuteXbez) |
| | polyCoeffsY = chebyPolyCoeffs(p, involuteYbez) |
| | for i in range(p + 1): |
| | bx = bezCoeff(i, p, polyCoeffsX) |
| | by = bezCoeff(i, p, polyCoeffsY) |
| | bzCoeffs.append((bx, by)) |
| | return bzCoeffs |
| |
|