SITCOMTN-146

WET-007: Compare CWFS approaches with WET-001 ComCam data#

Last Verified to Run: 2024-11-08
Software Version:

  • ts_wep: 12.5.0

  • lsst_distrib: w_2024_45

In this document we compare the wavefront estimation between using TIE and Danish using simulated WET-001 ComCam data. As for LsstCam (see SITCOMTN-144), each extra/intra/in-focus triplet is simulated for 100 random optical states. The defocal offset of R22 is achieved by moving a piston by +/-1.5mm.

Imports#

from lsst.daf import butler as dafButler
import matplotlib.pyplot as plt 
from astropy.visualization import ZScaleInterval
from astropy.time import Time
import astropy.units as u
import numpy as np
from lsst.obs.lsst import LsstComCam
from lsst.ts.wep.task.generateDonutDirectDetectTask import (
GenerateDonutDirectDetectTask, GenerateDonutDirectDetectTaskConfig)
from copy import copy
from astropy.visualization import ZScaleInterval
from lsst.ts.wep.task.cutOutDonutsScienceSensorTask import (
CutOutDonutsScienceSensorTask, CutOutDonutsScienceSensorTaskConfig
)

Ingest raws, perform ISR#

Below we ingest the raw simulated data to the central aos_imsim repository. Then we perform the instrument signature removal (ISR):

butler register-instrument /sdf/data/rubin/repo/aos_imsim lsst.obs.lsst.LsstComCam 

butler ingest-raws /sdf/data/rubin/repo/aos_imsim/ /sdf/data/rubin/user/gmegias/projects/commissioning_sims/WET-001_comcam/state_*/amp* --update-records -t direct

butler write-curated-calibrations /sdf/data/rubin/repo/aos_imsim  lsst.obs.lsst.LsstComCam 

butler define-visits /sdf/data/rubin/repo/aos_imsim   lsst.obs.lsst.LsstComCam

In /sdf/group/rubin/shared/scichris/DM-46763_WET-007, make site_bps.yaml, containing

site:
  s3df:
    profile:
      condor:
        +Walltime: 7200
butlerRootPath = '/sdf/data/rubin/repo/aos_imsim/'
butler = dafButler.Butler(butlerRootPath)
dataRefs = butler.registry.queryDatasets('raw', collections=['LSSTComCam/raw/all'],
                             where=f"instrument='LSSTComCam' and visit.day_obs = 20240723 and exposure.science_program = '1'").expanded()
print(len(list(dataRefs)))
refs = []
for ref in dataRefs:
    refs.append(ref)
2640

We’re short of 9x300 = 2700, because for some optical states not all detectors got simulated.

Run ISR on all states:

cd /sdf/group/rubin/shared/scichris/DM-46763_WET-007

allocateNodes.py -v -n 10 -c 64 -m 60:00:00 -q milano -g 1800 s3df --account rubin:commissioning


description: basic ISR
# Here we specify the corresponding instrument for the data we
# will be using.
instrument: lsst.obs.lsst.LsstComCam
# Then we can specify each task in our pipeline by a name
# and then specify the class name corresponding to that task
tasks:
  isr:
    class: lsst.ip.isr.isrTask.IsrTask
    # Below we specify the configuration settings we want to use
    # when running the task in this pipeline. Since our data doesn't
    # include bias or flats we only want to use doApplyGains and
    # doOverscan in our isr task.
    config:
      connections.outputExposure: 'postISRCCD'
      doBias: False
      doVariance: True
      doLinearize: False
      doCrosstalk: False
      doDefect: False
      doNanMasking: False
      doInterpolate: False
      doBrighterFatter: False
      doDark: False
      doFlat: False
      doApplyGains: True
      doFringe: False
      doOverscan: True
      python: OverscanCorrectionTask.ConfigClass.fitType = 'MEDIAN'

  

bps submit site_bps.yaml  -b  /sdf/data/rubin/repo/aos_imsim -i  LSSTComCam/raw/all,LSSTComCam/calib/unbounded -o WET-001_lsstComCam_ISR -p /sdf/group/rubin/shared/scichris/DM-46763_WET-007/lsstComCamPipelineISR.yaml  -d "instrument='LSSTComCam' and exposure.science_program = '1'"
collection = 'WET-001_lsstComCam_ISR'
butlerRootPath = '/sdf/data/rubin/repo/aos_imsim/'
butler = dafButler.Butler(butlerRootPath)
dataRefs = butler.registry.queryDatasets('postISRCCD', collections=[collection],
                             where=f"instrument='LSSTComCam' and exposure.day_obs = 20240723").expanded()
print(len(list(dataRefs)))
2640

Find all intra and extra-focal exposure ids corresponding to a particular state:

for ref in list(dataRefs):
    seq_num = int(str(ref.dataId.exposure.seq_num)[1:])
    
    # for N-th state, the extra states 
    # have seqNum of 2N-1,  intra states have 2N
    state = seq_num // 2 
    defocus = str(ref.dataId.exposure.seq_num)[0] # 5 for in-focus,  6 for defocal 
    
    raft = ref.dataId.detector.id
    if defocus == '6' and state == 4:
        print(state, seq_num, raft, ref.dataId.exposure.id)
4 9 3 5024072306009
4 9 6 5024072306009
4 8 4 5024072306008
4 9 5 5024072306009
4 8 0 5024072306008
4 9 2 5024072306009
4 8 6 5024072306008
4 8 5 5024072306008
4 8 7 5024072306008
4 8 8 5024072306008
4 9 7 5024072306009
4 9 4 5024072306009
4 8 3 5024072306008
4 8 2 5024072306008
4 9 1 5024072306009
4 8 1 5024072306008
4 9 8 5024072306009
4 9 0 5024072306009

