Esempio n. 1
0
def addAmp(ampCatalog, i, eparams):
    """ Add an amplifier to an AmpInfoCatalog

    @param ampCatalog: An instance of an AmpInfoCatalog object to fill with amp properties
    @param i which amplifier? (i == 0 ? left : right)
    @param eparams: Electronic parameters.  This is a list of tuples with (i, params),
                    where params is a dictionary of electronic parameters.
    """
    #
    # Layout of active and overclock pixels in the as-readout data  The layout is:
    #     Amp0 || extended | overclock | data || data | overclock | extended || Amp1
    # for each row; all rows are identical in drift-scan data
    #
    height = 1361  # number of rows in a frame
    width = 1024  # number of data pixels read out through one amplifier
    nExtended = 8  # number of pixels in the extended register
    nOverclock = 32  # number of (horizontal) overclock pixels
    #
    # Construct the needed bounding boxes given that geometrical information.
    #
    # Note that all the offsets are relative to the origin of this amp, not to its eventual
    # position in the CCD
    #
    record = ampCatalog.addNew()
    xtot = width + nExtended + nOverclock
    allPixels = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(xtot, height))
    biasSec = geom.BoxI(geom.PointI(nExtended if i == 0 else width, 0),
                        geom.ExtentI(nOverclock, height))
    dataSec = geom.BoxI(
        geom.PointI(nExtended + nOverclock if i == 0 else 0, 0),
        geom.ExtentI(width, height))
    emptyBox = geom.BoxI()
    bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
    bbox.shift(geom.Extent2I(width * i, 0))

    shiftp = geom.Extent2I(xtot * i, 0)
    allPixels.shift(shiftp)
    biasSec.shift(shiftp)
    dataSec.shift(shiftp)

    record.setBBox(bbox)
    record.setRawXYOffset(geom.ExtentI(0, 0))
    record.setName('left' if i == 0 else 'right')
    record.setReadoutCorner(afwTable.LL if i == 0 else afwTable.LR)
    record.setGain(eparams['gain'])
    record.setReadNoise(eparams['readNoise'])
    record.setSaturation(eparams['fullWell'])
    record.setSuspectLevel(float("nan"))
    record.setLinearityType(NullLinearityType)
    record.setLinearityCoeffs([
        1.,
    ])
    record.setHasRawInfo(True)
    record.setRawFlipX(False)
    record.setRawFlipY(False)
    record.setRawBBox(allPixels)
    record.setRawDataBBox(dataSec)
    record.setRawHorizontalOverscanBBox(biasSec)
    record.setRawVerticalOverscanBBox(emptyBox)
    record.setRawPrescanBBox(emptyBox)
Esempio n. 2
0
def addDefects(exp, nBadCols=10):
    img = exp.getMaskedImage().getImage()
    (xsize, ysize) = img.getDimensions()
    defectList = measAlg.Defects()

    # set some bad cols and add them to a defect list
    for xi in numpy.random.randint(0, xsize, nBadCols):
        yi = numpy.random.randint(0, ysize)
        xi, yi = int(xi), int(yi)
        bbox = geom.Box2I(geom.PointI(xi, 0), geom.ExtentI(1, yi+1))
        subIm = afwImage.ImageF(img, bbox)
        subIm.set(1e7)
        defectList.append(bbox)
    # set a 15 pixel box of defects at the upper left corner to demonstrate fallbackValue
    bbox = geom.Box2I(geom.PointI(0, ysize-15), geom.ExtentI(15, 15))
    subIm = afwImage.ImageF(img, bbox)
    subIm.set(1e7)
    defectList.append(bbox)
    return defectList
Esempio n. 3
0
def make_cutout(exposure, x, y, cutout_size=60):
    """Make a cutout exposure at (x, y) pixel coordinate.
       exposure:      lsst.afw.image.exposure.exposure.ExposureF
       x:             x pixel coordinate
       y:             y pixel coordinate
       cutout_size:   Width(in pixel unit) of the postage stamp , default value is 60
    """
    cutout_extent = geom.ExtentI(cutout_size, cutout_size)
    radec = geom.SpherePoint(exposure.getWcs().pixelToSky(x, y))
    cutout_image = exposure.getCutout(radec, cutout_extent)
    return cutout_image
