Skip to content

Shapes

Factory class providing predefined DNA shape control points.

mdna.utils.Shapes

Factory for parametric 3-D curves used as DNA path control points.

Each class method returns an (n, 3) array of points tracing a named shape that can be passed directly to :class:~mdna.SplineFrames.

Source code in mdna/utils.py
class Shapes:
    """Factory for parametric 3-D curves used as DNA path control points.

    Each class method returns an ``(n, 3)`` array of points tracing a named
    shape that can be passed directly to :class:`~mdna.SplineFrames`.
    """

    def __init__(self, parametric_function, t_values=None, num_points=100):
        """Initialize a Shapes instance from a parametric function.

        Args:
            parametric_function (callable): ``f(t) -> (x, y, z)``.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.
        """
        self.num_points = num_points
        self.parametric_function = parametric_function
        self.points = self._generate_points(t_values)

    def _generate_points(self,t_values=None):
        x_values, y_values, z_values = self.parametric_function(t_values)
        return np.stack((x_values, y_values, z_values), axis=1)

    @classmethod
    def circle(cls, radius=1, t_values=None, num_points=100):
        """Create a circle in 3D space.

        Args:
            radius (float): Radius of the circle.
            t_values (numpy.ndarray, optional): Parameter values; defaults
                to ``linspace(0, 2π, num_points)``.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the circle, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)
        parametric_function = lambda t_values: (
            radius * np.cos(t_values),
            radius * np.sin(t_values),
            np.zeros_like(t_values)
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def line(cls, length=1, num_points=100):
        """Create a straight line along the x-axis.

        Args:
            length (float): Length of the line.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the line, shape ``(num_points, 3)``.
        """
        t_values = np.linspace(0, 1, num=num_points)
        parametric_function = lambda t_values: (
            t_values * length,
            np.zeros_like(t_values),
            np.zeros_like(t_values)
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def helix(cls, radius=1, pitch=1, height=1, num_turns=1, num_points=100):
        """Create a helical curve.

        Args:
            radius (float): Helix radius.
            pitch (float): Pitch factor (rise per turn).
            height (float): Overall height scaling.
            num_turns (int): Number of helical turns.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the helix, shape ``(num_points, 3)``.
        """
        t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
        parametric_function = lambda t_values: (
            radius * np.cos(t_values),
            radius * np.sin(t_values),
            height * t_values / (2 * np.pi) - pitch * num_turns * t_values / (2 * np.pi)
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def spiral(cls, radius=1, pitch=1, height=1, num_turns=1, num_points=100):
        """Create an expanding spiral (radius grows with *t*).

        Args:
            radius (float): Initial radius scaling.
            pitch (float): Pitch factor.
            height (float): Height scaling.
            num_turns (int): Number of turns.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the spiral, shape ``(num_points, 3)``.
        """
        t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
        parametric_function = lambda t_values: (
            radius * t_values * np.cos(t_values),
            radius * t_values * np.sin(t_values),
            height * t_values / (2 * np.pi) * pitch
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def mobius_strip(cls, radius=1, width=0.5, num_twists=1, t_values=None, num_points=100):
        """Create a Möbius strip centre-line.

        Args:
            radius (float): Major radius of the strip.
            width (float): Half-width of the strip.
            num_twists (int): Number of half-twists.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the Möbius strip, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)
        u_values = np.linspace(0, width, num=num_points)
        u, t = np.meshgrid(u_values, t_values)
        x_values = (radius + u * np.cos(t / 2) * np.cos(num_twists * t)) * np.cos(t)
        y_values = (radius + u * np.cos(t / 2) * np.cos(num_twists * t)) * np.sin(t)
        z_values = u * np.sin(t / 2) * np.cos(num_twists * t)
        parametric_function = lambda t_values: (
            x_values.flatten(),
            y_values.flatten(),
            z_values.flatten()
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def square(cls, side_length=1,t_values=None,num_points=100):
        """Create a square path (first definition — piecewise).

        Args:
            side_length (float): Length of each side.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the square, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 1, num=num_points)
        parametric_function = lambda t_values: (
            side_length * (2 * (t_values < 0.25) - 1),
            side_length * (2 * (t_values >= 0.25) & (t_values < 0.5)) - side_length,
            np.zeros_like(t_values)
        )
        return cls(parametric_function, t_values).points

    @classmethod
    def trefoil(cls, radius=1, num_turns=1,t_values=None,num_points=100):
        """Create a trefoil knot.

        Args:
            radius (float): Scaling factor.
            num_turns (int): Number of traversals around the knot.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the trefoil knot, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
        x_values = np.sin(t_values) + 2 * np.sin(2 * t_values)
        y_values = np.cos(t_values) - 2 * np.cos(2 * t_values)
        z_values = -np.sin(3 * t_values)
        parametric_function = lambda t_values: (
            radius * x_values,
            radius * y_values,
            radius * z_values
        )
        return cls(parametric_function, t_values).points

    @classmethod
    def square(cls, side=1, t_values=None,num_points=100):
        """Create a square path (segment-wise construction).

        Args:
            side (float): Length of each side.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the square, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 4, num=num_points)
        # Calculate x and y coordinates based on t_values
        x_values = np.zeros_like(t_values)
        y_values = np.zeros_like(t_values)
        for i, t in enumerate(t_values):
            if 0 <= t < 1:
                x_values[i] = t * side
                y_values[i] = 0
            elif 1 <= t < 2:
                x_values[i] = side
                y_values[i] = (t - 1) * side
            elif 2 <= t < 3:
                x_values[i] = (3 - t) * side
                y_values[i] = side
            elif 3 <= t <= 4:
                x_values[i] = 0
                y_values[i] = (4 - t) * side
        z_values = np.zeros_like(t_values)
        parametric_function = lambda t_values: (
            x_values,
            y_values,
            z_values
        )
        return cls(parametric_function, t_values).points

    @classmethod
    def heart(cls, a=1, b=1, c=1,t_values=None,num_points=100):
        """Create a heart-shaped curve.

        Args:
            a (float): Amplitude of the x-component.
            b (float): Amplitude of the first cosine term.
            c (float): Amplitude of the second cosine term.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the heart shape, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(-np.pi, np.pi, num=num_points)
        x_values = a * np.sin(t_values) ** 3
        y_values = b * np.cos(t_values) - c * np.cos(2 * t_values)
        z_values = np.zeros_like(t_values)
        parametric_function = lambda t_values: (x_values, y_values, z_values)
        return cls(parametric_function, t_values).points

    @classmethod
    def ellipse(cls, a=1, b=1, t_values=None,num_points=100):
        """Create an ellipse in the xy-plane.

        Args:
            a (float): Semi-major axis (x).
            b (float): Semi-minor axis (y).
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the ellipse, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)
        x_values = a * np.cos(t_values)
        y_values = b * np.sin(t_values)
        z_values = np.zeros_like(t_values)
        parametric_function = lambda t_values: (x_values, y_values, z_values)
        return cls(parametric_function, t_values).points

    @classmethod
    def lemniscate_of_bernoulli(cls, a=1, b=1, t_values=None,num_points=100):
        """Create a lemniscate of Bernoulli (figure-eight curve).

        Args:
            a (float): x-scaling factor.
            b (float): y-scaling factor.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the lemniscate, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)
        x_values = a * np.sqrt(2) * np.cos(t_values) / (np.sin(t_values) ** 2 + 1)
        y_values = b * np.sqrt(2) * np.cos(t_values) * np.sin(t_values) / (np.sin(t_values) ** 2 + 1)
        z_values = np.zeros_like(t_values)
        parametric_function = lambda t_values: (x_values, y_values, z_values)
        return cls(parametric_function, t_values).points

    @classmethod
    def torus_helix(cls, R=1, r=2, num_windings=3, t_values=None, num_points=100):
        """Create a helix wound around a torus.

        Args:
            R (float): Major radius of the torus.
            r (float): Minor radius (tube radius).
            num_windings (int): Number of windings around the torus.
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the torus helix, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)

        parametric_function = lambda t_values: (
            (R + r * np.cos(num_windings*t_values)) * np.cos( t_values),
            (R + r * np.cos(num_windings*t_values)) * np.sin( t_values),
            r * np.sin(t_values)
        )
        return cls(parametric_function, t_values, num_points=num_points).points

    @classmethod
    def bonus(cls, t_values=None,num_points=100):
        """Create a bonus (Batman) shape.

        Credit: https://www.geogebra.org/m/pH8wD3rW, Author: Simona Riva.

        Args:
            t_values (numpy.ndarray, optional): Parameter values.
            num_points (int): Number of sample points.

        Returns:
            points (numpy.ndarray): Points on the bonus shape, shape ``(num_points, 3)``.
        """
        if t_values is None:
            t_values = np.linspace(0, 2 * np.pi, num=num_points)
        t = t_values
        parametric_function = lambda t: (
                                        -(721*np.sin(t))/4 + 196/3*np.sin(2*t) - 86/3*np.sin(3*t) - 131/2*np.sin(4*t) + 477/14*np.sin(5*t) 
                                        + 27*np.sin(6*t) - 29/2*np.sin(7*t) + 68/5*np.sin(8*t) + 1/10*np.sin(9*t) + 23/4*np.sin(10*t) 
                                        - 19/2*np.sin(12*t) - 85/21*np.sin(13*t) + 2/3*np.sin(14*t) + 27/5*np.sin(15*t) + 7/4*np.sin(16*t) 
                                        + 17/9*np.sin(17*t) - 4*np.sin(18*t) - 1/2*np.sin(19*t) + 1/6*np.sin(20*t) + 6/7*np.sin(21*t) 
                                        - 1/8*np.sin(22*t) + 1/3*np.sin(23*t) + 3/2*np.sin(24*t) + 13/5*np.sin(25*t) + np.sin(26*t) 
                                        - 2*np.sin(27*t) + 3/5*np.sin(28*t) - 1/5*np.sin(29*t) + 1/5*np.sin(30*t) + (2337*np.cos(t))/8 
                                        - 43/5*np.cos(2*t) + 322/5*np.cos(3*t) - 117/5*np.cos(4*t) - 26/5*np.cos(5*t) - 23/3*np.cos(6*t) 
                                        + 143/4*np.cos(7*t) - 11/4*np.cos(8*t) - 31/3*np.cos(9*t) - 13/4*np.cos(10*t) - 9/2*np.cos(11*t) 
                                        + 41/20*np.cos(12*t) + 8*np.cos(13*t) + 2/3*np.cos(14*t) + 6*np.cos(15*t) + 17/4*np.cos(16*t) 
                                        - 3/2*np.cos(17*t) - 29/10*np.cos(18*t) + 11/6*np.cos(19*t) + 12/5*np.cos(20*t) + 3/2*np.cos(21*t) 
                                        + 11/12*np.cos(22*t) - 4/5*np.cos(23*t) + np.cos(24*t) + 17/8*np.cos(25*t) - 7/2*np.cos(26*t) 
                                        - 5/6*np.cos(27*t) - 11/10*np.cos(28*t) + 1/2*np.cos(29*t) - 1/5*np.cos(30*t),
                                        -(637/2)*np.sin(t) - (188/5)*np.sin(2*t) - (11/7)*np.sin(3*t) - (12/5)*np.sin(4*t) + (11/3)*np.sin(5*t)
                                        - (37/4)*np.sin(6*t) + (8/3)*np.sin(7*t) + (65/6)*np.sin(8*t) - (32/5)*np.sin(9*t) - (41/4)*np.sin(10*t)
                                        - (38/3)*np.sin(11*t) - (47/8)*np.sin(12*t) + (5/4)*np.sin(13*t) - (41/7)*np.sin(14*t) - (7/3)*np.sin(15*t)
                                        - (13/7)*np.sin(16*t) + (17/4)*np.sin(17*t) - (9/4)*np.sin(18*t) + (8/9)*np.sin(19*t) + (3/5)*np.sin(20*t)
                                        - (2/5)*np.sin(21*t) + (4/3)*np.sin(22*t) + (1/3)*np.sin(23*t) + (3/5)*np.sin(24*t) - (3/5)*np.sin(25*t)
                                        + (6/5)*np.sin(26*t) - (1/5)*np.sin(27*t) + (10/9)*np.sin(28*t) + (1/3)*np.sin(29*t) - (3/4)*np.sin(30*t)
                                        - (125/2)*np.cos(t) - (521/9)*np.cos(2*t) - (359/3)*np.cos(3*t) + (47/3)*np.cos(4*t) - (33/2)*np.cos(5*t)
                                        - (5/4)*np.cos(6*t) + (31/8)*np.cos(7*t) + (9/10)*np.cos(8*t) - (119/4)*np.cos(9*t) - (17/2)*np.cos(10*t)
                                        + (22/3)*np.cos(11*t) + (15/4)*np.cos(12*t) - (5/2)*np.cos(13*t) + (19/6)*np.cos(14*t) + (7/4)*np.cos(15*t)
                                        + (31/4)*np.cos(16*t) - np.cos(17*t) + (11/10)*np.cos(18*t) - (2/3)*np.cos(19*t) + (13/3)*np.cos(20*t)
                                        - (5/4)*np.cos(21*t) + (2/3)*np.cos(22*t) + (1/4)*np.cos(23*t) + (5/6)*np.cos(24*t) + (3/4)*np.cos(26*t)
                                        - (1/2)*np.cos(27*t) - (1/10)*np.cos(28*t) - (1/3)*np.cos(29*t) - (1/19)*np.cos(30*t),
                                        np.zeros_like(t))
        return cls(parametric_function, t_values).points*0.1

__init__(parametric_function, t_values=None, num_points=100)

Initialize a Shapes instance from a parametric function.

Parameters:

Name Type Description Default
parametric_function callable

f(t) -> (x, y, z).

required
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100
Source code in mdna/utils.py
def __init__(self, parametric_function, t_values=None, num_points=100):
    """Initialize a Shapes instance from a parametric function.

    Args:
        parametric_function (callable): ``f(t) -> (x, y, z)``.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.
    """
    self.num_points = num_points
    self.parametric_function = parametric_function
    self.points = self._generate_points(t_values)

bonus(t_values=None, num_points=100) classmethod

Create a bonus (Batman) shape.

Credit: https://www.geogebra.org/m/pH8wD3rW, Author: Simona Riva.

Parameters:

Name Type Description Default
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the bonus shape, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def bonus(cls, t_values=None,num_points=100):
    """Create a bonus (Batman) shape.

    Credit: https://www.geogebra.org/m/pH8wD3rW, Author: Simona Riva.

    Args:
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the bonus shape, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)
    t = t_values
    parametric_function = lambda t: (
                                    -(721*np.sin(t))/4 + 196/3*np.sin(2*t) - 86/3*np.sin(3*t) - 131/2*np.sin(4*t) + 477/14*np.sin(5*t) 
                                    + 27*np.sin(6*t) - 29/2*np.sin(7*t) + 68/5*np.sin(8*t) + 1/10*np.sin(9*t) + 23/4*np.sin(10*t) 
                                    - 19/2*np.sin(12*t) - 85/21*np.sin(13*t) + 2/3*np.sin(14*t) + 27/5*np.sin(15*t) + 7/4*np.sin(16*t) 
                                    + 17/9*np.sin(17*t) - 4*np.sin(18*t) - 1/2*np.sin(19*t) + 1/6*np.sin(20*t) + 6/7*np.sin(21*t) 
                                    - 1/8*np.sin(22*t) + 1/3*np.sin(23*t) + 3/2*np.sin(24*t) + 13/5*np.sin(25*t) + np.sin(26*t) 
                                    - 2*np.sin(27*t) + 3/5*np.sin(28*t) - 1/5*np.sin(29*t) + 1/5*np.sin(30*t) + (2337*np.cos(t))/8 
                                    - 43/5*np.cos(2*t) + 322/5*np.cos(3*t) - 117/5*np.cos(4*t) - 26/5*np.cos(5*t) - 23/3*np.cos(6*t) 
                                    + 143/4*np.cos(7*t) - 11/4*np.cos(8*t) - 31/3*np.cos(9*t) - 13/4*np.cos(10*t) - 9/2*np.cos(11*t) 
                                    + 41/20*np.cos(12*t) + 8*np.cos(13*t) + 2/3*np.cos(14*t) + 6*np.cos(15*t) + 17/4*np.cos(16*t) 
                                    - 3/2*np.cos(17*t) - 29/10*np.cos(18*t) + 11/6*np.cos(19*t) + 12/5*np.cos(20*t) + 3/2*np.cos(21*t) 
                                    + 11/12*np.cos(22*t) - 4/5*np.cos(23*t) + np.cos(24*t) + 17/8*np.cos(25*t) - 7/2*np.cos(26*t) 
                                    - 5/6*np.cos(27*t) - 11/10*np.cos(28*t) + 1/2*np.cos(29*t) - 1/5*np.cos(30*t),
                                    -(637/2)*np.sin(t) - (188/5)*np.sin(2*t) - (11/7)*np.sin(3*t) - (12/5)*np.sin(4*t) + (11/3)*np.sin(5*t)
                                    - (37/4)*np.sin(6*t) + (8/3)*np.sin(7*t) + (65/6)*np.sin(8*t) - (32/5)*np.sin(9*t) - (41/4)*np.sin(10*t)
                                    - (38/3)*np.sin(11*t) - (47/8)*np.sin(12*t) + (5/4)*np.sin(13*t) - (41/7)*np.sin(14*t) - (7/3)*np.sin(15*t)
                                    - (13/7)*np.sin(16*t) + (17/4)*np.sin(17*t) - (9/4)*np.sin(18*t) + (8/9)*np.sin(19*t) + (3/5)*np.sin(20*t)
                                    - (2/5)*np.sin(21*t) + (4/3)*np.sin(22*t) + (1/3)*np.sin(23*t) + (3/5)*np.sin(24*t) - (3/5)*np.sin(25*t)
                                    + (6/5)*np.sin(26*t) - (1/5)*np.sin(27*t) + (10/9)*np.sin(28*t) + (1/3)*np.sin(29*t) - (3/4)*np.sin(30*t)
                                    - (125/2)*np.cos(t) - (521/9)*np.cos(2*t) - (359/3)*np.cos(3*t) + (47/3)*np.cos(4*t) - (33/2)*np.cos(5*t)
                                    - (5/4)*np.cos(6*t) + (31/8)*np.cos(7*t) + (9/10)*np.cos(8*t) - (119/4)*np.cos(9*t) - (17/2)*np.cos(10*t)
                                    + (22/3)*np.cos(11*t) + (15/4)*np.cos(12*t) - (5/2)*np.cos(13*t) + (19/6)*np.cos(14*t) + (7/4)*np.cos(15*t)
                                    + (31/4)*np.cos(16*t) - np.cos(17*t) + (11/10)*np.cos(18*t) - (2/3)*np.cos(19*t) + (13/3)*np.cos(20*t)
                                    - (5/4)*np.cos(21*t) + (2/3)*np.cos(22*t) + (1/4)*np.cos(23*t) + (5/6)*np.cos(24*t) + (3/4)*np.cos(26*t)
                                    - (1/2)*np.cos(27*t) - (1/10)*np.cos(28*t) - (1/3)*np.cos(29*t) - (1/19)*np.cos(30*t),
                                    np.zeros_like(t))
    return cls(parametric_function, t_values).points*0.1