Choosing the maximum number of Zernike terms#

We find that there is a difference in fit results depending on tha maximum number of Zernike terms used. This means we’re fitting Zk4: ZkN. The default N for a long time was Zk22. For imSim the default of OPD header was up to Zk29, which motivated the change in ts_wep defaults, and introduction of maxNollIndex parameter in estimateZernikes. To illustrate the point we run interactive steps of a pipeline for a single detector pair.

First, donut detection and cutouts:

dataIdIntra = {'instrument': 'LSSTComCam', 'detector': 0, 'exposure': 5024072306008, 'day_obs': 20240723, } # 2N 
dataIdExtra = {'instrument': 'LSSTComCam', 'detector': 0, 'exposure': 5024072306007, 'day_obs': 20240723, } # 2N-1

collection = 'WET-001_lsstComCam_ISR'
exposureIntra = butler.get('postISRCCD', dataId=dataIdIntra, collections=[collection])
exposureExtra = butler.get('postISRCCD', dataId=dataIdExtra, collections=[collection]) 
config = GenerateDonutDirectDetectTaskConfig()
config.measurementTask.nSigmaDetection = 5
config.donutSelector.useCustomMagLimit = True

camera = LsstComCam().getCamera()

task = GenerateDonutDirectDetectTask(config=config)
taskOut = task.run(copy(exposureIntra), camera)

# plot the exposure and detected donuts
fig = plt.figure()

d = exposureIntra.image.array
zscale = ZScaleInterval()
vmin,vmax = zscale.get_limits(d)
plt.imshow(d, vmin=vmin, vmax=vmax, origin='lower')
plt.scatter(taskOut.donutCatalog['centroid_x'], taskOut.donutCatalog['centroid_y'], 
            marker='+', c='r', label=f'doDonutSelection:{config.doDonutSelection}'
                )
INFO:lsst.generateDonutDirectDetectTask.measurementTask:Found 159 sources in exposure
INFO:lsst.generateDonutDirectDetectTask.measurementTask:Measured 159 of 159 sources in exposure
INFO:lsst.generateDonutDirectDetectTask:Running Donut Selector
INFO:lsst.generateDonutDirectDetectTask.donutSelector:Selected 46/159 references
<matplotlib.collections.PathCollection at 0x7f72b96c7310>
_images/5d1fbe7e38e650b4f00a209496dd6374542705b5771fa52b718e2854dece536d.png

That’s a sufficient number of donuts to compare wavefront retrieval as a function of maxNollIndex. Prepare cutouts:

config = CutOutDonutsScienceSensorTaskConfig()
task = CutOutDonutsScienceSensorTask(config=config)
taskOut = task.run([exposureIntra, exposureExtra],
                   [taskOut.donutCatalog,taskOut.donutCatalog],
                   camera
                  )
INFO:lsst.CutOutDonutsScienceSensorTask:Median Recentering Shift: (3.0, 4.0)
INFO:lsst.CutOutDonutsScienceSensorTask:Median Recentering Shift: (1.0, 1.0)
# illustrate the donuts 
fig,axs = plt.subplots(5,5, figsize=(10,10))
ax = np.ravel(axs)
i=0
donutStamps = taskOut.donutStampsExtra
for stamp in donutStamps:
    if i < len(ax):
        ax[i].imshow(stamp.stamp_im.image.array, origin='lower')
        ax[i].set_xticks([])
        ax[i].set_yticks([])
        i += 1 
fig.subplots_adjust(hspace=0.05, wspace=0.05)

if len(donutStamps)<len(ax):
    for i in range(len(donutStamps), len(ax)):
        ax[i].axis('off')
_images/271ba69625f507f5ede8eb3df2f4079bbff878af6653a1e349358d8c5fdead73.png
# run TIE and Danish fit, store the intermediate results for plotting
from lsst.ts.wep.task.calcZernikesTask import ( CalcZernikesTask, CalcZernikesTaskConfig )
from lsst.ts.wep.task import EstimateZernikesTieTask
from lsst.ts.wep.task import EstimateZernikesDanishTask
state = 4 
raft = 0
config = CalcZernikesTaskConfig()
config.estimateZernikes.retarget(EstimateZernikesTieTask)
#config.estimateZernikes.retarget(EstimateZernikesDanishTask)
tieOutput = {}
for maxNoll in range(22,29):
    config.estimateZernikes.maxNollIndex = maxNoll
    task = CalcZernikesTask(config=config)
    taskOut = task.run( donutStampsExtra, donutStampsIntra)
    tieOutput[maxNoll] = taskOut
tieFile  = f'DM-46763_WET-001_lsstComCam_state{state}_det{raft}_tie'
np.save(tieFile, tieOutput)
from lsst.ts.wep.task.calcZernikesTask import ( CalcZernikesTask, CalcZernikesTaskConfig )
from lsst.ts.wep.task import EstimateZernikesTieTask
from lsst.ts.wep.task import EstimateZernikesDanishTask

config = CalcZernikesTaskConfig()
config.estimateZernikes.retarget(EstimateZernikesDanishTask)
danishOutput = {}
for maxNoll in range(22,29):
    config.estimateZernikes.maxNollIndex = maxNoll
    task = CalcZernikesTask(config=config)
    taskOut = task.run( donutStampsExtra, donutStampsIntra)
    danishOutput[maxNoll] = taskOut
danishFile = f'DM-46763_WET-001_lsstComCam_state{state}_det{raft}_danish'
np.save(danishFile, danishOutput)

First plot the fit results for Danish - note that the result does not depend much on the maxNollIndex fitted. In the figuere below the errorbars correspond to the standard deviation of the fitted Zernikes.

