def make_lens(R1, R2, d, z_offset, material=None, external_material=None): """ The resulting lens faces in direction of positive z-axis. The sign convention here is as in https://en.wikipedia.org/wiki/Lens#Lensmaker's_equation . That is: - R1 is curvature of lens closer to source (larger z). - R > 0 means center of curvature is at more negative z (farther along in path of light). - So for a common convex lens, R1 > 0 and R2 < 0. Careful: This is the opposite as above. The z-axis intersects first surface at z = z_offset + d/2 and the 2nd surface at z = z_offset - d/2. The default material is BK7. The lensmaker's equation: 1/f = (n - 1)*[1/R1 - 1/R2 + (n-1)*d/(n * R1 * R2)] (n = ior) """ # We handle defaults this way so that if None is passed we can fill in the default. if material is None: material = bk7 if external_material is None: external_material = air center1 = z_offset + d / 2 - R1 center2 = z_offset - d / 2 - R2 sphere1 = Quadric.sphere(R1).untransform(translation3f(0, 0, -center1)) sphere2 = Quadric.sphere(R2).untransform(translation3f(0, 0, -center2)) # TODO: worry where the two surfaces meet and introduce appropriate clipping element1 = SubElement(sphere1, None, material=material) element2 = SubElement(sphere2, None, material=external_material) return Compound([element1, element2], comment="lens")
def standard_source(z, radius): """A source pointing "down" (direction of rays is (0,0,-1)) from (0,0,z) with given radius""" debug = False if debug: return LinearSource(radius, z) source_orientation = np.diag([-1, 1, -1, 1]) source_T = translation3f(0, 0, z).dot(source_orientation) source = CircularSource(source_T, radius) return source
def make_conic(R, K, z_offset, material=None, reverse_normal=False): """ See https://en.wikipedia.org/wiki/Conic_constant r^2 - 2Rz + (K+1)z^2 = 0 Be careful about the sign convention for radius of curvature. We follow the convention in https://en.wikipedia.org/wiki/Conic_constant but this is opposite the convention in https://en.wikipedia.org/wiki/Lens#Lensmaker's_equation . Args: R: radius of curvature; use R > 0 for concave "up" (direction of positive z-axis) while R < 0 is concave "down" K: conic constant; should be < -1 for hyperboloids, -1 for paraboloids, > -1 for ellipses (including 0 for spheres). The relationship with eccentricity e is K = -e^2 (when K <= 0). z_offset: z-coordinate where the surface intersects z-axis material: mostly self explanatory; None means reflector reverse_normal: If true, surface points in direction of negative z-axis rather than positive z-axis """ M = np.diag([1, 1, (K + 1), 0]) M[2, 3] = -R M[3, 2] = M[2, 3] # For either sign of R, we want the convention that gradient points up at origin. # That gradient is (0,0,-R). # When R < 0, we already have that. # For R > 0, we need to negate M to get that. if R > 0: M *= -1 if reverse_normal: M *= -1 quad = Quadric(M) geometry = quad.untransform(translation3f(0, 0, -z_offset)) if R > 0: # We want to keep the top sheet. # TODO: Let clip_z be halfway between the two foci. clip_z = z_offset - 1e-6 clip = Plane(make_bound_vector(point(0, 0, clip_z), vector(0, 0, -1))) else: clip_z = z_offset + 1e-6 clip = Plane(make_bound_vector(point(0, 0, clip_z), vector(0, 0, 1))) return SubElement(geometry, clip, material=material)
def setback(self, offset): """Positive offset means move sensor back (away from the direction its pointing)""" M = self.M.dot(translation3f(0, 0, -offset)) return PlanarSensor(M)
def make_classical_cassegrain(focal_length, d, b, aperture_radius): """A classical Cassegrain scope: parabolic primary, hyperbolic secondary. One thing that's a little tricky is it's hard to infer the parameters from product descriptions (not that they're classical Cassegrains, but still...). Eyeballing some pictures it looks like b is typically about half of d. Note that d is significantly shorter than total length of the tube. Args: focal_length: focal length d: distance between primary and secondary (along main axis) b: backfocus (how far behind the primary the focal plane is) aperture_radius: aperture radius #f1: focal length of primary reflector Returns: The resulting Instrument """ # Our notation follows https://ccdsh.konkoly.hu/wiki/Cassegrain_Optics # Some math: # M := secondary magnification = f / f1 where f1 is focal length of primary # M should also be the ratio of the distances to the two focal points from # the secondary. # Our actual focal plane is at z = -b (with back of primary at z=0). # The secondary is at z = d. # So consider a proposed f1; then one focal point is at z=f1. # The two distances to the foci are d+b and (f1-d), so # M = (d+b)/(f1-d) # so f1 = d + (d + b)/M, one of the formulas we see on that page. # This yields f1 = d + (d+b)/(f/f1) = d + f1(d+b)/f # (1 - (d+b)/f) f1 = d # f1 = d / (1 - (d+b)/f) # Thinking in the (r,z) plane, we want the hyperbola through the point # (0,d) with foci (0,f1) and (0,-b) # The hyperbola is centered at z=(f1 - b)/2 =: s (our notation) # Let z' = z - s; switching to the (r,z') plane, the hyperbola goes through # (0, d-s) with foci (0, f1 - s) and (0, -(f1-s)). # Let c = f1 - s, a = d - s # We'll have z'^2 / a^2 - r^2/beta^2 = 1 # Write this as r^2/beta^2 - z'^2/a^2 + 1 = 0 (so gradient points in direction # we want). # (I'm using beta rather than the b you'll see in a textbook because b is # already in scope.) # The relationship is c^2 = a^2 + beta^2 # So, beta^2 = c^2 - a^2 f = focal_length f1 = d / (1 - (d + b) / f) # M = f / f1 # f2 = -(d + b) / (M - 1) # By convention, we'll say the hyperboloid has negative focal length. s = (f1 - b) / 2 c = f1 - s c_alt = s + b print(f"d={d}, b={b}, s={s}, f1={f1}, c={c}, c_alt={c_alt}") assert np.isclose(c, c_alt) a = d - s beta2 = c**2 - a**2 translated_secondary_M = np.diag([1 / beta2, 1 / beta2, -1 / (a**2), 1]) translated_secondary_geometry = Quadric(translated_secondary_M) secondary_geometry = translated_secondary_geometry.untransform( translation3f(0, 0, -s)) secondary = SubElement(secondary_geometry, clip=None) primary = make_paraboloid(f1) aperture0 = CircularAperture(point(0, 0, d), vector(0, 0, -aperture_radius)) # Reflector is z = r^2 / (4 focal length), so at edge, # we have: z_reflector_edge = aperture_radius**2 / (4 * f1) aperture1 = CircularAperture(point(0, 0, z_reflector_edge), vector(0, 0, -aperture_radius)) #source = standard_source(focal_length, aperture_radius) source = standard_source(d, aperture_radius) elements = Compound([aperture0, aperture1, primary, secondary]) sensor = PlanarSensor.of_q_x_y(point(0, 0, -b), vector(1, 0, 0), vector(0, 1, 0)) return Instrument(source, elements, sensor)