Skip to content

Telescopes¤

Toliman

dLuxToliman.telescopes.Toliman ¤

Bases: Telescope

A pre-built dLux telescope object for the Toliman telescope.

Attributes:

Name Type Description
optics BaseOptics

The optics object to be used in the telescope.

source BaseSource

The source object to be used in the telescope.

Methods:

Name Description
normalise

Normalises the source flux to 1.

full_model

Models with diffraction spikes (experimental).

perturb

Under Construction.

Source code in dLuxToliman/telescopes.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class Toliman(dLux.Telescope):
    """
    A pre-built dLux telescope object for the Toliman telescope.

    Attributes
    ----------
    optics : dLux.core.BaseOptics
        The optics object to be used in the telescope.
    source : dLux.sources.BaseSource
        The source object to be used in the telescope.

    Methods
    -------
    normalise()
        Normalises the source flux to 1.
    full_model()
        Models with diffraction spikes (experimental).
    perturb(X, parameters)
        Under Construction.
    """

    optics: BaseOpticalSystem
    source: BaseSource

    def __init__(self, optics, source):
        """
        Parameters
        ----------
        optics : dLux.core.BaseOptics
            The optical system to be used in the telescope.
        source : dLux.sources.BaseSource
            The source object to be used in the telescope.
        """
        self.optics = optics
        self.source = source
        super().__init__(optics, source)

    def __getattr__(self, key):
        """
        Returns an attribute of the instrument given a key.

        Parameters
        ----------
        key : str
            The key of the attribute to be returned.
        """
        for attribute in self.__dict__.values():
            if hasattr(attribute, key):
                return getattr(attribute, key)
        # if key in self.sources.keys():
        #     return self.sources[key]
        raise AttributeError(f"{self.__class__.__name__} has no attribute " f"{key}.")

    def normalise(self):
        """
        Normalises the source flux to 1.
        """
        return self.set("source", self.source.normalise())

    def full_model(self):
        """
        "Oh pretty sure that was for modelling the diffraction spikes in the corners as well as the central psf
        for some computer frying multi-hundred gb covariance matrix calculations
        tbh you wouldnt even need that random number game just run that model on someones computer and
        watch it die" - L. Desdoigts, 2023.
        """
        # TODO may break in dLux 0.14
        return self.optics.full_model(self.source)

    def perturb(self, X, parameters):
        """
        Under Construction.
        """
        # TODO : fix this
        for parameter, x in zip(parameters, X):
            perturbed_self = self.add(parameter, x)
        return perturbed_self

__getattr__(key) ¤

Returns an attribute of the instrument given a key.

Parameters:

Name Type Description Default
key str

The key of the attribute to be returned.

required
Source code in dLuxToliman/telescopes.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __getattr__(self, key):
    """
    Returns an attribute of the instrument given a key.

    Parameters
    ----------
    key : str
        The key of the attribute to be returned.
    """
    for attribute in self.__dict__.values():
        if hasattr(attribute, key):
            return getattr(attribute, key)
    # if key in self.sources.keys():
    #     return self.sources[key]
    raise AttributeError(f"{self.__class__.__name__} has no attribute " f"{key}.")

__init__(optics, source) ¤

Parameters:

Name Type Description Default
optics BaseOptics

The optical system to be used in the telescope.

required
source BaseSource

The source object to be used in the telescope.

required
Source code in dLuxToliman/telescopes.py
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(self, optics, source):
    """
    Parameters
    ----------
    optics : dLux.core.BaseOptics
        The optical system to be used in the telescope.
    source : dLux.sources.BaseSource
        The source object to be used in the telescope.
    """
    self.optics = optics
    self.source = source
    super().__init__(optics, source)

full_model() ¤

"Oh pretty sure that was for modelling the diffraction spikes in the corners as well as the central psf for some computer frying multi-hundred gb covariance matrix calculations tbh you wouldnt even need that random number game just run that model on someones computer and watch it die" - L. Desdoigts, 2023.

