Loading & manipulating data

Interferometric data from all modern optical-infrared interferometric instruments are stored in FITS files following the OIFITS2 (Optical Interferometry FITS) standard defined in Duvert et al. (2017).

In oimodeler optical-interferometry data are stored in an oimData object. This object uses astropy.io.fits , the standard module to load, save and manipulated FITS files.

Non-interferometric photometric or spectroscopic data can be added to an oimData object using the oimFluxData class.

Finally data can be filtered using the oimDataFilter class.

Interferometric data

This complete code corresponding to this section is available in InterferometricData.py

In oimodeler, data are stored in a oimData object. Other modules that requires access to data such as the oimSimulator and oimFitter are all working with an instance of oimData.

The initialisation method of the oimData class allows to pass either a filename (in standard string or pathlib.Path), a list of filenames, a hdulist or a list of hdulist.

filenames = list(data_dir.glob("*.fits"))
data = oim.oimData(filenames)

Data in astropy format

The OIFITS data stored as a list of astropy.io.fits.hdulist can be accessed using the oimData.data attribute of the oimData object.

data.data
[[<astropy.io.fits.hdu.image.PrimaryHDU object at 0x0000017847722D10>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847677490>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x000001784767F6D0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847677DD0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847689AD0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847697D10>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847689950>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847573210>],
 [<astropy.io.fits.hdu.image.PrimaryHDU object at 0x000001784783BCD0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847689310>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x000001784755A410>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847554E10>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178475571D0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x000001784754BC10>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178475385D0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847556690>],
 [<astropy.io.fits.hdu.image.PrimaryHDU object at 0x00000178476BC710>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x000001784752E3D0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x0000017847522410>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178474B8E10>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x000001784752CE90>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178474C58D0>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178474C4450>, <astropy.io.fits.hdu.table.BinTableHDU object at 0x00000178474D4E10>]]

In this example, we loaded 3 oifits files so that our data variable is a list of three hdulist.

len(data.data)
3

We can have more readable informations calling the oimData.info method.

data.info()
════════════════════════════════════════════════════════════════════════════════
file 0: 2018-12-07T063809_HD45677_A0B2D0C1_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════
file 1: 2018-12-07T063809_HD45677_A0B2D0C1_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════
file 2: 2018-12-07T063809_HD45677_A0B2D0C1_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════

In our case the OIFITS files contains the data extension OI_VIS2, OI_VIS, OI_T3 and OI_FLUX.

For each element of the list, we can call the info method of the hdulist. class which lists all extensions and gives some basics infos on what they contain.

data.data[0].info()
Filename: C:travailGitHuboimodelerdataFSCMa_MATISSE2018-12-07T063809_HD45677_A0B2D0C1_IR-LM_LOW_Chop_cal_oifits_0.fits
No.    Name      Ver    Type      Cards   Dimensions   Format
  0  PRIMARY       1 PrimaryHDU    1330   ()
  1  OI_TARGET     1 BinTableHDU     60   1R x 18C   [1I, 7A, 1D, 1D, 1E, 1D, 1D, 1D, 8A, 8A, 1D, 1D, 1D, 1D, 1E, 1E, 7A, 3A]
  2  OI_ARRAY      1 BinTableHDU     35   4R x 7C   [3A, 2A, 1I, 1E, 3D, 1D, 6A]
  3  OI_WAVELENGTH    1 BinTableHDU     20   64R x 2C   ['1E', '1E']
  4  OI_VIS2       1 BinTableHDU     41   6R x 10C   ['1I', '1D', '1D', '1D', '64D', '64D', '1D', '1D', '2I', '64L']
  5  OI_T3         1 BinTableHDU     53   4R x 14C   ['1I', '1D', '1D', '1D', '64D', '64D', '64D', '64D', '1D', '1D', '1D', '1D', '3I', '64L']
  6  OI_VIS        1 BinTableHDU     49   6R x 12C   ['1I', '1D', '1D', '1D', '64D', '64D', '64D', '64D', '1D', '1D', '2I', '64L']
  7  OI_FLUX       1 BinTableHDU     37   1R x 8C   ['I', 'D', 'D', 'D', '64D', '64D', 'I', '64L']

If needed, the user can access and modify the data directly.

For instance the following code double the errors on the VIS2DATA from the last OIFITS file:

data.data[2]["OI_VIS2"].data["VIS2ERR"]*= 2

Optimized data

In order to reduce the computation time when simulating data during model fitting, the oim:func:oimData.data <oimodeler.oimData.oimData> class also contain the data coordinates as single vectors and the logic to pass from optimized data (in a single vector) to unoptimized form (as a list of hdulist) as more complex structures (stored as lists of lists)

