Commit 06867319 authored by Liu Dezi's avatar Liu Dezi
Browse files

Upload New File

parent 47a3d5d5
Loading
Loading
Loading
Loading
Loading
+517 −0
Original line number Diff line number Diff line
import unittest

import numpy as np
import galsim
import os
import sys
from astropy.table import Table
from scipy import interpolate
import pickle


class test_field_distortion(unittest.TestCase):
    def __init__(self, methodName="runTest"):
        super(test_field_distortion, self).__init__(methodName)
        self.dataMainPath = os.path.join(
            os.getenv("UNIT_TEST_DATA_ROOT"), "csst_msc_sim/field_distortion"
        )
        self.dataInputPath = os.path.join(self.dataMainPath, "input_catalog")
        self.fdModelName = "FieldDistModel_v2.0_test.pickle"

    def test_fd_model(self):
        cat_dir = self.dataInputPath
        model_dir = self.dataMainPath
        model_date = "2024-05-08"
        model_name = self.fdModelName
        field_distortion_model(
            cat_dir,
            model_dir,
            poly_degree=4,
            model_date=model_date,
            model_name=model_name,
        )

    def test_fd_apply(self):
        model_name = self.fdModelName
        model_dir = self.dataMainPath
        cat_dir = self.dataMainPath
        field_distortion_apply(
            model_name, model_dir, cat_dir, ra_cen=60.0, dec_cen=-40.0, img_rot=0.0
        )


def ccdParam():
    """
    Basic CCD size and noise parameters.
    """
    # CCD size
    xt, yt = 59516, 49752
    x0, y0 = 9216, 9232
    xgap, ygap = (534, 1309), 898
    xnchip, ynchip = 6, 5
    ccdSize = xt, yt, x0, y0, xgap, ygap, xnchip, ynchip

    # other parameters
    readNoise = 5.0  # e/pix
    darkNoise = 0.02  # e/pix/s
    pixel_scale = 0.074  # pixel scale
    gain = 1.0
    ccdBase = readNoise, darkNoise, pixel_scale, gain

    return ccdSize, ccdBase