Source code in dLuxToliman/telescopes.py
70
71
72
73
74
75
76
77
78
def full_model(self):
    """
    "Oh pretty sure that was for modelling the diffraction spikes in the corners as well as the central psf
    for some computer frying multi-hundred gb covariance matrix calculations
    tbh you wouldnt even need that random number game just run that model on someones computer and
    watch it die" - L. Desdoigts, 2023.
    """
    # TODO may break in dLux 0.14
    return self.optics.full_model(self.source)

normalise() ¤

Normalises the source flux to 1.

Source code in dLuxToliman/telescopes.py
64
65
66
67
68
def normalise(self):
    """
    Normalises the source flux to 1.
    """
    return self.set("source", self.source.normalise())

perturb(X, parameters) ¤

Under Construction.

Source code in dLuxToliman/telescopes.py
80
81
82
83
84
85
86
87
def perturb(self, X, parameters):
    """
    Under Construction.
    """
    # TODO : fix this
    for parameter, x in zip(parameters, X):
        perturbed_self = self.add(parameter, x)
    return perturbed_self
JitteredToliman

dLuxToliman.telescopes.JitteredToliman ¤

Bases: Toliman

A child class of Toliman to facilitate the modelling of telescope jitter. This is done by modelling and summing offset PSFs - as a result, the compute time increases linearly with the number of PSFs. The jitter is assumed to be either a linear smear of simple harmonic vibration.

Attributes:

Name Type Description
optics BaseOptics

The optics object to be used in the telescope.

source BaseSource

The source object to be used in the telescope.

jitter_mag Array | float

The magnitude of the jitter in arcseconds.

jitter_angle Array | float

The angle of the jitter in degrees.

n_psfs int

The number of PSFs to be modelled.

jitter_shape str

The shape of the jitter. Either "linear" or "shm".

Methods:

Name Description
jitter_model

Models the jittered PSF.

