Skip to content

model_fits

Fitting helpers and ModelFit subclasses for resolved sources.

Full API

Fitting helpers and ModelFit subclasses for resolved sources.

This module provides ModelFit classes for fitting resolved sources using amigo, including utilities to handle the log distribution parameter, rotation by parallactic angle, and simulation of resolved source interferograms.

ResolvedFit ¤

Bases: _BaseResolvedFit, ModelFit

Model fit for resolved (extended) sources.

This class extends :class:amigo.model_fits.ModelFit to add support for a spatial distribution parameter (kept as its base-10 logarithm, stored under the key log_dist). It supplies sensible default initialisation and maps the log_dist parameter into the expected keyed parameter namespace for per-filter fitting.

Parameters:

Name Type Description Default
file str or path - like

Path to the data file or exposure to be passed to :class:ModelFit.

required
use_cov bool

Whether to use the covariance information from the data, by default True.

required
Source code in src/dorito/model_fits.py
 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
class ResolvedFit(_BaseResolvedFit, ModelFit):
    """Model fit for resolved (extended) sources.

    This class extends :class:`amigo.model_fits.ModelFit` to add support for a
    spatial distribution parameter (kept as its base-10 logarithm, stored
    under the key ``log_dist``). It supplies sensible default initialisation
    and maps the ``log_dist`` parameter into the expected keyed parameter
    namespace for per-filter fitting.

    Parameters
    ----------
    file : str or path-like
        Path to the data file or exposure to be passed to :class:`ModelFit`.
    use_cov : bool, optional
        Whether to use the covariance information from the data, by default
        ``True``.
    """

    def get_key(self, param):
        match param:
            case "log_dist":
                return self.filter

        return super().get_key(param)

    def map_param(self, param):
        match param:
            case "log_dist":
                return f"{param}.{self.get_key(param)}"

        return super().map_param(param)

    def initialise_params(self, optics, distribution):

        params = super().initialise_params(optics)

        # log distribution
        params["log_dist"] = (
            self.get_key("log_dist"),
            np.log10(distribution / distribution.sum()),
        )

        return params

    def model_interferogram(
        self,
        psf,
        model,
        rotate: bool = None,
    ):
        return psf.convolve(model.get_distribution(self, rotate=rotate), method="fft")

DynamicResolvedFit ¤

Bases: ResolvedFit

Resolved fit where each exposure has its own distribution.

For time-series or sequence data where every exposure can have an independent resolved-source distribution, this class modifies the parameter keying so that distribution parameters are unique per exposure (the key includes the exposure self.key and the filter).

Notes

Only the keying behaviour differs from :class:ResolvedFit — the underlying parameter representation and simulation pipeline remain the same.

Source code in src/dorito/model_fits.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class DynamicResolvedFit(ResolvedFit):
    """Resolved fit where each exposure has its own distribution.

    For time-series or sequence data where every exposure can have an
    independent resolved-source distribution, this class modifies the
    parameter keying so that distribution parameters are unique per
    exposure (the key includes the exposure ``self.key`` and the filter).

    Notes
    -----
    Only the keying behaviour differs from :class:`ResolvedFit` — the
    underlying parameter representation and simulation pipeline remain the
    same.
    """

    def get_key(self, param):
        match param:
            case "log_dist":
                return "_".join([self.key, self.filter])

        return super().get_key(param)

TransformedResolvedFit ¤

Bases: ResolvedFit

Resolved-source fit using coefficients describing a transformed basis.

This variant initialises the log_dist parameter from a set of coefficients (for example a set of basis coefficients or a compressed representation) rather than from an explicit full image distribution.

Source code in src/dorito/model_fits.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class TransformedResolvedFit(ResolvedFit):
    """Resolved-source fit using coefficients describing a transformed basis.

    This variant initialises the ``log_dist`` parameter from a set of
    coefficients (for example a set of basis coefficients or a compressed
    representation) rather than from an explicit full image distribution.

    """

    def initialise_params(self, optics, coeffs):

        params = ModelFit.initialise_params(self, optics)

        # log distribution
        params["log_dist"] = (self.get_key("log_dist"), coeffs)

        return params

PointResolvedFit ¤

Bases: TransformedResolvedFit

Resolved fit combining an unresolved point-like component and an extended component.

This fit represents the source as a superposition of a point source component and a resolved (extended) component. This is useful for modelling systems like young stars with extended protoplanetary disks.

Notes

Parameters for building the transformed/resolved component are described on :meth:initialise_params (for example optics, coeffs and contrast are the arguments used when initialising parameters).

