diff --git a/pylinkage/__init__.py b/pylinkage/__init__.py index f5faf4f..807145d 100644 --- a/pylinkage/__init__.py +++ b/pylinkage/__init__.py @@ -23,15 +23,16 @@ from .interface import ( UnbuildableError, HypostaticError, - Static, + NotCompletelyDefinedError, Pivot, - Crank, - Fixed, - Linkage + Linkage, ) from .joints import ( + Crank, + Fixed, Linear, Revolute, + Static, ) from .optimization import ( generate_bounds, diff --git a/pylinkage/interface/__init__.py b/pylinkage/interface/__init__.py index ff844df..273730d 100644 --- a/pylinkage/interface/__init__.py +++ b/pylinkage/interface/__init__.py @@ -6,10 +6,13 @@ HypostaticError, NotCompletelyDefinedError, ) -from .joint import ( +from ..joints import ( Static, + Revolute, Crank, + Linear, Fixed, ) -from ..joints.revolute import Revolute, Pivot +# Will be deleted in next major release +from ..joints.revolute import Pivot from .linkage import Linkage diff --git a/pylinkage/interface/joint.py b/pylinkage/interface/joint.py deleted file mode 100644 index 8fccad9..0000000 --- a/pylinkage/interface/joint.py +++ /dev/null @@ -1,331 +0,0 @@ -""" -Definition of the different joints used for pylinkage. -""" -import abc -from math import atan2 - -from ..geometry import cyl_to_cart -from .exceptions import HypostaticError - - -def joint_syntax_parser(joint): - """ - Syntactic parser that understand a joint definition. - - :param joint: Input joint definition to be parsed. - :type joint: Joint | tuple[float, float] | None - - :return: New static joint definition if possible, or None. - :rtype: Static | None - """ - if joint is None or isinstance(joint, Joint): - return joint - return Static(*joint) - - -class Joint(abc.ABC): - """ - Geometric constraint expressed by two joints. - - Abstract class should always be inherited. - """ - - __slots__ = "x", "y", "joint0", "joint1", "name" - - def __init__(self, x=0, y=0, joint0=None, joint1=None, name=None): - """ - Create a Joint abstract object. - - :param x: Position on horizontal axis. The default is 0. - :type x: float | None - :param y: Position on vertical axis. The default is O. - :type y: float | None - :param name: Friendly name for human readability. Will default to object if None. - :type name: str | None - :param joint0: First linked joint (geometric constraints). The default is None. - :type joint0: Joint | tuple[float, float] | None - :param joint1: Other revolute joint linked. The default is None. - :type joint1: Joint | tuple[float, float] | None - """ - self.x, self.y = x, y - self.joint0 = joint_syntax_parser(joint0) - self.joint1 = joint_syntax_parser(joint1) - self.name = name - if name is None: - self.name = str(id(self)) - - def __repr__(self): - """Represent an object with class name, coordinates, name and state.""" - return "{}(x={}, y={}, name={})".format( - self.__class__.__name__, self.x, self.y, self.name - ) - - def __get_joints__(self): - """Return constraint joints as a tuple.""" - return self.joint0, self.joint1 - - def coord(self): - """ - Return cartesian coordinates. - - :rtype: tuple[float | None, float | None] - """ - return self.x, self.y - - def set_coord(self, *args): - """Take a sequence or two scalars, and assign them to object x, y. - - :param args: Coordinates to set, either as two elements or as a tuple of 2 elements - :type args: tuple[float, float] | tuple[tuple[float, float]] - - """ - if len(args) == 1: - self.x, self.y = args[0] - else: - self.x, self.y = args[0], args[1] - - @abc.abstractmethod - def get_constraints(self): - """Return geometric constraints applying to this Joint.""" - raise NotImplementedError( - "You can't call a constraint from an abstract class." - ) - - @abc.abstractmethod - def set_constraints(self, *args): - """Set geometric constraints applying to this Joint.""" - raise NotImplementedError( - "You can't set a constraint from an abstract class." - ) - - -class Static(Joint): - """Special case of Joint that should not move. - - Mostly used for the frame. - """ - - __slots__ = tuple() - - def __init__(self, x=0, y=0, name=None): - """ - A Static joint is a point in space to use as anchor by other joints. - - It is NOT a kind of joint as viewed in engineering terms! - - x : float, optional - Position on horizontal axis. The default is 0. - y : float, optional - Position on vertical axis. The default is O. - name : str, optional - Friendly name for human readability. The default is None. - """ - super().__init__(x, y, name=name) - - def reload(self): - """Do nothing, for consistency only.""" - pass - - def get_constraints(self): - """Return an empty tuple.""" - return tuple() - - def set_constraints(self, *args): - """Do nothing, for consistency only. - - :param args: Unused - - """ - pass - - def set_anchor0(self, joint): - """First joint anchor. - - :param joint: - - """ - self.joint0 = joint - - def set_anchor1(self, joint): - """Second joint anchor. - - :param joint: - - """ - self.joint1 = joint - - -class Fixed(Joint): - """Define a joint using parents locations only, with no ambiguity.""" - - __slots__ = "r", "angle" - - def __init__(self, x=None, y=None, joint0=None, joint1=None, - distance=None, angle=None, name=None): - """ - Create a point, of position fully defined by its two references. - - Arguments - --------- - x : float, optional - Position on horizontal axis. The default is 0. - y : float, optional - Position on vertical axis. The default is O. - name : str, optional - Friendly name for human readability. The default is None. - joint0 : Union[Joint, tuple[float]], optional - Linked revolute joint 1 (geometric constraints). The default is None. - joint1 : Union[Joint, tuple[float]], optional - Other revolute joint linked. The default is None. - distance : float, optional - Distance to keep constant between joint0 and self. The default is - None. - angle : float, optional - Angle (joint1, joint0, self). Should be in radian and in trigonometric - order. The default is None. - """ - super().__init__(x, y, joint0, joint1, name) - self.angle = angle - self.r = distance - - def reload(self): - """Compute point coordinates. - - We know point position relative to its two parents, which gives a local - space. - We know the orientation of local space, so we can solve the - whole. Local space is defined by link[0] as the origin and - (link[0], link[1]) as abscissas axis. - """ - if self.joint0 is None: - return - if self.joint0 is None or self.joint1 is None: - raise HypostaticError(f'Not enough constraints for {self}') - # Rotation angle of local space relative to global - rot = atan2(self.joint1.y - self.joint0.y, - self.joint1.x - self.joint0.x) - # Position in global space - self.x, self.y = cyl_to_cart(self.r, self.angle + rot, - self.joint0.coord()) - - def get_constraints(self): - """Return the constraining distance and angle parameters.""" - return self.r, self.angle - - def set_constraints(self, distance=None, angle=None): - """Set geometric constraints. - - :param distance: (Default value = None) - :param angle: (Default value = None) - - """ - self.r, self.angle = distance or self.r, angle or self.angle - - def set_anchor0(self, joint, distance=None, angle=None): - """First joint anchor and characteristics. - - :param joint: - :param distance: (Default value = None) - :param angle: (Default value = None) - - """ - self.joint0 = joint - self.set_constraints(distance, angle) - - def set_anchor1(self, joint): - """Second joint anchor. - - :param joint: - - """ - self.joint1 = joint - - -class Crank(Joint): - """Define a crank joint.""" - - __slots__ = "r", "angle" - - def __init__( - self, - x=None, - y=None, - joint0=None, - distance=None, - angle=None, - name=None - ): - """ - Define a crank (circular motor). - - Parameters - ---------- - x : float, optional - initial horizontal position, won't be used thereafter. - The default is None. - y : float, optional - initial vertical position. The default is None. - joint0 : Union[Joint, tuple[float]], optional - first reference joint. The default is None. - distance : float, optional - distance to keep between joint0 and self. The default is None. - angle : float, optional - It is the angle (horizontal axis, joint0, self). - Should be in radian and in trigonometric order. - The default is None. - name : str, optional - user-friendly name. The default is None. - - Returns - ------- - None. - - """ - super().__init__(x, y, joint0, name=name) - self.r, self.angle = distance, angle - - def reload(self, dt=1): - """Make a step of crank. - - :param dt: Fraction of steps to take (Default value = 1) - :type dt: float - - """ - if self.joint0 is None: - return - if None in self.joint0.coord(): - raise HypostaticError( - f'{self.joint0} has None coordinates. ' - f'{self} cannot be calculated' - ) - # Rotation angle of local space relative to global - rot = atan2(self.y - self.joint0.y, self.x - self.joint0.x) - self.x, self.y = cyl_to_cart( - self.r, rot + self.angle * dt, - self.joint0.coord() - ) - - def get_constraints(self): - """Return the distance to the center of rotation.""" - return (self.r,) - - def set_constraints(self, distance=None, *args): - """Set geometric constraints, only self.r is affected. - - :param distance: Distance from the reference point. - (Default value = None) - :type distance: float - :param args: Unused, but preserves the object structure. - - """ - self.r = distance or self.r - - def set_anchor0(self, joint, distance=None): - """First joint anchor and fixed distance. - - :param joint: - :param distance: (Default value = None) - - """ - self.joint0 = joint - self.set_constraints(distance=distance) diff --git a/pylinkage/interface/linkage.py b/pylinkage/interface/linkage.py index 23921ba..f2badbb 100644 --- a/pylinkage/interface/linkage.py +++ b/pylinkage/interface/linkage.py @@ -10,12 +10,7 @@ import warnings from math import gcd, tau from .exceptions import HypostaticError -from .joint import ( - Static, - Crank, - Fixed -) -from ..joints import Revolute +from ..joints import (Revolute, Fixed, Crank, Static) class Linkage: diff --git a/pylinkage/joints/__init__.py b/pylinkage/joints/__init__.py index 91e60fb..c152e84 100644 --- a/pylinkage/joints/__init__.py +++ b/pylinkage/joints/__init__.py @@ -1,6 +1,7 @@ """Definition of joints.""" +from .joint import Static +from .crank import Crank +from .fixed import Fixed from .linear import Linear from .revolute import Revolute - -from ..interface.joint import (Static, Fixed, Crank,) diff --git a/pylinkage/joints/crank.py b/pylinkage/joints/crank.py new file mode 100644 index 0000000..e21c697 --- /dev/null +++ b/pylinkage/joints/crank.py @@ -0,0 +1,89 @@ +""" +Crank joint definition. +""" +from math import atan2 + +from . import joint as pl_joint +from .. import geometry as pl_geom +from ..interface import exceptions as pl_exceptions + + +class Crank(pl_joint.Joint): + """Define a crank joint.""" + + __slots__ = "r", "angle" + + def __init__( + self, + x=None, + y=None, + joint0=None, + distance=None, + angle=None, + name=None + ): + """ + Define a crank (circular motor). + + :param x: Initial horizontal position, won't be used thereafter. + The default is None. + :type x: float | None + :param y: Initial vertical position. The default is None. + :type y: float | None + :param joint0: First reference joint. The default is None. + :type joint0: pylinkage.Joint | tuple[float, float] | None + :param distance: Distance to keep between joint0 and self. The default is None. + :type distance: float | None + :param angle: It is the angle (horizontal axis, joint0, self). + Should be in radian and in trigonometric order. + The default is None. + :type angle: float | None + :param str | None name: Human-readable name. The default is None. + """ + super().__init__(x, y, joint0, name=name) + self.r, self.angle = distance, angle + + def reload(self, dt=1): + """Make a step of crank. + + :param dt: Fraction of steps to take (Default value = 1) + :type dt: float + """ + if self.joint0 is None: + return + if None in self.joint0.coord(): + raise pl_exceptions.HypostaticError( + f'{self.joint0} has None coordinates. ' + f'{self} cannot be calculated' + ) + # Rotation angle of local space relative to global + rot = atan2(self.y - self.joint0.y, self.x - self.joint0.x) + self.x, self.y = pl_geom.cyl_to_cart( + self.r, rot + self.angle * dt, + self.joint0.coord() + ) + + def get_constraints(self): + """Return the distance to the center of rotation.""" + return (self.r,) + + def set_constraints(self, distance=None, *args): + """Set geometric constraints, only self.r is affected. + + :param distance: Distance from the reference point. + (Default value = None) + :type distance: float + :param args: Unused, but preserves the object structure. + + """ + self.r = distance or self.r + + def set_anchor0(self, joint, distance=None): + """First joint anchor and fixed distance. + + :param joint: + :param distance: (Default value = None) + + """ + self.joint0 = joint + self.set_constraints(distance=distance) diff --git a/pylinkage/joints/fixed.py b/pylinkage/joints/fixed.py new file mode 100644 index 0000000..5cb76cf --- /dev/null +++ b/pylinkage/joints/fixed.py @@ -0,0 +1,95 @@ +""" +Fixed joint. +""" +import math + +from .. import geometry as pl_geom +from ..interface import exceptions as pl_exceptions +from . import joint as pl_joint + + +class Fixed(pl_joint.Joint): + """Define a joint using parents locations only, with no ambiguity.""" + + __slots__ = "r", "angle" + + def __init__(self, x=None, y=None, joint0=None, joint1=None, + distance=None, angle=None, name=None): + """ + Create a point, of position fully defined by its two references. + + Arguments + --------- + x : float, optional + Position on horizontal axis. The default is 0. + y : float, optional + Position on vertical axis. The default is O. + name : str, optional + Friendly name for human readability. The default is None. + joint0 : Union[Joint, tuple[float]], optional + Linked revolute joint 1 (geometric constraints). The default is None. + joint1 : Union[Joint, tuple[float]], optional + Other revolute joint linked. The default is None. + distance : float, optional + Distance to keep constant between joint0 and self. The default is + None. + angle : float, optional + Angle (joint1, joint0, self). Should be in radian and in trigonometric + order. The default is None. + """ + super().__init__(x, y, joint0, joint1, name) + self.angle = angle + self.r = distance + + def reload(self): + """Compute point coordinates. + + We know point position relative to its two parents, which gives a local + space. + We know the orientation of local space, so we can solve the + whole. Local space is defined by link[0] as the origin and + (link[0], link[1]) as abscissas axis. + """ + if self.joint0 is None: + return + if self.joint0 is None or self.joint1 is None: + raise pl_exceptions.HypostaticError(f'Not enough constraints for {self}') + # Rotation angle of local space relative to global + rot = math.atan2(self.joint1.y - self.joint0.y, + self.joint1.x - self.joint0.x) + # Position in global space + self.x, self.y = pl_geom.cyl_to_cart( + self.r, self.angle + rot, self.joint0.coord() + ) + + def get_constraints(self): + """Return the constraining distance and angle parameters.""" + return self.r, self.angle + + def set_constraints(self, distance=None, angle=None): + """Set geometric constraints. + + :param distance: (Default value = None) + :param angle: (Default value = None) + + """ + self.r, self.angle = distance or self.r, angle or self.angle + + def set_anchor0(self, joint, distance=None, angle=None): + """First joint anchor and characteristics. + + :param joint: + :param distance: (Default value = None) + :param angle: (Default value = None) + + """ + self.joint0 = joint + self.set_constraints(distance, angle) + + def set_anchor1(self, joint): + """Second joint anchor. + + :param joint: Joint to set as anchor + + """ + self.joint1 = joint diff --git a/pylinkage/joints/joint.py b/pylinkage/joints/joint.py new file mode 100644 index 0000000..a594e5d --- /dev/null +++ b/pylinkage/joints/joint.py @@ -0,0 +1,151 @@ +""" +Definition of the different joints used for pylinkage. +""" +import abc + + +def joint_syntax_parser(joint): + """ + Syntactic parser that understand a joint definition. + + :param joint: Input joint definition to be parsed. + :type joint: Joint | tuple[float, float] | None + + :return: New static joint definition if possible, or None. + :rtype: Joint | Static | None + """ + if joint is None or isinstance(joint, Joint): + return joint + return Static(*joint) + + +class Joint(abc.ABC): + """ + Geometric constraint expressed by two joints. + + Abstract class should always be inherited. + """ + + __slots__ = "x", "y", "joint0", "joint1", "name" + + def __init__(self, x=0, y=0, joint0=None, joint1=None, name=None): + """ + Create a Joint abstract object. + + :param x: Position on horizontal axis. The default is 0. + :type x: float | None + :param y: Position on vertical axis. The default is O. + :type y: float | None + :param name: Friendly name for human readability. Will default to object if None. + :type name: str | None + :param joint0: First linked joint (geometric constraints). The default is None. + :type joint0: Joint | tuple[float, float] | None + :param joint1: Other revolute joint linked. The default is None. + :type joint1: Joint | tuple[float, float] | None + """ + self.x, self.y = x, y + self.joint0 = joint_syntax_parser(joint0) + self.joint1 = joint_syntax_parser(joint1) + self.name = name + if name is None: + self.name = str(id(self)) + + def __repr__(self): + """Represent an object with class name, coordinates, name and state.""" + return "{}(x={}, y={}, name={})".format( + self.__class__.__name__, self.x, self.y, self.name + ) + + def __get_joints__(self): + """Return constraint joints as a tuple.""" + return self.joint0, self.joint1 + + def coord(self): + """ + Return cartesian coordinates. + + :rtype: tuple[float | None, float | None] + """ + return self.x, self.y + + def set_coord(self, *args): + """Take a sequence or two scalars, and assign them to object x, y. + + :param args: Coordinates to set, either as two elements or as a tuple of 2 elements + :type args: tuple[float, float] | tuple[tuple[float, float]] + + """ + if len(args) == 1: + self.x, self.y = args[0] + else: + self.x, self.y = args[0], args[1] + + @abc.abstractmethod + def get_constraints(self): + """Return geometric constraints applying to this Joint.""" + raise NotImplementedError( + "You can't call a constraint from an abstract class." + ) + + @abc.abstractmethod + def set_constraints(self, *args): + """Set geometric constraints applying to this Joint.""" + raise NotImplementedError( + "You can't set a constraint from an abstract class." + ) + + +class Static(Joint): + """Special case of Joint that should not move. + + Mostly used for the frame. + """ + + __slots__ = tuple() + + def __init__(self, x=0, y=0, name=None): + """ + A Static joint is a point in space to use as anchor by other joints. + + It is NOT a kind of joint as viewed in engineering terms! + + x : float, optional + Position on horizontal axis. The default is 0. + y : float, optional + Position on vertical axis. The default is O. + name : str, optional + Friendly name for human readability. The default is None. + """ + super().__init__(x, y, name=name) + + def reload(self): + """Do nothing, for consistency only.""" + pass + + def get_constraints(self): + """Return an empty tuple.""" + return tuple() + + def set_constraints(self, *args): + """Do nothing, for consistency only. + + :param args: Unused + + """ + pass + + def set_anchor0(self, joint): + """First joint anchor. + + :param joint: Joint to set as anchor. + + """ + self.joint0 = joint + + def set_anchor1(self, joint): + """Second joint anchor. + + :param joint: Joint to set as anchor. + + """ + self.joint1 = joint diff --git a/pylinkage/joints/linear.py b/pylinkage/joints/linear.py index 280c797..e9be0b7 100644 --- a/pylinkage/joints/linear.py +++ b/pylinkage/joints/linear.py @@ -1,7 +1,7 @@ """ Definition of a linear joint. """ -from ..interface import joint as pl_joint +from . import joint as pl_joint from ..interface import exceptions as pl_exceptions from .. import geometry as geom diff --git a/pylinkage/joints/revolute.py b/pylinkage/joints/revolute.py index c6ac84e..5cb01ee 100644 --- a/pylinkage/joints/revolute.py +++ b/pylinkage/joints/revolute.py @@ -9,7 +9,7 @@ from .. import geometry as pl_geom from ..interface import exceptions as pl_exceptions -from ..interface import joint as pl_joint +from . import joint as pl_joint class Revolute(pl_joint.Joint): @@ -18,14 +18,14 @@ class Revolute(pl_joint.Joint): __slots__ = "r0", "r1" def __init__( - self, - x=0, - y=0, - joint0=None, - joint1=None, - distance0=None, - distance1=None, - name=None + self, + x=0, + y=0, + joint0=None, + joint1=None, + distance0=None, + distance1=None, + name=None ): """ Set point position, parents, and if it is fixed for this turn. @@ -145,8 +145,6 @@ def set_anchor0(self, joint, distance=None): :type joint: Joint | tuple[float] :param distance: Distance to keep constant from the anchor. The default is None. :type distance: float - - """ self.joint0 = joint self.set_constraints(distance0=distance) diff --git a/pylinkage/joints/static.py b/pylinkage/joints/static.py new file mode 100644 index 0000000..203cc9c --- /dev/null +++ b/pylinkage/joints/static.py @@ -0,0 +1,54 @@ +""" +Static joint definition file. +""" + +from . import joint as pl_joint + + +class Static(pl_joint.Joint): + """Special case of Joint that should not move. + + Mostly used for the frame. + """ + + __slots__ = tuple() + + def __init__(self, x=0, y=0, name=None): + """ + A Static joint is a point in space to use as anchor by other joints. + + :param float x: Position on horizontal axis. The default is 0. + :param float y: Position on vertical axis. The default is 0. + :param name: Friendly name for human readability. The default is None. + :type name: str | None + """ + super().__init__(x, y, name=name) + + def reload(self): + """Do nothing, for consistency only.""" + pass + + def get_constraints(self): + """Return an empty tuple.""" + return tuple() + + def set_constraints(self, *args): + """Do nothing, for consistency only. + + :param args: Unused + """ + pass + + def set_anchor0(self, joint): + """First joint anchor. + + :param Joint joint: Other joint to join with. + """ + self.joint0 = joint + + def set_anchor1(self, joint): + """Second joint anchor. + + :param Joint joint: Other joint to join with. + """ + self.joint1 = joint diff --git a/tests/test_joints.py b/tests/test_joints.py index a55eec7..a113343 100644 --- a/tests/test_joints.py +++ b/tests/test_joints.py @@ -10,8 +10,7 @@ import math from pylinkage import UnbuildableError -from pylinkage.interface.joint import Fixed -from pylinkage.joints import Revolute +from pylinkage.joints import Revolute, Fixed class TestPivot(unittest.TestCase):