Source code in dLuxToliman/telescopes.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
class JitteredToliman(Toliman):
    """
    A child class of Toliman to facilitate the modelling of telescope jitter.
    This is done by modelling and summing offset PSFs - as a result, the compute
    time increases linearly with the number of PSFs. The jitter is assumed to be
    either a linear smear of simple harmonic vibration.

    Attributes
    ----------
    optics : dLux.core.BaseOptics
        The optics object to be used in the telescope.
    source : dLux.sources.BaseSource
        The source object to be used in the telescope.
    jitter_mag : Array | float
        The magnitude of the jitter in arcseconds.
    jitter_angle : Array | float
        The angle of the jitter in degrees.
    n_psfs : int
        The number of PSFs to be modelled.
    jitter_shape : str
        The shape of the jitter. Either "linear" or "shm".

    Methods
    -------
    jitter_model()
        Models the jittered PSF.
    """

    jitter_mag: Array | float
    jitter_angle: Array | float
    n_psfs: int
    jitter_shape: str

    def __init__(
        self,
        optics,
        source,
        jitter_mag: Array | float,
        jitter_angle: Array | float,
        n_psfs: int = 6,
        jitter_shape: str = "linear",
    ):
        """
        Parameters
        ----------
        optics : dLux.core.BaseOptics
            The optical system to be used in the telescope.
        source : dLux.sources.BaseSource
            The source object to be used in the telescope.
        jitter_mag : float
            The magnitude of the jitter in arcseconds.
        jitter_angle : float
            The angle of the jitter in degrees.
        n_psfs : int
            The number of PSFs to be modelled.
        jitter_shape : str
            The shape of the jitter. Either "linear" or "shm".
        """

        super().__init__(optics, source)  # calling super
        self.jitter_mag = np.asarray(jitter_mag, dtype=float)
        self.jitter_angle = np.asarray(jitter_angle, dtype=float)
        if n_psfs < 2:
            raise ValueError("n_psfs must be greater than 1.")
        self.n_psfs = n_psfs
        if jitter_shape not in ["linear", "shm"]:
            raise ValueError(
                "Only jitter_shape values of 'linear' or 'shm' are supported."
            )
        self.jitter_shape = jitter_shape

    @staticmethod
    def _get_bounds(xs):
        """
        Helper method to grab the integration bounds for shm jitter_model.
        """
        return np.concatenate(
            (
                np.array(
                    [
                        xs[0],
                    ]
                ),
                np.array(xs[1:] + xs[:-1]) / 2,
                np.array(
                    [
                        xs[-1],
                    ]
                ),
            )
        )

    @staticmethod
    def _machine_epsilon(dtype):
        """
        Method to fetch machine epsilon for a given dtype (e.g. float32, float64).

        Parameters
        ----------
        dtype : np.dtype
            The dtype to fetch the machine epsilon for.
        """
        info = np.finfo(dtype)
        return info.eps

    def inv_shm(self, x):
        """
        Inverse function for the simple harmonic equation x(t) = Asin(t)
        """
        return np.arcsin(np.divide(x, self.jitter_mag / 2))

    def _centre_and_model(self, x, y):
        """
        Function to offset the source and model the PSF. This is vmapped over
        in the jitter_model method.
        """
        recentered_tel = self.set(["x_position", "y_position"], [x, y])
        return recentered_tel.model()

    def jitter_model(self):
        """
        Method to model the jittered PSF. This is done by modelling and summing
        offset PSFs - as a result, the compute time increases linearly with the
        number of PSFs. The jitter is assumed to be either a linear smear of
        simple harmonic vibration. The shape is determined by the `jitter_shape`
        parameter.
        """
        # horizontal and vertical components of the jitter
        x = np.cos(dLux.utils.deg2rad(self.jitter_angle))
        y = np.sin(dLux.utils.deg2rad(self.jitter_angle))

        # grabbing machine epsilon to avoid function evaluation at the asymptote
        eps = self._machine_epsilon(self.jitter_mag.dtype)

        # PSF positions as a fraction of the amplitude, jitter_mag / 2. Offsetting
        # by eps here rather than in arcseconds keeps the end points off the
        # asymptote at any jitter_mag, which would otherwise make the gradient NaN
        fractions = np.linspace(-1 + eps, 1 - eps, self.n_psfs)

        # generating the base array for PSF positions
        spacing = fractions * self.jitter_mag / 2

        # grabbing the x and y positions of the PSFs
        xs = spacing * x + self.x_position  # arcseconds
        ys = spacing * y + self.y_position  # arcseconds

        # for linear jitter
        if self.jitter_shape == "linear":
            weights = np.ones(self.n_psfs) / self.n_psfs  # equal weighting

        # for simple harmonic jitter
        if self.jitter_shape == "shm":
            bounds = self._get_bounds(fractions)  # grabbing bounds for integration
            # weighting by value of integral between bounds, arcsin being the
            # inverse of x(t) = sin(t) in units of the amplitude
            weights = np.arcsin(bounds[:-1]) - np.arcsin(bounds[1:])
            weights /= weights.sum()  # normalising

        # vmapping over all PSF positions
        psfs = vmap(self._centre_and_model, in_axes=(0, 0))(xs, ys)

        return np.tensordot(
            psfs, weights, axes=(0, 0)
        )  # multiplying by weights and summing

__init__(optics, source, jitter_mag, jitter_angle, n_psfs=6, jitter_shape='linear') ¤

Parameters:

Name Type Description Default
optics BaseOptics

The optical system to be used in the telescope.

required
source BaseSource

The source object to be used in the telescope.

required
jitter_mag float

The magnitude of the jitter in arcseconds.

required
jitter_angle float

The angle of the jitter in degrees.

required
n_psfs int

The number of PSFs to be modelled.

6
jitter_shape str

The shape of the jitter. Either "linear" or "shm".