Source code in src/dorito/model_fits.py
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
class PointResolvedFit(TransformedResolvedFit):
    """Resolved fit combining an unresolved point-like component and an extended component.

    This fit represents the source as a superposition of a point source
    component and a resolved (extended) component. This is useful for
    modelling systems like young stars with extended protoplanetary disks.

    Notes
    -----
    Parameters for building the transformed/resolved component are described
    on :meth:`initialise_params` (for example ``optics``, ``coeffs`` and
    ``contrast`` are the arguments used when initialising parameters).
    """

    def get_key(self, param):

        match param:
            case "contrast":
                return self.filter

        return super().get_key(param)

    def map_param(self, param):

        # Map the appropriate parameter to the correct key
        if param in ["contrast"]:
            return f"{param}.{self.get_key(param)}"

        # Else its global
        return super().map_param(param)

    def initialise_params(self, optics, coeffs, contrast):

        params = ModelFit.initialise_params(self, optics)

        # log distribution
        params["log_dist"] = (self.get_key("log_dist"), coeffs)
        params["contrast"] = self.get_key("contrast"), np.array(contrast)

        return params

    def model_interferogram(
        self,
        psf,
        model,
        rotate: bool = None,
    ):

        contrast = model.params["contrast"][self.get_key("contrast")]
        psf1 = psf * (1 - contrast)
        psf2 = psf * (contrast)

        # convolve source with PSF
        resolved_component = model.get_distribution(self, rotate=rotate)
        return psf1 + psf2.convolve(resolved_component, method="fft").data

ResolvedOIFit ¤

Bases: _OIFit, _BaseResolvedFit

OI-data backed resolved-source fit utilities.

This class mixes the OI-data wrapper behaviour from :class:_OIFit with the resolved-source helpers in :class:_BaseResolvedFit. It provides methods to convert distributions into OTFs/visibilities, produce model DISCO outputs, and compute dirty images usable for visualisation and normalisation.

Methods:

Name Description
initialise_params

Prepare log_dist and base_uv parameters for an OI fit.

to_otf

Return a dLux MFT representing the distribution in OTF space.

to_cvis

Convert an image distribution into flattened complex visibilities suitable for DISCO-style modelling.

dirty_image

Compute a dirty image from the underlying observed OI visibilities.

__call__

Produce the amplitudes/phases used by DISCO from the model distribution.