def chipLim(chip):
    ccdSize, ccdBase = ccdParam()
    xt, yt, x0, y0, gx, gy, xnchip, ynchip = ccdSize
    gx1, gx2 = gx

    rowID = ((chip - 1) % 5) + 1
    colID = 6 - ((chip - 1) // 5)

    xrem = 2 * (colID - 1) - (xnchip - 1)
    xcen = (x0 // 2 + gx1 // 2) * xrem
    if chip <= 5 or chip == 10:
        xcen = (x0 // 2 + gx1 // 2) * xrem + (gx2 - gx1)
    if chip >= 26 or chip == 21:
        xcen = (x0 // 2 + gx1 // 2) * xrem - (gx2 - gx1)
    nx0 = xcen - x0 // 2 + 1
    nx1 = xcen + x0 // 2

    yrem = (rowID - 1) - ynchip // 2
    ycen = (y0 + gy) * yrem
    ny0 = ycen - y0 // 2 + 1
    ny1 = ycen + y0 // 2

    return nx0, nx1, ny0, ny1


def chip_filter(nchip):
    """
    return filter name of a given chip
    """
    filtype = ["nuv", "u", "g", "r", "i", "z", "y"]

    # updated configurations
    # if nchip>24 or nchip<7: raise ValueError("!!! Chip ID: [7,24]")
    if nchip in [6, 15, 16, 25]:
        filter_name = "y"
    if nchip in [11, 20]:
        filter_name = "z"
    if nchip in [7, 24]:
        filter_name = "i"
    if nchip in [14, 17]:
        filter_name = "u"
    if nchip in [9, 22]:
        filter_name = "r"
    if nchip in [12, 13, 18, 19]:
        filter_name = "nuv"
    if nchip in [8, 23]:
        filter_name = "g"

    filter_id = filtype.index(filter_name)

    return filter_id, filter_name


def skyLim(wcs, x0, x1, y0, y1):
    """
    The sky coverage of a single exposure image
    """
    r2d = 180.0 / np.pi
    # xt, yt, x0, y0, gx, gy, xnchip, ynchip = ccdSize()
    s1 = wcs.toWorld(galsim.PositionD(x0, y0))
    s2 = wcs.toWorld(galsim.PositionD(x0, y1))

    s3 = wcs.toWorld(galsim.PositionD(x1, y0))
    s4 = wcs.toWorld(galsim.PositionD(x1, y1))
    ra = [s1.ra.rad * r2d, s2.ra.rad * r2d, s3.ra.rad * r2d, s4.ra.rad * r2d]
    dec = [s1.dec.rad * r2d, s2.dec.rad * r2d, s3.dec.rad * r2d, s4.dec.rad * r2d]

    return min(ra), max(ra), min(dec), max(dec)


def wcsMain(imgRotation=0.0, raCenter=0.0, decCenter=0.0):
    ccdSize, ccdBase = ccdParam()
    xsize, ysize, _, _, _, _, _, _ = ccdSize
    _, _, pixelScale, _ = ccdBase
    xmcen, ymcen = 0.0, 0.0
    imrot = imgRotation * galsim.degrees
    racen = raCenter * galsim.degrees
    deccen = decCenter * galsim.degrees

    # define the wcs
    dudx = -np.cos(imrot.rad) * pixelScale
    dudy = +np.sin(imrot.rad) * pixelScale
    dvdx = -np.sin(imrot.rad) * pixelScale
    dvdy = -np.cos(imrot.rad) * pixelScale
    moscen = galsim.PositionD(x=xmcen, y=ymcen)
    skyCenter = galsim.CelestialCoord(ra=racen, dec=deccen)
    affine = galsim.AffineTransform(dudx, dudy, dvdx, dvdy, origin=moscen)
    wcs = galsim.TanWCS(affine, skyCenter, units=galsim.arcsec)

    return wcs


# FD model
def field_distortion_model(
    cat_dir,
    model_dir,
    poly_degree=4,
    model_date="2024-05-08",
    model_name="FieldDistModel_v2.0_test.pickle",
):
    # default parameter setup
    nccd, nwave, npsf = 30, 4, 30 * 30

    # load a CSST-like wcs
    wcs = wcsMain()
    cd11, cd12 = wcs.cd[0, 0], wcs.cd[0, 1]
    cd21, cd22 = wcs.cd[1, 0], wcs.cd[1, 1]
    xmcen, ymcen = wcs.crpix

    # obtain the interpolation model
    fdFunList = {}
    fdFunList["date"] = model_date
    for iwave in range(1, nwave + 1):
        # if iwave!=1: continue
        iwaveKey = "wave%d" % iwave

        # first construct the global interpolation
        xwList, ywList = [], []
        xdList, ydList = [], []
        fdFunList[iwaveKey] = {}
        for iccd in range(1, nccd + 1):
            # if iccd!=9: continue
            iccdKey = "ccd" + str("0%d" % (iccd))[-2:]

            # load PSF data
            ipsfDatn = cat_dir + "ccd%d_%s.dat" % (iccd, iwaveKey)
            ipsfDat = Table.read(ipsfDatn, format="ascii")
            for ipsf in range(1, npsf + 1):
                # if ipsf!=2: continue
                xField = ipsfDat["field_x"][ipsf - 1]
                yField = ipsfDat["field_y"][ipsf - 1]

                # image coordinate with field distortion
                xImage = 100.0 * (
                    ipsfDat["image_x"][ipsf - 1] + ipsfDat["centroid_x"][ipsf - 1]
                )
                yImage = 100.0 * (
                    ipsfDat["image_y"][ipsf - 1] + ipsfDat["centroid_y"][ipsf - 1]
                )

                # image coordinate only with wcs projection
                xwcs = (cd12 * yField - cd22 * xField) / (
                    cd12 * cd21 - cd11 * cd22
                ) + xmcen
                ywcs = (cd21 * xField - cd11 * yField) / (
                    cd12 * cd21 - cd11 * cd22
                ) + ymcen

                xwList += [xwcs]
                ywList += [ywcs]
                xdList += [xImage]
                ydList += [yImage]

        # global interpolation
        xImageFun = interpolate.SmoothBivariateSpline(
            xwList, ywList, xdList, kx=poly_degree, ky=poly_degree
        )
        yImageFun = interpolate.SmoothBivariateSpline(
            xwList, ywList, ydList, kx=poly_degree, ky=poly_degree
        )
        fdFunList[iwaveKey] = {
            "xImagePos": xImageFun,
            "yImagePos": yImageFun,
            "interpLimit": [
                np.min(xwList),
                np.max(xwList),
                np.min(ywList),
                np.max(ywList),
            ],
        }

        # construct the residual interpolation
        fdFunList[iwaveKey]["residual"] = {}
        for iccd in range(1, nccd + 1):
            # if iccd!=1: continue
            iccdKey = "ccd" + str("0%d" % (iccd))[-2:]
            # open the ditortion data
            ipsfDatn = cat_dir + "ccd%d_%s.dat" % (iccd, iwaveKey)
            ipsfDat = Table.read(ipsfDatn, format="ascii")

            ixwList, iywList = [], []
            idxList, idyList = [], []
            for ipsf in range(1, npsf + 1):
                # if ipsf!=1: continue
                print(
                    "^_^ loading: iccd-{:} iwave-{:} ipsf-{:}".format(iccd, iwave, ipsf)
                )
                xField = ipsfDat["field_x"][ipsf - 1]
                yField = ipsfDat["field_y"][ipsf - 1]
                xImage = 100.0 * (
                    ipsfDat["image_x"][ipsf - 1] + ipsfDat["centroid_x"][ipsf - 1]
                )
                yImage = 100.0 * (
                    ipsfDat["image_y"][ipsf - 1] + ipsfDat["centroid_y"][ipsf - 1]
                )

                # image coordinate only with wcs projection
                xwcs = (cd12 * yField - cd22 * xField) / (
                    cd12 * cd21 - cd11 * cd22
                ) + xmcen
                ywcs = (cd21 * xField - cd11 * yField) / (
                    cd12 * cd21 - cd11 * cd22
                ) + ymcen

                ixPred = xImageFun(xwcs, ywcs)[0][0]
                iyPred = yImageFun(xwcs, ywcs)[0][0]
                idx = xImage - ixPred
                idy = yImage - iyPred
                # print(idx, idy)
                ixwList += [xwcs]
                iywList += [ywcs]
                idxList += [idx]
                idyList += [idy]

            # interpolation
            xResFun = interpolate.SmoothBivariateSpline(
                ixwList, iywList, idxList, kx=poly_degree, ky=poly_degree
            )
            yResFun = interpolate.SmoothBivariateSpline(
                ixwList, iywList, idyList, kx=poly_degree, ky=poly_degree
            )

            fdFunList[iwaveKey]["residual"][iccdKey] = {
                "xResidual": xResFun,
                "yResidual": yResFun,
                "interpLimit": [
                    np.min(ixwList),
                    np.max(ixwList),
                    np.min(iywList),
                    np.max(iywList),
                ],
            }

    # save the interpolation functions
    model_name_full = os.path.join(model_dir, model_name)
    with open(model_name_full, "wb") as out:
        pickle.dump(fdFunList, out, pickle.HIGHEST_PROTOCOL)

    return


def field_distortion_apply(
    model_name, model_dir, cat_dir, ra_cen=60.0, dec_cen=-40.0, img_rot=0.0
):
    # CCD and observation
    ccdSize, ccdBase = ccdParam()
    xsize, ysize, xchip, ychip, xgap, ygap, xnchip, ynchip = ccdSize
    nchip = xnchip * ynchip

    #################################################
    xmcen, ymcen = 0.0, 0.0
    #################################################

    badchip = list(range(1, 6)) + list(range(26, 31)) + [10, 21]

    # define the wcs of the image mosaic
    print(
        "^_^ Construct the wcs of the entire image mosaic using Gnomonic/TAN projection"
    )
    wcs = wcsMain(imgRotation=img_rot, raCenter=ra_cen, decCenter=dec_cen)

    #################################################
    # load the field distortion model
    model_name_full = os.path.join(model_dir, model_name)
    with open(model_name_full, "rb") as f:
        fdModel = pickle.load(f)
    #################################################

    raLow, raUp, decLow, decUp = skyLim(
        wcs, -xsize // 2 + 1, xsize // 2, -ysize // 2 + 1, ysize // 2
    )
    dra = (raUp - raLow) * np.cos(dec_cen * np.pi / 180.0)
    ddec = decUp - decLow
    print(
        "    Image pixel size: %d*%d; center: (Ra, Dec)=(%.3f, %.3f)."
        % (xsize, ysize, ra_cen, dec_cen)
    )
    print("    Field of Veiw: %.2f * %.2f deg^2." % (dra, ddec))

    # filters and corresponding bounds in the image mosaic
    fbound = {}
    print("    Model the filter distributions in the image mosaic ...")
    stats = {}
    for i in range(nchip):
        chip_id = i + 1
        if chip_id in badchip:
            continue
        cx0, cx1, cy0, cy1 = chipLim(chip_id)
        chip_bound = galsim.BoundsD(cx0 - 1, cx1 - 1, cy0 - 1, cy1 - 1)
        chip_filter_id, chip_filt = chip_filter(chip_id)
        # print "^_^ CHIP %d, Filter %s"%(chip_id,chip_filter)
        fbound[chip_id] = [chip_filter_id, chip_filt, chip_bound]
        stats[chip_id] = [0, 0, 0]

    # generate object grid
    ra_input = np.arange(ra_cen - 1.0, ra_cen + 1.0, 0.00125)
    dec_input = np.arange(dec_cen - 1.0, dec_cen + 1.0, 0.00125)
    nobj = len(ra_input) * len(dec_input)
    crdCat = np.zeros((nobj, 2))
    cid = 0
    for id1 in range(len(ra_input)):
        ira = ra_input[id1]
        for id2 in range(len(dec_input)):
            idec = dec_input[id2]
            crdCat[cid, :] = ira, idec
            cid += 1
    print("^_^ Total %d objects are generaged" % nobj)

    # main program
    for i in range(nchip):
        # if i not in [6]: continue
        if i + 1 in badchip:
            continue
        filtidk, filtnmk, boundk = fbound[i + 1]
        idStr = str("0%d" % (i + 1))[-2:]

        ###################################################################
        # 1) Use global field distortion model: FieldDistModelGlobal_v2.0.pickle
        ifdModel = fdModel["wave1"]
        irsModel = fdModel["wave1"]["residual"]["ccd" + idStr]
        xLowI, xUpI, yLowI, yUpI = ifdModel["interpLimit"]
        xlLowI, xlUpI, ylLowI, ylUpI = irsModel["interpLimit"]

        # field distortion model along x/y-axis
        ixfdModel = ifdModel["xImagePos"]
        iyfdModel = ifdModel["yImagePos"]
        ixrsModel = irsModel["xResidual"]
        iyrsModel = irsModel["yResidual"]

        # first-order derivatives of the global field distortion model
        ifx_dx = ixfdModel.partial_derivative(1, 0)
        ifx_dy = ixfdModel.partial_derivative(0, 1)
        ify_dx = iyfdModel.partial_derivative(1, 0)
        ify_dy = iyfdModel.partial_derivative(0, 1)
        # first-order derivatives of the residual field distortion model
        irx_dx = ixrsModel.partial_derivative(1, 0)
        irx_dy = ixrsModel.partial_derivative(0, 1)
        iry_dx = iyrsModel.partial_derivative(1, 0)
        iry_dy = iyrsModel.partial_derivative(0, 1)
        ###################################################################

        # construct the image mosaic firstly
        xorigin, yorigin = xmcen - boundk.xmin, ymcen - boundk.ymin
        print("    Construct the chip mosaic ...")
        fimage = galsim.ImageF(xchip, ychip)
        fimage.setOrigin(boundk.xmin, boundk.ymin)

        fimage.wcs = wcs
        raLow, raUp, decLow, decUp = skyLim(
            wcs, boundk.xmin, boundk.xmax, boundk.ymin, boundk.ymax
        )
        dra = (raUp - raLow) * np.cos(dec_cen * np.pi / 180.0)
        ddec = decUp - decLow
        print("    Image coverage: %.2f * %.2f arcmin^2." % (dra * 60.0, ddec * 60.0))
        # enlarge the sky coverage in order to catch the galaxies at the chip edge
        raLow -= 0.2 / 60.0
        decLow -= 0.2 / 60.0
        raUp += 0.2 / 60.0
        decUp += 0.2 / 60.0
        print(
            "    Range: RA=[%.4f, %.4f]; DEC=[%.4f, %.4f]"
            % (raLow, raUp, decLow, decUp)
        )

        # generate the galaxy and star images
        catxxn = os.path.join(
            cat_dir, "csst_mainfocus_field_distortion_ccd%s_%s.cat" % (idStr, filtnmk)
        )
        hdrxx = "#id_obj id_chip filter ra_true dec_ture x_image_ture y_image_ture x_image y_image g1_fd g2_fd\n"
        fmtxx = "%8d %3d %4s %12.6f %12.6f %13.6f %13.6f %13.6f %13.6f %9.5f %9.5f\n"
        catxx = open(catxxn, "w")
        catxx.write(hdrxx)
        oidxx = 0
        for k in range(nobj):
            # if k != 0: continue
            # input galaxy parameters
            rak = crdCat[k, 0]
            deck = crdCat[k, 1]

            # reject objects out of the image
            if (rak - raLow) * (rak - raUp) > 0.0 or (deck - decLow) * (
                deck - decUp
            ) > 0.0:
                continue

            world_pos = galsim.CelestialCoord(
                ra=rak * galsim.degrees, dec=deck * galsim.degrees
            )
            image_pos = fimage.wcs.toImage(world_pos)
            xk_true = image_pos.x
            yk_true = image_pos.y

            #################################################################
            # field distortion
            if (xLowI - xk_true) * (xUpI - xk_true) > 0 or (yLowI - yk_true) * (
                yUpI - yk_true
            ) > 0:
                continue
            xk = ixfdModel(xk_true, yk_true)[0][0]
            yk = iyfdModel(xk_true, yk_true)[0][0]

            # global offset correction
            if (xlLowI - xk) * (xlUpI - xk) > 0 or (ylLowI - yk) * (ylUpI - yk) > 0:
                continue
            dxk = ixrsModel(xk, yk)[0][0]
            dyk = iyrsModel(xk, yk)[0][0]
            xk = xk + dxk
            yk = yk + dyk

            # field distortion induced ellipticity
            ix_dx = ifx_dx(xk, yk) + irx_dx(xk, yk)
            ix_dy = ifx_dy(xk, yk) + irx_dy(xk, yk)
            iy_dx = ify_dx(xk, yk) + iry_dx(xk, yk)
            iy_dy = ify_dy(xk, yk) + iry_dy(xk, yk)

            g1k_fd = 0.0 + (iy_dy - ix_dx) / (iy_dy + ix_dx)
            g2k_fd = 0.0 - (iy_dx + ix_dy) / (iy_dy + ix_dx)
            #################################################################
            dxk_true, dyk_true = xk_true - xmcen, yk_true - ymcen
            xLock_true, yLock_true = dxk_true + xorigin + 1.0, dyk_true + yorigin + 1.0

            dxk, dyk = xk - xmcen, yk - ymcen
            xLock, yLock = dxk + xorigin + 1.0, dyk + yorigin + 1.0

            if (xLock_true < 0) or (xLock_true > xchip):
                continue
            if (yLock_true < 0) or (yLock_true > ychip):
                continue
            if (xLock < 0) or (xLock > xchip):
                continue
            if (yLock < 0) or (yLock > ychip):
                continue
            linexx = fmtxx % (
                k + 1,
                i + 1,
                filtnmk.lower(),
                rak,
                deck,
                xLock_true,
                yLock_true,
                xLock,
                yLock,
                g1k_fd[0][0],
                g2k_fd[0][0],
            )
            catxx.write(linexx)

        catxx.close()

    return


if __name__ == "__main__":
    unittest.main()