circle(radius=1, t_values=None, num_points=100) classmethod

Create a circle in 3D space.

Parameters:

Name Type Description Default
radius float

Radius of the circle.

1
t_values ndarray

Parameter values; defaults to linspace(0, 2π, num_points).

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the circle, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def circle(cls, radius=1, t_values=None, num_points=100):
    """Create a circle in 3D space.

    Args:
        radius (float): Radius of the circle.
        t_values (numpy.ndarray, optional): Parameter values; defaults
            to ``linspace(0, 2π, num_points)``.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the circle, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)
    parametric_function = lambda t_values: (
        radius * np.cos(t_values),
        radius * np.sin(t_values),
        np.zeros_like(t_values)
    )
    return cls(parametric_function, t_values, num_points=num_points).points

ellipse(a=1, b=1, t_values=None, num_points=100) classmethod

Create an ellipse in the xy-plane.

Parameters:

Name Type Description Default
a float

Semi-major axis (x).

1
b float

Semi-minor axis (y).

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the ellipse, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def ellipse(cls, a=1, b=1, t_values=None,num_points=100):
    """Create an ellipse in the xy-plane.

    Args:
        a (float): Semi-major axis (x).
        b (float): Semi-minor axis (y).
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the ellipse, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)
    x_values = a * np.cos(t_values)
    y_values = b * np.sin(t_values)
    z_values = np.zeros_like(t_values)
    parametric_function = lambda t_values: (x_values, y_values, z_values)
    return cls(parametric_function, t_values).points