if os.path.exists(danishFile):
    danishOutput = np.load(danishFile, allow_pickle=True).item()
fig,ax = plt.subplots(1,1,figsize=(10,6))
for maxNoll in range(22,29):
    taskOut = danishOutput[maxNoll]
    zkr = taskOut.outputZernikesRaw
    zka = taskOut.outputZernikesAvg
    zks = taskOut.zernikes
    mask_used = zks['used'][1:]
    zkr_used = zkr[mask_used]
    ax.errorbar(np.arange(4, maxNoll+1), zka[0], yerr=np.std(zkr_used, axis=0), label=f"Danish up to Zk{maxNoll}", capsize=4)
  

ax.set_xlabel('Zk mode')
ax.set_ylabel('Fit [microns]')
ax.set_title(f'Optical state {state}, detector {raft}')
ax.legend()
<matplotlib.legend.Legend at 0x7f7295290090>
_images/dfb4bab9060f39cd1fec8b9300b349d68b959dfa834206950b8e4f9ca868d73b.png

For TIE the situation is vastly different - we obtain similar results only up to maxNollIndex of 26.

if os.path.exists(tieFile):
    tieOutput = np.load(tieFile, allow_pickle=True).item()
fig,ax = plt.subplots(1,1,figsize=(10,6))
for maxNoll in range(22,27):# tieOutput.keys():
    taskOut = tieOutput[maxNoll]
    zkr = taskOut.outputZernikesRaw
    zka = taskOut.outputZernikesAvg
    zks = taskOut.zernikes
    mask_used = zks['used'][1:]
    zkr_used = zkr[mask_used]

    ax.errorbar(np.arange(4, maxNoll+1), zka[0], 
                yerr=np.std(zkr_used, axis=0), 
                label=f"TIE up to Zk{maxNoll}", capsize=4)

ax.set_xlabel('Zk mode')
ax.set_ylabel('Fit [microns]')
ax.set_title(f'Optical state {state}, detector {raft}')
ax.legend()
<matplotlib.legend.Legend at 0x7f7295060090>
_images/c93882f74425e84e175b2cc1363a088239d3637e08490a70b2aeb94c2968b0be.png

For maxNollIndex between 27-29, the situation becomes more unconstrained. In both plots we include the results with maxNollIndex set to 26 for reference:

fig,ax = plt.subplots(1,1,figsize=(10,6))
for maxNoll in range(26,29):
    taskOut = tieOutput[maxNoll]
    zkr = taskOut.outputZernikesRaw
    zka = taskOut.outputZernikesAvg
    zks = taskOut.zernikes
    mask_used = zks['used'][1:]
    zkr_used = zkr[mask_used]
    ax.errorbar(np.arange(4, maxNoll+1), zka[0], 
                yerr=np.std(zkr_used, axis=0), 
                label=f"TIE up to Zk{maxNoll}", capsize=4)

ax.set_xlabel('Zk mode')
ax.set_ylabel('Fit [microns]')
ax.set_title(f'Optical state {state}, detector {raft}')
ax.legend()
<matplotlib.legend.Legend at 0x7f7294ef2410>
_images/e97c2c40c1760b47c6decf3a778fae9e82b04b3c7d4dab79558c236ccf5f8904.png

Given that, we run WEP pipeline with maxNollIndex set to 29 (current default) and to 22 (previous default), especially given how it affects the fit results.

Run the WEP pipeline with bps#

To run the wavefront estimation pipeline (WEP), we use the following pipelines yamls:

  • TIE pipeline (lsstComCamPipelineDonutsZernikesTIE.yaml)

      description: run pipeline from ISR to Zk with TIE
      
      # Here we specify the corresponding instrument for the data we
      # will be using.
      instrument: lsst.obs.lsst.LsstComCam
      
      # Then we can specify each task in our pipeline by a name
      # and then specify the class name corresponding to that task
      tasks:
        generateDonutDirectDetectTask:
          class: lsst.ts.wep.task.generateDonutDirectDetectTask.GenerateDonutDirectDetectTask
          config:
            donutSelector.useCustomMagLimit: True
            measurementTask.nSigmaDetection: 5
        cutOutDonutsScienceSensorGroupTask::
          class: lsst.ts.wep.task.cutOutDonutsScienceSensorTask.CutOutDonutsScienceSensorTask
        calcZernikesTask:
          class: lsst.ts.wep.task.calcZernikesTask.CalcZernikesTask
          config:
            estimateZernikes.maxNollIndex: 29
    
  • Danish pipeline (lsstComCamPipelineDonutsZernikesDanish.yaml):

      description: run pipeline from ISR to Zk with Danish
      
      # Here we specify the corresponding instrument for the data we
      # will be using.
      instrument: lsst.obs.lsst.LsstComCam
      
      # Then we can specify each task in our pipeline by a name
      # and then specify the class name corresponding to that task
      tasks:
        generateDonutDirectDetectTask:
          class: lsst.ts.wep.task.generateDonutDirectDetectTask.GenerateDonutDirectDetectTask
          config:
            donutSelector.useCustomMagLimit: True
            measurementTask.nSigmaDetection: 5
        cutOutDonutsScienceSensorTask::
          class: lsst.ts.wep.task.cutOutDonutsScienceSensorTask.CutOutDonutsScienceSensorTask
        calcZernikesTask:
          class: lsst.ts.wep.task.calcZernikesTask.CalcZernikesTask
          config:
             estimateZernikes.maxNollIndex: 29
             python: |
                from lsst.ts.wep.task import EstimateZernikesTieTask, EstimateZernikesDanishTask
                config.estimateZernikes.retarget(EstimateZernikesDanishTask)
    

