dispcraft.optics

optics

Camera

Camera(f)

Camera: beam direction [rad] → focal-plane position [m], in 2D. x = -f * theta

Parameters:
  • f (float — focal length [m]) –
Source code in dispcraft/optics/elements.py
61
62
def __init__(self, f):
    self.f = f

forward

forward(theta)

theta : (2,) or (2, N) array → position, same shape.

Source code in dispcraft/optics/elements.py
64
65
66
def forward(self, theta):
    """theta : (2,) or (2, N) array → position, same shape."""
    return -self.f * theta

Collimator

Collimator(f)

Collimator: focal-plane position [m] → beam direction [rad], in 2D. theta = -x / f

Parameters:
  • f (float — focal length [m]) –
Source code in dispcraft/optics/elements.py
79
80
def __init__(self, f):
    self.f = f

forward

forward(x)

x : (2,) or (2, N) array → direction, same shape.

Source code in dispcraft/optics/elements.py
82
83
84
def forward(self, x):
    """x : (2,) or (2, N) array → direction, same shape."""
    return -x / self.f

Detector

Detector(pixel_size, offset=None)

Detector: focal-plane position [m] → pixel coordinates, in 2D.

pixel = (x + offset) / pixel_size

Parameters:
  • pixel_size (float — pixel size [m/pix]) –
  • offset
Source code in dispcraft/optics/elements.py
127
128
129
def __init__(self, pixel_size, offset=None):
    self.pixel_size = pixel_size
    self.offset = np.zeros(2) if offset is None else np.asarray(offset, dtype=float)

forward

forward(x)

x : (2,) or (2, N) array → pixel coordinates, same shape.

Source code in dispcraft/optics/elements.py
131
132
133
134
135
def forward(self, x):
    """x : (2,) or (2, N) array → pixel coordinates, same shape."""
    x = np.asarray(x)
    offset = self.offset if x.ndim == 1 else self.offset[:, None]
    return (x + offset) / self.pixel_size

Grating

Grating(m, rho)

Diffraction grating: angular deviation δ(λ) = m * λ / d.

Parameters:
  • m
  • rho (float — groove density [grooves/mm]) –
Source code in dispcraft/optics/elements.py
34
35
36
37
def __init__(self, m, rho):
    self.m = m
    self.rho = rho
    self.d = 1e3 / rho  # groove spacing [µm]

deviation

deviation(wavelength)

Angular deviation [rad]. wavelength in µm.

Source code in dispcraft/optics/elements.py
39
40
41
def deviation(self, wavelength):
    """Angular deviation [rad]. wavelength in µm."""
    return self.m * wavelength / self.d

Grism

Grism(prism, grating, tilt=0.0)

Grism (2D): prism + grating, with a tilt angle coupling x and y.

The grating term subtracts from the prism term: refraction and diffraction bend the beam in opposite rotational senses when both are measured relative to the optical axis.

Parameters:
  • prism
  • grating (Grating) –
  • tilt
Source code in dispcraft/optics/elements.py
153
154
155
156
def __init__(self, prism, grating, tilt=0.0):
    self.prism = prism
    self.grating = grating
    self.R = rotation_matrix(tilt)

deviation

deviation(wavelength)

Total angular deviation [rad] (scalar or array). wavelength in µm.

Source code in dispcraft/optics/elements.py
158
159
160
161
def deviation(self, wavelength):
    """Total angular deviation [rad] (scalar or array). wavelength in µm."""
    return (self.prism.deviation(wavelength)
            - self.grating.deviation(wavelength))

forward

forward(angle, wavelength)

angle : (2,) or (2, N) array — beam direction [rad] wavelength : float or (N,) array — wavelength [µm]

Returns : (2,) or (2, N) array — dispersed beam direction [rad]

Source code in dispcraft/optics/elements.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def forward(self, angle, wavelength):
    """
    angle      : (2,) or (2, N) array — beam direction [rad]
    wavelength : float or (N,) array  — wavelength [µm]

    Returns : (2,) or (2, N) array — dispersed beam direction [rad]
    """
    angle = np.asarray(angle, dtype=float)
    rotated = self.R @ angle  # align with prism apex
    dev = np.asarray(self.deviation(wavelength), dtype=float)

    if rotated.ndim == 1 and dev.ndim == 1 and dev.shape[0] > 1:
        # single ray, many wavelengths -> broadcast to (2, N)
        rotated = np.broadcast_to(rotated[:, None], (2, dev.shape[0])).copy()
    else:
        rotated = rotated.copy()

    rotated[1] = rotated[1] + dev  # dispersion on the y' axis
    return self.R.T @ rotated  # rotate back

Material

Material(n0, k)

Optical glass: Cauchy dispersion model.

Parameters:
  • n0 (float — base refractive index) –
  • k
Source code in dispcraft/optics/materials.py
11
12
13
def __init__(self, n0, k):
    self.n0 = n0
    self.k = k

refractive_index

refractive_index(wavelength)

Compute n(λ). wavelength in µm.

Source code in dispcraft/optics/materials.py
15
16
17
def refractive_index(self, wavelength):
    """Compute n(λ). wavelength in µm."""
    return self.n0 + self.k / wavelength**2

Prism