Source code in src/dorito/model_fits.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
class ResolvedOIFit(_OIFit, _BaseResolvedFit):
    """OI-data backed resolved-source fit utilities.

    This class mixes the OI-data wrapper behaviour from :class:`_OIFit` with
    the resolved-source helpers in :class:`_BaseResolvedFit`. It provides
    methods to convert distributions into OTFs/visibilities, produce model
    DISCO outputs, and compute dirty images usable for visualisation and
    normalisation.

    Methods
    -------
    initialise_params(model, distribution)
        Prepare ``log_dist`` and ``base_uv`` parameters for an OI fit.
    to_otf(model, distribution)
        Return a dLux MFT representing the distribution in OTF space.
    to_cvis(model, distribution)
        Convert an image distribution into flattened complex visibilities
        suitable for DISCO-style modelling.
    dirty_image(...)
        Compute a dirty image from the underlying observed OI visibilities.
    __call__(model, rotate=None)
        Produce the amplitudes/phases used by DISCO from the model
        distribution.
    """

    def initialise_params(self, model, distribution):

        params = {}  # Initialise an empty dictionary for parameters
        distribution /= distribution.sum()  # normalise the distribution

        params["log_dist"] = self.get_key("log_dist"), np.log10(distribution)
        params["base_uv"] = self.get_key("base_uv"), self.get_base_uv(model, distribution.shape[0])

        return params

    def get_key(self, param):

        match param:
            case "log_dist":
                return self.filter
            case "base_uv":
                return self.filter  # this is probably unnecessary

    def map_param(self, param):

        # Map the appropriate parameter to the correct key
        if param in ["log_dist", "base_uv"]:
            return f"{param}.{self.get_key(param)}"

        # Else its global
        return param

    def get_base_uv(self, model, n_pix):
        """
        Get the base uv for normalisation

        Args:
            model: The model object containing the parameters.
            n_pix: The number of pixels in one axis of the distribution.
        Returns:
            Array: The base UV for normalisation, which is the Fourier transform of a delta function.
        """
        ind = n_pix // 2
        base_dist = np.zeros((n_pix, n_pix)).at[ind, ind].set(1.0)

        # base uv for normalisation
        base_uv = self.to_otf(model, base_dist)
        return base_uv

    def to_otf(self, model, distribution):
        """
        Transform the distribution to the OTF plane (Optical Transfer Function).
        This method performs a Matrix Fourier Transform of the distribution and returns the
        resulting visibilities in the OTF format.
        Args:
            model: The model object containing the parameters.
            distribution: The distribution of the resolved source.
        Returns:
            dlu.MFT: The OTF visibilities as a dLux MFT object.
        """

        return dlu.MFT(
            phasor=distribution + 0j,
            wavelength=self.wavel,
            pixel_scale_in=model.pscale_in,
            npixels_out=model.uv_npixels,
            pixel_scale_out=model.uv_pscale,
            inverse=True,
        )

    def to_cvis(self, model, distribution):
        """Convert an image distribution into complex visibilities for DISCO.

        The pipeline performed here is:
        1. Transform the image distribution to the OTF plane via
           :meth:`to_otf` (a dLux MFT).
        2. Normalise the complex u,v plane by the stored ``base_uv`` for this
           fit (see :meth:`initialise_params`).
        3. Downsample the u,v plane to the DISCO sampling using
           :func:`dlu.downsample`.
        4. Flatten the 2D u,v array and return the first half of the vector —
           for a real-valued image the Fourier transform is Hermitian symmetric
           and only half the plane is needed.

        Parameters
        ----------
        model : object
            Model object providing UV/OTF parameters and access to
            ``model.params['base_uv']`` for normalisation.
        distribution : array-like
            2D image array (npixels x npixels) describing the resolved source
            brightness distribution.

        Returns
        -------
        numpy.ndarray
            1-D complex array containing the flattened (half) complex
            visibilities suitable for DISCO-style modelling.

        Notes
        -----
        The returned vector contains only the first half of the flattened
        u,v array because of u/v symmetry; callers expecting a full u,v
        representation should reconstruct it using Hermitian symmetry.
        """

        # Perform MFT and move to OTF plane
        uv = self.to_otf(model, distribution)  # shape (102, 102)

        # Normalise the complex u,v plane
        uv /= model.params["base_uv"][self.get_key("base_uv")]

        # Downsample to the desired u,v resolution
        uv = dlu.downsample(uv, 2, mean=True)  # shape (51, 51)

        # flatten and take first half (u,v symmetry)
        cvis = uv.flatten()[: uv.size // 2]

        return cvis

    def model_disco(self, model, distribution):
        """
        Compute the model visibilities and phases for the given model object.
        """
        cvis = self.to_cvis(model, distribution)
        return self.flatten_model(cvis)

    def dirty_image(
        self, model, npix=None, rotate=True, otf_support=None, pad=None, pad_value=1 + 0j
    ):
        """
        Get the dirty image via MFT. This is the image that would be obtained
        if the visibilities were directly transformed back to the image plane.

        Args:
            model: The model object containing the parameters.
            npix: The number of pixels in one axis of the dirty image.
                    If None, uses the same size as the model source distribution.
            rotate: If True, rotates the dirty image by the parallactic angle.
                    If a float, rotates by that (-'ve) angle in radians.
        Returns:
            Array: The dirty image, normalised to sum to 1.
        """

        if npix is None:
            npix = model.get_distribution(self).shape[0]

        # converting to u,v visibilities
        log_vis = np.dot(np.linalg.pinv(self.vis_mat), self.vis)
        phase = np.dot(np.linalg.pinv(self.phi_mat), self.phi)
        vis_im, phase_im = vis_to_im(log_vis, phase, (51, 51))

        # exponentiating
        uv = np.exp(vis_im + 1j * phase_im)

        if pad is not None:
            # Pad the uv visibilities if a pad is specified
            uv = np.pad(uv, pad_width=pad, mode="constant", constant_values=pad_value)

        # If an OTF support is provided, apply it to the uv visibilities
        if otf_support is not None:
            uv *= otf_support

        # Getting the dirty image
        dirty_image = dlu.MFT(
            phasor=uv,
            wavelength=self.wavel,
            pixel_scale_in=2 * model.uv_pscale,
            npixels_out=npix,
            pixel_scale_out=model.pscale_in,
            inverse=True,
        )

        # Taking amplitudes
        dirty_image = np.abs(dirty_image)

        # Optional rotation of the dirty image
        if rotate:
            dirty_image = self.rotate(dirty_image)

        # Normalise the image
        return dirty_image / dirty_image.sum()

    def __call__(self, model, rotate: bool = None):
        """
        Simulate the DISCOs from the resolved source distribution.
        This method retrieves the distribution from the model, optionally rotates it,
        and then computes the DISCOs using the model_disco method.
        Args:
            model: The model object containing the parameters.
            rotate: If True, rotates the distribution by the parallactic angle.
                    If a float, rotates by that (-'ve) angle in radians.
        Returns:
            tuple: A tuple containing the amplitudes and phases in the DISCO basis.
        """
        # NOTE: Distribution must be odd number of pixels in one axis
        distribution = model.get_distribution(self, rotate=rotate)

        return self.model_disco(model, distribution=distribution)

get_base_uv(model, n_pix) ¤

Get the base uv for normalisation

Args: model: The model object containing the parameters. n_pix: The number of pixels in one axis of the distribution. Returns: Array: The base UV for normalisation, which is the Fourier transform of a delta function.

Source code in src/dorito/model_fits.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def get_base_uv(self, model, n_pix):
    """
    Get the base uv for normalisation

    Args:
        model: The model object containing the parameters.
        n_pix: The number of pixels in one axis of the distribution.
    Returns:
        Array: The base UV for normalisation, which is the Fourier transform of a delta function.
    """
    ind = n_pix // 2
    base_dist = np.zeros((n_pix, n_pix)).at[ind, ind].set(1.0)

    # base uv for normalisation
    base_uv = self.to_otf(model, base_dist)
    return base_uv

to_otf(model, distribution) ¤

Transform the distribution to the OTF plane (Optical Transfer Function). This method performs a Matrix Fourier Transform of the distribution and returns the resulting visibilities in the OTF format. Args: model: The model object containing the parameters. distribution: The distribution of the resolved source. Returns: dlu.MFT: The OTF visibilities as a dLux MFT object.

Source code in src/dorito/model_fits.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def to_otf(self, model, distribution):
    """
    Transform the distribution to the OTF plane (Optical Transfer Function).
    This method performs a Matrix Fourier Transform of the distribution and returns the
    resulting visibilities in the OTF format.
    Args:
        model: The model object containing the parameters.
        distribution: The distribution of the resolved source.
    Returns:
        dlu.MFT: The OTF visibilities as a dLux MFT object.
    """

    return dlu.MFT(
        phasor=distribution + 0j,
        wavelength=self.wavel,
        pixel_scale_in=model.pscale_in,
        npixels_out=model.uv_npixels,
        pixel_scale_out=model.uv_pscale,
        inverse=True,
    )

to_cvis(model, distribution) ¤

Convert an image distribution into complex visibilities for DISCO.

The pipeline performed here is: 1. Transform the image distribution to the OTF plane via :meth:to_otf (a dLux MFT). 2. Normalise the complex u,v plane by the stored base_uv for this fit (see :meth:initialise_params). 3. Downsample the u,v plane to the DISCO sampling using :func:dlu.downsample. 4. Flatten the 2D u,v array and return the first half of the vector — for a real-valued image the Fourier transform is Hermitian symmetric and only half the plane is needed.

Parameters:

Name Type Description Default
model object

Model object providing UV/OTF parameters and access to model.params['base_uv'] for normalisation.

required
distribution array - like

2D image array (npixels x npixels) describing the resolved source brightness distribution.

required

Returns:

Type Description
ndarray

1-D complex array containing the flattened (half) complex visibilities suitable for DISCO-style modelling.

Notes

The returned vector contains only the first half of the flattened u,v array because of u/v symmetry; callers expecting a full u,v representation should reconstruct it using Hermitian symmetry.

Source code in src/dorito/model_fits.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def to_cvis(self, model, distribution):
    """Convert an image distribution into complex visibilities for DISCO.

    The pipeline performed here is:
    1. Transform the image distribution to the OTF plane via
       :meth:`to_otf` (a dLux MFT).
    2. Normalise the complex u,v plane by the stored ``base_uv`` for this
       fit (see :meth:`initialise_params`).
    3. Downsample the u,v plane to the DISCO sampling using
       :func:`dlu.downsample`.
    4. Flatten the 2D u,v array and return the first half of the vector —
       for a real-valued image the Fourier transform is Hermitian symmetric
       and only half the plane is needed.

    Parameters
    ----------
    model : object
        Model object providing UV/OTF parameters and access to
        ``model.params['base_uv']`` for normalisation.
    distribution : array-like
        2D image array (npixels x npixels) describing the resolved source
        brightness distribution.

    Returns
    -------
    numpy.ndarray
        1-D complex array containing the flattened (half) complex
        visibilities suitable for DISCO-style modelling.

    Notes
    -----
    The returned vector contains only the first half of the flattened
    u,v array because of u/v symmetry; callers expecting a full u,v
    representation should reconstruct it using Hermitian symmetry.
    """

    # Perform MFT and move to OTF plane
    uv = self.to_otf(model, distribution)  # shape (102, 102)

    # Normalise the complex u,v plane
    uv /= model.params["base_uv"][self.get_key("base_uv")]

    # Downsample to the desired u,v resolution
    uv = dlu.downsample(uv, 2, mean=True)  # shape (51, 51)

    # flatten and take first half (u,v symmetry)
    cvis = uv.flatten()[: uv.size // 2]

    return cvis

model_disco(model, distribution) ¤

Compute the model visibilities and phases for the given model object.

Source code in src/dorito/model_fits.py
404
405
406
407
408
409
def model_disco(self, model, distribution):
    """
    Compute the model visibilities and phases for the given model object.
    """
    cvis = self.to_cvis(model, distribution)
    return self.flatten_model(cvis)

dirty_image(model, npix=None, rotate=True, otf_support=None, pad=None, pad_value=1 + 0j) ¤

Get the dirty image via MFT. This is the image that would be obtained if the visibilities were directly transformed back to the image plane.

Args: model: The model object containing the parameters. npix: The number of pixels in one axis of the dirty image. If None, uses the same size as the model source distribution. rotate: If True, rotates the dirty image by the parallactic angle. If a float, rotates by that (-'ve) angle in radians. Returns: Array: The dirty image, normalised to sum to 1.

Source code in src/dorito/model_fits.py
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
def dirty_image(
    self, model, npix=None, rotate=True, otf_support=None, pad=None, pad_value=1 + 0j
):
    """
    Get the dirty image via MFT. This is the image that would be obtained
    if the visibilities were directly transformed back to the image plane.

    Args:
        model: The model object containing the parameters.
        npix: The number of pixels in one axis of the dirty image.
                If None, uses the same size as the model source distribution.
        rotate: If True, rotates the dirty image by the parallactic angle.
                If a float, rotates by that (-'ve) angle in radians.
    Returns:
        Array: The dirty image, normalised to sum to 1.
    """

    if npix is None:
        npix = model.get_distribution(self).shape[0]

    # converting to u,v visibilities
    log_vis = np.dot(np.linalg.pinv(self.vis_mat), self.vis)
    phase = np.dot(np.linalg.pinv(self.phi_mat), self.phi)
    vis_im, phase_im = vis_to_im(log_vis, phase, (51, 51))

    # exponentiating
    uv = np.exp(vis_im + 1j * phase_im)

    if pad is not None:
        # Pad the uv visibilities if a pad is specified
        uv = np.pad(uv, pad_width=pad, mode="constant", constant_values=pad_value)

    # If an OTF support is provided, apply it to the uv visibilities
    if otf_support is not None:
        uv *= otf_support

    # Getting the dirty image
    dirty_image = dlu.MFT(
        phasor=uv,
        wavelength=self.wavel,
        pixel_scale_in=2 * model.uv_pscale,
        npixels_out=npix,
        pixel_scale_out=model.pscale_in,
        inverse=True,
    )

    # Taking amplitudes
    dirty_image = np.abs(dirty_image)

    # Optional rotation of the dirty image
    if rotate:
        dirty_image = self.rotate(dirty_image)

    # Normalise the image
    return dirty_image / dirty_image.sum()

__call__(model, rotate=None) ¤

Simulate the DISCOs from the resolved source distribution. This method retrieves the distribution from the model, optionally rotates it, and then computes the DISCOs using the model_disco method. Args: model: The model object containing the parameters. rotate: If True, rotates the distribution by the parallactic angle. If a float, rotates by that (-'ve) angle in radians. Returns: tuple: A tuple containing the amplitudes and phases in the DISCO basis.

Source code in src/dorito/model_fits.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def __call__(self, model, rotate: bool = None):
    """
    Simulate the DISCOs from the resolved source distribution.
    This method retrieves the distribution from the model, optionally rotates it,
    and then computes the DISCOs using the model_disco method.
    Args:
        model: The model object containing the parameters.
        rotate: If True, rotates the distribution by the parallactic angle.
                If a float, rotates by that (-'ve) angle in radians.
    Returns:
        tuple: A tuple containing the amplitudes and phases in the DISCO basis.
    """
    # NOTE: Distribution must be odd number of pixels in one axis
    distribution = model.get_distribution(self, rotate=rotate)

    return self.model_disco(model, distribution=distribution)