They are run with the following commands:

cd /sdf/group/rubin/shared/scichris/DM-46763_WET-007/
allocateNodes.py -v -n 10 -c 64 -m 60:00:00 -q milano -g 1800 s3df --account rubin:commissioning


bps submit site_bps.yaml  -b  /sdf/data/rubin/repo/aos_imsim/ -i WET-001_lsstComCam_ISR,LSSTComCam/calib/unbounded -o WET-001_lsstComCam_TIE_6001_6200_bps  -p /sdf/group/rubin/shared/scichris/DM-46763_WET-007/lsstComCamPipelineDonutsZernikesTIE.yaml -d "instrument='LSSTComCam' and exposure.day_obs = 20240723" 

bps submit site_bps.yaml  -b  /sdf/data/rubin/repo/aos_imsim/ -i WET-001_lsstComCam_ISR,LSSTComCam/calib/unbounded -o WET-001_lsstComCam_Danish_6001_6200_bps  -p /sdf/group/rubin/shared/scichris/DM-46763_WET-007/lsstComCamPipelineDonutsZernikesDanish.yaml -d "instrument='LSSTComCam' and exposure.day_obs = 20240723"

Also, exchanging estimateZernikes.maxNollIndex to 22 we run as:

bps submit site_bps.yaml  -b  /sdf/data/rubin/repo/aos_imsim/ -i WET-001_lsstComCam_ISR,LSSTComCam/calib/unbounded -o WET-001_lsstComCam_TIE_6001_6200_bps_zk22  -p /sdf/group/rubin/shared/scichris/DM-46763_WET-007/lsstComCamPipelineDonutsZernikesTIE.yaml -d "instrument='LSSTComCam' and exposure.day_obs = 20240723" 

bps submit site_bps.yaml  -b  /sdf/data/rubin/repo/aos_imsim/ -i WET-001_lsstComCam_ISR,LSSTComCam/calib/unbounded -o WET-001_lsstComCam_Danish_6001_6200_bps_zk22  -p /sdf/group/rubin/shared/scichris/DM-46763_WET-007/lsstComCamPipelineDonutsZernikesDanish.yaml -d "instrument='LSSTComCam' and exposure.day_obs = 20240723"

Inspect the differences in runtime#

To compare runtime we read the information stored in task_metadata, and task_log dataset types:

def calcTaskRuntime(metadata):
    t1 = Time(metadata['quantum']['prepUtc'].split('+')[0], format='isot', scale='utc')
    t2 = Time(metadata['quantum']['endUtc'].split('+')[0], format='isot', scale='utc')
    return (t2-t1).to_value(u.second)
butlerRootPath = '/sdf/data/rubin/repo/aos_imsim/'
butler = dafButler.Butler(butlerRootPath)
registry = butler.registry
results_timing = {}

for maxNollIndex in  [22, 29]:

    coll = '_zk22' if maxNollIndex == 22 else ''
    fname = f'DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_{maxNollIndex}_timing.npy'
    if os.path.exists(fname):
        results_timing[maxNollIndex] =  np.load( fname, allow_pickle=True).item()
        print(f'Reading from {fname}')
    else:
        results = {'tie':{}, 'danish':{}}
        for method in results.keys():
            coll_method = 'TIE' if method == 'tie' else 'Danish'
            results[method] = {'generateTime':[], 'Nobj':[],  'cutoutTime':  [],  'cwfsTime':[] }
            output_collection = f'WET-001_lsstComCam_{coll_method}_6001_6200_bps{coll}'
        
            # get the available refs 
            datasetRefs = list(registry.queryDatasets('zernikeEstimateAvg', collections=[output_collection],
                                                 ).expanded())
        
            for ref in datasetRefs:
        
                # generating donut catalog 
                meta = butler.get('generateDonutDirectDetectTask_metadata', dataId=ref.dataId, collections=[output_collection])
                results[method]['generateTime'].append(calcTaskRuntime(meta))
                
                # Message like : ['Measured', '135', 'of', '135', 'sources', 'in', 'exposure'] 
                log = butler.get('generateDonutDirectDetectTask_log', dataId=ref.dataId, collections=[output_collection])
                measuredSources = int(log[-4].message.split(' ')[1])
                results[method]['Nobj'].append(measuredSources)
                
                # Estimation 
                meta = butler.get('calcZernikesTask_metadata', dataId=ref.dataId, collections=[output_collection])
                results[method]['cwfsTime'].append(calcTaskRuntime(meta))
        results_timing[maxNollIndex] = results
        print(f'Saving  as {fname}')
        np.save(fname, results, )
Reading from DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_22_timing.npy
Reading from DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_29_timing.npy

Plot the donut detection as a function of N sources reported. The generation of sources should be largely independent of the CWFS algorithm choice - small differences are expected dure to CPU load, file I/O, and other factors making each runtime unique.

fig,ax = plt.subplots(1,1,figsize=(7,5))
for maxNollIndex in  [22, 29]:
    results =  results_timing[maxNollIndex]
    for method in results.keys():
        ax.scatter(results[method]['generateTime'], results[method]['Nobj'], label=f'{method} maxNoll {maxNollIndex}',
                  alpha=0.5)
ax.legend()
ax.set_xlabel('Time [sec]')
ax.set_ylabel('Number of detected sources')
ax.set_title('Generate donut direct detect')
Text(0.5, 1.0, 'Generate donut direct detect')
_images/6c610db7d7f856503ef6ccd2e0686bbfc70547d7448f0377f0577cbdfe0192ff.png

However, in case of Zernike estimation we expect the TIE algorithm to be faster than Danish:

fig,ax = plt.subplots(1,1,figsize=(7,5))
for maxNollIndex in  [22, 29]:
    results =  results_timing[maxNollIndex]
    for method in results.keys():
        ax.scatter(results[method]['cwfsTime'], results[method]['Nobj'], label=f'{method} maxNoll {maxNollIndex}',
                  alpha=0.5)
ax.legend()
ax.set_xlabel('Time [sec]')
ax.set_ylabel('Number of detected sources')
ax.set_title('Calculate Zernikes: \nruntime per detector')
Text(0.5, 1.0, 'Calculate Zernikes: \nruntime per detector')
_images/5b212df27577432248384620eecce668183d1c5023ab778e5a3c8bde95750bac.png

Given that maxNoll 29 implies we are fitting 7 additional Zernike modes, we can account for that by dividing the runtime by the number of fitted Zernikes:

fig,ax = plt.subplots(1,1,figsize=(7,5))
for maxNollIndex in  [22, 29]:
    results =  results_timing[maxNollIndex]
    for method in results.keys():
        ax.scatter(np.array(results[method]['cwfsTime']) / maxNollIndex, results[method]['Nobj'], label=f'{method} maxNoll {maxNollIndex}',
                  alpha=0.5)
ax.legend()
ax.set_xlabel('Time [sec] / Zk mode')
ax.set_ylabel('Number of detected sources')
ax.set_title('Calculate Zernikes: \nruntime per detector per mode')
Text(0.5, 1.0, 'Calculate Zernikes: \nruntime per detector per mode')
_images/94b19b09bc7871a9f89aa7da9e50c816721306f894baa78b55cd80842dde417e.png

Thus accounting for the number of Zernike modes fitted, TIE takes about 2 seconds per mode, whereas Danish 3-6 seconds.

We can also express the TIE / Danish difference in terms of a ratio of runtimes between two methods for a given maxNoll index:

fig,ax = plt.subplots(1,1,figsize=(7,5))
for maxNollIndex in  [22, 29]:
    results =  results_timing[maxNollIndex]
    ratio = np.array(results['danish']['cwfsTime'])/ np.array(results['tie']['cwfsTime'])
    ax.scatter(ratio, results[method]['Nobj'], label=f'maxNoll {maxNollIndex}',
                  alpha=0.5)
ax.legend()
ax.set_xlabel('Danish Time / TIE Time ')
ax.set_ylabel('Number of detected sources')
ax.set_title('Calculate Zernikes')
Text(0.5, 1.0, 'Calculate Zernikes')
_images/21fb0baa64e7a0cb92d145e7a65ad93fc5948a9d9b6647bffdfdf467c4974d64.png

This illustrates that as of writing, Danish is between 2-3 times slower than TIE for maxNollIndex of 22, but 3-5 times slower for maxNollIndex of 29. As above, if we perform per-mode ‘normalization’:

fig,ax = plt.subplots(1,1,figsize=(7,5))
for maxNollIndex in  [22, 29]:
    results =  results_timing[maxNollIndex]
    ratio = np.array(results['danish']['cwfsTime'])/ np.array(results['tie']['cwfsTime'])
    ax.scatter(ratio / maxNollIndex, results[method]['Nobj'], label=f'maxNoll {maxNollIndex}',
                  alpha=0.5)
ax.legend()
ax.set_xlabel('Danish Time / TIE Time / maxNoll ')
ax.set_ylabel('Number of detected sources')
ax.set_title('Calculate Zernikes')
Text(0.5, 1.0, 'Calculate Zernikes')
_images/f208e86cd4626cc1b26a2b9dc9b77ce4c8ef8a0b25b428b51ee1373a524e2341.png

Inspect the WEP fit results between Danish and TIE#

Now to compare the actual fit results, we read all the results for Danish and TIE:

# store all Danish vs TIE in a dictionary 
butlerRootPath = '/sdf/data/rubin/repo/aos_imsim/'
butler = dafButler.Butler(butlerRootPath)
registry = butler.registry
results_stored = {}
for maxNollIndex in  [22,29]:

    coll = '_zk22' if maxNollIndex == 22 else ''
    fname = f'DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_{maxNollIndex}.npy'
    if os.path.exists(fname):
        results_stored[maxNollIndex] =  np.load( fname, allow_pickle=True).item()
        print(f'Reading from {fname}')
    else:
        results = {'tie':{}, 'danish':{}}
        for method in results.keys():
            coll_method = 'TIE' if method == 'tie' else 'Danish'
            results[method] = {}
            output_collection = f'WET-001_lsstComCam_{coll_method}_6001_6200_bps{coll}'
       
            # get the available refs 
            datasetRefs = list(registry.queryDatasets('zernikeEstimateAvg', collections=[output_collection],
                                             ).expanded())
            seqs = np.array([int(str(ref.dataId.visit.seq_num)[1:]) for ref in datasetRefs])
            states = np.unique(np.array((seqs+1) / 2).astype(int) )
            for state in states:
                results[method][state] = {}
                for raft in range(9):
                    results[method][state][raft] = {}
                    
            for ref in datasetRefs:
            # read in the results of each method 
    
                seq_num = int(str(ref.dataId.visit.seq_num)[1:])
                state = int((seq_num+1)/2)
                raft = ref.dataId.detector.id
                results[method][state][raft]['zernikeEstimateAvg'] =  butler.get('zernikeEstimateAvg', 
                                  dataId=ref.dataId, 
                                  collections=[output_collection])
                results[method][state][raft]['zernikeEstimateRaw'] =  butler.get('zernikeEstimateRaw', 
                                  dataId=ref.dataId, 
                                  collections=[output_collection])
                results[method][state][raft]['zernikes'] =  butler.get('zernikes', 
                                  dataId=ref.dataId, 
                                  collections=[output_collection])
                
                results[method][state][raft]['dataId'] = ref.dataId
        results_stored[maxNollIndex] = results
        print(f'Saving {fname}')
        np.save(fname, results, )
