def intersection_beam(self, ray: Ray) -> List[Tuple[float, float]]:
        """
        Returns all couples :math:`(s, t)` such that there exist
        :math:`\\vec{X}` satisfying

        .. math::
            \\vec{X} = (1-s)^3 \\vec{p_0} + 3 s (1-s)^2 \\vec{p_1}
            + 3 s^2 (1-s) \\vec{p_2} + s^3 \\vec{p_3}
        and
        .. math::
            \\vec{X} = \\vec{o} + t \\vec{d}
        with :math:`0 \\lq s \\lq 1` and :math:`t >= 0`
        """

        p0, p1, p2, p3 = self._p
        a = orthogonal(ray.direction)
        a0 = np.dot(a, p0 - ray.origin)
        a1 = -3 * np.dot(a, p0 - p1)
        a2 = 3 * np.dot(a, p0 - 2 * p1 + p2)
        a3 = np.dot(a, -p0 + 3 * p1 - 3 * p2 + p3)
        roots = cubic_real_roots(a0, a1, a2, a3)
        intersection_points = [(1 - s)**3 * p0 + 3 * s * (1 - s)**2 * p1 +
                               3 * s**2 * (1 - s) * p2 + s**3 * p3
                               for s in roots]
        travel = [
            np.dot(X - ray.origin, ray.direction) for X in intersection_points
        ]

        def valid_domain(s, t):
            return 0 <= s <= 1 and t > Ray.min_travel

        return [(s, t) for (s, t) in zip(roots, travel) if valid_domain(s, t)]
    def normal(self, s: float) -> np.ndarray:
        """Returns a vector normal at the curve at curvilinear coordinate s"""

        return orthogonal(self.tangent(s))