Ripa Gen2 2Nd Simplified#

Source file: examples/ripa_gen2_2nd_simplified.py

Output#

Output for ripa_gen2_2nd_simplified.py

Code#

import numpy as np
import sys
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from copy import deepcopy

sys.path.append("../")
from optable import *

# ── Tunable parameters [value, min, max] ──
ripa_2nd_demo = {
    "R2dXMLA": [0.0, -0.3, 0.4],
    "R2dY4F": [0, -0.5, 0.5],
    "R2kappa": [-1.01705, -2, 2],
    "R2a4": [-4.00e-11, -2e-9, 2e-9],
    "R2a6": [3.180e-15, -2e-13, 2e-13],
}

presets = {"default": ripa_2nd_demo}

vars = ripa_2nd_demo
for var, val in vars.items():
    if var not in locals():
        exec(f"{var} = {val[0]}")

# ── Constants ──
BW = 6.834682  # Rb87 hyperfine splitting, in GHz
R2R = 0.98
R2wl = 780e-7  # all physical quantity is in unit of cm, 780nm
R2DMLA = 420e-4  # 420um
R2NMLA = 80
R2MMLA = 1
R2L = R2DMLA * R2NMLA / 2
R2Dm = 2.54 * 2
R2X_waist = 0

# ── Derived quantities ──
R2d = 3e8 / (2 * BW * 1e9) / 0.01  # half round-trip length set by BW, in cm
print("R2d=", R2d, "cm")
R2MLAroc = R2d * 2
R2w0 = np.sqrt(R2wl * R2MLAroc / (2 * np.pi))
print("R2w0=", R2w0 * 1e4, "um")
print("R2DMLA/R2w0=", R2DMLA / R2w0)
clipping_loss = np.exp(-2 * ((R2DMLA / 2) ** 2) / ((np.sqrt(2) * R2w0) ** 2))
print("clipping_loss=", clipping_loss)
print("NMLA*clipping_loss=", R2NMLA * clipping_loss)
R2theta0 = np.arctan(R2DMLA / R2d / 2)
print("R2theta0=", R2theta0)
R2ZRays = [(i - (R2MMLA - 1) / 2) * R2DMLA for i in range(R2MMLA)]


# ── Rays (reverse ray tracing, intersecting) ──
def get_r2_rays(idxs, sign, offset=0):
    return [
        Ray(
            [0, R2L - (i + offset) * R2DMLA, R2ZRay],
            [1, 0, 0],
            wavelength=R2wl,
            w0=R2w0,
            id=int(id + R2NMLA * int(sign == -1)),
        )
        .Propagate(-R2X_waist)
        ._RotAroundLocal([0, 0, 1], [0, 0, 0], -sign * R2theta0)
        for id, i in enumerate(idxs)
        for R2ZRay in R2ZRays
    ]


SPACING = 10  # Change this to increase/decrease the number of rays
OFFSET = 1
R2rays_m = get_r2_rays(range(0, R2NMLA, SPACING), sign=1)
R2rays_p = get_r2_rays(range(0, R2NMLA, SPACING), offset=OFFSET, sign=-1)
R2rays = deepcopy(R2rays_m + R2rays_p)


# ── Lens pair (LENS=6: Aspheric Parametric, f=EFL, UVFS glass) ──
EFL = 43.17
CT = 0.8
n = Glass_UVFS()
R = EFL * (n.n(780e-9) - 1)
print("R=", R * 10, "mm")
DL = 2.54 * 3
F1 = EFL
F2 = EFL

R2l0r = ASphericParametricLens(
    [F1, 0, 0],
    CT=CT,
    diameter=DL,
    R=R,
    n=n,
    kappa=R2kappa,
    a4=R2a4 * (1e-2 / 1e-3) ** 4,
    a6=R2a6 * (1e-2 / 1e-3) ** 6,
    name="L0",
).TY(R2DMLA / 2 + R2dY4F)