The data coordinates are stored in the following vectors:

  • The u-axis of the spatial frequencies data.vect_u

  • The v-axis of the spatial frequencies data.vect_v

  • The wavelength data.vect_wl

  • The time (as MJD) data data.vect_mjd

Let’s print their shape for our example:

print(data.vect_u.shape)
print(data.vect_v.shape)
print(data.vect_wl.shape)
print(data.vect_mjd.shape)
(5376,)
(5376,)
(5376,)
(5376,)

They contains all the coordinates of all the baselines at all wavelengths plus some zeros spatial frequencies data (used to computes flux and normaized visiblity).

On the other hand, to pass from the optimized data to unoptimized data are stored in members called struct_XXXX. For instance, in the followin we print the structures containing :

  • the number of baselines (including zero-frequency ones)

  • the number of wavelengths

  • the name of the extension

  • a code specifying which data type to compute: VIS2DATA, T3PHI, VIPHI (asbolute or differential) …

print(data.struct_nB)
print(data.struct_nwl)
print(data.struct_arrType)
print(data.struct_dataType)
[[7, 13, 7, 1], [7, 13, 7, 1], [7, 13, 7, 1]]
[[64, 64, 64, 64], [64, 64, 64, 64], [64, 64, 64, 64]]
[['OI_VIS2', 'OI_T3', 'OI_VIS', 'OI_FLUX'], ['OI_VIS2', 'OI_T3', 'OI_VIS', 'OI_FLUX'],
['OI_VIS2', 'OI_T3', 'OI_VIS', 'OI_FLUX']]
[[<oimDataType.VIS2DATA: 1>, <oimDataType.T3PHI: 128>, <oimDataType.VISAMP_ABS|VISPHI_DIF: 34>, <oimDataType.FLUXDATA: 256>],
[<oimDataType.VIS2DATA: 1>, <oimDataType.T3PHI: 128>, <oimDataType.VISAMP_ABS|VISPHI_DIF: 34>, <oimDataType.FLUXDATA: 256>],
[<oimDataType.VIS2DATA: 1>, <oimDataType.T3PHI: 128>, <oimDataType.VISAMP_ABS|VISPHI_DIF: 34>, <oimDataType.FLUXDATA: 256>]]

Note

The step of creating the optimized vectors and structures is done automatically when creating or updating an oimData object. However if you modify the data manually as shown above, you should call the oimData.prepareData method.

The oimData object also contains two methods to plot :

as a function of the spatial frequency, baseline length, position angle, or wavelength.

figuv, axuv = data.uvplot(color="byConfiguration")

figdata,axdata =  data.plot("SPAFREQ",["VIS2DATA","T3PHI"],cname="EFF_WAVE",
                           cunit="micron",errorbar=True,xunit="cycle/mas")
axdata[0].set_yscale("log")
Alternative text Alternative text
These pltting methods are based on the uvplot

and oiplot methods of the omiAxes class. See the plotting section for details and option in plotting oifits data with oimodeler.

Data Filtering

This complete code corresponding to this section is available in DataFiltering.py

Data filtering can be performed on oimData instances using the many filters that derived from the abstract oimDataFilterComponent

The available data filters

Here is the compherensive list of filters implemented in oimodeler

Available filter components

Filter Name

Short description

Class Keywords

oimDataFilterComponent

This is the class from which all filters derived

targets, arr

oimRemoveArrayFilter

Removing arrays by type: OI_VIS2, OI_T3…

targets, arr

oimRemoveInsnameFilter

Remove array by insname

targets, arr, insname

oimWavelengthRangeFilter

Cutting wavelength range(s)

targets, arr, wlRange, addCut, method

oimDataTypeFilter

Filtering by datatype(s) : VIS2DATA, VISAMP…

targets, arr, dataType

oimKeepDataType

Keep atatype that are listed: VIS2DATA, VISAMP…

targets, arr, dataType, removeArrIfPossible

oimWavelengthShiftFilter

Shifting wavelength table

targets, arr, wlShift

oimWavelengthSmoothingFilter

Spectral smoothing

targets, arr, smoothPix, normalizeError

oimWavelengthBinningFilter

Spectral Binning

targets, arr, bin, binGrid, normalizeError

oimWavelengthIntpBinFilter

Binning to wavelength grid with interpolation at window edges.

targets, arr, binGrid, binWindow, resetFlags, averageError, spectralChannels

oimFlagWithExpressionFilter

Flagging based on boolean expressions

targets, arr, expr, keepOldFlag

oimKeepBaselinesFilter

Selection based on baseline name(s)

targets, arr, baselines, keepOldFlag

oimRemoveBaselinesFilter

Selection based on baseline name(s)

targets, arr, baselines, keepOldFlag

oimKeepTelescopesFilter

Selection based on telescope name(s)

targets, arr, telescopes, keepOldFlag

oimRemoveTelescopesFilter

Selection based on telescope name(s)

