Exemplo n.º 1
0
def test_combinations():
    # Test some particular combinations of coord sys transformations

    # +90 around x, followed by shift to (1, 0, 0)
    # equivalent to shift first, and then rotation +90 about x
    # latter rotation can be either global or local, since origin is unaffected
    # (1, 0, 0) -> (1, 0, 0)
    # by convention for local rotation
    # and by coincidence (origin is on xaxis) for global rotation
    coordSys = batoid.CoordSys()
    coordSys1 = coordSys.rotateGlobal(batoid.RotX(np.pi / 2)).shiftGlobal(
        [1, 0, 0])
    coordSys2 = coordSys.shiftGlobal([1, 0,
                                      0]).rotateLocal(batoid.RotX(np.pi / 2))
    coordSys3 = coordSys.shiftGlobal([1, 0,
                                      0]).rotateGlobal(batoid.RotX(np.pi / 2))
    np.testing.assert_allclose(coordSys1.origin, coordSys2.origin)
    np.testing.assert_allclose(coordSys1.rot, coordSys2.rot)
    np.testing.assert_allclose(coordSys1.origin, coordSys3.origin)
    np.testing.assert_allclose(coordSys1.rot, coordSys3.rot)

    # +45 around x, followed by sqrt(2) in new y direction, which is the (0, 1, 1)
    # direction in global coords, followed by -45 about x.
    # Should be parallel to global coords, but origin at (0, 1, 1)
    coordSys1 = (batoid.CoordSys().rotateGlobal(batoid.RotX(
        np.pi / 4)).shiftLocal([0, np.sqrt(2),
                                0]).rotateLocal(batoid.RotX(-np.pi / 4)))
    coordSys2 = batoid.CoordSys().shiftLocal([0, 1, 1])
    np.testing.assert_allclose(coordSys1.origin,
                               coordSys2.origin,
                               rtol=0,
                               atol=1e-15)
    np.testing.assert_allclose(coordSys1.rot,
                               coordSys2.rot,
                               rtol=0,
                               atol=1e-15)

    # rotate +90 around point (1, 0, 0) with rot axis parallel to y axis.
    # moves origin from (0, 0, 0) to (1, 0, 1)
    coordSys = batoid.CoordSys()
    coordSys1 = coordSys.rotateGlobal(batoid.RotY(np.pi / 2), [1, 0, 0])
    coordSys2 = coordSys.rotateGlobal(batoid.RotY(np.pi / 2)).shiftGlobal(
        [1, 0, 1])
    # local coords of global (1, 0, 1) are (-1, 0, 1)
    coordSys3 = coordSys.rotateGlobal(batoid.RotY(np.pi / 2)).shiftLocal(
        [-1, 0, 1])
    np.testing.assert_allclose(coordSys1.origin, coordSys2.origin)
    np.testing.assert_allclose(coordSys1.rot, coordSys2.rot)
    np.testing.assert_allclose(coordSys1.origin, coordSys3.origin)
    np.testing.assert_allclose(coordSys1.rot, coordSys3.rot)
Exemplo n.º 2
0
def test_asphere_reflection_coordtransform():
    import random
    random.seed(5772156)
    asphere = batoid.Asphere(23.0, -0.97, [1e-5, 1e-6])
    for i in range(1000):
        x = random.gauss(0, 1)
        y = random.gauss(0, 1)
        vx = random.gauss(0, 1e-1)
        vy = random.gauss(0, 1e-1)
        v = np.array([vx, vy, 1])
        v /= np.linalg.norm(v)
        ray = batoid.Ray([x, y, -0.1], v, 0)
        ray2 = ray.copy()

        cs = batoid.CoordSys(
            [random.uniform(-0.01, 0.01),
             random.uniform(-0.01, 0.01),
             random.uniform(-0.01, 0.01)],
            (batoid.RotX(random.uniform(-0.01, 0.01))
             .dot(batoid.RotY(random.uniform(-0.01, 0.01)))
             .dot(batoid.RotZ(random.uniform(-0.01, 0.01)))
            )
        )

        rray = asphere.reflect(ray, coordSys=cs)
        rray2 = asphere.reflect(ray2.toCoordSys(cs))
        assert ray_isclose(rray, rray2)