Esempio n. 4
0
def convertfpM(infile, allPlanes=False):
    with fits.open(infile) as hdr:
        hdr[0].header['RUN']
        hdr[0].header['CAMCOL']
        hdr[0].header['FIELD']
        nRows = hdr[0].header['MASKROWS']
        nCols = hdr[0].header['MASKCOLS']
        hdr[0].header['NPLANE']

        names = hdr[-1].data.names
        if ("attributeName" not in names) or ("Value" not in names):
            raise LookupError("Missing data in fpM header")

        planes = hdr[-1].data.field("attributeName").tolist()
        mask = afwImage.Mask(geom.ExtentI(nCols, nRows))

        # Minimal sets of mask planes needed for LSST
        interpPlane = planes.index("S_MASK_INTERP") + 1
        satPlane = planes.index("S_MASK_SATUR") + 1
        crPlane = planes.index("S_MASK_CR") + 1

        interpBitMask = afwImage.Mask.getPlaneBitMask("INTRP")
        satBitMask = afwImage.Mask.getPlaneBitMask("SAT")
        crBitMask = afwImage.Mask.getPlaneBitMask("CR")

        listToSet = [(interpPlane, interpBitMask),
                     (satPlane, satBitMask),
                     (crPlane, crBitMask)]

        # Add the rest of the SDSS planes
        if allPlanes:
            for plane in ['S_MASK_NOTCHECKED', 'S_MASK_OBJECT', 'S_MASK_BRIGHTOBJECT',
                          'S_MASK_BINOBJECT', 'S_MASK_CATOBJECT', 'S_MASK_SUBTRACTED', 'S_MASK_GHOST']:
                idx = planes.index(plane) + 1
                planeName = re.sub("S_MASK_", "", plane)
                mask.addMaskPlane(planeName)
                planeBitMask = afwImage.Mask.getPlaneBitMask(planeName)
                listToSet.append((idx, planeBitMask))

        for plane, bitmask in listToSet:
            if len(hdr) < plane:
                continue

            if hdr[plane].data is None:
                continue

            nmask = len(hdr[plane].data)
            for i in range(nmask):
                frow = hdr[plane].data[i]
                Objmask(frow, bitmask).setMask(mask)

    return mask
Esempio n. 5
0
def ccdPixelToAmpPixel(xy, detector):
    r"""Given an position within a detector return position within an amplifier

    Parameters
    ----------
    xy : `lsst.geom.PointD`
       pixel position within detector
    detector : `lsst.afw.cameraGeom.Detector`
        The requested detector

    N.b. all pixel coordinates have the centre of the bottom-left pixel
    at (0.0, 0.0)

    Returns
    -------
    amp : `lsst.afw.table.AmpInfoRecord`
       The amplifier that the pixel lies in
    ampXY : `lsst.geom.PointI`
       The pixel coordinate relative to the corner of the single-amp image

    Raises
    ------
    RuntimeError
        If the requested pixel doesn't lie on the detector
    """

    found = False
    for amp in detector:
        if geom.BoxD(amp.getBBox()).contains(xy):
            found = True
            xy = geom.PointI(xy)     # pixel coordinates as ints
            break

    if not found:
        raise RuntimeError("Point (%g, %g) does not lie on detector %s" % (xy[0], xy[1], detector.getName()))

    x, y = xy - amp.getBBox().getBegin()   # offset from origin of amp's data segment

    # Allow for flips (due e.g. to physical location of the amplifiers)
    w, h = amp.getRawDataBBox().getDimensions()
    if amp.getRawFlipX():
        x = w - x - 1

    if amp.getRawFlipY():
        y = h - y - 1

    dxy = amp.getRawBBox().getBegin() - amp.getRawDataBBox().getBegin()   # correction for overscan etc.
    xy = geom.ExtentI(x, y) - dxy

    return amp, xy
