<jupyter_start><jupyter_text>Exploring the Gen-3 ButlerAuthor(s): **<NAME>** ([@kadrlica](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@kadrlica)), **<NAME>** ([@douglasleetucker](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@douglasleetucker))Maintainer(s): **<NAME>** ([@kadrlica](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@kadrlica)), **<NAME>** ([@douglasleetucker](https://github.com/LSSTScienceCollaborations/StackClub/issues/new?body=@douglasleetucker))Level: **Intermediate**Last Verified to Run: **2021-09-03**Verified Stack Release: **w_2021_33** Core ConceptsThis notebook provides a first look at the structure and organization of the DC2 repo created with the Gen-3 Butler. The Gen-3 Butler is still under development, so this notebook is expected to be updated after the official Gen-3 release. For the time being, be sure that you are using the verified version of the stack specified above. Learning Objectives:This notebook lays out features of how the Gen-3 butler functions:1. Create a Gen-3 butler2. Programmatically explore a Gen-3 repo3. Get some data 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. Setup<jupyter_code># Site, host, and stack version
! echo $EXTERNAL_INSTANCE_URL
! echo $HOSTNAME
! eups list -s | grep lsst_distrib
# Generic imports
import os,glob
import pylab as plt
# Stack imports
import lsst.daf.butler  as dafButler
import lsst.afw.display as afwDisplay
# 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='2.2i/runs/DP0.1'<jupyter_output><empty_output><jupyter_text>Gen-3 ButlerOne of the strengths of the Gen-3 butler relative to Gen-2 is the ability to explore a repo and find out what it contains. Starting from scratch, we want to be able to get going *with only the path to the repo*. We can do this by creating a butler without specifying the collection (since we have no idea what collections exist at this point).<jupyter_code>butler = dafButler.Butler(repo)<jupyter_output><empty_output><jupyter_text>With the butler created, we can now access the data `registry` (a database containing information about available data products)<jupyter_code>registry = butler.registry

# We can examine the registry with
#help(registry)<jupyter_output><empty_output><jupyter_text>The `registry` is a good tool for investigating a repo (more on the registry schema can be found [here](https://dmtn-073.lsst.io/)). For example, we can get a list of all collections, with<jupyter_code>for c in sorted(registry.queryCollections()):
    print(c)<jupyter_output><empty_output><jupyter_text>This is our first glimpse at the data contained in the repo, but it doesn't teach us *which* collection we are actually interested in. The names do give us some hints though...* `calib` - refers to calibration products that are used for instrument signature removal* `refcats` - refers to the reference catalogs* `skymaps` - are the geometric representations of the sky coverage* `u/` - collections that begin with `u/` are used for personal re-runsWe can generally get access to everything we are intersted in for DC2 Run 2.2i DP0.1 by selecting the collection `2.2i/runs/DP0.1`. This is a pointer to other collections that expand out recursively... More on collections can be found here: https://pipelines.lsst.io/v/weekly/modules/lsst.daf.butler/organizing.htmlcollections<jupyter_code># If this collection is a pointer to other collections, expand those out recursively.
print(collection)
for c in sorted(registry.queryCollections(collection,flattenChains=True)):
    print(c, registry.getCollectionType(c))
# Create a new butler with the collection of interest
butler = dafButler.Butler(repo,collections=collection)
registry = butler.registry<jupyter_output><empty_output><jupyter_text>DatasetTypes don't belong to collections, so when you query for them you always get all the DatasetTypes that belong to the repo. This is all datasetTypes that were created by anyone during any processing. There may be intermediate products that were created during processing, but no longer exist.<jupyter_code>for x in sorted(registry.queryDatasetTypes()):
    print(x)<jupyter_output><empty_output><jupyter_text>It is possible to get all `DatasetRef` (which include the `dataId`) for a specific `datasetType` in a specific collection with a query like this. Note that this doesn't necessarily guarentee that the specific data set still exists on disk...<jupyter_code>datasetRefs = registry.queryDatasets(datasetType='calexp',collections=collection)
for i,ref in enumerate(datasetRefs):
    print(ref.dataId.full)
    try: uri = butler.getURI(ref)
    except: print("File not found...")
    if i > 10: break<jupyter_output><empty_output><jupyter_text>Note that the files are not necessarily located in a subdirectory of the repo. We can get the path from the Uniform Resource Identifier (URI) that is returned by `butler.getURI`. Note that this URI refers to a local path on the filesystem, but that does not need to be the case.<jupyter_code>print("Repo path: ",repo)
uri = butler.getURI(ref)
print("File URI: ",uri)
!ls -lh {uri.ospath}<jupyter_output><empty_output><jupyter_text>Now say we want to restrict our selection to datasets associated with a specific filter. We can add that constraint to our query, but first we need to figure out what the filters are called... Looking at the dataId object, we see the attributes `physical_filter` and `band` look promising.<jupyter_code>ref.dataId.full
print(f"physical_filter = {ref.dataId['physical_filter']}")
print(f"band = {ref.dataId['band']}")<jupyter_output><empty_output><jupyter_text>It looks like `band` is what we want, so we put it in the `where` argument of `queryDatasets`. Let's try the $g$ band.<jupyter_code># You can also sub-select on specific properties of a data set
datasetRefs = registry.queryDatasets(datasetType='calexp',dataId={'band': 'g'}, where='visit > 700000', collections=collection)
for i,ref in enumerate(datasetRefs):
    print(ref.dataId.full)
    try: uri = butler.getURI(ref)
    except: print("File not found...")
    if i > 10: break<jupyter_output><empty_output><jupyter_text>Ok, now that we know what collections exist (`2.2i/runs/DP0.1` in particular), the `datasetTypes` that are defined for that collection, and the `datasetRefs` (which contain `dataIds`) for data products of the requested type. This is all the information that we need to get the dataset of interest! From the list above, let's choose one detector from one visit and grab a `calexp`.<jupyter_code># We could pass the datasetRef that we found above, but since the query may 
# return results in a different order we define the dataId explicitly for reproducibility. 
dataId = {'visit': '703697', 'detector': 80, 'band':'g'}
calexp = butler.get('calexp',dataId=dataId)<jupyter_output><empty_output><jupyter_text>Now to get the `src` catalog associated with this `calexp` we pass the `dataId` to the butler with the `src` datasetType. Note that this performs another query to the registry database to find the `src` catalog that matches our dataId requirements.<jupyter_code># We could get the src table using the dataId as we did above for the calexp, 
# but this would require the butler to perform another query of the database. 
# Instead, we can just pass the ref itself directly to butler.get
src = butler.get('src',dataId)
src = src.copy(True)
src.asAstropy()<jupyter_output><empty_output><jupyter_text>We can now plot the `calexp` with the `src` catalog overlaid. We leave an investigation of this image as an exercise to the user :)<jupyter_code># And plot!
afwDisplay.setDefaultBackend('matplotlib') 
fig = plt.figure(figsize=(10,8))
afw_display = afwDisplay.Display(1)
afw_display.scale('asinh', 'zscale')
afw_display.mtv(calexp)
plt.gca().axis('off')

with afw_display.Buffering():
    for s in src:
        afw_display.dot('+', s.getX(), s.getY(), ctype=afwDisplay.RED)
        afw_display.dot('o', s.getX(), s.getY(), size=20, ctype='orange')<jupyter_output><empty_output><jupyter_text>In the case above, both the `calexp` and `src` can be found by the registry, but this will not always necessarily be the case. The `queryDimensions` method provides a more flexible way to query for multiple datasets (requiring an instance of all datasets to be available for that dataId) or ask for different dataId keys than what is used to identify the dataset (which invokes various built-in relationships). An example of this is provided below:<jupyter_code># Use queryDimensions to provide more flexible access
dataIds = registry.queryDataIds(["visit", "detector", "band"], datasets=["calexp","src"], where='visit = 703697',
                                collections=collection)
for i,dataId in enumerate(dataIds):
    print(dataId.full)
    if i > 10: break
# Use queryDataIds to grab the dataIds for a subset
dataIds = registry.queryDataIds(["visit", "detector"], datasets=["calexp","src"], 
                                where="band='g' and detector=0 and visit > 700000",
                                collections=collection)
for i,dataId in enumerate(dataIds):
    print(dataId.full)
    if i > 10: break<jupyter_output><empty_output><jupyter_text>You can also get more metadata about a data product dimension. (Note that the record for an exposure and a visit are different.)<jupyter_code>for dim in ['exposure','visit','detector']:
    print(list(registry.queryDimensionRecords(dim, where='visit = 971990 and detector=0'))[0])
    print()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09814350225790267, 'fraction_numerical': 0.010436527847466131, 'mean_word_length': 5.307594936708861, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 71, 'https://': 7, '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': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '570422', 'license': 'MIT', 'n_tokens_mistral': 3039, 'n_tokens_neox': 2942, 'n_words': 1110, 'path': 'Basics/Gen3ButlerTutorial.ipynb', 'repo_name': 'rubin-dp0/StackClub', 'size': '10005'}