Note

The following shows the code used to run this marx test. You can inspect it and adapt it to your needs, but you cannot copy and paste it directly because it depends on local $PATH and other environment variables. For example, we use a python function to manage the directory structure for all the images generated by all the tests instead of giving the file name directly to save images.

On-axis PSF for an HRC-I observation

Python : Copy input spectra

Since the HRC has no intrisinc energy resolution, the source spectrum can not be taken from the observed data. Instead, we fitted a model to a grating spectrum of the same source and saved that model spectrum to a file. AR Lac is known to be time variable and to show stellar flares, so this spectrum is only an approximation, but it is the best we can do here.

Spectrum files are part of the marx-test distribution. Copy them from the source code into the right directory.

# Note that this code might not run if you directly copy and paste it:
# - Not all import statements are shown here
# - `self` is a reference to a test instance, which allows access to
#   parameters such as the directory where the test is run etc.

'''Copy input spectra

Since the HRC has no intrisinc energy resolution, the source spectrum
can not be taken from the observed data. Instead, we fitted a model
to a grating spectrum of the same source and saved that model
spectrum to a file. AR Lac is known to be time variable and to show
stellar flares, so this spectrum is only an approximation, but it
is the best we can do here.

Spectrum files are part of the marx-test distribution.
Copy them from the source code into the right directory.
'''
import shutil
shutil.copy(os.path.join(self.pkg_data, 'ARLac_input_spec_marx.tbl'),
            os.path.join(self.basepath, 'input_spec_marx.tbl'))
shutil.copy(os.path.join(self.pkg_data, 'ARLac_input_spec_saotrace.rdb'),
            os.path.join(self.basepath, 'input_spec_saotrace.rdb'))

shell : Unzip fits file.

MARX cannot read zipped fits files, so we need to unzip the .fits.gz asol files that we downloaded from the archive. On the other hand, CIAO tools work on both zipped or unzipped files, so there is no need to unzip all of them, just the files that MARX reads as input.

gunzip -f download/13182/primary/pcadf408913236N002_asol1.fits

marx : Set marx parameters appropriate for observation

marx RA_Nom=332.17106315813 Dec_Nom=45.73817165926 Roll_Nom=301.41391297181 GratingType=NONE ExposureTime=18161.46342998743 DitherModel=FILE DitherFile=download/13182/primary/pcadf408913236N002_asol1.fits TStart=408913236.56726 SourceRA=332.17 SourceDEC=45.742306 DetectorType=HRC-I DetOffsetX=0.0014262321171452097 DetOffsetZ=-0.0025143152978017724 SpectrumType=FILE SpectrumFile=input_spec_marx.tbl OutputDir=marx_only SourceFlux=-1

marx2fits : No EDSER is available for HRC data

marx2fits --pixadj=NONE marx_only marx_only.fits

Lua input for SAOTrace : SAOTrace input matching observation

ra_pnt = 332.17106315813
dec_pnt = 45.73817165926
roll_pnt = 301.41391297181

dither_asol_chandra{ file = "/melkor/d1/guenther/marx/test/testexp55/HRCIPSF/download/13182/primary/pcadf408913236N002_asol1.fits",
                     ra = ra_pnt, dec = dec_pnt, roll = roll_pnt }

point{ position = { ra = 332.17,
          dec = 45.742306,
          ra_aimpt = ra_pnt,
          dec_aimpt = dec_pnt,
       },
       spectrum = { { file = "input_spec_saotrace.rdb",
                      units = "photons/s/cm2",
                      scale = 1,
                      format = "rdb",
                      emin = "ENERG_LO",
                      emax = "ENERG_HI",
                      flux = "FLUX"} }
    }

SAOTrace :

CXO time numbers are large and round-off error can appear which make SAOTrace fail. Therefore, shorten all times by about 0.01 sec to make sure.

trace-nest tag=saotrace srcpars=saotrace_source.lua tstart=408913236.57726  limit=18161.44342998743 limit_type=sec

marx : Run marx with SAOTrace ray file as input

marx RA_Nom=332.17106315813 Dec_Nom=45.73817165926 Roll_Nom=301.41391297181 GratingType=NONE ExposureTime=18161.46342998743 DitherModel=FILE DitherFile=download/13182/primary/pcadf408913236N002_asol1.fits TStart=408913236.56726 SourceRA=332.17 SourceDEC=45.742306 DetectorType=HRC-I DetOffsetX=0.0014262321171452097 DetOffsetZ=-0.0025143152978017724 SpectrumType=FILE SpectrumFile=input_spec_marx.tbl OutputDir=marx_saotrace SourceType=SAOSAC SAOSACFile=saotrace.fits SourceFlux=-1

marx2fits : Same settings as the marx2fits run above

marx2fits --pixadj=NONE marx_saotrace marx_saotrace.fits

Python : Extract radial count distribution

# Note that this code might not run if you directly copy and paste it:
# - Not all import statements are shown here
# - `self` is a reference to a test instance, which allows access to
#   parameters such as the directory where the test is run etc.

def radial_distribution(x, y):
    '''Calculate the radial distance from the mean position for events.

    Parameters
    ----------
    x, y : np.array
        x and y coordinates of events

    Returns
    -------
    r : np.array
        radial distance from mean position of the events.
    '''
    centx = x - astropy.stats.sigma_clipped_stats(x, sigma=1.5, maxiters=20)[0]
    centy = y - astropy.stats.sigma_clipped_stats(y, sigma=1.5, maxiters=20)[0]

    xy = np.vstack([centx, centy])
    return np.linalg.norm(xy, axis=0)