heart(a=1, b=1, c=1, t_values=None, num_points=100) classmethod

Create a heart-shaped curve.

Parameters:

Name Type Description Default
a float

Amplitude of the x-component.

1
b float

Amplitude of the first cosine term.

1
c float

Amplitude of the second cosine term.

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the heart shape, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def heart(cls, a=1, b=1, c=1,t_values=None,num_points=100):
    """Create a heart-shaped curve.

    Args:
        a (float): Amplitude of the x-component.
        b (float): Amplitude of the first cosine term.
        c (float): Amplitude of the second cosine term.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the heart shape, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(-np.pi, np.pi, num=num_points)
    x_values = a * np.sin(t_values) ** 3
    y_values = b * np.cos(t_values) - c * np.cos(2 * t_values)
    z_values = np.zeros_like(t_values)
    parametric_function = lambda t_values: (x_values, y_values, z_values)
    return cls(parametric_function, t_values).points

helix(radius=1, pitch=1, height=1, num_turns=1, num_points=100) classmethod

Create a helical curve.

Parameters:

Name Type Description Default
radius float

Helix radius.

1
pitch float

Pitch factor (rise per turn).

1
height float

Overall height scaling.

1
num_turns int

Number of helical turns.

1
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the helix, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def helix(cls, radius=1, pitch=1, height=1, num_turns=1, num_points=100):
    """Create a helical curve.

    Args:
        radius (float): Helix radius.
        pitch (float): Pitch factor (rise per turn).
        height (float): Overall height scaling.
        num_turns (int): Number of helical turns.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the helix, shape ``(num_points, 3)``.
    """
    t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
    parametric_function = lambda t_values: (
        radius * np.cos(t_values),
        radius * np.sin(t_values),
        height * t_values / (2 * np.pi) - pitch * num_turns * t_values / (2 * np.pi)
    )
    return cls(parametric_function, t_values, num_points=num_points).points

lemniscate_of_bernoulli(a=1, b=1, t_values=None, num_points=100) classmethod

Create a lemniscate of Bernoulli (figure-eight curve).

Parameters:

Name Type Description Default
a float

x-scaling factor.

1
b float

y-scaling factor.

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the lemniscate, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def lemniscate_of_bernoulli(cls, a=1, b=1, t_values=None,num_points=100):
    """Create a lemniscate of Bernoulli (figure-eight curve).

    Args:
        a (float): x-scaling factor.
        b (float): y-scaling factor.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the lemniscate, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)
    x_values = a * np.sqrt(2) * np.cos(t_values) / (np.sin(t_values) ** 2 + 1)
    y_values = b * np.sqrt(2) * np.cos(t_values) * np.sin(t_values) / (np.sin(t_values) ** 2 + 1)
    z_values = np.zeros_like(t_values)
    parametric_function = lambda t_values: (x_values, y_values, z_values)
    return cls(parametric_function, t_values).points