Esempio n. 6
0
def display_exposure(exposure,
                     x,
                     y,
                     cutout_size=60,
                     coord_list=None,
                     scale=None,
                     frame=None,
                     show_colorbar=False,
                     title=None,
                     save_name=None):
    """This function displays the postage stamp of an exposure. The center of the postage stamp
       is marked by a red circle. We can also overlay blue circles corresponding to the coordinaes
       given in the coord_list on the postage stamp.
       exposure:       lsst.afw.image.exposure.exposure.ExposureF
       x:              x pixel coordinate
       y:              y pixel coordinate
       cutout_size:    Width(in pixel unit) of the postage stamp , default value is 60
       coord_list:     A list of coordinates where we can overlay blue circles on the postage stamp
       scale:          [min_val, max_val], set the min value and the max value for display,
                       default is None
       frame:          The frame of the afwDisplay.Display object
       show_colorbar:  Show colorbar of the postage stamp, default is False
       title:          Title of the postage stamp
       save_name:      If provided, the postage stamp will be saved as 'save_name.png' in the current
                       working directory, default if None
    """
    cutout_extent = geom.ExtentI(cutout_size, cutout_size)
    radec = geom.SpherePoint(exposure.getWcs().pixelToSky(x, y))
    cutout_image = exposure.getCutout(radec, cutout_extent)

    xy = geom.PointI(x, y)
    display = afwDisplay.Display(frame=frame, backend='matplotlib')
    if scale:
        display.scale("linear", scale[0], scale[1])
    else:
        display.scale("linear", "zscale")
    display.mtv(cutout_image)
    if show_colorbar:
        display.show_colorbar()
    display.dot('o', xy.getX(), xy.getY(), ctype='red')

    if coord_list:
        for coord in coord_list:
            coord_x, coord_y = coord
            display.dot('o', coord_x, coord_y, ctype='blue')

    plt.title(title)
    if save_name:
        plt.savefig(save_name, dpi=500)
Esempio n. 7
0
def ampPixelToCcdPixel(x, y, detector, channel):
    r"""Given a position in a raw amplifier return position on full detector

    Parameters
    ----------
    x : `int`
       column on amp segment
    y : `int`
       row on amp segment
    detector : `lsst.afw.cameraGeom.Detector`
        The requested detector
    channel: `int`
       Channel number of amplifier (1-indexed; identical to HDU)

    Returns
    -------
    ccdX : `int`
       The column pixel position relative to the corner of the detector
    ccdY : `int`
       The row pixel position relative to the corner of the detector
    """

    amp = channelToAmp(detector, channel)
    rawBBox, rawDataBBox = amp.getRawBBox(), amp.getRawDataBBox()
    # Allow for flips (due e.g. to physical location of the amplifiers)
    w, h = rawBBox.getDimensions()
    if amp.getRawFlipX():
        rawBBox.flipLR(w)
        rawDataBBox.flipLR(w)

        x = rawBBox.getWidth() - x - 1

    if amp.getRawFlipY():
        rawBBox.flipTB(h)
        rawDataBBox.flipTB(h)

        y = rawBBox.getHeight() - y - 1

    dxy = rawBBox.getBegin() - rawDataBBox.getBegin()   # correction for overscan etc.

    return amp.getBBox().getBegin() + dxy + geom.ExtentI(x, y)
Esempio n. 8
0
def make_cutout_radec(exposure, radec, cutout_size=60):
    cutout_extent = geom.ExtentI(cutout_size, cutout_size)
    cutout_image = exposure.getCutout(radec, cutout_extent)
    return cutout_image