targets, arr, telescopes, keepOldFlag

oimResetFlags

Set all the flags to False

targets, arr

oimDiffErrFilter

Compute differential error from std inside or outside a range

targets, arr, ranges, rangeType, excludeRange, dataType

oimSetMinErrFilter

Set minimum error on data in % for vis ans deg for phases

targets, arr, values, dataType

Applying filters to oimData

We first need to create a filter using one of the above function. For instance, here we a simple filter to remove the edge of our MATISSE data with the oimWavelengthRangeFilter. class.

filt_wl = oim.oimWavelengthRangeFilter(wlRange=[3.1e-6, 4e-6])

The oimWavelengthRangeFilter has two keywords:

  • targets: Which is common to all filter components: It specifies the targeted files within the data structure to which the filter applies. Possible values are: - "all" for all files (which we use in this example). - A single file specify by its index. - Or a list of indexes.

  • wlRange: The wavelength range to cut as a two elements list (min wavelength and max wavelength), or a list of multiple two-elements lists if you want to cut multiple wavelengths ranges simultaneously. In our example you have selected wavelength between 3 and 4 microns. Wavelengths outside this range will be removed from the data.

We then apply the filter using th oimData.setFilter method

data.setFilter(filt_wl)

After applying a filter on an oimData instance, this object will contain both the filtered and unfiltered data as two private members:

  • oimData._data: the unfiltered data

  • oimData._filteredData: the filtered data

oimData.data will be point toward the filtered

data unless the member oimData.data <oimodeler.oimData.oimData>.data()

We can temporary remove the filter by setting the oimData.useFilter member to False

data.useFilter = False

or we can remove the filter once and for all using the the oimData.setFilter method without argument.

data.setFilter()

Finally let’s plot the square visibility as the function of the spatial frequency for :

  • the unfiltered data in light grey.

  • the filtered data with a colorscale based on the wavlelength (in μm)

To plot the unfiltered data without removing the filter we can use the removeFilter=True

option of the oimData.plot method.

figcut,axcut = data.plot("SPAFREQ","VIS2DATA",yscale="log",xunit="cycle/mas",removeFilter=True,
                          label="Original data",color="orange",lw=4)
data.plot("SPAFREQ","VIS2DATA",axe=axcut,xunit="cycle/mas",label="Filtered data",color="k",lw=2)
axcut.legend()
axcut.set_title("Data cut in with 3.1<$\lambda$<4 microns")
Alternative text

A Few examples of filters

Spectral binning

Spectral binning can be applied easily using the oimWavelengthBinningFilter class. This might be useful to enhance the SNR on some noisy data or to reduce the

number data points in order to gain computing-time for model fitting.

Here we are binning some HIGH resolution YSO data from GRAVITY by a factor 100

and plotting the raw and binned data.

dir0 = Path(__file__).resolve().parents[2] / "data" / "RealData" / "GRAVITY" / "HD58647"
filenames = list(dir0.glob("*.fits"))
data = oim.oimData(filenames)

filt_bin=oim.oimWavelengthBinningFilter(bin=100,normalizeError=False)
data.setFilter(filt_bin)

figbin,axbin = data.plot("SPAFREQ","VIS2DATA",yscale="log",xunit="cycle/mas",removeFilter=True,
                         label="Original data",color="orange",lw=4)
data.plot("SPAFREQ","VIS2DATA",axe=axbin,xunit="cycle/mas",label="Filtered data",color="k",lw=2)
axbin.legend()
axbin.set_title("Data binned by a factor of 100")
Alternative text

Flagging with expressions

The oimFlagWithExpressionFilter

class can be used to remove data based on an expression based on standard OIFITS2 keywords (e.g. VIS2DATA, VIS2ERR, EFF_WAVE, UCOORD, MJD …) and a few additionnal quantities computed by oimodeler such as the baseline length (LENGTH) or orientation (PA).

Note

In the OIFITS2 format, all data extensions (OI_VIS2, OI_VIS, OI_T3, and OI_FLUX) contain a boolean column FLAG used to flag bad data. The flagged data are not used in oimodeler when computing \(chi^2\).

Typical use of the class oimFlagWithExpressionFilter are :

  • flagging baselines based on length or orientation to perform specific model-fitting

  • flagging data based on relative errors

For instance in the following we flag data with baselines longer than 50m for the MATISSE.

This can be useful to determine the caracterist size of object using simple models such as Gaussian or uniform disk and avoid being biased by longer baselines than would contain information on smaller structures.

path = Path(__file__).parent.parent.parent
dir0 = path / "data"  / "RealData" / "MATISSE"/ "FSCMa"
filenames = list(data_dir.glob("*.fits"))
data = oim.oimData(filenames)

filt_length=oim.oimFlagWithExpressionFilter(expr="LENGTH>50")
data.setFilter(filt_length)