Reading from DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_22.npy
Reading from DM-46763_WET-001_lsstComCam_tie_danish_maxNollIndex_29.npy

To compare TIE vs Danish, we first plot the actual fit results for each detector for a single optical state:

def plot_fit(state, results, title=''):
    fig,axs = plt.subplots(3,3,figsize=(16,10))
    ax = np.ravel(axs)

    # this plots  just the pairing results 
    colors = {'danish':'green',  'tie':'orange'}
    for i in range(9):
        
        ax[i].set_title(i)
        detId = i 
        for method in results.keys():
            # plot  averaged zks for danish and tie
            zkAvg = results[method][state][detId]['zernikeEstimateAvg'][0]
            minNoll = 4
            maxNoll = minNoll + len(zkAvg)
            ax[i].plot(np.arange(minNoll,maxNoll), zkAvg, marker='d', label=f'{method} ')
          
        ax[i].set_xticks(np.arange(minNoll,maxNoll,step=2))
        ax[i].axhline(0,ls='--', c='red')
        ax[i].legend()
       
    fig.text(0.5,0.05, 'Zk mode')
    fig.text(0.05,0.5, 'Zk value [microns]', rotation='vertical')
    fig.subplots_adjust(hspace=0.3)
    if title == '' :
      title = f'WET-007 comCam, state {state}, \n : TIE vs Danish'
    fig.suptitle(title)

Compare all detectors for a single simulated optical state#

state=1
maxNollIndex=22
plot_fit(state, results_stored[maxNollIndex], title=f'WET-007 comCam, state {state}, \n TIE vs Danish, maxNollIndex: {maxNollIndex}')
_images/4bbb288233e87d56c4591f88625484d883d13173bb566e0208d85c02cb3c62b2.png
state=1
maxNollIndex=29
plot_fit(state,  results_stored[maxNollIndex], title=f'WET-007 comCam, state {state}, \n TIE vs Danish, maxNollIndex: {maxNollIndex}')
_images/3cf7612962ba1345f799db21334521ee5b027a320d8da2f02bc49553a7d2e57e.png

Add information about the uncertainty in Zernike estimation. This could be either:

  • standard deviation of raw Zernike estimates per detector that went into per-detector average (sample N = number of donut pairs in each detector)

  • standard deviation of averaged Zernikes across all detectors (sample N=9, corresponding to the number of detectors)

Illustrate both methods. In the figure below the left panel shows the average result with solid green line, and the individual estimates per donut pair in light gray. The errorbars correspond here to the standard deviation of the scatter per mode. The right panel illustrates the average across all detectors, and uncertainty is the scatter in those estimates.

method = 'danish'
state = 4
raft = 8
maxNollIndex = 22
zkr =  results_stored[maxNollIndex][method][state][raft]['zernikeEstimateRaw']
zka =  results_stored[maxNollIndex][method][state][raft]['zernikeEstimateAvg'][0]
zks =  results_stored[maxNollIndex][method][state][raft]['zernikes']

mask_used = zks['used'][1:]
zkr_used = zkr[mask_used]


zka_dets = []
for det in range(9):
    zka_dets.append( results_stored[maxNollIndex][method][state][det]['zernikeEstimateAvg'][0])
    
minNoll = 4
maxNoll = minNoll + len(zka)

fig,ax = plt.subplots(1,2,figsize=(16,5))
ax[0].plot(np.arange(minNoll, maxNoll), zka)
Npts = len(zkr)
ax[0].errorbar(np.arange(minNoll, maxNoll), zka, yerr=np.std(zkr_used, axis=0), ls="--", c="C2", 
               label=f"mean per detector, N={Npts}", capsize=4)
for i in range(len(zkr_used)):
    ax[0].plot(np.arange(minNoll, maxNoll), zkr_used[i], alpha=0.2,c='grey')
ax[0].set_xlabel('Zk mode')
ax[0].set_ylabel('Fit [microns]')
ax[0].set_title(f'Optical state {state}, variation within detector {raft}, {method}')
ax[0].legend()
Npts = len(zka_dets)
ax[1].errorbar(np.arange(minNoll, maxNoll), 
               np.mean(zka_dets, axis=0), 
               yerr=np.std(zka_dets, axis=0),  
               ls="--", c="C2", label=f"mean across detectors, N={Npts}", capsize=4)
for i in range(len(zka_dets)):
    ax[1].plot(np.arange(minNoll, maxNoll), zka_dets[i], alpha=0.2,c='grey')
ax[1].set_title(f'Optical state {state}, variation between detectors, {method}')
ax[0].set_xticks(np.arange(minNoll,maxNoll,step=2))
ax[1].set_xticks(np.arange(minNoll,maxNoll,step=2))
ax[1].legend()
<matplotlib.legend.Legend at 0x7ff035fbe290>
_images/9e0e5abd80c9c12deff8814328f9e0071fb784d950a43b536880c5126d10be24.png

We plot that for all detectors for a single state, to show that the two methods have yield very similar results:

def plot_fit_errorbar(state, results, maxNollIndex):
    fig,axs = plt.subplots(3,3,figsize=(16,10))
    ax = np.ravel(axs)

    colors = {'danish':'green',  'tie':'orange'}
    for i in range(9):
        
        ax[i].set_title(i )
        detId=i
        for method in results.keys():
            # plot  averaged zks for danish and tie
            zkAvg = results[method][state][detId]['zernikeEstimateAvg'][0]
            zkRaw = results[method][state][detId]['zernikeEstimateRaw']
            zks = results[method][state][detId]['zernikes']
            mask_used = zks['used'][1:]
            zkr_used = zkRaw[mask_used]
            minNoll = 4
            maxNoll = minNoll + len(zkAvg)
            ax[i].errorbar(np.arange(minNoll, maxNoll), zkAvg, yerr=np.std(zkr_used, axis=0), 
                          label=f'{method} ', capsize=4)
            
        ax[i].set_xticks(np.arange(minNoll, maxNoll, step=2))
        ax[i].axhline(0,ls='--', c='red')
        ax[i].legend()
       
    fig.text(0.5,0.05, 'Zk mode')
    fig.text(0.05,0.5, 'Zk value [microns]', rotation='vertical')
    fig.subplots_adjust(hspace=0.3)
    fig.suptitle(f'WET-007 comCam, state {state}, \n : TIE vs Danish, maxNollIndex={maxNollIndex}')
maxNollIndex = 22 
plot_fit_errorbar(1, results_stored[maxNollIndex], maxNollIndex, )
_images/a405996a1ae8fa099b4ef7dc4143c9c3453800420f0230dae7af8ad0b85ab8ac.png

Illustrate the difference between the two methods:

def plot_diff(state, results, maxNollIndex):
    fig,axs = plt.subplots(3,3,figsize=(16,10))
    ax = np.ravel(axs)

    # this plots  just the pairing results 
    colors = {'danish':'green',  'tie':'orange'}
    for i in range(9):
        detId = i     
        ax[i].set_title(detId)
    
        # plot  difference between danish and tie
        zk1 = results['danish'][state][detId]['zernikeEstimateAvg'][0]
        zk2 = results['tie'][state][detId]['zernikeEstimateAvg'][0]
        minNoll = 4
        maxNoll = minNoll + len(zk1)
        ax[i].plot(np.arange(minNoll, maxNoll),zk1-zk2, marker='d', )
        ax[i].set_xticks(np.arange(minNoll, maxNoll,step=2))
        ax[i].axhline(0,ls='--', c='red')
       
    fig.text(0.5,0.05, 'Zk mode')
    fig.text(0.05,0.5, f'$\Delta$ zk fit (Danish-TIE) [microns]', rotation='vertical')
    fig.subplots_adjust(hspace=0.3)
    fig.suptitle(f'WET-007 comCam, state {state}, \n : TIE vs Danish, maxNollIndex={maxNollIndex}')
maxNollIndex = 22
plot_diff(2, results_stored[maxNollIndex], maxNollIndex)
_images/faa1bcba6ee7a35e5f4a6b226f18d374027aab6c6f2855e95061298121f5e9a6.png

We can also test eg. how does the difference in a single mode, such as astigmatism (Noll Zk5,6), compare between two fitting methods across a variety of simulated optical states:

maxNollIndex = 22
results = results_stored[maxNollIndex]

minNoll = 4

fig,axs = plt.subplots(3,3,figsize=(16,10))
ax = np.ravel(axs)

for measNoll in [5,6]: # oblique and vertical astigmatism
    measIndex = measNoll - minNoll
    for i in range(9):
        detId = i     
        ax[i].set_title(detId)
    
        diffs = []
        states = []
        for state in range(1,101):
            cond1 = len(results['danish'][state][detId])>1
            cond2 = len(results['tie'][state][detId])>1
            if cond1 and cond2 : 
                    
                # plot  difference for danish and tie
                zk1 = results['danish'][state][detId]['zernikeEstimateAvg'][0][measIndex]
                zk2 = results['tie'][state][detId]['zernikeEstimateAvg'][0][measIndex]
                diffPerMode = zk1-zk2
                
                diffs.append(diffPerMode)
                states.append(state)
        # x: all states ;  y: difference per mode 
        ax[i].plot(states, diffs,  marker='d', label=r'$\Delta$'+f'Zk{measNoll}')
for i in range(9):
    ax[i].axhline(0,ls='--', c='red')
    ax[i].set_ylim(-.5,.5)
    ax[i].legend()

fig.text(0.5,0.05, 'state #', fontsize=17)
fig.text(0.05,0.5, f'$\Delta$ Zk5,6 (Danish-TIE) [microns]', rotation='vertical', fontsize=17)
fig.subplots_adjust(hspace=0.3)
fig.suptitle(f'WET-007 comCam simulation, \n TIE vs Danish, primary astigmatism (zk5 and 6),  maxNollIndex={maxNollIndex}')
Text(0.5, 0.98, 'WET-007 comCam, \n TIE vs Danish, Zk6 only, maxNollIndex=22')
_images/f1cf8e2b5ef226fa501001af4859f02a7d1e2189340c3ead63a579618f8ff9d2.png

This shows that when there is a disagreement in astigmatism, it would be the opposite sign for the conjugate component (eg. for detector 6, all Danish fits give larger value to Zk5 than TIE, and for Zk6 it is reversed - all Danish fits result in a smaller Zk6 than TIE).

Compare all modes across optical states#

Calculate the RMS difference between TIE and Danish per detector per state:

def calc_rms_diff(results): 

    rmss={}
    states = {}
    rmserrDanish = {}
    rmserrTie = {}
    
    sumErrDanish  = {}
    sumErrTie = {}
    for raft in range(9):
       rmss[raft] = []
       states[raft] = [] 
       rmserrDanish[raft] = []
       rmserrTie[raft] = []
       sumErrDanish[raft] = []
       sumErrTie[raft]=[]
       for state in range(1,101):
            #print(raft, state)
            cond1 = len(results['danish'][state][raft])>1
            cond2 = len(results['tie'][state][raft])>1
            if cond1 and cond2 : 
                # calculate RMS difference between two estimates of Zk fits 
                zk1 = results['danish'][state][raft]['zernikeEstimateAvg'][0]
                zk2 = results['tie'][state][raft]['zernikeEstimateAvg'][0]
                rms_diff =  np.sqrt(np.mean(np.square(zk1-zk2)))
                rmss[raft].append(rms_diff)
                states[raft].append(state)
    
                # calculate RMS of uncertainties 
                for method, rmsDict, sumDict in zip(['danish', 'tie'],
                                           [rmserrDanish, rmserrTie],
                                            [sumErrDanish, sumErrTie]
                                          ):
                    zkRaw = results[method][state][raft]['zernikeEstimateRaw']
                    zks = results[method][state][raft]['zernikes']
                    mask_used = zks['used'][1:]
                    zkr_used = zkRaw[mask_used]
                    zkErr = np.std(zkr_used, axis=0) # uncertainty per Zk mode
                    rmsErr = np.sqrt(np.mean(np.square(zkErr))) # single number 
                    rmsDict[raft].append(rmsErr) 
                    sumDict[raft].append(np.sqrt(np.sum(np.square(zkErr)))) 
    
                if rms_diff > 10:
                    print(method, raft, state, rms_diff)
    return rmss, states, rmserrDanish, rmserrTie, sumErrDanish, sumErrTie

The total error per detector per state is the sum of variances. This is done for Danish and TIE:

\[ \sigma_{ttl}^{2} = \sum_{i=4}^{i=28}{\sigma_{i}^{2}} \]
maxNollIndex = 22
rmss, states, rmserrDanish, rmserrTie, sumErrDanish, sumErrTie =  calc_rms_diff(results_stored[maxNollIndex])

fig,axs = plt.subplots(3,3,figsize=(20,13))
ax = np.ravel(axs)

for raft in range(9):
    ax[raft].plot(states[raft], rmss[raft], marker='s', label=r'$\Delta$ RMS')
    ax[raft].plot(states[raft], sumErrDanish[raft], label=r'$\sigma_{ttl}$ Danish') 
    ax[raft].plot(states[raft], sumErrTie[raft], label=r'$\sigma_{ttl}$ TIE') 
    ax[raft].set_title(raft)
    ax[raft].set_ylim(0,0.5)
   
fig.text(0.5,0.06,'state #', fontsize=17)
fig.text(0.06,0.5,r'RMS ($\Delta$(zk Danish - zk TIE ) ) [microns] ', rotation=90, fontsize=17)
fig.suptitle('CWFS methods, maxNollIndex:22', fontsize=16)
ax[2].legend(bbox_to_anchor=[0.55,.6])
<matplotlib.legend.Legend at 0x7ff036d44610>
_images/0cfcf3d82407893ff2c30c2c6ff1601f6758ff509a9fdd816cb3542aa93b9bdf.png

This means that the RMS difference between TIE and Danish is overall less than the combined variances. The difference between TIE and Danish is larger when using maxNollIndex of 29:

maxNollIndex= 29 
rmss, states, rmserrDanish, rmserrTie, sumErrDanish, sumErrTie =  calc_rms_diff(results_stored[maxNollIndex])
fig,axs = plt.subplots(3,3,figsize=(20,13))
ax = np.ravel(axs)

for raft in range(9):
    ax[raft].plot(states[raft], rmss[raft], marker='s', label=r'$\Delta$ RMS')
    ax[raft].plot(states[raft], sumErrDanish[raft], label=r'$\sigma_{ttl}$ Danish') 
    ax[raft].plot(states[raft], sumErrTie[raft], label=r'$\sigma_{ttl}$ TIE') 
    ax[raft].set_title(raft)
    ax[raft].set_ylim(-0.1,1.5)
   
fig.text(0.5,0.06,'state #', fontsize=17)
fig.text(0.06,0.5,r'RMS ($\Delta$(zk Danish - zk TIE ) ) [microns] ', rotation=90, fontsize=17)
fig.suptitle('CWFS methods', fontsize=16)
ax[2].legend(bbox_to_anchor=[0.55,.6])
tie 1 45 608.7696423542536
<matplotlib.legend.Legend at 0x7ff02a820dd0>
_images/eccb4bd711b97679aa3e67488109b9f4ee2681115a8c4d71e3e8d5d15414e562.png

For most simulated states, the agreement is at 0.1 microns rms. Those which have a large TIE error (that also corresponds to largest difference in fit results), also correspond to cases where maxNollIndex=29 leads to unconstrained fit (i.e. fit stops at max iterations, and not because it converged). This situation is not seen with maxNollIndex=22.

Plot in detail that state: with zk22 as max TIE is much better behaved than with zk28.

maxNollIndex = 22 
plot_fit_errorbar(4, results_stored[maxNollIndex], maxNollIndex, )
_images/6ad068f7db272d564d55fd851d3e3de02afccabd15e3a65f01a58160664b6154.png
maxNollIndex = 29
plot_fit_errorbar(4, results_stored[maxNollIndex], maxNollIndex, )
_images/c064070a7d100cece9b13c7a1f66eda0844526962c9ab284e3ce2a1df7a43142.png

In summary, based on the simulation of 100 random optical states fitted with either Danish or TIE, exploring the fit of Z4:22 or Z4:29. We find that across all states the RMS difference across all modes between Danish and TIE is < 0.1 microns. Danish performs well even with larger amount of allowed Zk modes, TIE suffers from unstable behavior, especially when adding Zk 23-26.