Esempio n. 9
0
    def setUp(self):
        config = SingleFrameMeasurementTask.ConfigClass()
        config.slots.apFlux = 'base_CircularApertureFlux_12_0'
        self.schema = afwTable.SourceTable.makeMinimalSchema()

        self.measureSources = SingleFrameMeasurementTask(self.schema,
                                                         config=config)

        width, height = 110, 301

        self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height))
        self.mi.set(0)
        sd = 3  # standard deviation of image
        self.mi.getVariance().set(sd * sd)
        self.mi.getMask().addMaskPlane("DETECTED")

        self.ksize = 31  # size of desired kernel

        sigma1 = 1.75
        sigma2 = 2 * sigma1

        self.exposure = afwImage.makeExposure(self.mi)
        self.exposure.setPsf(
            measAlg.DoubleGaussianPsf(self.ksize, self.ksize, 1.5 * sigma1, 1,
                                      0.1))
        cdMatrix = np.array([1.0, 0.0, 0.0, 1.0])
        cdMatrix.shape = (2, 2)
        wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0),
                                 crval=geom.SpherePoint(
                                     0.0, 0.0, geom.degrees),
                                 cdMatrix=cdMatrix)
        self.exposure.setWcs(wcs)

        #
        # Make a kernel with the exactly correct basis functions.
        # Useful for debugging
        #
        basisKernelList = []
        for sigma in (sigma1, sigma2):
            basisKernel = afwMath.AnalyticKernel(
                self.ksize, self.ksize,
                afwMath.GaussianFunction2D(sigma, sigma))
            basisImage = afwImage.ImageD(basisKernel.getDimensions())
            basisKernel.computeImage(basisImage, True)
            basisImage /= np.sum(basisImage.getArray())

            if sigma == sigma1:
                basisImage0 = basisImage
            else:
                basisImage -= basisImage0

            basisKernelList.append(afwMath.FixedKernel(basisImage))

        order = 1  # 1 => up to linear
        spFunc = afwMath.PolynomialFunction2D(order)

        exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc)
        exactKernel.setSpatialParameters([[1.0, 0, 0],
                                          [0.0, 0.5 * 1e-2, 0.2e-2]])

        rand = afwMath.Random()  # make these tests repeatable by setting seed

        addNoise = True

        if addNoise:
            im = self.mi.getImage()
            afwMath.randomGaussianImage(im, rand)  # N(0, 1)
            im *= sd  # N(0, sd^2)
            del im

        xarr, yarr = [], []

        for x, y in [
            (20, 20),
            (60, 20),
            (30, 35),
            (50, 50),
            (20, 90),
            (70, 160),
            (25, 265),
            (75, 275),
            (85, 30),
            (50, 120),
            (70, 80),
            (60, 210),
            (20, 210),
        ]:
            xarr.append(x)
            yarr.append(y)

        for x, y in zip(xarr, yarr):
            dx = rand.uniform() - 0.5  # random (centered) offsets
            dy = rand.uniform() - 0.5

            k = exactKernel.getSpatialFunction(1)(
                x, y)  # functional variation of Kernel ...
            b = (k * sigma1**2 / ((1 - k) * sigma2**2)
                 )  # ... converted double Gaussian's "b"

            # flux = 80000 - 20*x - 10*(y/float(height))**2
            flux = 80000 * (1 + 0.1 * (rand.uniform() - 0.5))
            I0 = flux * (1 + b) / (2 * np.pi * (sigma1**2 + b * sigma2**2))
            for iy in range(y - self.ksize // 2, y + self.ksize // 2 + 1):
                if iy < 0 or iy >= self.mi.getHeight():
                    continue

                for ix in range(x - self.ksize // 2, x + self.ksize // 2 + 1):
                    if ix < 0 or ix >= self.mi.getWidth():
                        continue

                    II = I0 * psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b)
                    Isample = rand.poisson(II) if addNoise else II
                    self.mi.image[ix, iy, afwImage.LOCAL] += Isample
                    self.mi.variance[ix, iy, afwImage.LOCAL] += II

        bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height))
        self.cellSet = afwMath.SpatialCellSet(bbox, 100)

        self.footprintSet = afwDetection.FootprintSet(
            self.mi, afwDetection.Threshold(100), "DETECTED")

        self.catalog = self.measure(self.footprintSet, self.exposure)

        for source in self.catalog:
            try:
                cand = measAlg.makePsfCandidate(source, self.exposure)
                self.cellSet.insertCandidate(cand)

            except Exception as e:
                print(e)
                continue