def filter_events(evt, circle=None, energy=None, time=None):
    '''A simple event filter function.

    The parameters offer several filters, it set to ``None`` a filter is not
    applied.
    This simple function does not duplicate the entire :ciao:`dm` syntax, only
    a few simple filters useful for plotting in pure-python functions.

    Parameters
    ----------
    evt : `astropy.table.Table`
        Event table
    circle : tuple or ``None``
        Tuple of the form (x, y, r) where (x, y) is the center of
        a circle and r the radius. All number are in detector coordinates.
    energy : tuple or ``None``
        Tuple of the form (lower, upper) with all energies in eV.
    time : tuple or ``None``
        Tuple of the form (star, end) with all times in seconds.

    Returns
    -------
    evt_filt : ~astropy.table.Table`
        Filtered event list
    '''
    if circle is not None:
        d = np.sqrt((cc(evt, 'x') - circle[0])**2 + (cc(evt, 'y') - circle[1])**2)
        indcirc = d < circle[2]
    else:
        indcirc = np.ones(len(evt), dtype=bool)

    if energy is not None:
        en = cc(evt, 'energy')
        inden = (en > energy[0]) & (en < energy[1])
    else:
        inden = np.ones_like(indcirc)

    if time is not None:
        t = cc(evt, 'time')
        indt = (t > time[0]) & (t < time[1])
    else:
        indt = np.ones_like(indcirc)

    return evt[indcirc & inden & indt]

def plot_ecf(ax, files, filterargs, bgfilterargs):
    '''
    Parameters
    ----------
    ax : matplotlib axes
        Axes where the plot will be placed
    file : list
        List of three strings with filename for observations,
        Marx simulation, and SAOTRace + marx simulation
    filterargs : dict
        See ``filter_events`` for details
    bgfilterargs : dict
        Select a background region (same size, no automatic scaling)

    Returns
    -------
    err_marx : float
        Flux error if the MARX simulated PSF is assumed to be right.
        This is calculated as follows: We find the radius that encircles 90% of
        all counts in the MARX simulation. Then, we extract the observed counts
        using that radius. If the simulation is correct, that radius should
        contain 90% of the observed counts, too. If, e.g. the simulated radius
        is too small, we may extract only 80 % of the counts. The ratio between
        the two would be 8/9=0.88, meaning that all fluxes extracted using
        this radius are 12 % too small.
    err_sao: float
        Same for the simulation that used SAOTrace + MARX.
    '''
    # In this energy range we have the most counts and we limited
    # the simulations to the same range.
    obs = filter_events(Table.read(files[0]), **filterargs)
    bkg = filter_events(Table.read(files[0]), **bgfilterargs)
    r_obs = radial_distribution(obs['x'], obs['y'])
    r_obs.sort()
    ax.plot(r_obs, np.linspace(0, 1 + len(bkg) / len(r_obs), len(r_obs)),
            label='Observation')

    simmarx = filter_events(Table.read(files[1]), **filterargs)
    r_marx = radial_distribution(simmarx['X'], simmarx['Y'])
    r_marx.sort()
    ax.plot(r_marx, np.linspace(0, 1, len(r_marx)), label='MARX')

    simsao = filter_events(Table.read(files[2]), **filterargs)
    r_sao = radial_distribution(simsao['X'], simsao['Y'])
    r_sao.sort()
    ax.plot(r_sao, np.linspace(0, 1, len(r_sao)), label='SAOTrace + MARX')

    ax.set_xscale('power', power=0.5)
    ax.set_xlabel('radius [pixel]')
    ax.set_ylabel('enclosed count fraction')
    ax.legend(loc='lower right')
    ax.set_xticks([.1, .4, .7, 1, 2, 3, 4, 5])
    ax.set_xlim([0.1, 5])
    ax.set_ylim([0, 1.])

    psf90_marx = np.percentile(r_marx, 90)
    ecf_at_that_rad = np.argmin(np.abs(r_obs - psf90_marx)) / len(r_obs)
    err_marx = ecf_at_that_rad / 0.9 - 1

    psf90_sao = np.percentile(r_sao, 90)
    ecf_at_that_rad = np.argmin(np.abs(r_obs - psf90_sao)) / len(r_obs)
    err_sao = ecf_at_that_rad / 0.9 - 1

    return err_marx, err_sao

'''Extract radial count distribution'''
import matplotlib.pyplot as plt
filterargs = {'circle': (self.source['x'], self.source['y'],
                         5 * self.source['r'])
              }

bgfilterargs =  {'circle': (self.source['bg_x'], self.source['bg_y'],
                         5 * self.source['r'])
                }

evt2file = self.get_data_file('evt2')
files = [evt2file, 'marx_only.fits', 'marx_saotrace.fits']

fig = plt.figure()
ax = fig.add_subplot(111)

err_marx, err_sao = plot_ecf(ax, files, filterargs, bgfilterargs)
ax.set_xlim([0, 20.])

fig.savefig(self.figpath(list(self.figures.keys())[0]),
            bbox_inches='tight')

self.save_test_result('marx90', err_marx)
self.save_test_result('saotrace90', err_sao)