Exemplo n.º 3
0
def test_rotation():
    try:
        import galsim
    except ImportError:
        print("optic rotation test requires GalSim")
        return

    np.random.seed(57)

    telescope = batoid.Optic.fromYaml("HSC.yaml")

    rot = batoid.RotX(np.random.uniform(low=0.0, high=2*np.pi))
    rot = rot.dot(batoid.RotY(np.random.uniform(low=0.0, high=2*np.pi)))
    rot = rot.dot(batoid.RotZ(np.random.uniform(low=0.0, high=2*np.pi)))
    rotInv = np.linalg.inv(rot)

    # It's hard to test the two telescopes for equality due to rounding errors, so we test by
    # comparing zernikes
    rotTel = telescope.withLocalRotation(rot).withLocalRotation(rotInv)

    theta_x = np.random.uniform(-0.005, 0.005)
    theta_y = np.random.uniform(-0.005, 0.005)
    wavelength = 750e-9

    np.testing.assert_allclose(
        batoid.psf.zernike(telescope, theta_x, theta_y, wavelength),
        batoid.psf.zernike(rotTel, theta_x, theta_y, wavelength),
        atol=1e-5
    )

    for item in telescope.itemDict:
        rotTel = telescope.withLocallyRotatedOptic(item, rot)
        rotTel = rotTel.withLocallyRotatedOptic(item, rotInv)
        rotTel2 = telescope.withLocallyRotatedOptic(item, np.eye(3))
        theta_x = np.random.uniform(-0.005, 0.005)
        theta_y = np.random.uniform(-0.005, 0.005)
        np.testing.assert_allclose(
            batoid.psf.zernike(telescope, theta_x, theta_y, wavelength),
            batoid.psf.zernike(rotTel, theta_x, theta_y, wavelength),
            atol=1e-5
        )
        np.testing.assert_allclose(
            batoid.psf.zernike(telescope, theta_x, theta_y, wavelength),
            batoid.psf.zernike(rotTel2, theta_x, theta_y, wavelength),
            atol=1e-5
        )
    # Test with non-fully-qualified name
    rotTel = telescope.withLocallyRotatedOptic('G1', rot)
    rotTel = rotTel.withLocallyRotatedOptic('G1', rotInv)
    rotTel2 = rotTel.withLocallyRotatedOptic('G1', np.eye(3))
    np.testing.assert_allclose(
        batoid.psf.zernike(telescope, theta_x, theta_y, wavelength),
        batoid.psf.zernike(rotTel, theta_x, theta_y, wavelength),
        atol=1e-5
    )
    np.testing.assert_allclose(
        batoid.psf.zernike(telescope, theta_x, theta_y, wavelength),
        batoid.psf.zernike(rotTel2, theta_x, theta_y, wavelength),
        atol=1e-5
    )
Exemplo n.º 4
0
def randomCoordSys():
    import random
    return batoid.CoordSys(
        randomVec3(),
        (batoid.RotX(random.uniform(0, 1))
         .dot(batoid.RotY(random.uniform(0, 1)))
         .dot(batoid.RotZ(random.uniform(0, 1))))
    )