'linear'
Source code in dLuxToliman/telescopes.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def __init__(
    self,
    optics,
    source,
    jitter_mag: Array | float,
    jitter_angle: Array | float,
    n_psfs: int = 6,
    jitter_shape: str = "linear",
):
    """
    Parameters
    ----------
    optics : dLux.core.BaseOptics
        The optical system to be used in the telescope.
    source : dLux.sources.BaseSource
        The source object to be used in the telescope.
    jitter_mag : float
        The magnitude of the jitter in arcseconds.
    jitter_angle : float
        The angle of the jitter in degrees.
    n_psfs : int
        The number of PSFs to be modelled.
    jitter_shape : str
        The shape of the jitter. Either "linear" or "shm".
    """

    super().__init__(optics, source)  # calling super
    self.jitter_mag = np.asarray(jitter_mag, dtype=float)
    self.jitter_angle = np.asarray(jitter_angle, dtype=float)
    if n_psfs < 2:
        raise ValueError("n_psfs must be greater than 1.")
    self.n_psfs = n_psfs
    if jitter_shape not in ["linear", "shm"]:
        raise ValueError(
            "Only jitter_shape values of 'linear' or 'shm' are supported."
        )
    self.jitter_shape = jitter_shape

inv_shm(x) ¤

Inverse function for the simple harmonic equation x(t) = Asin(t)

Source code in dLuxToliman/telescopes.py
195
196
197
198
199
def inv_shm(self, x):
    """
    Inverse function for the simple harmonic equation x(t) = Asin(t)
    """
    return np.arcsin(np.divide(x, self.jitter_mag / 2))

jitter_model() ¤

Method to model the jittered PSF. This is done by modelling and summing offset PSFs - as a result, the compute time increases linearly with the number of PSFs. The jitter is assumed to be either a linear smear of simple harmonic vibration. The shape is determined by the jitter_shape parameter.

Source code in dLuxToliman/telescopes.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def jitter_model(self):
    """
    Method to model the jittered PSF. This is done by modelling and summing
    offset PSFs - as a result, the compute time increases linearly with the
    number of PSFs. The jitter is assumed to be either a linear smear of
    simple harmonic vibration. The shape is determined by the `jitter_shape`
    parameter.
    """
    # horizontal and vertical components of the jitter
    x = np.cos(dLux.utils.deg2rad(self.jitter_angle))
    y = np.sin(dLux.utils.deg2rad(self.jitter_angle))

    # grabbing machine epsilon to avoid function evaluation at the asymptote
    eps = self._machine_epsilon(self.jitter_mag.dtype)

    # PSF positions as a fraction of the amplitude, jitter_mag / 2. Offsetting
    # by eps here rather than in arcseconds keeps the end points off the
    # asymptote at any jitter_mag, which would otherwise make the gradient NaN
    fractions = np.linspace(-1 + eps, 1 - eps, self.n_psfs)

    # generating the base array for PSF positions
    spacing = fractions * self.jitter_mag / 2

    # grabbing the x and y positions of the PSFs
    xs = spacing * x + self.x_position  # arcseconds
    ys = spacing * y + self.y_position  # arcseconds

    # for linear jitter
    if self.jitter_shape == "linear":
        weights = np.ones(self.n_psfs) / self.n_psfs  # equal weighting

    # for simple harmonic jitter
    if self.jitter_shape == "shm":
        bounds = self._get_bounds(fractions)  # grabbing bounds for integration
        # weighting by value of integral between bounds, arcsin being the
        # inverse of x(t) = sin(t) in units of the amplitude
        weights = np.arcsin(bounds[:-1]) - np.arcsin(bounds[1:])
        weights /= weights.sum()  # normalising

    # vmapping over all PSF positions
    psfs = vmap(self._centre_and_model, in_axes=(0, 0))(xs, ys)

    return np.tensordot(
        psfs, weights, axes=(0, 0)
    )  # multiplying by weights and summing