line(length=1, num_points=100) classmethod

Create a straight line along the x-axis.

Parameters:

Name Type Description Default
length float

Length of the line.

1
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the line, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def line(cls, length=1, num_points=100):
    """Create a straight line along the x-axis.

    Args:
        length (float): Length of the line.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the line, shape ``(num_points, 3)``.
    """
    t_values = np.linspace(0, 1, num=num_points)
    parametric_function = lambda t_values: (
        t_values * length,
        np.zeros_like(t_values),
        np.zeros_like(t_values)
    )
    return cls(parametric_function, t_values, num_points=num_points).points

mobius_strip(radius=1, width=0.5, num_twists=1, t_values=None, num_points=100) classmethod

Create a Möbius strip centre-line.

Parameters:

Name Type Description Default
radius float

Major radius of the strip.

1
width float

Half-width of the strip.

0.5
num_twists int

Number of half-twists.

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the Möbius strip, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def mobius_strip(cls, radius=1, width=0.5, num_twists=1, t_values=None, num_points=100):
    """Create a Möbius strip centre-line.

    Args:
        radius (float): Major radius of the strip.
        width (float): Half-width of the strip.
        num_twists (int): Number of half-twists.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the Möbius strip, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)
    u_values = np.linspace(0, width, num=num_points)
    u, t = np.meshgrid(u_values, t_values)
    x_values = (radius + u * np.cos(t / 2) * np.cos(num_twists * t)) * np.cos(t)
    y_values = (radius + u * np.cos(t / 2) * np.cos(num_twists * t)) * np.sin(t)
    z_values = u * np.sin(t / 2) * np.cos(num_twists * t)
    parametric_function = lambda t_values: (
        x_values.flatten(),
        y_values.flatten(),
        z_values.flatten()
    )
    return cls(parametric_function, t_values, num_points=num_points).points

spiral(radius=1, pitch=1, height=1, num_turns=1, num_points=100) classmethod

Create an expanding spiral (radius grows with t).

Parameters:

Name Type Description Default
radius float

Initial radius scaling.

1
pitch float

Pitch factor.

1
height float

Height scaling.

1
num_turns int

Number of turns.

1
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the spiral, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def spiral(cls, radius=1, pitch=1, height=1, num_turns=1, num_points=100):
    """Create an expanding spiral (radius grows with *t*).

    Args:
        radius (float): Initial radius scaling.
        pitch (float): Pitch factor.
        height (float): Height scaling.
        num_turns (int): Number of turns.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the spiral, shape ``(num_points, 3)``.
    """
    t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
    parametric_function = lambda t_values: (
        radius * t_values * np.cos(t_values),
        radius * t_values * np.sin(t_values),
        height * t_values / (2 * np.pi) * pitch
    )
    return cls(parametric_function, t_values, num_points=num_points).points

square(side=1, t_values=None, num_points=100) classmethod

Create a square path (segment-wise construction).

Parameters:

Name Type Description Default
side float

Length of each side.

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the square, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def square(cls, side=1, t_values=None,num_points=100):
    """Create a square path (segment-wise construction).

    Args:
        side (float): Length of each side.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the square, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 4, num=num_points)
    # Calculate x and y coordinates based on t_values
    x_values = np.zeros_like(t_values)
    y_values = np.zeros_like(t_values)
    for i, t in enumerate(t_values):
        if 0 <= t < 1:
            x_values[i] = t * side
            y_values[i] = 0
        elif 1 <= t < 2:
            x_values[i] = side
            y_values[i] = (t - 1) * side
        elif 2 <= t < 3:
            x_values[i] = (3 - t) * side
            y_values[i] = side
        elif 3 <= t <= 4:
            x_values[i] = 0
            y_values[i] = (4 - t) * side
    z_values = np.zeros_like(t_values)
    parametric_function = lambda t_values: (
        x_values,
        y_values,
        z_values
    )
    return cls(parametric_function, t_values).points