Exemplo n.º 5
0
def test_simple_transform():
    rng = np.random.default_rng(5)
    size = 10_000

    # Worked a few examples out manually
    # Example 1
    coordSys1 = batoid.CoordSys()
    coordSys2 = batoid.CoordSys().shiftGlobal([0, 0, 1])
    rv = randomRayVector(rng, size, coordSys1)
    transform = batoid.CoordTransform(coordSys1, coordSys2)
    rv2 = batoid.applyForwardTransform(transform, rv.copy())
    np.testing.assert_allclose(rv.x, rv2.x)
    np.testing.assert_allclose(rv.y, rv2.y)
    np.testing.assert_allclose(rv.z - 1, rv2.z)
    # Repeat using toCoordSys
    rv3 = rv.copy().toCoordSys(coordSys2)
    np.testing.assert_allclose(rv.x, rv3.x)
    np.testing.assert_allclose(rv.y, rv3.y)
    np.testing.assert_allclose(rv.z - 1, rv3.z)
    # Transform of numpy array
    x, y, z = transform.applyForwardArray(rv.x, rv.y, rv.z)
    np.testing.assert_allclose(rv2.x, x)
    np.testing.assert_allclose(rv2.y, y)
    np.testing.assert_allclose(rv2.z, z)

    # Example 2
    # First for a single specific point I worked out
    coordSys1 = batoid.CoordSys()
    coordSys2 = batoid.CoordSys(origin=[1, 1, 1], rot=batoid.RotY(np.pi / 2))
    x = y = z = np.array([2])
    vx = vy = vz = np.array([0])
    rv = batoid.RayVector(x, y, z, vx, vy, vz)
    transform = batoid.CoordTransform(coordSys1, coordSys2)
    rv2 = batoid.applyForwardTransform(transform, rv.copy())
    np.testing.assert_allclose(rv2.r, [[-1, 1, 1]])
    # Transform of numpy array
    x, y, z = transform.applyForwardArray(rv.x, rv.y, rv.z)
    np.testing.assert_allclose(rv2.x, x)
    np.testing.assert_allclose(rv2.y, y)
    np.testing.assert_allclose(rv2.z, z)

    # Here's the generalization
    # Also using alternate syntax for applyForward here.
    rv = randomRayVector(rng, size, coordSys1)
    rv2 = transform.applyForward(rv.copy())
    np.testing.assert_allclose(rv2.x, 1 - rv.z)
    np.testing.assert_allclose(rv2.y, rv.y - 1)
    np.testing.assert_allclose(rv2.z, rv.x - 1)
    rv3 = rv.copy().toCoordSys(coordSys2)
    np.testing.assert_allclose(rv3.x, 1 - rv.z)
    np.testing.assert_allclose(rv3.y, rv.y - 1)
    np.testing.assert_allclose(rv3.z, rv.x - 1)
    # Transform of numpy array
    x, y, z = transform.applyForwardArray(rv.x, rv.y, rv.z)
    np.testing.assert_allclose(rv2.x, x)
    np.testing.assert_allclose(rv2.y, y)
    np.testing.assert_allclose(rv2.z, z)
    def update(self, deltax):
        """
        Updates the optic.

        Parameters
        ----------
        deltax: aos.state.State
            The change in the optical state.

        Notes
        -----
        Rotations only commute for small angles; otherwise order matters.
        """
        camx, camy, camz, camrx, camry = deltax.camhex
        self.optic = self.optic.withGloballyShiftedOptic('LSST.LSSTCamera', [camx, camy, camz])
        camrot = np.dot(batoid.RotX(camrx), batoid.RotY(camry))
        self.optic = self.optic.withLocallyRotatedOptic('LSST.LSSTCamera', camrot)

        m2x, m2y, m2z, m2rx, m2ry = deltax.m2hex
        self.optic = self.optic.withGloballyShiftedOptic('LSST.M2', [m2x, m2y, m2z])
        m2rot = np.dot(batoid.RotX(m2rx), batoid.RotY(m2ry))
        self.optic = self.optic.withLocallyRotatedOptic('LSST.M2', m2rot)