Esempio n. 10
0
def assemble_raw(dataId, componentInfo, cls):
    """Called by the butler to construct the composite type "raw".

    Note that we still need to define "_raw" and copy various fields over.

    Parameters
    ----------
    dataId : `lsst.daf.persistence.dataId.DataId`
        The data ID.
    componentInfo : `dict`
        dict containing the components, as defined by the composite definition
        in the mapper policy.
    cls : 'object'
        Unused.

    Returns
    -------
    exposure : `lsst.afw.image.Exposure`
        The assembled exposure.
    """
    from lsst.ip.isr import AssembleCcdTask

    config = AssembleCcdTask.ConfigClass()
    config.doTrim = False

    assembleTask = AssembleCcdTask(config=config)

    ampExps = componentInfo['raw_amp'].obj
    if len(ampExps) == 0:
        raise RuntimeError("Unable to read raw_amps for %s" % dataId)

    ccd = ampExps[0].getDetector()      # the same (full, CCD-level) Detector is attached to all ampExps
    #
    # Check that the geometry in the metadata matches cameraGeom
    #
    logger = lsst.log.Log.getLogger("LsstCamMapper")
    warned = False
    for i, (amp, ampExp) in enumerate(zip(ccd, ampExps)):
        ampMd = ampExp.getMetadata().toDict()

        if amp.getRawBBox() != ampExp.getBBox():  # Oh dear. cameraGeom is wrong -- probably overscan
            if amp.getRawDataBBox().getDimensions() != amp.getBBox().getDimensions():
                raise RuntimeError("Active area is the wrong size: %s v. %s" %
                                   (amp.getRawDataBBox().getDimensions(), amp.getBBox().getDimensions()))
            if not warned:
                logger.warn("amp.getRawBBox() != data.getBBox(); patching. (%s v. %s)",
                            amp.getRawBBox(), ampExp.getBBox())
                warned = True

            w, h = ampExp.getBBox().getDimensions()
            ow, oh = amp.getRawBBox().getDimensions()  # "old" (cameraGeom) dimensions
            #
            # We could trust the BIASSEC keyword, or we can just assume that
            # they've changed the number of overscan pixels (serial and/or
            # parallel).  As Jim Chiang points out, the latter is safer
            #
            bbox = amp.getRawHorizontalOverscanBBox()
            hOverscanBBox = geom.BoxI(bbox.getBegin(),
                                      geom.ExtentI(w - bbox.getBeginX(), bbox.getHeight()))
            bbox = amp.getRawVerticalOverscanBBox()
            vOverscanBBox = geom.BoxI(bbox.getBegin(),
                                      geom.ExtentI(bbox.getWidth(), h - bbox.getBeginY()))

            amp.setRawBBox(ampExp.getBBox())
            amp.setRawHorizontalOverscanBBox(hOverscanBBox)
            amp.setRawVerticalOverscanBBox(vOverscanBBox)
            #
            # This gets all the geometry right for the amplifier, but the size
            # of the untrimmed image will be wrong and we'll put the amp
            # sections in the wrong places, i.e.
            #   amp.getRawXYOffset()
            # will be wrong.  So we need to recalculate the offsets.
            #
            xRawExtent, yRawExtent = amp.getRawBBox().getDimensions()

            x0, y0 = amp.getRawXYOffset()
            ix, iy = x0//ow, y0/oh
            x0, y0 = ix*xRawExtent, iy*yRawExtent
            amp.setRawXYOffset(geom.ExtentI(ix*xRawExtent, iy*yRawExtent))
        #
        # Check the "IRAF" keywords, but don't abort if they're wrong
        #
        # Only warn about the first amp, use debug for the others
        #
        detsec = bboxFromIraf(ampMd["DETSEC"]) if "DETSEC" in ampMd else None
        datasec = bboxFromIraf(ampMd["DATASEC"]) if "DATASEC" in ampMd else None
        biassec = bboxFromIraf(ampMd["BIASSEC"]) if "BIASSEC" in ampMd else None

        logCmd = logger.warn if i == 0 else logger.debug
        if detsec and amp.getBBox() != detsec:
            logCmd("DETSEC doesn't match for %s (%s != %s)", dataId, amp.getBBox(), detsec)
        if datasec and amp.getRawDataBBox() != datasec:
            logCmd("DATASEC doesn't match for %s (%s != %s)", dataId, amp.getRawDataBBox(), detsec)
        if biassec and amp.getRawHorizontalOverscanBBox() != biassec:
            logCmd("BIASSEC doesn't match for %s (%s != %s)",
                   dataId, amp.getRawHorizontalOverscanBBox(), detsec)

    ampDict = {}
    for amp, ampExp in zip(ccd, ampExps):
        ampDict[amp.getName()] = ampExp

    exposure = assembleTask.assembleCcd(ampDict)

    md = componentInfo['raw_hdu'].obj
    fix_header(md)  # No mapper so cannot specify the translator class
    exposure.setMetadata(md)
    visitInfo = LsstCamMakeRawVisitInfo(logger)(md)
    exposure.getInfo().setVisitInfo(visitInfo)

    boresight = visitInfo.getBoresightRaDec()
    rotangle = visitInfo.getBoresightRotAngle()

    if boresight.isFinite():
        exposure.setWcs(getWcsFromDetector(exposure.getDetector(), boresight,
                                           90*geom.degrees - rotangle))
    else:
        # Should only warn for science observations but VisitInfo does not know
        logger.warn("Unable to set WCS for %s from header as RA/Dec/Angle are unavailable" %
                    (dataId,))

    return exposure
