def test_traceReverse(): if __name__ == '__main__': nside = 128 else: nside = 32 fn = os.path.join(batoid.datadir, "HSC", "HSC.yaml") config = yaml.load(open(fn)) telescope = batoid.parse.parse_optic(config['opticalSystem']) init_rays = batoid.rayGrid(20, 12.0, 0.005, 0.005, -1.0, nside, 500e-9, 1.0, batoid.ConstMedium(1.0)) forward_rays, _ = telescope.trace(init_rays, outCoordSys=batoid.CoordSys()) # Now, turn the result rays around and trace backwards forward_rays = forward_rays.propagatedToTime(40.0) reverse_rays = batoid.RayVector( [batoid.Ray(r.r, -r.v, -r.t, r.wavelength) for r in forward_rays]) final_rays, _ = telescope.traceReverse(reverse_rays, outCoordSys=batoid.CoordSys()) # propagate all the way to t=0 final_rays = final_rays.propagatedToTime(0.0) w = np.where(np.logical_not(final_rays.vignetted))[0] for idx in w: np.testing.assert_allclose(init_rays[idx].x, final_rays[idx].x) np.testing.assert_allclose(init_rays[idx].y, final_rays[idx].y) np.testing.assert_allclose(init_rays[idx].z, final_rays[idx].z) np.testing.assert_allclose(init_rays[idx].vx, -final_rays[idx].vx) np.testing.assert_allclose(init_rays[idx].vy, -final_rays[idx].vy) np.testing.assert_allclose(init_rays[idx].vz, -final_rays[idx].vz) np.testing.assert_allclose(final_rays[idx].t, 0)
def test_intersect(): rng = np.random.default_rng(577) tanx = rng.uniform(-0.1, 0.1) tany = rng.uniform(-0.1, 0.1) size = 10_000 tiltedCoordSys = batoid.CoordSys(origin=[0, 0, -1]) tilted = batoid.Tilted(tanx, tany) x = rng.normal(0.0, 1.0, size=size) y = rng.normal(0.0, 1.0, size=size) z = np.full_like(x, -100.0) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -100.0) rv2 = batoid.intersect(tilted, rv.copy(), tiltedCoordSys) assert rv2.coordSys == tiltedCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, tilted.sag(x, y) - 1, rtol=0, atol=1e-12) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(tiltedCoordSys) batoid.intersect(tilted, rv2) assert rv2.coordSys == tiltedCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, tilted.sag(x, y) - 1, rtol=0, atol=1e-12)
def test_intersect(): rng = np.random.default_rng(5772) size = 10_000 for i in range(100): R = 1./rng.normal(0.0, 0.3) conic = rng.uniform(-2.0, 1.0) quad = batoid.Quadric(R, conic) quadCoordSys = batoid.CoordSys(origin=[0, 0, -1]) lim = min(0.7*abs(R)/np.sqrt(1+conic) if conic > -1 else 10, 10) x = rng.uniform(-lim, lim, size=size) y = rng.uniform(-lim, lim, size=size) z = np.full_like(x, -100.0) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -100.0) rv2 = batoid.intersect(quad, rv.copy(), quadCoordSys) assert rv2.coordSys == quadCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, quad.sag(x, y)-1, rtol=0, atol=1e-9) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(quadCoordSys) batoid.intersect(quad, rv2) assert rv2.coordSys == quadCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, quad.sag(x, y)-1, rtol=0, atol=1e-9)
def test_ne(): objs = [ batoid.Ray((0, 0, 0), (0, 0, 0)), batoid.Ray((0, 0, 0), (0, 0, 0), coordSys=batoid.CoordSys((0, 0, 1))), batoid.Ray((0, 0, 1), (0, 0, 0)), batoid.Ray((0, 1, 0), (0, 0, 0)), batoid.Ray((0, 0, 0), (0, 0, 0), t=1), batoid.Ray((0, 0, 0), (0, 0, 0), wavelength=500e-9), batoid.Ray((0, 0, 0), (0, 0, 0), wavelength=500e-9, flux=1.2), batoid.Ray((0, 0, 0), (0, 0, 0), vignetted=True), batoid.Ray(failed=True), (0, 0, 0), batoid.RayVector([ batoid.Ray((0, 0, 1), (0, 0, 0)), batoid.Ray((0, 0, 0), (0, 0, 0)) ]), batoid.RayVector([ batoid.Ray((0, 0, 1), (0, 0, 0), coordSys=batoid.CoordSys((0, 0, 1))), batoid.Ray((0, 0, 0), (0, 0, 0), coordSys=batoid.CoordSys((0, 0, 1))) ]), batoid.RayVector([ batoid.Ray((0, 0, 0), (0, 0, 0)), batoid.Ray((0, 0, 1), (0, 0, 0)) ]), batoid.RayVector([batoid.Ray((0, 0, 0), (0, 0, 0))]) ] all_obj_diff(objs)
def test_intersect(): rng = np.random.default_rng(577) size = 10_000 planeCoordSys = batoid.CoordSys(origin=[0, 0, -1]) plane = batoid.Plane() x = rng.normal(0.0, 1.0, size=size) y = rng.normal(0.0, 1.0, size=size) z = np.full_like(x, -100.0) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -100.0) rv2 = batoid.intersect(plane, rv.copy(), planeCoordSys) assert rv2.coordSys == planeCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, -1, rtol=0, atol=1e-12) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(planeCoordSys) batoid.intersect(plane, rv2) assert rv2.coordSys == planeCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, -1, rtol=0, atol=1e-12)
def test_ne(): objs = [ batoid.CoordSys(), batoid.CoordTransform(batoid.CoordSys(), batoid.CoordSys()), batoid.CoordTransform(batoid.CoordSys(), batoid.CoordSys(origin=(0, 0, 1))) ] all_obj_diff(objs)
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 wavefront(optic, theta_x, theta_y, wavelength, nx=32, sphereRadius=None): """Compute wavefront. Parameters ---------- optic : batoid.Optic Optic for which to compute wavefront. theta_x, theta_y : float Field of incoming rays (gnomic projection) wavelength : float Wavelength of incoming rays nx : int, optional Size of ray grid to generate to compute wavefront. Default: 32 sphereRadius : float, optional Radius of reference sphere in meters. If None, then use optic.sphereRadius. Returns ------- wavefront : batoid.Lattice A batoid.Lattice object containing the wavefront values in waves and the primitive lattice vectors of the entrance pupil grid in meters. """ dirCos = gnomicToDirCos(theta_x, theta_y) rays = batoid.rayGrid( optic.dist, optic.pupilSize, dirCos[0], dirCos[1], -dirCos[2], nx, wavelength, 1.0, optic.inMedium ) if sphereRadius is None: sphereRadius = optic.sphereRadius outCoordSys = batoid.CoordSys() optic.traceInPlace(rays, outCoordSys=outCoordSys) w = np.where(1-rays.vignetted)[0] point = np.mean(rays.r[w], axis=0) # We want to place the vertex of the reference sphere one radius length away from the # intersection point. So transform our rays into that coordinate system. transform = batoid.CoordTransform( outCoordSys, batoid.CoordSys(point+np.array([0,0,sphereRadius]))) transform.applyForwardInPlace(rays) sphere = batoid.Sphere(-sphereRadius) sphere.intersectInPlace(rays) w = np.where(1-rays.vignetted)[0] # Should potentially try to make the reference time w.r.t. the chief ray instead of the mean # of the good (unvignetted) rays. t0 = np.mean(rays.t[w]) arr = np.ma.masked_array((t0-rays.t)/wavelength, mask=rays.vignetted).reshape(nx, nx) primitiveVectors = np.vstack([[optic.pupilSize/nx, 0], [0, optic.pupilSize/nx]]) return batoid.Lattice(arr, primitiveVectors)
def test_ne(): objs = [ batoid.CoordSys(), batoid.CoordSys([0,0,1]), batoid.CoordSys([0,1,0]), batoid.CoordSys(batoid.RotX(0.1)), batoid.CoordTransform(batoid.CoordSys(), batoid.CoordSys()), batoid.CoordTransform(batoid.CoordSys(), batoid.CoordSys([0,0,1])), batoid.CoordTransform(batoid.CoordSys(), batoid.CoordSys(batoid.RotX(0.1))) ] all_obj_diff(objs)
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)
def test_intersect(): rng = np.random.default_rng(5772) size = 10_000 for _ in range(10): def f(x, y): a = rng.uniform(size=5) return ( a[0]*x**2*y - a[1]*y**2*x + a[2]*3*x - a[3] + a[4]*np.sin(y)*np.cos(x)**2 ) xs = np.linspace(0, 1, 1000) ys = np.linspace(0, 1, 1000) zs = f(*np.meshgrid(xs, ys)) bc = batoid.Bicubic(xs, ys, zs) bcCoordSys = batoid.CoordSys(origin=[0, 0, -1]) x = rng.uniform(0.1, 0.9, size=size) y = rng.uniform(0.1, 0.9, size=size) z = np.full_like(x, -10.0) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -10.0) rv2 = batoid.intersect(bc, rv.copy(), bcCoordSys) assert rv2.coordSys == bcCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose( rv2.z, bc.sag(x, y)-1, rtol=0, atol=1e-12 ) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(bcCoordSys) batoid.intersect(bc, rv2) assert rv2.coordSys == bcCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose( rv2.z, bc.sag(x, y)-1, rtol=0, atol=1e-12 )
def test_shift(): rng = np.random.default_rng(57) globalCoordSys = batoid.CoordSys() for i in range(30): x, y, z = rng.normal(0.1, 2.3, size=3) newCoordSys = globalCoordSys.shiftGlobal([x, y, z]) do_pickle(newCoordSys) np.testing.assert_array_equal(newCoordSys.xhat, [1, 0, 0]) np.testing.assert_array_equal(newCoordSys.yhat, [0, 1, 0]) np.testing.assert_array_equal(newCoordSys.zhat, [0, 0, 1]) np.testing.assert_array_equal(newCoordSys.origin, [x, y, z]) np.testing.assert_array_equal(newCoordSys.rot, np.eye(3)) for j in range(30): x2, y2, z2 = rng.normal(0.1, 2.3, size=3) newNewCoordSys = newCoordSys.shiftGlobal([x2, y2, z2]) np.testing.assert_array_equal(newNewCoordSys.xhat, [1, 0, 0]) np.testing.assert_array_equal(newNewCoordSys.yhat, [0, 1, 0]) np.testing.assert_array_equal(newNewCoordSys.zhat, [0, 0, 1]) np.testing.assert_array_equal(newNewCoordSys.origin, [x + x2, y + y2, z + z2]) np.testing.assert_array_equal(newNewCoordSys.rot, np.eye(3)) newNewCoordSys = newCoordSys.shiftLocal([x2, y2, z2]) np.testing.assert_array_equal(newNewCoordSys.xhat, [1, 0, 0]) np.testing.assert_array_equal(newNewCoordSys.yhat, [0, 1, 0]) np.testing.assert_array_equal(newNewCoordSys.zhat, [0, 0, 1]) np.testing.assert_array_equal(newNewCoordSys.origin, [x + x2, y + y2, z + z2]) np.testing.assert_array_equal(newNewCoordSys.rot, np.eye(3))
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)
def parse_coordSys(config, coordSys=batoid.CoordSys()): """ @param config configuration dictionary @param coordSys sys to which transformations in config are added """ shift = [0.0, 0.0, 0.0] if any(x in config for x in ['x', 'y', 'z']): if 'shift' in config: raise ValueError("Cannot specify both shift and x/y/z") x = config.pop('x', 0.0) y = config.pop('y', 0.0) z = config.pop('z', 0.0) shift = [x, y, z] elif 'shift' in config: shift = config.pop('shift') if shift != [0.0, 0.0, 0.0]: coordSys = coordSys.shiftLocal(shift) # At most one (nonzero) rotation can be included and is applied after the shift. rotXYZ = np.array([config.pop('rot' + axis, 0.0) for axis in 'XYZ']) axes = np.where(rotXYZ != 0)[0] if len(axes) > 1: raise ValueError('Cannot specify rotation about more than one axis.') elif len(axes) == 1: axis, angle = axes[0], rotXYZ[axes[0]] rotator = (batoid.RotX, batoid.RotY, batoid.RotZ)[axis](angle) coordSys = coordSys.rotateLocal(rotator) return coordSys
def test_ne(): objs = [ batoid.Mirror(batoid.Plane()), batoid.Detector(batoid.Plane()), batoid.Baffle(batoid.Plane()), batoid.RefractiveInterface(batoid.Plane()), batoid.Mirror(batoid.Paraboloid(0.1)), batoid.Detector(batoid.Paraboloid(0.1)), batoid.Baffle(batoid.Paraboloid(0.1)), batoid.RefractiveInterface(batoid.Paraboloid(0.1)), batoid.Mirror(batoid.Plane(), obscuration=batoid.ObscCircle(0.1)), batoid.Mirror(batoid.Plane(), inMedium=batoid.ConstMedium(1.1)), batoid.Mirror(batoid.Plane(), outMedium=batoid.ConstMedium(1.1)), batoid.Mirror(batoid.Plane(), coordSys=batoid.CoordSys([0, 0, 1])), batoid.CompoundOptic( [batoid.Mirror(batoid.Plane()), batoid.Mirror(batoid.Plane())]), batoid.CompoundOptic( [batoid.Mirror(batoid.Plane()), batoid.Baffle(batoid.Plane())]), batoid.CompoundOptic([ batoid.RefractiveInterface(batoid.Plane()), batoid.RefractiveInterface(batoid.Plane()) ]), batoid.Lens([ batoid.RefractiveInterface(batoid.Plane()), batoid.RefractiveInterface(batoid.Plane()) ]), ] all_obj_diff(objs)
def wavefront(optic, wavelength, theta_x=0, theta_y=0, nx=32, rays=None, saveRays=False, sphereRadius=None): if rays is None: xcos = np.sin(theta_x) ycos = np.sin(theta_y) zcos = -np.sqrt(1.0 - xcos**2 - ycos**2) rays = batoid.rayGrid(optic.dist, optic.pupilSize, xcos, ycos, zcos, nx, wavelength, optic.inMedium) if saveRays: rays = batoid.RayVector(rays) if sphereRadius is None: sphereRadius = optic.sphereRadius outCoordSys = batoid.CoordSys() optic.traceInPlace(rays, outCoordSys=outCoordSys) goodRays = batoid._batoid.trimVignetted(rays) point = np.array( [np.mean(goodRays.x), np.mean(goodRays.y), np.mean(goodRays.z)]) # We want to place the vertex of the reference sphere one radius length away from the # intersection point. So transform our rays into that coordinate system. transform = batoid.CoordTransform( outCoordSys, batoid.CoordSys(point + np.array([0, 0, sphereRadius]))) transform.applyForwardInPlace(rays) sphere = batoid.Sphere(-sphereRadius) sphere.intersectInPlace(rays) goodRays = batoid._batoid.trimVignetted(rays) # Should potentially try to make the reference time w.r.t. the chief ray instead of the mean # of the good (unvignetted) rays. t0 = np.mean(goodRays.t0) ts = rays.t0[:] isV = rays.isVignetted[:] ts -= t0 ts /= wavelength wf = np.ma.masked_array(ts, mask=isV) return wf
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)))) )
def test_intersect(): rng = np.random.default_rng(57721) size = 10_000 for i in range(10): jmax = rng.integers(4, 100) coef = rng.normal(size=jmax+1)*1e-3 R_outer = rng.uniform(0.5, 5.0) R_inner = rng.uniform(0.0, 0.65*R_outer) zernike = batoid.Zernike(coef, R_outer=R_outer, R_inner=R_inner) lim = 0.7*R_outer zernikeCoordSys = batoid.CoordSys(origin=[0, 0, -1]) x = rng.uniform(-lim, lim, size=size) y = rng.uniform(-lim, lim, size=size) z = np.full_like(x, -10.0) # If we shoot rays straight up, then it's easy to predict the # intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -10.0) rv2 = batoid.intersect(zernike, rv.copy(), zernikeCoordSys) assert rv2.coordSys == zernikeCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose( rv2.z, zernike.sag(x, y)-1, rtol=0, atol=1e-12 ) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(zernikeCoordSys) batoid.intersect(zernike, rv2) assert rv2.coordSys == zernikeCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose( rv2.z, zernike.sag(x, y)-1, rtol=0, atol=1e-12 )
def test_ne(): objs = [ batoid.SimpleCoating(0.0, 1.0), batoid.SimpleCoating(0.0, 0.1), batoid.SimpleCoating(0.1, 0.1), batoid.CoordSys() ] all_obj_diff(objs)
def test_HSC_trace(): fn = os.path.join(batoid.datadir, "HSC", "HSC_old.yaml") config = yaml.load(open(fn)) telescope = batoid.parse.parse_optic(config['opticalSystem']) # Zemax has a number of virtual surfaces that we don't trace in batoid. Also, the HSC.yaml # above includes Baffle surfaces not in Zemax. The following lists select out the surfaces in # common to both models. HSC_surfaces = [ 3, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 24, 25, 28, 29, 31 ] surface_names = [ 'PM', 'G1_entrance', 'G1_exit', 'G2_entrance', 'G2_exit', 'ADC1_entrance', 'ADC1_exit', 'ADC2_entrance', 'ADC2_exit', 'G3_entrance', 'G3_exit', 'G4_entrance', 'G4_exit', 'G5_entrance', 'G5_exit', 'F_entrance', 'F_exit', 'W_entrance', 'W_exit', 'D' ] for fn in [ "HSC_raytrace_1.txt", "HSC_raytrace_2.txt", "HSC_raytrace_3.txt" ]: filename = os.path.join(directory, "testdata", fn) with open(filename) as f: arr = np.loadtxt(f, skiprows=22, usecols=list(range(0, 12))) arr0 = arr[0] ray = batoid.Ray(arr0[1] / 1000, arr0[2] / 1000, 16.0, arr0[4], arr0[5], -arr0[6], t=0, wavelength=750e-9) tf = telescope.traceFull(ray) i = 0 for surface in tf: if surface['name'] != surface_names[i]: continue s = surface['out'] v = s.v / np.linalg.norm(s.v) transform = batoid.CoordTransform(surface['outCoordSys'], batoid.CoordSys()) s = transform.applyForward(s) bt_isec = np.array([s.x, s.y, s.z - 16.0]) zx_isec = arr[HSC_surfaces[i] - 1][1:4] / 1000 np.testing.assert_allclose(bt_isec, zx_isec, rtol=0, atol=1e-9) # nanometer agreement bt_angle = np.array([v[0], v[1], v[2]]) zx_angle = arr[HSC_surfaces[i] - 1][4:7] # direction cosines agree to 1e-9 np.testing.assert_allclose(bt_angle, zx_angle, rtol=0, atol=1e-9) i += 1
def test_HSC_trace(): telescope = batoid.Optic.fromYaml("HSC_old.yaml") # Zemax has a number of virtual surfaces that we don't trace in batoid. # Also, the HSC.yaml above includes Baffle surfaces not in Zemax. The # following lists select out the surfaces in common to both models. HSC_surfaces = [ 3, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18, 19, 20, 21, 24, 25, 28, 29, 31 ] surface_names = [ 'PM', 'G1_entrance', 'G1_exit', 'G2_entrance', 'G2_exit', 'ADC1_entrance', 'ADC1_exit', 'ADC2_entrance', 'ADC2_exit', 'G3_entrance', 'G3_exit', 'G4_entrance', 'G4_exit', 'G5_entrance', 'G5_exit', 'F_entrance', 'F_exit', 'W_entrance', 'W_exit', 'D' ] for fn in [ "HSC_raytrace_1.txt", "HSC_raytrace_2.txt", "HSC_raytrace_3.txt" ]: filename = os.path.join(directory, "testdata", fn) with open(filename) as f: arr = np.loadtxt(f, skiprows=22, usecols=list(range(0, 12))) arr0 = arr[0] rv = batoid.RayVector(arr0[1] / 1000, arr0[2] / 1000, 16.0, arr0[4], arr0[5], -arr0[6], t=0, wavelength=750e-9) tf = telescope.traceFull(rv) i = 0 for name in surface_names: surface = tf[name] srv = surface['out'] srv.toCoordSys(batoid.CoordSys()) bt_isec = np.array([srv.x, srv.y, srv.z - 16.0]).T[0] zx_isec = arr[HSC_surfaces[i] - 1][1:4] / 1000 # nanometer agreement np.testing.assert_allclose(bt_isec, zx_isec, rtol=0, atol=1e-9) v = srv.v / np.linalg.norm(srv.v) bt_angle = v[0] zx_angle = arr[HSC_surfaces[i] - 1][4:7] # direction cosines agree to 1e-9 np.testing.assert_allclose(bt_angle, zx_angle, rtol=0, atol=1e-9) i += 1
def test_intersect(): rng = np.random.default_rng(5772) size = 10_000 for _ in range(100): s1 = batoid.Sphere(1. / rng.normal(0., 0.2)) s2 = batoid.Paraboloid(rng.uniform(1, 3)) sum = batoid.Sum([s1, s2]) sumCoordSys = batoid.CoordSys(origin=[0, 0, -1]) x = rng.uniform(-1, 1, size=size) y = rng.uniform(-1, 1, size=size) z = np.full_like(x, -10.0) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -10.0) rv2 = batoid.intersect(sum, rv.copy(), sumCoordSys) assert rv2.coordSys == sumCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, sum.sag(x, y) - 1, rtol=0, atol=1e-12) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(sumCoordSys) batoid.intersect(sum, rv2) assert rv2.coordSys == sumCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, sum.sag(x, y) - 1, rtol=0, atol=1e-12)
def test_intersect(): rng = np.random.default_rng(5772) size = 10_000 for i in range(100): R = 1. / rng.normal(0.0, 0.3) sphereCoordSys = batoid.CoordSys(origin=[0, 0, -1]) sphere = batoid.Sphere(R) x = rng.uniform(-0.3 * abs(R), 0.3 * abs(R), size=size) y = rng.uniform(-0.3 * abs(R), 0.3 * abs(R), size=size) z = np.full_like(x, -2 * abs(R)) # If we shoot rays straight up, then it's easy to predict the intersection vx = np.zeros_like(x) vy = np.zeros_like(x) vz = np.ones_like(x) rv = batoid.RayVector(x, y, z, vx, vy, vz) np.testing.assert_allclose(rv.z, -2 * abs(R)) rv2 = batoid.intersect(sphere, rv.copy(), sphereCoordSys) assert rv2.coordSys == sphereCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, sphere.sag(x, y) - 1, rtol=0, atol=1e-9) # Check default intersect coordTransform rv2 = rv.copy().toCoordSys(sphereCoordSys) batoid.intersect(sphere, rv2) assert rv2.coordSys == sphereCoordSys rv2 = rv2.toCoordSys(batoid.CoordSys()) np.testing.assert_allclose(rv2.x, x) np.testing.assert_allclose(rv2.y, y) np.testing.assert_allclose(rv2.z, sphere.sag(x, y) - 1, rtol=0, atol=1e-9)
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)
def parse_coordSys(config, coordSys=batoid.CoordSys()): """ @param config configuration dictionary @param coordSys sys to which transformations in config are added """ if any(x in config for x in ['x', 'y', 'z']): if 'shift' in config: raise ValueError("Cannot specify both shift and x/y/z") x = config.pop('x', 0.0) y = config.pop('y', 0.0) z = config.pop('z', 0.0) shift = [x, y, z] elif 'shift' in config: shift = config.pop('shift') # Leaving rotation out for the moment... if shift != [0.0, 0.0, 0.0]: coordSys = coordSys.shiftLocal(shift) return coordSys
def test_ne(): rng = np.random.default_rng(57) N1 = rng.integers(1, 5) N2 = rng.integers(1, 5) N3 = rng.integers(1, 5) arr = np.ones((N1, N2, N3)) pv1 = rng.uniform(-1.0, 1.0, size=3) pv2 = rng.uniform(-1.0, 1.0, size=3) pv3 = rng.uniform(-1.0, 1.0, size=3) lattice1 = batoid.Lattice(arr, np.vstack([pv1, pv2, pv3])) lattice2 = batoid.Lattice(arr[..., 0], np.vstack([pv1, pv2, pv3])[:2, :2]) lattice3 = batoid.Lattice(arr, 2 * np.vstack([pv1, pv2, pv3])) lattice4 = batoid.Lattice(2 * arr, np.vstack([pv1, pv2, pv3])) objs = [batoid.CoordSys(), lattice1, lattice2, lattice3, lattice4] all_obj_diff(objs)
def test_shift(): import random random.seed(5) globalCoordSys = batoid.CoordSys() for i in range(30): x = random.gauss(0.1, 2.3) y = random.gauss(0.1, 2.3) z = random.gauss(0.1, 2.3) newCoordSys = globalCoordSys.shiftGlobal([x, y, z]) do_pickle(newCoordSys) np.testing.assert_array_equal(newCoordSys.xhat, [1,0,0]) np.testing.assert_array_equal(newCoordSys.yhat, [0,1,0]) np.testing.assert_array_equal(newCoordSys.zhat, [0,0,1]) np.testing.assert_array_equal(newCoordSys.origin, [x,y,z]) np.testing.assert_array_equal(newCoordSys.rot, np.eye(3)) coordTransform = batoid.CoordTransform(globalCoordSys, newCoordSys) do_pickle(coordTransform) for j in range(30): x2 = random.gauss(0.1, 2.3) y2 = random.gauss(0.1, 2.3) z2 = random.gauss(0.1, 2.3) newNewCoordSys = newCoordSys.shiftGlobal([x2, y2, z2]) np.testing.assert_array_equal(newNewCoordSys.xhat, [1,0,0]) np.testing.assert_array_equal(newNewCoordSys.yhat, [0,1,0]) np.testing.assert_array_equal(newNewCoordSys.zhat, [0,0,1]) np.testing.assert_array_equal(newNewCoordSys.origin, [x+x2, y+y2, z+z2]) np.testing.assert_array_equal(newNewCoordSys.rot, np.eye(3)) newNewCoordSys = newCoordSys.shiftLocal([x2, y2, z2]) np.testing.assert_array_equal(newNewCoordSys.xhat, [1,0,0]) np.testing.assert_array_equal(newNewCoordSys.yhat, [0,1,0]) np.testing.assert_array_equal(newNewCoordSys.zhat, [0,0,1]) np.testing.assert_array_equal(newNewCoordSys.origin, [x+x2, y+y2, z+z2]) np.testing.assert_array_equal(newNewCoordSys.rot, np.eye(3))
def test_huygens_paraboloid(plot=False): if __name__ == '__main__': obscurations = [0.0, 0.25, 0.5, 0.75] else: obscurations = [0.25] print("Testing HuygensPSF") # Just do a single parabolic mirror test focalLength = 1.5 diam = 0.3 R = 2*focalLength for obscuration in obscurations: telescope = batoid.CompoundOptic( items = [ batoid.Mirror( batoid.Paraboloid(R), name="Mirror", obscuration=batoid.ObscNegation( batoid.ObscAnnulus(0.5*obscuration*diam, 0.5*diam) ) ), batoid.Detector( batoid.Plane(), name="detector", coordSys=batoid.CoordSys(origin=[0, 0, focalLength]) ) ], pupilSize=diam, backDist=10.0, inMedium=batoid.ConstMedium(1.0) ) airy_size = 1.22*500e-9/diam * 206265 print() print("Airy radius: {:4.2f} arcsec".format(airy_size)) # Start with the HuygensPSF npix = 96 size = 3.0 # arcsec dsize = size/npix dsize_X = dsize*focalLength/206265 # meters psf = batoid.huygensPSF( telescope, 0.0, 0.0, 500e-9, nx=npix, dx=dsize_X, dy=dsize_X ) psf.array /= np.max(psf.array) scale = np.sqrt(np.abs(np.linalg.det(psf.primitiveVectors))) # meters scale *= 206265/focalLength # arcsec obj = galsim.Airy(lam=500, diam=diam, obscuration=obscuration) # Need to shift by half a pixel. obj = obj.shift(scale/2, scale/2) im = obj.drawImage(nx=npix, ny=npix, scale=scale, method='no_pixel') arr = im.array/np.max(im.array) gs_mom = galsim.hsm.FindAdaptiveMom(im) psfim = galsim.Image(psf.array) jt_mom = galsim.hsm.FindAdaptiveMom(psfim) print("GalSim shape: ", gs_mom.observed_shape) print("batoid shape: ", jt_mom.observed_shape) print("GalSim centroid: ", gs_mom.moments_centroid) print("batoid centroid: ", jt_mom.moments_centroid) print("GalSim size: ", gs_mom.moments_sigma) print("batoid size: ", jt_mom.moments_sigma) print("GalSim rho4: ", gs_mom.moments_rho4) print("batoid rho4: ", jt_mom.moments_rho4) np.testing.assert_allclose( gs_mom.observed_shape.g1, jt_mom.observed_shape.g1, rtol=0.0, atol=3e-3 ) np.testing.assert_allclose( gs_mom.observed_shape.g2, jt_mom.observed_shape.g2, rtol=0.0, atol=3e-3 ) np.testing.assert_allclose( gs_mom.moments_centroid.x, jt_mom.moments_centroid.x, rtol=0.0, atol=1e-9 ) np.testing.assert_allclose( gs_mom.moments_centroid.y, jt_mom.moments_centroid.y, rtol=0.0, atol=1e-9 ) np.testing.assert_allclose( gs_mom.moments_sigma, jt_mom.moments_sigma, rtol=1e-2 # why not better?! ) np.testing.assert_allclose( gs_mom.moments_rho4, jt_mom.moments_rho4, rtol=2e-2 ) if plot: size = scale*npix import matplotlib.pyplot as plt fig = plt.figure(figsize=(15, 4)) ax1 = fig.add_subplot(131) im1 = ax1.imshow( np.log10(arr), extent=np.r_[-1,1,-1,1]*size/2, vmin=-7, vmax=0 ) plt.colorbar(im1, ax=ax1, label=r'$\log_{10}$ flux') ax1.set_title('GalSim') ax1.set_xlabel("arcsec") ax1.set_ylabel("arcsec") sizeX = dsize_X * npix * 1e6 # microns ax2 = fig.add_subplot(132) im2 = ax2.imshow( np.log10(psf.array), extent=np.r_[-1,1,-1,1]*sizeX/2, vmin=-7, vmax=0 ) plt.colorbar(im2, ax=ax2, label=r'$\log_{10}$ flux') ax2.set_title('batoid') ax2.set_xlabel(r"$\mu m$") ax2.set_ylabel(r"$\mu m$") ax3 = fig.add_subplot(133) im3 = ax3.imshow( (psf.array-arr)/np.max(arr), vmin=-0.01, vmax=0.01, cmap='seismic' ) plt.colorbar(im3, ax=ax3, label="(batoid-GalSim)/max(GalSim)") ax3.set_title('resid') ax3.set_xlabel(r"$\mu m$") ax3.set_ylabel(r"$\mu m$") fig.tight_layout() plt.show()
def parse_optic(config, coordSys=batoid.CoordSys(), inMedium=batoid.ConstMedium(1.0), outMedium=None): """ @param config configuration dictionary @param coordSys sys to which transformations in config are added @param inMedium default in Medium, often set by optic parent @param outMedium default out Medium, often set by optic parent """ if 'obscuration' in config: obscuration = parse_obscuration(config.pop('obscuration')) else: obscuration = None name = config.pop('name', "") if 'coordSys' in config: coordSys = parse_coordSys(config.pop('coordSys'), coordSys) inMedium = parse_medium(config.pop('inMedium', inMedium)) outMedium = parse_medium(config.pop('outMedium', outMedium)) if outMedium is None: outMedium = inMedium typ = config.pop('type') if typ == 'Mirror': surface = parse_surface(config.pop('surface')) return batoid.optic.Mirror(surface, name=name, coordSys=coordSys, obscuration=obscuration, inMedium=inMedium, outMedium=outMedium) elif typ == 'RefractiveInterface': surface = parse_surface(config.pop('surface')) return batoid.optic.RefractiveInterface(surface, name=name, coordSys=coordSys, obscuration=obscuration, inMedium=inMedium, outMedium=outMedium) elif typ == 'OPDScreen': surface = parse_surface(config.pop('surface')) screen = parse_surface(config.pop('screen')) return batoid.optic.OPDScreen(surface, screen, name=name, coordSys=coordSys, obscuration=obscuration, inMedium=inMedium, outMedium=outMedium) elif typ == 'Baffle': surface = parse_surface(config.pop('surface')) return batoid.optic.Baffle(surface, name=name, coordSys=coordSys, obscuration=obscuration, inMedium=inMedium, outMedium=outMedium) elif typ == 'Detector': surface = parse_surface(config.pop('surface')) return batoid.optic.Detector(surface, name=name, coordSys=coordSys, obscuration=obscuration, inMedium=inMedium, outMedium=outMedium) elif typ == 'Lens': medium = parse_medium(config.pop('medium')) itemsConfig = config.pop('items') items = [ parse_optic(itemsConfig[0], coordSys=coordSys, inMedium=inMedium, outMedium=medium), parse_optic(itemsConfig[1], coordSys=coordSys, inMedium=medium, outMedium=outMedium) ] return batoid.optic.Lens(items, name=name, coordSys=coordSys, inMedium=inMedium, outMedium=outMedium) elif typ == 'CompoundOptic': itemsConfig = config.pop('items') items = [ parse_optic(iC, coordSys=coordSys, inMedium=inMedium, outMedium=outMedium) for iC in itemsConfig ] # Look for a few more possible attributes kwargs = {} for k in ['backDist', 'sphereRadius', 'pupilSize', 'pupilObscuration']: if k in config: kwargs[k] = config[k] if 'stopSurface' in config: kwargs['stopSurface'] = parse_optic(config['stopSurface']) return batoid.optic.CompoundOptic(items, inMedium=inMedium, outMedium=outMedium, name=name, coordSys=coordSys, **kwargs) elif typ == 'Interface': surface = parse_surface(config.pop('surface')) return batoid.optic.Interface(surface, name=name, coordSys=coordSys) else: raise ValueError(f"Unknown optic type: {typ}")
def randomCoordSys(rng): return batoid.CoordSys( rng.uniform(size=3), (batoid.RotX(rng.uniform()).dot( batoid.RotY(rng.uniform())).dot(batoid.RotZ(rng.uniform()))))