R2l1r = (
    ASphericParametricLens(
        [F1 + 2 * F2, 0, 0],
        CT=CT,
        diameter=DL,
        R=R,
        n=n,
        kappa=R2kappa,
        a4=R2a4 * (1e-2 / 1e-3) ** 4,
        a6=R2a6 * (1e-2 / 1e-3) ** 6,
        name="L1",
    )
    .RotZ(np.pi)
    .TY(R2DMLA / 2 + R2dY4F)
)


# ── Monitors ──
R2Mon0 = Monitor([-0.5, -2.5, 0], width=5, height=5, name="R2 Monitor 0").TY(R2L)
R2Mon1 = Monitor([2 * F1 + 2 * F2, 0, 0], width=5, height=5, name="R2 Monitor 1")
R2Mon2 = Monitor(
    [2 * F1 + 2 * F2 + R2d + 0, 0, 0], width=5, height=5, name="R2 Monitor 2"
)

# ── Assemble component group ──
ripa2 = ComponentGroup([0, 0, 0], name="RIPA2")
ripa2.add_rays(R2rays)
ripa2.add_components([R2l0r, R2l1r])
ripa2.add_monitors([R2Mon0, R2Mon1, R2Mon2])

# ── Ray tracing ──
table = OpticalTable()
table.add_components([ripa2])
table.add_monitors(ripa2.monitors)
table.ray_tracing(ripa2.rays)


if __name__ == "__main__":
    fig = plt.figure(figsize=(12, 6))
    gs = gridspec.GridSpec(1, 2, width_ratios=[3, 1])
    ax0 = plt.subplot(gs[0])
    gs1 = gridspec.GridSpecFromSubplotSpec(3, 1, subplot_spec=gs[1])
    ax1 = [plt.subplot(gs1[i]) for i in range(3)]

    table.render(
        ax0,
        type="Z",
        roi=(-5, 2 * F1 + 2 * F2 + R2d + 2, -5, 5, -1, 1),
        gaussian_beam=True,
        label=False,
        physical_color=False,
    )

    table.render(
        ax1[0],
        type="Z",
        roi=(
            2 * F1 + 2 * F2 - 0.3,
            2 * F1 + 2 * F2 + R2d + 2,
            -R2L * 2,
            R2L * 2,
        ),
        gaussian_beam=True,
    )

    table.render(
        ax1[1],
        type="Z",
        roi=(-0.2 - R2d, 2 - R2d, -1, 1),
        gaussian_beam=True,
    )

    # ── Solve ray-ray intersections ──
    R2NNMLA = len(R2rays_m)
    rays_Mon = R2Mon2.get_rays(sort="ID")
    print(len(rays_Mon))
    PList = []
    nList = []
    rocList = []
    pathLengthList = []
    for i in range(R2NNMLA):
        ray0i = rays_Mon[i]
        ray0i_id = ray0i._id
        ray1i, _ = R2Mon2.get_ray_id(ray0i_id + R2NMLA)
        t0, t1, P, n = solve_ray_ray_intersection(
            ray0i.origin, ray0i.direction, ray1i.origin, ray1i.direction
        )
        d0 = ray0i.distance_to_waist(ray0i.q_at_z(t0))
        d1 = ray1i.distance_to_waist(ray1i.q_at_z(t1))
        pl0 = ray0i.pathlength(t0)
        pl1 = ray1i.pathlength(t1)
        d = R2d
        roci = 2 * (d**2 + d0 * d1) / (d0 + d1)
        ax0.scatter(P[0], P[1], color="magenta", s=10)
        ax1[0].scatter(P[0], P[1], color="magenta", s=10)
        # print(d1, roci, P, n)
        PList.append(P)
        nList.append(n)
        rocList.append(roci)
        pathLengthList.append((pl0 + pl1) / 2)

    stdCX = np.std([P[0] for P in PList])
    print("std of intersection points in X direction (cm)=", stdCX)

    pathlengths = np.array(pathLengthList) / R2wl
    pathlengths -= np.min(pathlengths)
    print("pathlengths (in unit of wavelength)=", pathlengths)

    print("See magenta dots: ray-ray intersection points")
    plt.show()