def paperclip_shape_rotate_midsegment(h, params):
    """
    This move rotates the midsegment of h, keeping the other segments fixed.

    Because we constrain midsegment to be aligned with the x axis, this rotation is implemented by rotating the other
    segments. Crucially, we want it to look like only the midsegment is rotated; so we rotate the viewpoint to do that.
    """
    hp = h.copy()

    # if the object has only a midsegment
    if hp.joint_count == 2:
        return hp, 1.0, 1.0

    # get random rotation axis and angle
    theta = np.random.rand() * 360.0 - 180.0
    phi = np.random.rand() * 180.0
    rotation_axis = geom_3d.spherical_to_cartesian((1.0, theta, phi))

    kappa = 1 / (params['ROTATE_MIDSEGMENT_VARIANCE'] * np.pi**2 / 180**2)
    rotation_angle = np.random.vonmises(0.0, kappa) * 180.0 / np.pi

    # rotate midsegment
    try:  # we might not be able to rotate midsegment by rotation_angle around rotation_axis
        hp.rotate_midsegment(rotation_axis, rotation_angle)
    except ValueError:
        return hp, 1.0, 1.0

    # rotate each viewpoint to correct for the rotation of the joints (we want only the midsegment to change how it
    # looks)
    for i in range(len(hp.viewpoint)):
        vp_cartesian = geom_3d.spherical_to_cartesian(hp.viewpoint[i])
        new_vp_cartesian = geom_3d.rotate_axis_angle(vp_cartesian, rotation_axis, -rotation_angle)
        hp.viewpoint[i] = geom_3d.cartesian_to_spherical(new_vp_cartesian)

    return hp, 1.0, 1.0
    def test_change_viewpoint(self):
        vp1 = np.array((1.0, 0.0, 0.0))
        vp2 = np.array((2.0, 150.0, 0.0))
        h = hyp.I3DHypothesis(forward_model=None,
                              viewpoint=[vp1, vp2],
                              params=None)

        for i in range(1000):
            hp, p0, p1 = change_viewpoint(
                h,
                {'CHANGE_VIEWPOINT_VARIANCE': np.square(5.0 / 180.0 * np.pi)})
            self.assertAlmostEqual(p0, 1.0)
            self.assertAlmostEqual(p1, 1.0)

            vp = hp.viewpoint[0]
            self.assertAlmostEqual(vp[0], 1.0)
            # because the variance is 1.0, we would expect the angle between vectors to be small
            self.assertTrue(
                geom_3d.angle_between_vectors(
                    geom_3d.spherical_to_cartesian(vp),
                    geom_3d.spherical_to_cartesian(vp1)) < 20.0)

            vp = hp.viewpoint[1]
            self.assertAlmostEqual(vp[0], 2.0)
            self.assertTrue(
                geom_3d.angle_between_vectors(
                    geom_3d.spherical_to_cartesian(vp),
                    geom_3d.spherical_to_cartesian(vp2)) < 20.0)
    def test_change_viewpoint(self):
        vp1 = np.array((1.0, 0.0, 0.0))
        vp2 = np.array((2.0, 150.0, 0.0))
        h = hyp.I3DHypothesis(forward_model=None, viewpoint=[vp1, vp2], params=None)

        for i in range(1000):
            hp, p0, p1 = change_viewpoint(h, {'CHANGE_VIEWPOINT_VARIANCE': np.square(5.0 / 180.0 * np.pi)})
            self.assertAlmostEqual(p0, 1.0)
            self.assertAlmostEqual(p1, 1.0)

            vp = hp.viewpoint[0]
            self.assertAlmostEqual(vp[0], 1.0)
            # because the variance is 1.0, we would expect the angle between vectors to be small
            self.assertTrue(geom_3d.angle_between_vectors(geom_3d.spherical_to_cartesian(vp),
                                                          geom_3d.spherical_to_cartesian(vp1)) < 20.0)

            vp = hp.viewpoint[1]
            self.assertAlmostEqual(vp[0], 2.0)
            self.assertTrue(geom_3d.angle_between_vectors(geom_3d.spherical_to_cartesian(vp),
                                                          geom_3d.spherical_to_cartesian(vp2)) < 20.0)
def _get_random_vector_along(z_vector, min_angle=30.0, max_angle=180.0):
    """
    Get a random vector that makes more than min_angles and less than max_angles degrees with the `z_vector`.

    This method is used by ``paperclip_shape_add_remove_joint`` move. Note that the angle between the returned vector
    and the -z_vector (NEGATIVE z_vector, not the z_vector) will be in (min_angle, max_angle). If we add such a vector
    to z_vector, the angle between z_vector and the new vector will be in (min_angle, max_angle).
    """
    if max_angle < min_angle:
        raise ValueError("Maximum angle cannot be smaller than minimum angle.")

    max_phi = 180.0 - min_angle
    min_phi = 180.0 - max_angle
    phi = min_phi + (np.random.rand() * (max_phi - min_phi))
    theta = np.random.rand() * 360.0
    coords = geom_3d.spherical_to_cartesian((1.0, theta, phi))
    
    v = geom_3d.rotate_vector_by_vector(coords, old_z=np.array([0., 0., 1.]), new_z=z_vector)
    return v