Exemplo n.º 7
0
def test_rotation():
    try:
        import galsim
    except ImportError:
        print("optic rotation test requires GalSim")
        return

    np.random.seed(57)

    fn = os.path.join(batoid.datadir, "HSC", "HSC.yaml")
    config = yaml.load(open(fn))
    telescope = batoid.parse.parse_optic(config['opticalSystem'])

    rot = batoid.RotX(np.random.uniform(low=0.0, high=2 * np.pi))
    rot = rot.dot(batoid.RotY(np.random.uniform(low=0.0, high=2 * np.pi)))
    rot = rot.dot(batoid.RotZ(np.random.uniform(low=0.0, high=2 * np.pi)))
    rotInv = np.linalg.inv(rot)

    # It's hard to test the two telescopes for equality due to rounding errors, so we test by
    # comparing zernikes
    rotTel = telescope.withLocalRotation(rot).withLocalRotation(rotInv)

    theta_x = np.random.uniform(-0.005, 0.005)
    theta_y = np.random.uniform(-0.005, 0.005)
    wavelength = 750e-9

    np.testing.assert_allclose(batoid.psf.zernike(telescope, theta_x, theta_y,
                                                  wavelength),
                               batoid.psf.zernike(rotTel, theta_x, theta_y,
                                                  wavelength),
                               atol=1e-5)

    for item in telescope.itemDict:
        rotTel = telescope.withLocallyRotatedOptic(item, rot)
        rotTel = rotTel.withLocallyRotatedOptic(item, rotInv)
        rotTel2 = telescope.withLocallyRotatedOptic(item, np.eye(3))
        theta_x = np.random.uniform(-0.005, 0.005)
        theta_y = np.random.uniform(-0.005, 0.005)
        np.testing.assert_allclose(batoid.psf.zernike(telescope, theta_x,
                                                      theta_y, wavelength),
                                   batoid.psf.zernike(rotTel, theta_x, theta_y,
                                                      wavelength),
                                   atol=1e-5)
        np.testing.assert_allclose(batoid.psf.zernike(telescope, theta_x,
                                                      theta_y, wavelength),
                                   batoid.psf.zernike(rotTel2, theta_x,
                                                      theta_y, wavelength),
                                   atol=1e-5)
Exemplo n.º 8
0
def test_properties():
    rng = np.random.default_rng(5)
    size = 10
    for i in range(100):
        x = rng.normal(size=size)
        y = rng.normal(size=size)
        z = rng.normal(size=size)
        vx = rng.normal(size=size)
        vy = rng.normal(size=size)
        vz = rng.normal(size=size)
        t = rng.normal(size=size)
        w = rng.normal(size=size)
        fx = rng.normal(size=size)
        vig = rng.choice([True, False], size=size)
        fa = rng.choice([True, False], size=size)
        cs = batoid.CoordSys(
            origin=rng.normal(size=3),
            rot=batoid.RotX(rng.normal()) @ batoid.RotY(rng.normal()))

        rv = batoid.RayVector(x, y, z, vx, vy, vz, t, w, fx, vig, fa, cs)

        np.testing.assert_array_equal(rv.x, x)
        np.testing.assert_array_equal(rv.y, y)
        np.testing.assert_array_equal(rv.z, z)
        np.testing.assert_array_equal(rv.r[:, 0], x)
        np.testing.assert_array_equal(rv.r[:, 1], y)
        np.testing.assert_array_equal(rv.r[:, 2], z)
        np.testing.assert_array_equal(rv.vx, vx)
        np.testing.assert_array_equal(rv.vy, vy)
        np.testing.assert_array_equal(rv.vz, vz)
        np.testing.assert_array_equal(rv.v[:, 0], vx)
        np.testing.assert_array_equal(rv.v[:, 1], vy)
        np.testing.assert_array_equal(rv.v[:, 2], vz)
        np.testing.assert_array_equal(rv.k[:, 0], rv.kx)
        np.testing.assert_array_equal(rv.k[:, 1], rv.ky)
        np.testing.assert_array_equal(rv.k[:, 2], rv.kz)
        np.testing.assert_array_equal(rv.t, t)
        np.testing.assert_array_equal(rv.wavelength, w)
        np.testing.assert_array_equal(rv.flux, fx)
        np.testing.assert_array_equal(rv.vignetted, vig)
        np.testing.assert_array_equal(rv.failed, fa)
        assert rv.coordSys == cs

        rv._syncToDevice()
        do_pickle(rv)
Exemplo n.º 9
0
def randomCoordSys(rng):
    return batoid.CoordSys(
        rng.uniform(size=3), (batoid.RotX(rng.uniform()).dot(
            batoid.RotY(rng.uniform())).dot(batoid.RotZ(rng.uniform()))))
