Download src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 21.3 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py
-
curl -L -o TestInvoluteGear.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/PartDesign/PartDesignTests/TestInvoluteGear.py
21.3 kB
| # *************************************************************************** | |
| # * Copyright (c) 2021 Jonas Bähr <jonas.baehr@web.de> * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * This program is distributed in the hope that it will be useful, * | |
| # * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
| # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
| # * GNU Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| import unittest | |
| import pathlib | |
| from math import pi, tan, cos, acos | |
| import FreeCAD | |
| Quantity = FreeCAD.Units.Quantity # FIXME from FreeCAD.Units import Quantity doesn't work | |
| from FreeCAD import Vector | |
| from Part import makeCircle, Precision, Solid | |
| import InvoluteGearFeature | |
| FIXTURE_PATH = pathlib.Path(__file__).parent / "Fixtures" | |
| class TestInvoluteGear(unittest.TestCase): | |
| def setUp(self): | |
| self.Doc = FreeCAD.newDocument("PartDesignTestInvoluteGear") | |
| FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True") | |
| def tearDown(self): | |
| FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "") | |
| FreeCAD.closeDocument(self.Doc.Name) | |
| def testDefaultGearProfile(self): | |
| InvoluteGearFeature.makeInvoluteGear("TestGear") | |
| gear = self.Doc.getObject("TestGear") | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testDefaultInternalGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.ExternalGear = False | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testLowPrecisionGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.HighPrecision = False | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testLowPrecisionInternalGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.ExternalGear = False | |
| gear.HighPrecision = False | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testExternalGearProfileOrientation(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("TestGear") | |
| self.assertSuccessfulRecompute(gear) | |
| tip_diameter = (gear.NumberOfTeeth + 2 * gear.AddendumCoefficient) * gear.Modules | |
| delta = 0.01 # yes, we do not reach micrometer precision | |
| tip_probe = makeCircle(delta, Vector(tip_diameter / 2, 0, 0)) | |
| self.assertIntersection( | |
| gear.Shape, tip_probe, msg=f"First tooth tip does not lay on the positive X-axis" | |
| ) | |
| def testInternalGearProfileOrientation(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("TestGear") | |
| gear.ExternalGear = False | |
| self.assertSuccessfulRecompute(gear) | |
| tip_diameter = (gear.NumberOfTeeth - 2 * gear.AddendumCoefficient) * gear.Modules | |
| delta = 0.01 # yes, we do not reach micrometer precision | |
| tip_probe = makeCircle(delta, Vector(tip_diameter / 2, 0, 0)) | |
| self.assertIntersection( | |
| gear.Shape, tip_probe, msg=f"First tooth tip does not lay on the positive X-axis" | |
| ) | |
| def testCustomizedGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| z = 12 | |
| m = 1 | |
| gear.NumberOfTeeth = z | |
| gear.Modules = f"{m} mm" | |
| gear.PressureAngle = "14.5 deg" | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| pitch_diameter = m * z | |
| default_addendum = 1 | |
| default_dedendum = 1.25 | |
| tip_diameter = pitch_diameter + 2 * default_addendum * m | |
| root_diameter = pitch_diameter - 2 * default_dedendum * m | |
| # the test purpose here is just to ensure the gear's parameters are used, | |
| # not super precise profile verification. Thus a lax delta is just file here. | |
| delta = 0.01 | |
| self.assertIntersection( | |
| gear.Shape, makeCircle(pitch_diameter / 2), "Expecting intersection at pitch circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(tip_diameter / 2 + delta), "Teeth extent beyond tip circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(root_diameter / 2 - delta), "Teeth extend below root circle" | |
| ) | |
| def testCustomizedGearProfileForSplinedShaft(self): | |
| spline = InvoluteGearFeature.makeInvoluteGear("InvoluteSplinedShaft") | |
| z = 12 | |
| m = 2 | |
| add_coef = 0.5 | |
| ded_coef = 0.9 | |
| spline.NumberOfTeeth = z | |
| spline.Modules = f"{m} mm" | |
| spline.PressureAngle = "30 deg" | |
| spline.AddendumCoefficient = add_coef | |
| spline.DedendumCoefficient = ded_coef | |
| spline.RootFilletCoefficient = 0.4 | |
| self.assertSuccessfulRecompute(spline) | |
| self.assertClosedWire(spline.Shape) | |
| pitch_diameter = m * z | |
| tip_diameter = pitch_diameter + 2 * add_coef * m | |
| root_diameter = pitch_diameter - 2 * ded_coef * m | |
| # the test purpose here is just to ensure the gear's parameters are used, | |
| # not super precise profile verification. Thus a lax delta is just file here. | |
| delta = 0.01 | |
| self.assertIntersection( | |
| spline.Shape, makeCircle(pitch_diameter / 2), "Expecting intersection at pitch circle" | |
| ) | |
| self.assertNoIntersection( | |
| spline.Shape, makeCircle(tip_diameter / 2 + delta), "Teeth extent beyond tip circle" | |
| ) | |
| self.assertNoIntersection( | |
| spline.Shape, makeCircle(root_diameter / 2 - delta), "Teeth extend below root circle" | |
| ) | |
| def testCustomizedGearProfileForSplinedHub(self): | |
| hub = InvoluteGearFeature.makeInvoluteGear("InvoluteSplinedHub") | |
| hub.ExternalGear = False | |
| z = 12 | |
| m = 2 | |
| add_coef = 0.5 | |
| ded_coef = 0.9 | |
| hub.NumberOfTeeth = z | |
| hub.Modules = f"{m} mm" | |
| hub.PressureAngle = "30 deg" | |
| hub.AddendumCoefficient = add_coef | |
| hub.DedendumCoefficient = ded_coef | |
| hub.RootFilletCoefficient = 0.4 | |
| self.assertSuccessfulRecompute(hub) | |
| self.assertClosedWire(hub.Shape) | |
| pitch_diameter = m * z | |
| tip_diameter = pitch_diameter - 2 * add_coef * m | |
| root_diameter = pitch_diameter + 2 * ded_coef * m | |
| # the test purpose here is just to ensure the gear's parameters are used, | |
| # not super precise profile verification. Thus a lax delta is just file here. | |
| delta = 0.01 | |
| self.assertIntersection( | |
| hub.Shape, makeCircle(pitch_diameter / 2), "Expecting intersection at pitch circle" | |
| ) | |
| self.assertNoIntersection( | |
| hub.Shape, makeCircle(tip_diameter / 2 - delta), "Teeth extent below tip circle" | |
| ) | |
| self.assertNoIntersection( | |
| hub.Shape, makeCircle(root_diameter / 2 + delta), "Teeth extend beyond root circle" | |
| ) | |
| def testShiftedExternalGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.NumberOfTeeth = 9 # odd number to have a tooth space on the negative X-axis | |
| gear.ProfileShiftCoefficient = 0.6 | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| # first, verify the radial dimensions | |
| xm = gear.ProfileShiftCoefficient * gear.Modules | |
| Rref = gear.NumberOfTeeth * gear.Modules / 2 | |
| Rtip = Rref + gear.AddendumCoefficient * gear.Modules + xm | |
| Rroot = Rref - gear.DedendumCoefficient * gear.Modules + xm | |
| delta = Quantity("20 um") # 20 micron is as good as it gets | |
| self.assertIntersection( | |
| gear.Shape, makeCircle(Rref), "Expecting intersection at reference circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(Rtip + delta), "Teeth extent beyond tip circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(Rroot - delta), "Teeth extend below root circle" | |
| ) | |
| # to verify the angular dimensions, we use an "over pin measurement" | |
| Dpin, Rc = external_pin_diameter_and_distance( | |
| z=gear.NumberOfTeeth, | |
| m=gear.Modules.getValueAs("mm"), | |
| a=gear.PressureAngle.getValueAs("rad"), | |
| x=gear.ProfileShiftCoefficient, | |
| ) | |
| Rpin = Quantity(f"{Dpin/2} mm") | |
| delta = Quantity("1 um") # our angular precision is much greater then the radial one | |
| self.assertIntersection( | |
| gear.Shape, | |
| makeCircle(Rpin + delta, Vector(-Rc)), | |
| msg="Expecting intersection with enlarged pin", | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, | |
| makeCircle(Rpin - delta, Vector(-Rc)), | |
| msg="Expecting no intersection with reduced pin", | |
| ) | |
| def testShiftedInternalGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.NumberOfTeeth = 11 # odd number to have a tooth space on the negative X-axis | |
| gear.ExternalGear = False # to ensure "clean" flanks we need to tweak some more props | |
| gear.ProfileShiftCoefficient = 0.4 | |
| gear.AddendumCoefficient = 0.6 | |
| gear.DedendumCoefficient = 0.8 | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| # first, verify the radial dimensions | |
| xm = gear.ProfileShiftCoefficient * gear.Modules | |
| Rref = gear.NumberOfTeeth * gear.Modules / 2 | |
| # For internal, too, positive shift is outwards. So this is *not* inverted. | |
| Rtip = Rref - gear.AddendumCoefficient * gear.Modules + xm | |
| Rroot = Rref + gear.DedendumCoefficient * gear.Modules + xm | |
| delta = Quantity("20 um") # 20 micron is as good as it gets | |
| self.assertIntersection( | |
| gear.Shape, makeCircle(Rref), "Expecting intersection at reference circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(Rtip - delta), "Teeth extent below tip circle" | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, makeCircle(Rroot + delta), "Teeth extend beyond root circle" | |
| ) | |
| # to verify the angular dimensions, we use an "over pin measurement" | |
| Dpin, Rc = internal_pin_diameter_and_distance( | |
| z=gear.NumberOfTeeth, | |
| m=gear.Modules.getValueAs("mm"), | |
| a=gear.PressureAngle.getValueAs("rad"), | |
| x=gear.ProfileShiftCoefficient, | |
| ) | |
| Rpin = Quantity(f"{Dpin/2} mm") | |
| delta = Quantity("1 um") # our angular precision is much greater then the radial one | |
| self.assertIntersection( | |
| gear.Shape, | |
| makeCircle(Rpin + delta, Vector(-Rc)), | |
| msg="Expecting intersection with enlarged pin", | |
| ) | |
| self.assertNoIntersection( | |
| gear.Shape, | |
| makeCircle(Rpin - delta, Vector(-Rc)), | |
| msg="Expecting no intersection with reduced pin", | |
| ) | |
| def testZeroFilletExternalGearProfile_BaseAboveRoot(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| # below 42 teeth, with default dedendum 1.25, we have some non-involute flanks | |
| gear.NumberOfTeeth = 41 | |
| gear.RootFilletCoefficient = 0 | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testZeroFilletExternalGearProfile_BaseBelowRoot(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| # above 41 teeth, with default dedendum 1.25, the root is within the involute flank | |
| gear.NumberOfTeeth = 42 | |
| gear.RootFilletCoefficient = 0 | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testZeroFilletInternalGearProfile(self): | |
| gear = InvoluteGearFeature.makeInvoluteGear("InvoluteGear") | |
| gear.ExternalGear = False | |
| gear.RootFilletCoefficient = 0 | |
| self.assertSuccessfulRecompute(gear) | |
| self.assertClosedWire(gear.Shape) | |
| def testUsagePadGearProfile(self): | |
| profile = InvoluteGearFeature.makeInvoluteGear("GearProfile") | |
| body = self.Doc.addObject("PartDesign::Body", "GearBody") | |
| body.addObject(profile) | |
| pad = body.newObject("PartDesign::Pad", "GearPad") | |
| pad.Profile = profile | |
| pad.Length = "5 mm" # that our gear's "Face Width" | |
| self.assertSuccessfulRecompute() | |
| self.assertSolid(pad.Shape) | |
| def testUsagePocketInternalGearProfile(self): | |
| profile = InvoluteGearFeature.makeInvoluteGear("GearProfile") | |
| profile.ExternalGear = False | |
| # boolean cuts with lots of B-splines are quite slow, so let's make it less complex | |
| profile.HighPrecision = False | |
| profile.NumberOfTeeth = 8 | |
| body = self.Doc.addObject("PartDesign::Body", "GearBody") | |
| body.addObject(profile) | |
| cylinder = body.newObject("PartDesign::AdditiveCylinder", "GearCylinder") | |
| default_dedendum = 1.25 | |
| rim_width = 3 * FreeCAD.Units.MilliMetre | |
| cylinder.Height = "5 mm" # that our gear's "Face Width" | |
| cylinder.Radius = ( | |
| profile.NumberOfTeeth * profile.Modules / 2 | |
| + default_dedendum * profile.Modules | |
| + rim_width | |
| ) | |
| pocket = body.newObject("PartDesign::Pocket", "GearPocket") | |
| pocket.Profile = profile | |
| pocket.Reversed = True # need to "pocket upwards" into the cylinder | |
| pocket.Type = "ThroughAll" | |
| self.assertSuccessfulRecompute() | |
| self.assertSolid( | |
| Solid(pocket.Shape) | |
| ) # Can be a compound, make that into a Solid if needed. | |
| def testRecomputeExternalGearFromV020(self): | |
| FreeCAD.closeDocument(self.Doc.Name) # this was created in setUp(self) | |
| self.Doc = FreeCAD.openDocument(str(FIXTURE_PATH / "InvoluteGear_v0-20.FCStd")) | |
| created_with = f"created with {self.Doc.getProgramVersion()}" | |
| gear = self.Doc.InvoluteGear # from fixture | |
| fixture_length = 187.752 # from fixture, rounded to micrometer | |
| self.assertClosedWire(gear.Shape) # no recompute yet, i.e. check original | |
| self.assertAlmostEqual( | |
| fixture_length, | |
| gear.Shape.Length, | |
| places=3, | |
| msg=f"Total wire length does not match fixture for gear {created_with}", | |
| ) | |
| gear.enforceRecompute() | |
| self.assertSuccessfulRecompute(gear, msg=f"Cannot recompute gear {created_with}") | |
| relative_tolerance_per_tooth = 1e-3 # wild guess: changes of <0.1%/tooth are ok | |
| length_delta = fixture_length * relative_tolerance_per_tooth * gear.NumberOfTeeth | |
| self.assertAlmostEqual( | |
| fixture_length, | |
| gear.Shape.Length, | |
| delta=length_delta, | |
| msg=f"Total wire length changed after recomputing gear {created_with}", | |
| ) | |
| def testRecomputeInternalGearFromV020(self): | |
| FreeCAD.closeDocument(self.Doc.Name) # this was created in setUp(self) | |
| self.Doc = FreeCAD.openDocument(str(FIXTURE_PATH / "InternalInvoluteGear_v0-20.FCStd")) | |
| created_with = f"created with {self.Doc.getProgramVersion()}" | |
| gear = self.Doc.InvoluteGear # from fixture | |
| fixture_length = 165.408 # from fixture, rounded to micrometer | |
| self.assertClosedWire(gear.Shape) # no recompute yet, i.e. check original | |
| self.assertAlmostEqual( | |
| fixture_length, | |
| gear.Shape.Length, | |
| places=3, | |
| msg=f"Total wire length does not match fixture for gear {created_with}", | |
| ) | |
| gear.enforceRecompute() | |
| self.assertSuccessfulRecompute(gear, msg=f"Cannot recompute gear {created_with}") | |
| relative_tolerance_per_tooth = 1e-3 # wild guess: changes of <0.1%/tooth are ok | |
| length_delta = fixture_length * relative_tolerance_per_tooth * gear.NumberOfTeeth | |
| self.assertAlmostEqual( | |
| fixture_length, | |
| gear.Shape.Length, | |
| delta=length_delta, | |
| msg=f"Total wire length changed after recomputing gear {created_with}", | |
| ) | |
| def assertSuccessfulRecompute(self, *objs, msg=None): | |
| if len(objs) == 0: | |
| self.Doc.recompute() | |
| objs = self.Doc.Objects | |
| else: | |
| self.Doc.recompute(objs) | |
| failed_objects = [o.Name for o in objs if "Invalid" in o.State] | |
| if len(failed_objects) > 0: | |
| self.fail(msg or f"Recompute failed for {failed_objects}") | |
| def assertClosedWire(self, shape, msg=None): | |
| self.assertEqual(shape.ShapeType, "Wire", msg=msg) | |
| self.assertTrue(shape.isClosed(), msg=msg) | |
| def assertIntersection(self, shape1, shape2, msg=None): | |
| self.assertTrue( | |
| self._check_intersection(shape1, shape2), msg or "Given shapes do not intersect." | |
| ) | |
| def assertNoIntersection(self, shape1, shape2, msg=None): | |
| self.assertFalse(self._check_intersection(shape1, shape2), msg or "Given shapes intersect.") | |
| def _check_intersection(self, shape1, shape2): | |
| distance, _, _ = shape1.distToShape(shape2) | |
| return distance < Precision.intersection() | |
| def assertSolid(self, shape, msg=None): | |
| # we don't check shape.ShapeType for 'Solid' as with body.AllowCompound==True | |
| # we get, also in the good case, our solid wrapped in a compound. | |
| self.assertEqual(len(shape.Solids), 1, msg=msg) | |
| def inv(a): | |
| """the involute function""" | |
| return tan(a) - a | |
| def external_pin_diameter_and_distance(z, m, a, x): | |
| """Calculates the ideal pin diameter for over pins measurement and its distance | |
| for extrnal spur gears. | |
| z is the number of teeth | |
| m is the module, in millimeter | |
| a is the pressure angle, in radians | |
| x is the profile shift coefficient | |
| returns the tuple of ideal pin diameter and its center distance from the gear's center | |
| """ | |
| # Equations taken from http://qtcgears.com/tools/catalogs/PDF_Q420/Tech.pdf | |
| # Table 10-13 (1-4) and Table 10-14 (4a) | |
| # 1. Half Tooth Space Angle at Base Circle | |
| nu = pi / (2 * z) - inv(a) - 2 * x * tan(a) / z | |
| # 2. The Pressure Angle at the Point Pin is Tangent to Tooth Surface | |
| ap = acos(z * m * cos(a) / (z * m + 2 * x * m)) | |
| # 3. The Pressure Angle at Pin Center | |
| phi = tan(ap) + nu | |
| # 4. Ideal Pin Diameter | |
| dp = z * m * cos(a) * (inv(phi) + nu) | |
| # 4a. Over Pins Measurement, even number of teeth | |
| # As we return the distance from the gear's center, we need dm to pass thought this center | |
| # and that's only the case for a dm for an even number of teeth. However, this center distance | |
| # is also valid for an odd number of teeth, as we don't measure pin-to-pin but pin-to-center. | |
| dm = z * m * cos(a) / cos(phi) + dp | |
| # Eq. 10-12 on page T46 | |
| rc = (dm - dp) / 2 | |
| return (dp, rc) | |
| def internal_pin_diameter_and_distance(z, m, a, x): | |
| """Calculates the ideal pin diameter for over pins measurement and its distance | |
| for intrnal spur gears. | |
| z is the number of teeth | |
| m is the module, in millimeter | |
| a is the pressure angle, in radians | |
| x is the profile shift coefficient | |
| returns the tuple of ideal pin diameter and its center distance from the gear's center | |
| """ | |
| # Equations taken from http://qtcgears.com/tools/catalogs/PDF_Q420/Tech.pdf | |
| # Table 10-17 (1-4) and Table 10-18 (4a) | |
| # 1. Half Tooth Space Angle at Base Circle | |
| nu = pi / (2 * z) + inv(a) + 2 * x * tan(a) / z | |
| # 2. The Pressure Angle at the Point Pin is Tangent to Tooth Surface | |
| ap = acos(z * m * cos(a) / (z * m + 2 * x * m)) | |
| # 3. The Pressure Angle at Pin Center | |
| phi = tan(ap) - nu | |
| # 4. Ideal Pin Diameter | |
| dp = z * m * cos(a) * (nu - inv(phi)) | |
| # 4a. Over Pins Measurement, even number of teeth | |
| # As we return the distance from the gear's center, we need dm to pass thought this center | |
| # and that's only the case for a dm for an even number of teeth. However, this center distance | |
| # is also valid for an odd number of teeth, as we don't measure pin-to-pin but pin-to-center. | |
| dm = z * m * cos(a) / cos(phi) - dp | |
| rc = (dm + dp) / 2 | |
| return (dp, rc) | |