Prism(material, A)

Thin prism: angular deviation δ(λ) = A * (n(λ) - 1).

Parameters:
  • material (Material — the glass (provides n(λ))) –
  • A
Source code in dispcraft/optics/elements.py
14
15
16
def __init__(self, material, A):
    self.material = material
    self.A = A

deviation

deviation(wavelength)

Angular deviation [rad]. wavelength in µm.

Source code in dispcraft/optics/elements.py
18
19
20
21
def deviation(self, wavelength):
    """Angular deviation [rad]. wavelength in µm."""
    n = self.material.refractive_index(wavelength)
    return self.A * (n - 1)

Telescope

Telescope(f, sky0=None, angle=0.0)

Bases: Camera

2D telescope: sky position → focal-plane position.

Parameters:
  • f
  • sky0
  • angle (float — field rotation / roll angle [rad], default: 0.0 ) –
Source code in dispcraft/optics/elements.py
 98
 99
100
101
102
def __init__(self, f, sky0=None, angle=0.0):
    super().__init__(f)
    self.sky0 = np.zeros(2) if sky0 is None else np.asarray(sky0, dtype=float)
    self.angle = angle
    self.R = rotation_matrix(angle)

forward

forward(sky)

sky : (2,) or (2, N) array [alpha, delta] [rad] Returns : same shape — focal-plane position [m]

Source code in dispcraft/optics/elements.py
104
105
106
107
108
109
110
111
112
def forward(self, sky):
    """
    sky : (2,) or (2, N) array [alpha, delta]  [rad]
    Returns : same shape — focal-plane position [m]
    """
    sky = np.asarray(sky)
    sky0 = self.sky0 if sky.ndim == 1 else self.sky0[:, None]
    offset = sky - sky0  # angular offset from pointing
    return super().forward(self.R @ offset)  # rotate, then focus

build_instrument_from_config

build_instrument_from_config(config)

Build the (Telescope, Collimator, Grism, Camera, Detector) tuple from a parameter dict shaped like models/stage1_instrument.toml.

Returns:
  • (tel, coll, grism, cam, det)
Source code in dispcraft/optics/instrument.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def build_instrument_from_config(config: dict):
    """
    Build the (Telescope, Collimator, Grism, Camera, Detector) tuple from a
    parameter dict shaped like `models/stage1_instrument.toml`.

    Returns
    -------
    tel, coll, grism, cam, det
    """
    tel_cfg = config["telescope"]
    tel = Telescope(
        f=tel_cfg["f"],
        sky0=tel_cfg.get("sky0"),
        angle=np.radians(tel_cfg.get("roll_deg", 0.0)),
    )

    coll = Collimator(f=config["collimator"]["f"])
    cam = Camera(f=config["camera"]["f"])

    mat_cfg = config["material"]
    material = Material(n0=mat_cfg["n0"], k=mat_cfg["k"])

    prism = Prism(material=material, A=np.radians(config["prism"]["A_deg"]))
    grating = Grating(m=config["grating"]["m"], rho=config["grating"]["rho"])
    grism = Grism(prism, grating, tilt=np.radians(config["grism"].get("tilt_deg", 0.0)))

    det_cfg = config["detector"]
    det = Detector(pixel_size=det_cfg["pixel_size"], offset=det_cfg.get("offset"))

    return tel, coll, grism, cam, det

rotation_matrix

rotation_matrix(angle)

2D rotation matrix for a given angle [rad].

Source code in dispcraft/optics/elements.py
44
45
46
47
48
def rotation_matrix(angle):
    """2D rotation matrix for a given angle [rad]."""
    c, s = np.cos(angle), np.sin(angle)
    return np.array([[c, -s],
                      [s, c]])

trace_instrument

trace_instrument(sky, wavelength, tel, coll, grism, cam, det)

2D forward model: sky position + wavelength → detector pixel (x, y).

Parameters:
  • sky
  • wavelength (float or (N,) array — [µm]) –
  • tel
  • coll
  • grism
  • cam
  • det
Returns:
  • pixel( (2,) or (2, N) array — [pixel_x, pixel_y] ) –
Source code in dispcraft/optics/instrument.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def trace_instrument(sky, wavelength, tel, coll, grism, cam, det):
    """
    2D forward model: sky position + wavelength → detector pixel (x, y).

    Parameters
    ----------
    sky        : (2,) array          — [alpha, delta]  [rad]
    wavelength : float or (N,) array — [µm]
    tel        : Telescope  — sky position → focal plane [m]
    coll       : Collimator — position     → beam direction [rad]
    grism      : Grism      — angle        → dispersed angle (λ-dep.)
    cam        : Camera     — angle        → detector position [m]
    det        : Detector   — position     → pixel

    Returns
    -------
    pixel : (2,) or (2, N) array — [pixel_x, pixel_y]
    """
    pos_foc = tel.forward(sky)  # sky      → focal plane [m]
    angle_col = coll.forward(pos_foc)  # position → beam direction [rad]
    angle_gr = grism.forward(angle_col, wavelength)  # dispersed angle (λ-dep.)
    pos_cam = cam.forward(angle_gr)  # angle    → detector position [m]
    return det.forward(pos_cam)  # position → pixel