Exemplo n.º 10
0
def test_rotation():
    rng = np.random.default_rng(57)

    telescope = batoid.Optic.fromYaml("HSC.yaml")

    rot = batoid.RotX(rng.uniform(low=0.0, high=2 * np.pi))
    rot = rot.dot(batoid.RotY(rng.uniform(low=0.0, high=2 * np.pi)))
    rot = rot.dot(batoid.RotZ(rng.uniform(low=0.0, high=2 * np.pi)))
    rotInv = np.linalg.inv(rot)

    # It's hard to test the two telescopes for equality due to rounding errors,
    # so we test by comparing zernikes
    rotTel = telescope.withLocalRotation(rot).withLocalRotation(rotInv)

    theta_x = rng.uniform(-0.005, 0.005)
    theta_y = rng.uniform(-0.005, 0.005)
    wavelength = 750e-9

    for k in telescope.itemDict.keys():
        np.testing.assert_allclose(telescope[k].coordSys.origin,
                                   rotTel[k].coordSys.origin,
                                   rtol=0,
                                   atol=1e-14)
        np.testing.assert_allclose(telescope[k].coordSys.rot,
                                   rotTel[k].coordSys.rot,
                                   rtol=0,
                                   atol=1e-14)

    np.testing.assert_allclose(batoid.zernikeGQ(telescope, theta_x, theta_y,
                                                wavelength),
                               batoid.zernikeGQ(rotTel, theta_x, theta_y,
                                                wavelength),
                               atol=1e-7)

    for item in telescope.itemDict:
        rotTel = telescope.withLocallyRotatedOptic(item, rot)
        rotTel = rotTel.withLocallyRotatedOptic(item, rotInv)
        rotTel2 = telescope.withLocallyRotatedOptic(item, np.eye(3))
        theta_x = rng.uniform(-0.005, 0.005)
        theta_y = rng.uniform(-0.005, 0.005)
        np.testing.assert_allclose(batoid.zernikeGQ(telescope, theta_x,
                                                    theta_y, wavelength),
                                   batoid.zernikeGQ(rotTel, theta_x, theta_y,
                                                    wavelength),
                                   atol=1e-7)
        np.testing.assert_allclose(batoid.zernikeGQ(telescope, theta_x,
                                                    theta_y, wavelength),
                                   batoid.zernikeGQ(rotTel2, theta_x, theta_y,
                                                    wavelength),
                                   atol=1e-7)
    # Test with non-fully-qualified name
    rotTel = telescope.withLocallyRotatedOptic('G1', rot)
    rotTel = rotTel.withLocallyRotatedOptic('G1', rotInv)
    rotTel2 = rotTel.withLocallyRotatedOptic('G1', np.eye(3))
    np.testing.assert_allclose(batoid.zernikeGQ(telescope, theta_x, theta_y,
                                                wavelength),
                               batoid.zernikeGQ(rotTel, theta_x, theta_y,
                                                wavelength),
                               atol=1e-7)
    np.testing.assert_allclose(batoid.zernikeGQ(telescope, theta_x, theta_y,
                                                wavelength),
                               batoid.zernikeGQ(rotTel2, theta_x, theta_y,
                                                wavelength),
                               atol=1e-7)
Exemplo n.º 11
0
def test_params():
    rng = np.random.default_rng(5)
    for _ in range(30):
        origin = rng.uniform(size=3)
        rot = (batoid.RotX(rng.uniform()) @ batoid.RotY(rng.uniform())
               @ batoid.RotZ(rng.uniform()))
        coordSys = batoid.CoordSys(origin, rot)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        coordSys = batoid.CoordSys(origin=origin)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, np.eye(3))
        np.testing.assert_equal(coordSys.xhat, [1, 0, 0])
        np.testing.assert_equal(coordSys.yhat, [0, 1, 0])
        np.testing.assert_equal(coordSys.zhat, [0, 0, 1])

        coordSys = batoid.CoordSys(rot=rot)
        np.testing.assert_equal(coordSys.origin, np.zeros(3))
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        coordSys = batoid.CoordSys()
        np.testing.assert_equal(coordSys.origin, np.zeros(3))
        np.testing.assert_equal(coordSys.rot, np.eye(3))
        np.testing.assert_equal(coordSys.xhat, [1, 0, 0])
        np.testing.assert_equal(coordSys.yhat, [0, 1, 0])
        np.testing.assert_equal(coordSys.zhat, [0, 0, 1])

        coordSys = batoid.CoordSys()
        coordSys = coordSys.rotateGlobal(rot).shiftGlobal(origin)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        coordSys = batoid.CoordSys()
        coordSys = coordSys.rotateLocal(rot).shiftGlobal(origin)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        coordSys = batoid.CoordSys()
        coordSys = coordSys.shiftLocal(origin).rotateLocal(rot)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        coordSys = batoid.CoordSys()
        coordSys = coordSys.shiftGlobal(origin).rotateLocal(rot)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])

        # Can't simply do a global rotation after a shift, since that will
        # change the origin too.  Works if we manually specify the rotation
        # center though
        coordSys = batoid.CoordSys()
        coordSys1 = coordSys.shiftGlobal(origin)
        coordSys = coordSys1.rotateGlobal(rot, origin, coordSys)
        np.testing.assert_equal(coordSys.origin, origin)
        np.testing.assert_equal(coordSys.rot, rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])
