<jupyter_start><jupyter_text>afwTables: A Guided TourAuthor(s): **<NAME>** ([@ih64](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@ih64))Maintainer(s): **<NAME>** ([@douglasleetucker](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@douglasleetucker))Level: **Introductory**Last Verified to Run: **2021-09-03**Verified Stack Release: **w_2021_33**Catalogs of astronomical objects and their many  measurements will be a primary data product that LSST provides. Queries of those catalogs will be the starting point for almost all LSST science analyses. On the way to filling the LSST database with these catalogs, the science pipelines will generate and manipulate a lot of internal tables; the python class that the Stack defines and uses for these tables is called an "afwTable".  Learning Objectives:After working through this tutorial you should be able to: 1. Make a bare bones afw schema and table;2. Set and get values in a schema and table;3. Navigate large schemas;4. Read and write a source detection catalog table;5. Learn to use source detection catalog methods, and to avoid common pitfalls;6. Learn to use source match vectors. LogisticsThis notebook is intended to be run at `lsst-lsp-stable.ncsa.illinois.edu` or `data.lsst.cloud` from a local git clone of the [StackClub](https://github.com/LSSTScienceCollaborations/StackClub) repo. Set-up<jupyter_code># Site, host, and stack version
! echo $EXTERNAL_INSTANCE_URL
! echo $HOSTNAME
! eups list -s | grep lsst_distrib<jupyter_output><empty_output><jupyter_text>We'll need the `stackclub` package to be installed. If you are not developing this package, you can install it using `pip`, like this:```pip install git+git://github.com/LSSTScienceCollaborations/StackClub.gitegg=stackclub```If you are developing the `stackclub` package (eg by adding modules to it to support the Stack Club tutorial that you are writing, you'll need to make a local, editable installation. In the top level folder of the `StackClub` repo, do:<jupyter_code>! cd .. && python setup.py -q develop --user && cd -<jupyter_output><empty_output><jupyter_text>When editing the `stackclub` package files, we want the latest version to be imported when we re-run the import command. To enable this, we need the %autoreload magic command.<jupyter_code>%load_ext autoreload
%autoreload 2<jupyter_output><empty_output><jupyter_text>For this tutorial we'll need the following modules:<jupyter_code>%matplotlib inline
#%matplotlib ipympl

import os
import warnings
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.style.use('seaborn-poster')
from IPython.display import IFrame, display, Markdown
plt.style.use('seaborn-talk')
from astropy.io import ascii
import lsst.daf.butler as dafButler
import lsst.daf.base as dafBase
import lsst.geom
import lsst.afw.table as afwTable<jupyter_output><empty_output><jupyter_text>Your first tableTo begin, we will make a bare-bones afw table so that we can clearly showcase some important concepts. First we will make the simplest possible table, by hand. While creating tables by hand will not likely be the standard use case, it is useful from a tutorial standpoint, as it will allow us to excercise some concepts one at a time<jupyter_code># afw tables need a schema to tell the table how its data are organized
# Lets have a look at a simple schema:
min_schema = afwTable.SourceTable.makeMinimalSchema()
# But what is the schema exactly? Printing it out can be informative
print(min_schema)<jupyter_output><empty_output><jupyter_text>Our schema contains 4 Fields: one for each celestial coordinate, an id that uniquely defines it, and a 'parent', which lists the id of the source this source was deblended from. We will deal with the parent column in more detail in a few cells, but for now you can ignore it.Each field has some accompanying information to go along with it. In addition to its name, we get a helpful docstring describing it. We also get the units that values for this field must have. For example, any value associated with the id key has to be a long integer, and all entries for celestial coordniates have to be instances of an Angle class. We will showcase the Angle class shortly.If printing out the schema gives you more information that you want, you can get the names. If the names are informative enough, this might be all you need.<jupyter_code>min_schema.getNames()
# We can also add another field to the schema, using a call pattern like this:
min_schema.addField("r_mag", type=np.float32, doc="r band flux", units="mag")
# Lets make sure the field was added by printing out the schema once more:
print(min_schema)<jupyter_output><empty_output><jupyter_text>You can also ask for a ordered list of names. In this case where our schema contains only 4 names, there is not a clear advantage. However, we will soon encounter schemas that contain many dozens of names. Sifting through a ordered list of these may be preferable. The ordering here is not alphabetical, but instead mirrors the ordering of these fields in the schema. You can check that is the case by comparing the ordering of output in the next cell to the one above<jupyter_code>min_schema.getOrderedNames()<jupyter_output><empty_output><jupyter_text>> We pause here to point out some caveats. 1. Schemas are append only. You can add new fields, but you cannot remove them. 2. The units you use have to be understood by astropy. You can find a list of acceptable units at the bottom of this page http://docs.astropy.org/en/stable/units/index.htmlmodule-astropy.units3. Specific types are allowed. The short and long of it is you may use floats, ints, longs, strings, Angle objects, and arrays. For more details you can go to the bottom of this page http://doxygen.lsst.codes/stack/doxygen/x_masterDoxyDoc/afw_table.html Now that we have a schema, we can use it to make a table.<jupyter_code>min_table = afwTable.BaseCatalog(min_schema)
# our table is empty, and we can check this by looking at its length
print('our minimal table has {} rows'.format(len(min_table)))<jupyter_output><empty_output><jupyter_text>Now we will add some data to our minimal catalog. Catalogs are collections of 'records', which themselves contain data. Therefore, we must first create records, and hand those records over in turn to our Table. Records must adhere to the schema that the Table has, and so we must add data in field by field.<jupyter_code># make a new record.
rec = min_table.addNew()
# grab a hold of the keys for the record. We will use these to add data 
field_dict = min_schema.extract('*') #this returns a dictionary of all the fields
field_dict.keys()
# access the dictionary one field at a time, and grab each field's key. 
# note these are instances of a Key object, and not to be confused with python dictionary keys.
id_key = field_dict['id'].key
ra_key = field_dict['coord_ra'].key
dec_key = field_dict['coord_dec'].key
parent_key = field_dict['parent'].key
r_mag_key = field_dict['r_mag'].key

#use the keys to add data in our record
rec.set(id_key, 1)
rec.set(r_mag_key, 19.0)
rec.set(ra_key, lsst.geom.Angle(0.2, units=lsst.geom.radians))
rec.set(dec_key, lsst.geom.Angle(-3.14, units=lsst.geom.radians))
rec.set(parent_key, 0)<jupyter_output><empty_output><jupyter_text>Notice to set the ra and dec, we needed to create `geom.Angle` objects for them. The object contains both the value of the angle, and the units the angle is in. The units keyword is set to radians by default, and all the DM code works on radians internally. To keep consistency with this, it is largely considered good pratice to work in radians too, setting the keyword for clarity. If you insisted that the angle be in other units, you can set them using the the units keyword. Other typical choices are degrees, arcminutes, arcseconds. You can learn more about `lsst.geom.Angle` objects [here]( http://doxygen.lsst.codes/stack/doxygen/x_masterDoxyDoc/classlsst_1_1geom_1_1_angle.html)Additionally, we set the parent to zero. This means this record refers to the object before any deblending occoured. Lets look at our table now to see how it stands.<jupyter_code>min_table<jupyter_output><empty_output><jupyter_text>We will flesh out the parent column a bit more by adding our next record. Notice we can keep using the keys we defined above. Also notice our second record's parent is listed as 1. This means the object 2 was the result of being deblended from object 1, i.e. object 2 is a child object of object 1.<jupyter_code>rec = min_table.addNew()
rec.set(id_key, 2)
rec.set(r_mag_key, 18.5)
rec.set(ra_key, lsst.geom.Angle(3.14, units=lsst.geom.radians))
rec.set(dec_key, lsst.geom.Angle(2.0, units=lsst.geom.radians))
rec.set(parent_key, 1)
min_table<jupyter_output><empty_output><jupyter_text>One more caveat to note: in the output in the cell above, the table prints coordinates in radians by default<jupyter_code># your turn. add one more record to our table


# now that we have multiple records in our table, we can select particular ones
# tables support indexing
min_table[1]
# you may iterate over them too
for rec in min_table:
    print(rec.get(id_key))
# you can grab values from particular records by using our schema keys
min_table[1].get(id_key)<jupyter_output><empty_output><jupyter_text>Using source catalogs produced by DMA more typical use case will be to read in a catalog that is produced by a DM process.  We will show how to read in and work with a source catalog produced from the Data Management Stack in the following section.   Data accessIf you know the path to your source catalog, there is a quick way to read it in. However, it is often more powerful to use the 'data butler' to fetch data for you. The butler knows about camera geometry, sensor characteristics, where data are located, and so forth. Having this anciliary information on hand is often very useful. For completeness we will demonstrate both ways of reading in a source catalog, with the note that it is largely considered better practice to use the data butler. The data butler has its own tutorial(s), and so we will defer further details on it until later. For now, you may think of it as an abstraction that allows you to quickly fetch catalogs for you. The user just needs to point the butler to where to look and what to look for.We use the DESC DC2 data set as an example.<jupyter_code># Location of the DC2 Gen3 repository on this site
URL = os.getenv('EXTERNAL_INSTANCE_URL')
if URL.endswith('data.lsst.cloud'): # IDF
    repo = "s3://butler-us-central1-dp01"
elif URL.endswith('ncsa.illinois.edu'): # NCSA
    repo = "/repo/dc2"
else:
    raise Exception(f"Unrecognized URL: {URL}")

# Collection should be the same on all sites
collection='2.2i/runs/DP0.1'

# Define a dictionary with the band, ccd, and visit we wish to view
dataId = {'band':'i', 'visit':512055, 'detector':76}
# Create an instance of the butler to get the catalog
butler = dafButler.Butler(repo,collections=collection)
    
# use the dataId and the 'src' to get the source catalog. 
source_cat = butler.get('src', **dataId)<jupyter_output><empty_output><jupyter_text>You can find the path to the file again with the `butler.getURI` function<jupyter_code># We want to grab the dataset reference for this dataId 
#  to pass to the gen3 butler.getURI method...
# For this dataId, there really should only be one entry;
#  so really not necessary to loop...
datasetRefs = butler.registry.queryDatasets(datasetType='src',dataId=dataId, collections=collection)
for i,ref in enumerate(datasetRefs):
    try: uri = butler.getURI(ref)
    except: print("File not found...")
    if i > 10: break
uri = butler.getURI(ref)
print(uri)
dtypes = butler.registry.queryDatasetTypes()
for i,dt in enumerate(dtypes):
    print(dt)<jupyter_output><empty_output><jupyter_text>A few comments are in order on questions you may be having about the butler, and the previous cell. First, for the `gen2` butler, there is no good way to know which `dataId`s exist. That means you have to know ahead of time which `dataId`s it makes sense to use. DM fixed this for the `gen3` butler (e.g., one can use the `butler.registry.queryDatasets()` command to find the `dataId`s for different sets of data). Second, the string `'src'` refers to a very specific data product in the DM philosophy, which is a catalog that contains _the results of different measurement algorithms on detected sources on an individual CCD image_. We will meet some other catalogs later in the tutorial. For now, lets get to know this `src` catalog.  afw source catalog schemas<jupyter_code>#check its schema. Heads up, the schema is pretty big
source_cat.getSchema()<jupyter_output><empty_output><jupyter_text>These schemas tend to be large if many measurement algorithms used. Several algorithms are a part of the base measurement process, like aperture photometry, SDSS shape and centroid measurements. Other measurements are associated with previous calibration steps, like identifying sources to be candidates for PSF modeling. If deblending was run, several outputs from the process are stored in the table as well. Other measurement processess, like shape measurement processes, are considered extensions. All of these measurements often have many fields and analagous flag fields associated with them. To get a list of names of these high level processes only, we can provide the `topOnly=True` keyword arguement to the `getNames` method we met earlier in the tutorial.<jupyter_code>source_cat.getSchema().getNames(topOnly=True)<jupyter_output><empty_output><jupyter_text>However, this may be too vague. For example, if you ran KSB and HSM shape measurement, both will be lumped into the 'ext' catagory in the output above, but you may wish to search the schema for one in particular. We can use unix-like pattern matching with the `extract()` method to search the schema. This returns a dictionary where the keys are the schema fields whose names match the pattern you specified, and the values are the fields themselves.<jupyter_code>source_cat.getSchema().extract('*HSM*Psf*')<jupyter_output><empty_output><jupyter_text>Schemas in source catalogs include fields for measurements, and the associated flag fields for those measurements.  To tally how many fields, flag fields, and non-flag fields are contained, we can use several count methods.<jupyter_code>nFields = source_cat.schema.getFieldCount()
nFlagFields = source_cat.schema.getFlagFieldCount()
nNonFlagFields = source_cat.schema.getNonFlagFieldCount()
print('the schema contains {} fields, \
{} flags fields and {} non-flag fields'.format(nFields, nFlagFields, nNonFlagFields ))<jupyter_output><empty_output><jupyter_text>If we are just interested in the field names, we can use the `extract()` method, which supports regex pattern matching. `extract()` returns a python dictionary of key-value pairs, where the keys are the names of the schema fields, and the values are the schema items themselves. We are going to tack on the `keys` method to this dictionary so we just get the ids back.<jupyter_code>for k in source_cat.getSchema().extract('*HSM*Psf*').keys():
    print(k)<jupyter_output><empty_output><jupyter_text>If you already know the id of the field you are interested in, schema's have a `find` method that will return the field in question. For example, it is a safe bet that a schema will contain an id field<jupyter_code>source_cat.getSchema().find('id')<jupyter_output><empty_output><jupyter_text>When we dumped the entire schema, the very bottom of the schema contained fields  are named 'slot_'. These are called aliases in the schema, and can help you deal with any ambiguity in the table. For example, there are several algorithms used to measure the centroid, and many fileds with 'centroid' in their name as a result. If you want to have quick access to one algorithms measurement result, you can set up a slot alias for it. Lets do a working example on the first record in our table.<jupyter_code>slot_centroid = source_cat[0].getCentroid()
naive_cent_x, naive_cent_y = (source_cat['base_NaiveCentroid_x'][0], source_cat['base_NaiveCentroid_y'][0])
sdss_cent_x, sdss_cent_y = (source_cat['base_SdssCentroid_x'][0], source_cat['base_SdssCentroid_y'][0])

print('sloan centroid is {}, {}'.format(sdss_cent_x, sdss_cent_y))
print('naive centroid is {}, {}'.format(naive_cent_x, naive_cent_y))
print('slot centroid is {}'.format(slot_centroid))
# aliasing works with other methods
psf_flux_key = source_cat.getPsfFluxSlot().getMeasKey()
id_key = source_cat.getIdKey()<jupyter_output><empty_output><jupyter_text>As advertised, the slot centroid and SDSS centroid are the same. We also used some syntactic sugar to access the `naive` centroids and `sdss` centroids, which will be familiar to you if you are an astropy tables user. Speaking of astropy tables, you can make an astropy table version of a source catalog:<jupyter_code>source_cat.asAstropy()<jupyter_output><empty_output><jupyter_text>In this vein, you may find it more convenient to grab columns from the table and work with them as numpy arrays. As we mentioned above, the defacto unit choice of DM is radians. If you would prefer degrees, you can do something like this<jupyter_code>ra_deg = np.rad2deg(source_cat['coord_ra'])<jupyter_output><empty_output><jupyter_text>Now we will set aside the schema for this table, and look at the table itself so we can examine its methods. afw source catalogsSource catalogs support a lot of fast operations for common use cases which we will now discuss.<jupyter_code># Sorting is supported. by default catalogs are sorted by id
source_cat.isSorted(id_key)
# Find aperture flux columns
[k for k in source_cat.getSchema().extract('*ApFlux*').keys()]
# You can cut on the catalog.
# e.g. Make a boolean array to only keep sources with positive psf flux:
psf_mask = source_cat['slot_PsfFlux_instFlux'] > 0
psf_mask &= np.isfinite(source_cat['slot_ApFlux_instFlux'])
psf_mask &= np.isfinite(source_cat['slot_ApFlux_instFluxErr'])
psf_mask &= np.isfinite(source_cat['base_ClassificationExtendedness_value'])
pos_flux = source_cat.subset(psf_mask)

# You can sort on other keys too:
flux_key = pos_flux.getPsfFluxSlot().getMeasKey()
pos_flux.sort(flux_key)
pos_flux.isSorted(flux_key)
# You can get the children of particular objects.
# This is useful if you want to understand how one object was deblended, for example:

print(source_cat.getChildren(34363434455795246)) #the argument is the id of the parent object

# Note that this will only work if the source catalog is sorted on id or parent<jupyter_output><empty_output><jupyter_text>You can check if catalogs are contiguous in memory.<jupyter_code>source_cat.isContiguous()
pos_flux.isContiguous()<jupyter_output><empty_output><jupyter_text>Some operations are quicker if catalogs are contiguous in memory, like using numpy-like syntax to create masks. <NAME> performed some benchmark tests showing this is the case for a catalog with about half a million enteries. You can find the full details in a Slack thread [here](https://lsstc.slack.com/archives/C2JPL2DGD/p1525799998000344). Additionally, certain operations will not work on non-contiguous tables.<jupyter_code># uncomment the last line if you are curious and want to prove it wont work
# this will not work and give you an error message complaining pos_flux is not contiguous
# pos_flux.asAstropy()<jupyter_output><empty_output><jupyter_text>You can always force a table to be contiguous by making a deep copy of it.<jupyter_code>pos_flux = pos_flux.copy(deep=True)
pos_flux.isContiguous()<jupyter_output><empty_output><jupyter_text>In the next few cells, we show a few methods that can be used to search through tables<jupyter_code># Use the between method to get the indices of values within a range:
pos_flux.between(1e4,1e5,psf_flux_key)
# The slice object tells you the (start, stop, stride) for values that fit our query.
# You can check to see that the first record outside the slice is above the flux threshold
print(pos_flux[1076].get(psf_flux_key))
# and that the last element in the slice is inside the threshold
print(pos_flux[1584].get(psf_flux_key))
# your turn. confirm the lower limits of the between query
# pos_flux[...].getPsfFlux

#the upper and lower bound methods work similarly
pos_flux.upper_bound(1e4, psf_flux_key)
pos_flux.lower_bound(1e5, psf_flux_key)<jupyter_output><empty_output><jupyter_text>Example: Star-Galaxy SeparationNow that we have introduced the functionality of the source catalog and its schema, we will do a toy example of star-galaxy separation. This small demo will also flags and fields that users are use, and ultimately make a plot.<jupyter_code># let's select sources that were not deblended
select_mask = source_cat['deblend_nChild'] == 0
select_mask &= source_cat['parent'] == 0

# use the extendedness column for a mock star/galaxy seperation
# we only want to use columns where this algorithm worked
# the flag is set true if there was a failure, so we invert the flag values here
select_mask &= ~source_cat['base_ClassificationExtendedness_flag']

# we will also use the sloan shapes to measure size
select_mask &= ~source_cat['base_SdssShape_flag']

# and a simple aperture flux for the brightness
select_mask &= ~source_cat['base_CircularApertureFlux_12_0_flag']

# only consider sources with positive flux
select_mask &= source_cat['base_CircularApertureFlux_12_0_instFlux'] > 0

size_bright_cat = source_cat.subset(select_mask)<jupyter_output><empty_output><jupyter_text>Now we make a crude size magnitude diagram, color coding the data by their 'extendedness value'. The extendedness will be 1 for extended sources-like galaxies-and 0 for point sources-like stars. One hopes the stars will all live on the stellar locus...<jupyter_code>plt.scatter(np.log(size_bright_cat['base_CircularApertureFlux_12_0_instFlux']), 
           size_bright_cat['base_SdssShape_xx'] + size_bright_cat['base_SdssShape_yy'],
           c=size_bright_cat['base_ClassificationExtendedness_value'],
            cmap='bwr',
           s=4)
plt.xlabel('log flux')
plt.ylabel('size px^2')
plt.ylim([0,20]) #zoom in to make the stellar locus clearer
plt.xlim([5,15])<jupyter_output><empty_output><jupyter_text>Our plot shows some star galaxy separation, but also has other interesting features. Some detected sources appear to be smaller than the PSF, some of the point sources have a (crudely) calculated size that occupy the same parameter space as extended sources, and there are a few extremely faint detected point sources. We will leave it to you to delve into this mystery further as a homework assignment since we are primarily focused on understanding tables in this tutorial. By making this plot we exercised some of the methods of the catalog and its schema, to do a minimal analysis example.  Operations with multiple tables/catalogsIn the next section we will show operations which involve two or more catalogs. Table concatenation<jupyter_code># Grab a second catalog using the butler:
dataId2 = {'band':'i', 'visit':512055, 'detector':75}
source_cat2 = butler.get('src', dataId2)
# Put our catalogs in a list:
catalogList = [source_cat, source_cat2]

# The following concatenation function is courtesy of <NAME>:
def concatenate(catalogList):
    from functools import reduce
    """Concatenate multiple catalogs (FITS tables from lsst.afw.table)"""
    schema = catalogList[0].schema
    for i, c in enumerate(catalogList[1:]):
        #check that the schema in the tables are the same
        #we can only cat them if this is true
        if c.schema != schema:
            raise RuntimeError("Schema for catalog %d not consistent" % (i+1))

    # prepare the master catalog
    out = afwTable.BaseCatalog(schema)
    num = reduce(lambda n, c: n + len(c), catalogList, 0)
    # set aside enough space for all the records and their pointers
    out.reserve(num)

    # stick in all the records from all catalogs into the master catalog
    for catalog in catalogList:
        for record in catalog:
            out.append(out.table.copyRecord(record))

    return out

cat_source = concatenate(catalogList)<jupyter_output><empty_output><jupyter_text>Catalog matchingQuick positional matching is supported by the stack, and offers some useful functionality. In the next example, we will match two overlapping observations from different filters together. Getting this data will require us to use a new data repository, so we will have to set up a new butler, then ask for our two tables<jupyter_code># For the rest of this tutorial, we are going to work with coadd data products
tract = 4851
patch = 29   #gen3 analog to gen2 patch id '1,4'
bandList = ["i", "r"]
filterList = bandList   # using filterList for consistency with cells folllowing below<jupyter_output><empty_output><jupyter_text>Let's grab some forced photometry for the two bands. They will have the same tract and patch, which ensures they will overlap.<jupyter_code>objects = []
for filter in filterList:
    dataId = {'band':filter, 'tract':tract, 'patch':patch}
    objects.append(butler.get("deepCoadd_forced_src", dataId=dataId))
iSources, rSources = objects<jupyter_output><empty_output><jupyter_text>We will need these calib objects for our two bands so that we can calculate magnitudes a little later<jupyter_code>#dataId = {'band':filter, 'tract':tract, 'patch':patch}
#dcal = butler.get("deepCoadd_calexp", dataId=dataId)
#calib = dcal.getPhotoCalib()
#print(calib)
calibs = []
for filter in filterList:
    dataId = {'band':filter, 'tract':tract, 'patch':patch}
    dcal = butler.get("deepCoadd_calexp", dataId=dataId)
    calib = dcal.getPhotoCalib()
    calibs.append(calib)
iCalib, rCalib = calibs<jupyter_output><empty_output><jupyter_text>Some quality control flags to prune down the data to give us stars with a signal to noise ratio of 10 or higher. We will use this to index into our catalogs when we do the matching. Note that because these are forced photometry catalogs, the records in each catalog line up so that they should be refering to the same astrophysical sources. That is why we will be able to use our mask on both iSources and rSources. This is not true in general of afwTables.<jupyter_code>noChildren = iSources['deblend_nChild'] == 0
isGoodFlux = ~iSources['modelfit_CModel_flag']
isStellar = iSources['base_ClassificationExtendedness_value'] < 1.
snr = iSources['modelfit_CModel_instFlux']/iSources['modelfit_CModel_instFluxErr'] > 10

star_flag = noChildren & isGoodFlux & isStellar & snr<jupyter_output><empty_output><jupyter_text>In order to match catalogs, we must provide a `MatchControl` instance. The `MatchControl` provides configurations for catalog matching. It has three 'switches' in the form of class attributes. They are defined as follows:1. `findOnlyClosest`: True by default. If False, all other sources within a search radius are also matched 2. `includeMismatches`: False by default. If False, sources with no match are not reported in the match catalog. If True, sources with no match are included in the match catalog with Null as their match3. `symmetricMatch`: False by default. If False, the match between source a from catalog a with source b from catalog b is reported alone. If True, the symmetric match between source b and a is also reported.<jupyter_code># get a match control, we will keep the default configuration
mc = afwTable.MatchControl()

# match our two catalogs, setting the match threshold to be one arcsecond
matches = afwTable.matchRaDec(iSources[star_flag], rSources[star_flag],
                              lsst.geom.Angle(1,lsst.geom.arcseconds), mc)<jupyter_output><empty_output><jupyter_text>`afwTable.matchRaDec` returns a list, where each element is an instance of a `Match` class. The `Match` class has three attributes, which gives us information about the matched sources. Let us unpack this a bit before moving on to some analysis<jupyter_code># how many sources were actually matched?
len(matches)
# lets examine the first element in the matches list
# we can grab the record corresponding to this source in the i band catalog
# using the first attribute
matches[0].first
# likewise the second attribute gives us the record from the r band catalog
matches[0].second
#finally the angular seperation is given in radians in the distance attribute
matches[0].distance<jupyter_output><empty_output><jupyter_text>Now that we have matched stars in two bands, lets make a color magnitude diagram, displaying how to use matches in a little more depthfirst we start out simple and demonstrate how to get the magnitude and magnitude error for one record in our matches object<jupyter_code># use the calib object to get magnitudes
# pass in a record and the name of the flux field you are interested in
iCalib.instFluxToMagnitude(matches[0].first, 'modelfit_CModel')
# now we make some loops to grab all the magnitudes in the iband and rband
# remember that the i band catalog is accessed with the first attribute
# and the r band catalog is accessed with the second attribute
iMag = [iCalib.instFluxToMagnitude(m.first, 'modelfit_CModel').value for m in matches]
rMag = [rCalib.instFluxToMagnitude(m.second, 'modelfit_CModel').value for m in matches]

plt.scatter(np.array(rMag) - (iMag), iMag)
plt.ylim([26,18])
plt.xlim([-0.5,3])
plt.xlabel('$r-i$')
plt.ylabel('$i$')<jupyter_output><empty_output><jupyter_text>If you are only interested in the ids and the angular separation, you can pack the matches into a table.<jupyter_code>matches_table = afwTable.packMatches(matches)
matches_table<jupyter_output><empty_output><jupyter_text>You can unpack the matches too:<jupyter_code>unpack_matches = afwTable.unpackMatches(matches_table, iSources, rSources)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0726260729613734, 'fraction_numerical': 0.007208959227467811, 'mean_word_length': 5.334961767204758, 'pattern_counts': {'":': 0, '<': 166, '<?xml version=': 0, '>': 170, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '40', 'dir': 'jupyter-structured-clean-dedup', 'id': '74778', 'license': 'MIT', 'n_tokens_mistral': 8844, 'n_tokens_neox': 8546, 'n_words': 3778, 'path': 'Basics/afw_table_guided_tour.ipynb', 'repo_name': 'rubin-dp0/StackClub', 'size': '29844'}