figflag,axflag = data.plot("SPAFREQ","VIS2DATA",xunit="cycle/mas",removeFilter=True,
                            color="orange",label="Original data",lw=5)
data.plot("SPAFREQ","VIS2DATA",axe=axflag,xunit="cycle/mas",label="Filtered data",color="k",lw=2)
axflag.legend()
axflag.set_title("Removing (=flaggging) data where  B>50m")
Alternative text

Selection by baseline name(s)

The oimKeepBaselinesFilter class can be used to select data by baseline name. For instance in the following we keep the data for the MATISSE data for the A0-B2 and A0-D0 baselines. Other data are flagged and ths will not be used for chi2 computation and model fitting.

This can be useful to determine the caracterist size of object using simple models such as Gaussian or uniform disk and avoid being biased by longer baselines than would contain information on smaller structures.

path = Path(__file__).parent.parent.parent
dir0 = path / "data"  / "RealData" / "MATISSE"/ "FSCMa"
filenames = list(dir0.glob("*.fits"))
data = oim.oimData(filenames)

baselines=["A0-B2","A0-D0"]
filt_baselines=oim.oimKeepBaselinesFilter(baselines=baselines,arr="OI_VIS2")
data.setFilter(filt_baselines)
figflag,axflag = data.plot("SPAFREQ","VIS2DATA",xunit="cycle/mas",removeFilter=True,
                           color="orange",label="Original data",lw=5)
data.plot("SPAFREQ","VIS2DATA",axe=axflag,xunit="cycle/mas",
          label="Filtered data",color="k",lw=2)
axflag.legend()
axflag.set_title(f"Keep only baselines {baselines}")
Alternative text

Photometric and spectroscopic data

This complete code corresponding to this section is available in PhotometricAndSpectroscopicData.py

The OIFITS2 format allow to use flux or spectrum measurements using the OI_FLUX extension. The oimFluxdata class allows to convert flux or spectroscopic measurement into and OIFITS file containing the OI_FLUX and extension as well as the compulsory OI_WAVELENGTH, OI_TARGET and OI_ARRAY extensions.

To build some flux data you need to provide the oimFluxdata with:

  • oitarget: a OI_TARGET extension with the proper target name (can be copied from a OIFITS file)

  • wl : the spectral channel central wavelengths for your flux/spectrum (unit in meter).

  • dwl: the spectral channels width (can be put to some dummy values)

  • flx: the fluxes measurements.

  • flxerr: the uncertainties on the fluxes measurements.

Let’s assume that we have a 3 columns ascii files (named iso_spectrum_fname) for a ISO spectrum with: - the wavelengths in microns - the flux in Jansky, - and the uncertainties on the fluxes in Jansky.

The following code allows to load the ascii file, using astropy.io.ascii module, create a oimFluxdata object and add it to some previously created oimData object

isodata=ascii.read(iso_spectrum_fname)

wl  = isodata.columns['col1'].data*1e-6 # in m
dwl = 1e-9 #dwl is currently not used in oimodeler

flx = isodata.columns['col2'].data      # in Jy
err_flx = isodata.columns['col3'].data  # in Jy

oitarget=data.data[0]["OI_TARGET"].copy()

isoFluxData = oim.oimFluxData(oitarget,wl,dwl,flx,err_flx)
data.addData(isoFluxData)

The data can then used as standard OIFITS2 format data in oimodeler

We can print the content of the data showing our ISO fluxes added as the third unamed file.

data.info()
════════════════════════════════════════════════════════════════════════════════
file 0: 2018-12-07T063809_HD45677_A0B2D0C1_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════
file 1: 2018-12-09T060636_HD45677_K0B2D0J3_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════
file 2: 2018-12-13T081156_HD45677_A0G1J2J3_IR-LM_LOW_Chop_cal_oifits_0.fits
────────────────────────────────────────────────────────────────────────────────
4)   OI_VIS2 :       (nB,nλ) = (6, 64)       dataTypes = ['VIS2DATA']
5)   OI_T3   :       (nB,nλ) = (4, 64)       dataTypes = ['T3PHI']
6)   OI_VIS  :       (nB,nλ) = (6, 64)       dataTypes = ['VISAMP', 'VISPHI']
7)   OI_FLUX :       (nB,nλ) = (1, 64)       dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════
file 3:
────────────────────────────────────────────────────────────────────────────────
4)   OI_FLUX :       (nB,nλ) = (1, 29510)    dataTypes = ['FLUXDATA']
════════════════════════════════════════════════════════════════════════════════

We can plot the spectrum of the MATISSE and ISO data to compare them (note that they are both in Jansky).

data.plot("EFF_WAVE","FLUXDATA",color="byFile",xunit="micron",errorbar=True)
Alternative text