Exemplo n.º 12
0
def test_rotate():
    rng = np.random.default_rng(57)
    for _ in range(10):
        r1 = batoid.RotX(rng.uniform())
        r2 = batoid.RotY(rng.uniform())
        r3 = batoid.RotZ(rng.uniform())
        r4 = batoid.RotX(rng.uniform())
        r5 = batoid.RotY(rng.uniform())
        r6 = batoid.RotZ(rng.uniform())

        rot = r6 @ r5 @ r4 @ r3 @ r2 @ r1
        coordSys = batoid.CoordSys().rotateGlobal(rot)
        np.testing.assert_equal(coordSys.xhat, rot[:, 0])
        np.testing.assert_equal(coordSys.yhat, rot[:, 1])
        np.testing.assert_equal(coordSys.zhat, rot[:, 2])
        np.testing.assert_equal(coordSys.origin, 0)

        rot1 = r3 @ r2 @ r1
        rot2 = r6 @ r5 @ r4

        coordSys = batoid.CoordSys().rotateGlobal(rot1).rotateGlobal(rot2)
        np.testing.assert_allclose(coordSys.xhat, rot[:, 0])
        np.testing.assert_allclose(coordSys.yhat, rot[:, 1])
        np.testing.assert_allclose(coordSys.zhat, rot[:, 2])
        np.testing.assert_equal(coordSys.origin, 0)

        coordSys = batoid.CoordSys(rot=rot1)
        coordSys2 = coordSys.rotateLocal(batoid.RotX(np.pi / 2))
        # Since second rotation was about the local X, both should have same
        # xhat
        np.testing.assert_allclose(coordSys.xhat, coordSys2.xhat)
        # 90 degree positive rotation then means y -> -z, z -> y
        np.testing.assert_allclose(coordSys.yhat, -coordSys2.zhat)
        np.testing.assert_allclose(coordSys.zhat, coordSys2.yhat)

        # Try a loop
        coordSys = batoid.CoordSys(rot=rot)
        coordSys = coordSys.rotateGlobal(batoid.RotX(0.1))
        coordSys = coordSys.rotateGlobal(batoid.RotZ(np.pi))
        coordSys = coordSys.rotateGlobal(batoid.RotX(0.1))
        coordSys = coordSys.rotateGlobal(batoid.RotZ(np.pi))
        # Should be back where we started...
        np.testing.assert_allclose(coordSys.rot, rot)

        # Miscentered origins
        origin = rng.uniform(size=3)
        coordSys = batoid.CoordSys(origin=origin, rot=rot)
        np.testing.assert_equal(origin, coordSys.origin)
        coordSys2 = coordSys.rotateGlobal(rot)
        np.testing.assert_equal(rot @ origin, coordSys2.origin)
        coordSys3 = coordSys.rotateLocal(rot)
        np.testing.assert_equal(origin, coordSys3.origin)

        # Miscentered rotation axes
        # Global with center specified is same as local
        coordSys = batoid.CoordSys(origin=origin, rot=rot)
        coordSys2 = coordSys.rotateLocal(rot)
        coordSys3 = coordSys.rotateGlobal(rot, origin, batoid.CoordSys())
        np.testing.assert_allclose(coordSys2.origin, origin)
        np.testing.assert_allclose(coordSys2.origin, coordSys3.origin)
        np.testing.assert_allclose(coordSys2.rot, coordSys3.rot)