Esempio n. 11
0
def subtractXTalk(mi, coeffs, minPixelToMask=45000, crosstalkStr="CROSSTALK"):
    """Subtract the crosstalk from MaskedImage mi given a set of coefficients

    The pixels affected by signal over minPixelToMask have the crosstalkStr
    bit set
    """
    sctrl = afwMath.StatisticsControl()
    sctrl.setAndMask(mi.getMask().getPlaneBitMask("BAD"))
    bkgd = afwMath.makeStatistics(mi, afwMath.MEDIAN, sctrl).getValue()
    #
    # These are the pixels that are bright enough to cause crosstalk (more
    # precisely, the ones that we label as causing crosstalk; in reality all
    # pixels cause crosstalk)
    #
    tempStr = "TEMP"                    # mask plane used to record the bright pixels that we need to mask
    msk = mi.getMask()
    msk.addMaskPlane(tempStr)
    try:
        fs = afwDetect.FootprintSet(mi, afwDetect.Threshold(minPixelToMask), tempStr)

        mi.getMask().addMaskPlane(crosstalkStr)
        afwDisplay.getDisplay().setMaskPlaneColor(crosstalkStr, afwDisplay.MAGENTA)
        # the crosstalkStr bit will now be set whenever we subtract crosstalk
        fs.setMask(mi.getMask(), crosstalkStr)
        crosstalk = mi.getMask().getPlaneBitMask(crosstalkStr)

        width, height = mi.getDimensions()
        for i in range(nAmp):
            bbox = geom.BoxI(geom.PointI(i*(width//nAmp), 0), geom.ExtentI(width//nAmp, height))
            ampI = mi.Factory(mi, bbox)
            for j in range(nAmp):
                if i == j:
                    continue

                bbox = geom.BoxI(geom.PointI(j*(width//nAmp), 0), geom.ExtentI(width//nAmp, height))
                if (i + j)%2 == 1:
                    ampJ = afwMath.flipImage(mi.Factory(mi, bbox), True, False)  # no need for a deep copy
                else:
                    ampJ = mi.Factory(mi, bbox, afwImage.LOCAL, True)

                msk = ampJ.getMask()
                if np.all(msk.getArray() & msk.getPlaneBitMask("BAD")):
                    # Bad amplifier; ignore it completely --- its effect will
                    # come out in the bias
                    continue
                msk &= crosstalk

                ampJ -= bkgd
                ampJ *= coeffs[j][i]

                ampI -= ampJ
        #
        # Clear the crosstalkStr bit in the original bright pixels, where
        # tempStr is set
        #
        msk = mi.getMask()
        temp = msk.getPlaneBitMask(tempStr)
        xtalk_temp = crosstalk | temp
        np_msk = msk.getArray()
        mask_indicies = np.where(np.bitwise_and(np_msk, xtalk_temp) == xtalk_temp)
        np_msk[mask_indicies] &= getattr(np, np_msk.dtype.name)(~crosstalk)

    finally:
        msk.removeAndClearMaskPlane(tempStr, True)  # added in afw #1853
Esempio n. 12
0
def makeBBoxFromList(ylist):
    """Given a list [(x0, y0), (xsize, ysize)], probably from a yaml file,
    return a BoxI
    """
    (x0, y0), (xsize, ysize) = ylist
    return geom.BoxI(geom.PointI(x0, y0), geom.ExtentI(xsize, ysize))
Esempio n. 13
0
def makeAmplifierList(ccd):
    """Construct a list of AmplifierBuilder objects
    """
    # Much of this will need to be filled in when we know it.
    assert len(ccd) > 0
    amp = list(ccd['amplifiers'].values())[0]

    rawBBox = makeBBoxFromList(amp['rawBBox'])  # total in file
    xRawExtent, yRawExtent = rawBBox.getDimensions()

    readCorners = {"LL": ReadoutCorner.LL,
                   "LR": ReadoutCorner.LR,
                   "UL": ReadoutCorner.UL,
                   "UR": ReadoutCorner.UR}

    amplifierList = []
    for name, amp in sorted(ccd['amplifiers'].items(), key=lambda x: x[1]['hdu']):
        amplifier = Amplifier.Builder()
        amplifier.setName(name)

        ix, iy = amp['ixy']
        perAmpData = amp['perAmpData']
        if perAmpData:
            x0, y0 = 0, 0           # origin of data within each amp image
        else:
            x0, y0 = ix*xRawExtent, iy*yRawExtent

        rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])  # Photosensitive area
        xDataExtent, yDataExtent = rawDataBBox.getDimensions()
        amplifier.setBBox(geom.BoxI(
            geom.PointI(ix*xDataExtent, iy*yDataExtent), rawDataBBox.getDimensions()))

        rawBBox = makeBBoxFromList(amp['rawBBox'])
        rawBBox.shift(geom.ExtentI(x0, y0))
        amplifier.setRawBBox(rawBBox)

        rawDataBBox = makeBBoxFromList(amp['rawDataBBox'])
        rawDataBBox.shift(geom.ExtentI(x0, y0))
        amplifier.setRawDataBBox(rawDataBBox)

        rawSerialOverscanBBox = makeBBoxFromList(amp['rawSerialOverscanBBox'])
        rawSerialOverscanBBox.shift(geom.ExtentI(x0, y0))
        amplifier.setRawHorizontalOverscanBBox(rawSerialOverscanBBox)

        rawParallelOverscanBBox = makeBBoxFromList(amp['rawParallelOverscanBBox'])
        rawParallelOverscanBBox.shift(geom.ExtentI(x0, y0))
        amplifier.setRawVerticalOverscanBBox(rawParallelOverscanBBox)

        rawSerialPrescanBBox = makeBBoxFromList(amp['rawSerialPrescanBBox'])
        rawSerialPrescanBBox.shift(geom.ExtentI(x0, y0))
        amplifier.setRawPrescanBBox(rawSerialPrescanBBox)

        if perAmpData:
            amplifier.setRawXYOffset(geom.Extent2I(ix*xRawExtent, iy*yRawExtent))
        else:
            amplifier.setRawXYOffset(geom.Extent2I(0, 0))

        amplifier.setReadoutCorner(readCorners[amp['readCorner']])
        amplifier.setGain(amp['gain'])
        amplifier.setReadNoise(amp['readNoise'])
        amplifier.setSaturation(amp['saturation'])
        amplifier.setSuspectLevel(amp.get('suspect', np.nan))

        # flip data when assembling if needs be (e.g. data from the serial at the top of a CCD)
        flipX, flipY = amp.get("flipXY")

        amplifier.setRawFlipX(flipX)
        amplifier.setRawFlipY(flipY)
        # linearity placeholder stuff
        amplifier.setLinearityCoeffs([float(val) for val in amp['linearityCoeffs']])
        amplifier.setLinearityType(amp['linearityType'])
        amplifier.setLinearityThreshold(float(amp['linearityThreshold']))
        amplifier.setLinearityMaximum(float(amp['linearityMax']))
        amplifier.setLinearityUnits("DN")
        amplifierList.append(amplifier)
    return amplifierList