torus_helix(R=1, r=2, num_windings=3, t_values=None, num_points=100) classmethod

Create a helix wound around a torus.

Parameters:

Name Type Description Default
R float

Major radius of the torus.

1
r float

Minor radius (tube radius).

2
num_windings int

Number of windings around the torus.

3
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the torus helix, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def torus_helix(cls, R=1, r=2, num_windings=3, t_values=None, num_points=100):
    """Create a helix wound around a torus.

    Args:
        R (float): Major radius of the torus.
        r (float): Minor radius (tube radius).
        num_windings (int): Number of windings around the torus.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the torus helix, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, 2 * np.pi, num=num_points)

    parametric_function = lambda t_values: (
        (R + r * np.cos(num_windings*t_values)) * np.cos( t_values),
        (R + r * np.cos(num_windings*t_values)) * np.sin( t_values),
        r * np.sin(t_values)
    )
    return cls(parametric_function, t_values, num_points=num_points).points

trefoil(radius=1, num_turns=1, t_values=None, num_points=100) classmethod

Create a trefoil knot.

Parameters:

Name Type Description Default
radius float

Scaling factor.

1
num_turns int

Number of traversals around the knot.

1
t_values ndarray

Parameter values.

None
num_points int

Number of sample points.

100

Returns:

Name Type Description
points ndarray

Points on the trefoil knot, shape (num_points, 3).

Source code in mdna/utils.py
@classmethod
def trefoil(cls, radius=1, num_turns=1,t_values=None,num_points=100):
    """Create a trefoil knot.

    Args:
        radius (float): Scaling factor.
        num_turns (int): Number of traversals around the knot.
        t_values (numpy.ndarray, optional): Parameter values.
        num_points (int): Number of sample points.

    Returns:
        points (numpy.ndarray): Points on the trefoil knot, shape ``(num_points, 3)``.
    """
    if t_values is None:
        t_values = np.linspace(0, num_turns * 2 * np.pi, num=num_points)
    x_values = np.sin(t_values) + 2 * np.sin(2 * t_values)
    y_values = np.cos(t_values) - 2 * np.cos(2 * t_values)
    z_values = -np.sin(3 * t_values)
    parametric_function = lambda t_values: (
        radius * x_values,
        radius * y_values,
        radius * z_values
    )
    return cls(parametric_function, t_values).points