Skip to content

Optical Layers¤

ApplyBasisCLIMB

dLuxToliman.layers.optical_layers.ApplyBasisCLIMB ¤

Bases: BasisLayer()

Adds an array of binary phase values to the input wavefront from a set of continuous basis vectors. This uses the CLIMB algorithm in order to generate the binary values in a continuous manner as described in the paper Wong et al. 2021. The basis vectors are taken as an Optical Path Difference (OPD), and applied to the phase of the wavefront. The ideal wavelength parameter described the wavelength that will have a perfect anti-phase relationship given by the Optical Path Difference.

TODO: Many of the methods in the class still need doccumentation. TODO: This currently only outputs 256 pixel arrays and uses a 3x oversample, therefore requiring a 768 pixel basis array.

"This class is the crazy lady we keep in the attic, but turns out her name is actually Ben." - L. Desdoigts, 2023.

Attributes:

Name Type Description
basis Array

Arrays holding the continous pre-calculated basis vectors.

coefficients Array

The Array of coefficients to be applied to each basis vector.

ideal_wavelength Array

The target wavelength at which a perfect anti-phase relationship is applied via the OPD.

Source code in dLuxToliman/layers/optical_layers.py
 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
 88
 89
 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
class ApplyBasisCLIMB(BasisLayer()):
    """
    Adds an array of binary phase values to the input wavefront from a set of
    continuous basis vectors. This uses the CLIMB algorithm in order to
    generate the binary values in a continuous manner as described in the
    paper Wong et al. 2021. The basis vectors are taken as an Optical Path
    Difference (OPD), and applied to the phase of the wavefront. The ideal
    wavelength parameter described the wavelength that will have a perfect
    anti-phase relationship given by the Optical Path Difference.

    TODO: Many of the methods in the class still need doccumentation.
    TODO: This currently only outputs 256 pixel arrays and uses a 3x oversample,
    therefore requiring a 768 pixel basis array.

    "This class is the crazy lady we keep in the attic, but turns out her name is actually Ben." - L. Desdoigts, 2023.

    Attributes
    ----------
    basis: Array
        Arrays holding the continous pre-calculated basis vectors.
    coefficients: Array
        The Array of coefficients to be applied to each basis vector.
    ideal_wavelength : Array
        The target wavelength at which a perfect anti-phase relationship is
        applied via the OPD.
    """

    # basis            : Array
    # coefficients     : Array
    ideal_wavelength: Array

    def __init__(
        self: OpticalLayer(),
        basis: Array,
        ideal_wavelength: Array,
        coefficients: Array = None,
    ) -> OpticalLayer():
        """
        Constructor for the ApplyBasisCLIMB class.

        Parameters
        ----------
        basis : Array
            Arrays holding the continous pre-calculated basis vectors. This must
            be a 3d array of shape (nterms, npixels, npixels), with the final
            two dimensions matching that of the wavefront at time of
            application. This is currently required to be a nx768x768 shaped
            array.
        ideal_wavelength : Array
            The target wavelength at which a perfect anti-phase relationship is
            applied via the OPD.
        coefficients : Array = None
            The Array of coefficients to be applied to each basis vector. This
            must be a one dimensional array with leading dimension equal to the
            leading dimension of the basis vectors. Default is None which
            initialises an array of zeros.
        """
        super().__init__(basis=basis, coefficients=coefficients)
        # self.basis            = np.asarray(basis, dtype=float)
        self.ideal_wavelength = np.asarray(ideal_wavelength, dtype=float)
        # self.coefficients     = np.array(coefficients).astype(float) \
        #             if coefficients is not None else np.zeros(len(self.basis))

        # # Inputs checks
        # assert self.basis.ndim == 3, \
        # ("basis must be a 3 dimensional array, ie (nterms, npixels, npixels).")
        # assert self.basis.shape[-1] == 768, \
        # ("Basis must have shape (n, 768, 768).")
        # assert self.coefficients.ndim == 1 and \
        # self.coefficients.shape[0] == self.basis.shape[0], \
        # ("coefficients must be a 1 dimensional array with length equal to the "
        # "First dimension of the basis array.")
        # assert self.ideal_wavelength.ndim == 0, ("ideal_wavelength must be a "
        #                                          "scalar array.")

    def __call__(self: OpticalLayer(), wavefront: Wavefront) -> Wavefront:
        """
        Generates and applies the binary OPD array to the wavefront in a
        differentiable manner.

        Parameters
        ----------
        wavefront : Wavefront
            The wavefront to operate on.

        Returns
        -------
        wavefront : Wavefront
            The wavefront with the binary OPD applied.
        """
        latent = self.get_opd(self.basis, self.coefficients)
        binary_phase = np.pi * self.CLIMB(latent, ppsz=wavefront.npixels)
        opd = self.phase_to_opd(binary_phase, self.ideal_wavelength)
        return wavefront.add_opd(opd)

    @property
    def applied_shape(self):
        return tuple(np.array(self.basis.shape[-2:]) // 3)

    def opd_to_phase(self, opd, wavel):
        return 2 * np.pi * opd / wavel

    def phase_to_opd(self, phase, wavel):
        return phase * wavel / (2 * np.pi)

    def get_opd(self, basis, coefficients):
        return np.dot(basis.T, coefficients)

    def get_total_opd(self):
        return self.get_opd(self.basis, self.coefficients)

    def get_binary_phase(self):
        latent = self.get_opd(self.basis, self.coefficients)
        binary_phase = np.pi * self.CLIMB(latent)
        return binary_phase

    def lsq_params(self, img):
        xx, yy = np.meshgrid(
            np.linspace(0, 1, img.shape[0]), np.linspace(0, 1, img.shape[1])
        )
        A = np.vstack([xx.ravel(), yy.ravel(), np.ones_like(xx).ravel()]).T
        matrix = np.linalg.inv(np.dot(A.T, A)).dot(A.T)
        return matrix, xx, yy, A

    def lsq(self, img):
        matrix, _, _, _ = self.lsq_params(img)
        return np.dot(matrix, img.ravel())

    def area(self, img, epsilon=1e-15):
        a, b, c = self.lsq(img)
        a = np.where(a == 0, epsilon, a)
        b = np.where(b == 0, epsilon, b)
        c = np.where(c == 0, epsilon, c)
        x1 = (-b - c) / (a)  # don't divide by zero
        x2 = -c / (a)  # don't divide by zero
        x1, x2 = np.min(np.array([x1, x2])), np.max(np.array([x1, x2]))
        x1, x2 = np.max(np.array([x1, 0])), np.min(np.array([x2, 1]))

        dummy = (
            x1
            + (-c / b) * x2
            - (0.5 * a / b) * x2**2
            - (-c / b) * x1
            + (0.5 * a / b) * x1**2
        )

        # Set the regions where there is a defined gradient
        dummy = np.where(dummy >= 0.5, dummy, 1 - dummy)

        # Colour in regions
        dummy = np.where(np.mean(img) >= 0, dummy, 1 - dummy)

        # rescale between 0 and 1?
        dummy = np.where(np.all(img > 0), 1, dummy)
        dummy = np.where(np.all(img <= 0), 0, dummy)

        # undecided region
        dummy = np.where(np.any(img == 0), np.mean(dummy > 0), dummy)

        # rescale between 0 and 1
        dummy = np.clip(dummy, 0, 1)

        return dummy

    def CLIMB(self, wf, ppsz=256):
        psz = ppsz * 3
        dummy = np.array(np.split(wf, ppsz))
        dummy = np.array(np.split(np.array(dummy), ppsz, axis=2))
        subarray = dummy[:, :, 0, 0]

        flat = dummy.reshape(-1, 3, 3)
        vmap_mask = vmap(self.area, in_axes=(0))

        soft_bin = vmap_mask(flat).reshape(ppsz, ppsz)

        return soft_bin

__call__(wavefront) ¤

Generates and applies the binary OPD array to the wavefront in a differentiable manner.

Parameters:

Name Type Description Default
wavefront Wavefront

The wavefront to operate on.

required

Returns:

Name Type Description
wavefront Wavefront

The wavefront with the binary OPD applied.

Source code in dLuxToliman/layers/optical_layers.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def __call__(self: OpticalLayer(), wavefront: Wavefront) -> Wavefront:
    """
    Generates and applies the binary OPD array to the wavefront in a
    differentiable manner.

    Parameters
    ----------
    wavefront : Wavefront
        The wavefront to operate on.

    Returns
    -------
    wavefront : Wavefront
        The wavefront with the binary OPD applied.
    """
    latent = self.get_opd(self.basis, self.coefficients)
    binary_phase = np.pi * self.CLIMB(latent, ppsz=wavefront.npixels)
    opd = self.phase_to_opd(binary_phase, self.ideal_wavelength)
    return wavefront.add_opd(opd)

__init__(basis, ideal_wavelength, coefficients=None) ¤

Constructor for the ApplyBasisCLIMB class.

Parameters:

Name Type Description Default
basis Array

Arrays holding the continous pre-calculated basis vectors. This must be a 3d array of shape (nterms, npixels, npixels), with the final two dimensions matching that of the wavefront at time of application. This is currently required to be a nx768x768 shaped array.

required
ideal_wavelength Array

The target wavelength at which a perfect anti-phase relationship is applied via the OPD.

required
coefficients Array = None

The Array of coefficients to be applied to each basis vector. This must be a one dimensional array with leading dimension equal to the leading dimension of the basis vectors. Default is None which initialises an array of zeros.

None
Source code in dLuxToliman/layers/optical_layers.py
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
def __init__(
    self: OpticalLayer(),
    basis: Array,
    ideal_wavelength: Array,
    coefficients: Array = None,
) -> OpticalLayer():
    """
    Constructor for the ApplyBasisCLIMB class.

    Parameters
    ----------
    basis : Array
        Arrays holding the continous pre-calculated basis vectors. This must
        be a 3d array of shape (nterms, npixels, npixels), with the final
        two dimensions matching that of the wavefront at time of
        application. This is currently required to be a nx768x768 shaped
        array.
    ideal_wavelength : Array
        The target wavelength at which a perfect anti-phase relationship is
        applied via the OPD.
    coefficients : Array = None
        The Array of coefficients to be applied to each basis vector. This
        must be a one dimensional array with leading dimension equal to the
        leading dimension of the basis vectors. Default is None which
        initialises an array of zeros.
    """
    super().__init__(basis=basis, coefficients=coefficients)
    # self.basis            = np.asarray(basis, dtype=float)
    self.ideal_wavelength = np.asarray(ideal_wavelength, dtype=float)