{"action":"created","comment":{"url":"https://api.github.com/repos/lsst/afw/pulls/comments/200693011","pull_request_review_id":135063578,"id":200693011,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwMDY5MzAxMQ==","diff_hunk":"@@ -0,0 +1,1093 @@\n+# This file is part of afw.\n+#\n+# Developed for the LSST Data Management System.\n+# This product includes software developed by the LSST Project\n+# (http://www.lsst.org).\n+# See the COPYRIGHT file at the top-level directory of this distribution\n+# for details of code ownership.\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n+\n+__all__ = [\"MultibandPixel\", \"MultibandImage\", \"MultibandMask\", \"MultibandMaskedImage\", \"MultibandExposure\"]\n+\n+import numpy as np\n+\n+from lsst.geom import Point2I, Box2I, Extent2I\n+from . import ImageF, MaskedImageF, Mask, ExposureF\n+from . import maskedImage as afwMaskedImage\n+from lsst.afw.multiband import MultibandBase\n+\n+\n+class MultibandPixel(MultibandBase):\n+    \"\"\"Multiband Pixel class\n+\n+    This represent acts as a container for a single pixel\n+    (scaler) in multiple bands.\n+    There are a few methods from `MultibandBase` that are overloaded,\n+    since a `MultibandPixel` has a `Point2I` for it's boudning box\n+    as opposed to a `Box2I`.\n+    \"\"\"\n+    def __init__(self, filters, singles, bbox):\n+        \"\"\"Initialize a `MultibandPixel` object\n+\n+        Parameters\n+        ----------\n+        singles: list or `MultibandPixel`\n+            Either a list of single band objects or\n+            an instance of a `MultibandPixel`\n+        filters: list\n+            List of filter names. If `singles` is an `OrderedDict` or\n+            a `MultibandPixel` then this arguement is ignored,\n+            otherwise it is required.\n+        bbox: Point2I\n+            Location of the pixel in the parent image.\n+            Unlike other objects that inherit from `MultibandBase`,\n+            `MultibandPixel` objects don't have a full `Box2I`\n+            bounding box, since they only contain a single pixel,\n+            so the bounding box cannot be inherited from the\n+            list of `singles`.\n+        \"\"\"\n+        if any([arg is None for arg in [singles, filters, bbox]]):\n+            err = \"Expected an array of `singles`, a list of `filters, and a `bbox`\"\n+            raise NotImplementedError(err)\n+\n+        self._singles = np.array(singles)\n+        self._filters = tuple(filters)\n+        self._bbox = bbox\n+        self._singleType = self.singles.dtype\n+\n+        # Make sure that the bounding box has been setup properly\n+        if not isinstance(self.getBBox(), Point2I):\n+            err = (\"Something went wrong, the bounding box for a `MultibandPixel` \"\n+                   \"should always be a `Point2I`, received {0}\")\n+            raise RuntimeError(err.format(self.getBBox()))\n+\n+    def _getArray(self):\n+        \"\"\"Data cube array in multiple bands\n+\n+        Since `self._singles` is just a 1D array,\n+        `array` just returns `self._singles`.\n+        \"\"\"\n+        return self.singles\n+\n+    def _setArray(self, value):\n+        assert value.shape == self.array.shape\n+        self._singles = value\n+\n+    array = property(_getArray, _setArray)\n+\n+    def copy(self, deep=False):\n+        \"\"\"Make a copy of the current instance\n+\n+        `MultibandPixel._singles` is an array,\n+        so this just makes a copy of the array\n+        (as opposed to a view of the parent array).\n+        \"\"\"\n+        if deep:\n+            filters = tuple([f for f in self.filters])\n+            singles = np.copy(self.singles)\n+            # For MultibandPixels, `bbox` is a `Point2I`\n+            coords = self.getBBox()\n+            bbox = Point2I(coords.getX(), coords.getY())\n+        else:\n+            filters = self.filters\n+            singles = self.singles\n+            bbox = self.getBBox()\n+        return MultibandPixel(filters=filters, singles=singles, bbox=bbox)\n+\n+    def __getitem__(self, indices):\n+        \"\"\"Get a slice of the underlying array\n+\n+        Since a `MultibandPixel` is a scalar in the\n+        spatial dimensions, it can only be indexed with\n+        a filter name, number, or slice.\n+        \"\"\"\n+        if isinstance(indices, tuple):\n+            err = (\"Too many indices: \"\n+                   \"`MultibandPixel has no spatial dimensions and \"\n+                   \"only accepts a filterIndex\")\n+            raise IndexError(err)\n+        # Make the index into a valid numpy index or slice\n+        filters, filterIndex = self.filterToIndex(indices)\n+        if len(filters) == 1 and not isinstance(filterIndex, slice):\n+            # The user only requested a pixel in a single band, so return it\n+            return self.array[filterIndex[0]]\n+\n+        result = self.copy()\n+        result._filters = filters\n+        result._singles = self._singles[filterIndex]\n+        # No need to update the bounding box, since pixels can only be sliced in the filter dimension\n+        return result\n+\n+    def setBBox(self, bbox):\n+        \"\"\"Overload `MultibandBase.setBBox`\n+\n+        Since the \"single band objects\" are elements\n+        of an array and do not have a bounding box,\n+        overloading `setBBox` is needed to prevent the\n+        code from attempting to set the bounding box of\n+        each item in `singles`.\n+\n+        Parameters\n+        ----------\n+        bbox: Point2I\n+            Pixel location in the parent image\n+        \"\"\"\n+        if not isinstance(bbox, Point2I):\n+            err = \"The bounding box for a `MultibandPixel` should always be a `Point2I`, received {0}\"\n+            raise ValueError(err.format(bbox))\n+        self._bbox = bbox\n+\n+\n+class MultibandImage(MultibandBase):\n+    \"\"\"Multiband Image class\n+\n+    This class acts as a container for multiple `afw.Image` objects.\n+    All images must be contained in the same bounding box,\n+    and have the same data type.\n+    \"\"\"\n+    def __init__(self, filters, singles=None, array=None, bbox=None,\n+                 singleType=ImageF, filterKwargs=None, **kwargs):\n+        \"\"\"Initialize a `MultibandBase` object\n+\n+        Parameters\n+        ----------\n+        singles: list\n+            A list of single band objects.\n+            If `array` is not `None`, then `singles` is ignored\n+        filters: list\n+            List of filter names. If `singles` is an `OrderedDict`\n+            then this argument is ignored, otherwise it is required.\n+        array: 3D numpy array\n+            Array (filters, y, x) of multiband data.\n+            If this is used to initialize a `MultibandImage`,\n+            either `bbox` or `singles` is also required.\n+        singleType: class\n+            Class of the single band objects.\n+            This is ignored unless `singles` and `array`\n+            are both `None`, in which case it is required.\n+        bbox: `Box2I`\n+            Location of the array in the parent image.\n+            This argument is ignored if `singles` is not `None`.\n+        filterKwargs: dict\n+            Keyword arguments to pass to `_fullInitialize` to\n+            initialize a new instance of an inherited class\n+            that are different for each filter.\n+            The keys are the names of the arguments and the values\n+            should also be dictionaries, with filter names as keys\n+            and the value of the argument for a given filter as values.\n+        kwargs: dict\n+            Keyword arguments to pass to `_fullInitialize` to\n+            initialize a new instance of an inherited class that are the\n+            same in all bands.\n+        \"\"\"\n+        # Create the single band objects from the array\n+        if array is not None and filters is not None:\n+            if len(array) != len(filters):\n+                raise ValueError(\"`array` and `filters` must have the same length\")\n+            self._array = array\n+            self._filters = filters\n+            if singles is None:\n+                if bbox is None:\n+                    bbox = Box2I(Point2I(0, 0), Extent2I(array[0].shape[1], array[0].shape[0]))\n+                self._bbox = bbox\n+                self._updateSingles(singleType)\n+            else:\n+                if len(singles) != len(array):\n+                    raise ValueError(\"`array` should have the same length as `singles`\")\n+                if not np.all([single.array.shape == array.shape[1:] for single in singles]):\n+                    err = (\"`array` and each item in `singles` must have the same spatial dimension, \"\n+                           \"received dimensions {0} for array with dimension {1}\")\n+                    raise ValueError(err.format([single.array.shape for single in singles], array.shape))\n+                self._singles = tuple(singles)\n+                self._bbox = singles[0].getBBox()\n+        else:\n+            # Extract the single band objects and filters\n+            if singles is None and filters is not None:\n+                # Attempt to load a set of images\n+                self._singleType = singleType\n+                singles = []\n+                for f in filters:\n+                    if filterKwargs is not None:\n+                        for key, value in filterKwargs:\n+                            kwargs[key] = value[f]\n+                    singles.append(singleType(**kwargs))\n+            if singles is not None:\n+                super().__init__(filters, singles)\n+            else:\n+                err = (\"Either a list of `singles` and `filters`; \"\n+                       \"or an `array`, `filters`, `bbox`, and `singleType`; \"\n+                       \"or a set of `kwargs` is required to initialize a `MultibandImage`.\")\n+                raise NotImplementedError(err)\n+\n+            self._array = np.array([image.array for image in self.singles])\n+            self._updateSingles(type(self.singles[0]))\n+\n+        # Make sure that all of the parameters have been setup appropriately\n+        if not isinstance(self._bbox, Box2I):\n+            raise RuntimeError(\"Something went wrong, `self._bbox` should be a `Box2I`\")\n+        if not np.all([single.getBBox() == self.getBBox() for single in self.singles]):\n+            raise ValueError(\"Single band images did not all have the same bounding box\")\n+\n+    def _getArray(self):\n+        \"\"\"Data cube array in multiple bands\n+        \"\"\"\n+        return self._array\n+\n+    def _setArray(self, value):\n+        self.array[:] = value\n+\n+    array = property(_getArray, _setArray)\n+\n+    def _updateSingles(self, singleType):\n+        \"\"\"Update the Image<X> in each band\n+\n+        This method is called when a `MultibandImage` is initialized.\n+        \"\"\"\n+        self._singleType = singleType\n+        self._singles = self._arrayToSingles(self.array, singleType, self.getXY0())\n+        if not len(self.filters) == len(self.singles):\n+            raise RuntimeError(\"The length of `filters` and `array` should be the same\")\n+\n+    def _arrayToSingles(self, array, singleType, xy0):\n+        \"\"\"Create a list of `Image<X>` objects from a 3D array\n+        \"\"\"\n+        return tuple([singleType(array=array[n], xy0=xy0) for n in range(len(array))])\n+\n+    def copy(self, deep=False):\n+        \"\"\"Copy the current object\n+\n+        Parameters\n+        ----------\n+        deep: bool\n+            Whether or not to make a deep copy\n+        \"\"\"\n+        if deep:\n+            filters = tuple([f for f in self.filters])\n+            array = np.copy(self.array)\n+            bbox = Box2I(self.getBBox())\n+            result = type(self)(filters=filters, array=array, bbox=bbox, singleType=self.singleType)\n+        else:\n+            result = type(self)(self.filters, self.singles, array=self.array)\n+        return result\n+\n+    def _slice(self, filters, filterIndex, indices):\n+        \"\"\"Slice the current object and return the result\n+\n+        See `Multiband._slice` for a list of the parameters.\n+        \"\"\"\n+        if len(indices) > 0:\n+            allSlices = [filterIndex, slice(None), slice(None)]\n+            sy, sx = self.imageIndicesToNumpy(indices)\n+            if sy is not None:\n+                allSlices[-2] = sy\n+            if sx is not None:\n+                allSlices[-1] = sx\n+            array = self._array[allSlices]\n+\n+            # Return a scalar or MultibandPixel\n+            # if the image indices are integers\n+            if np.issubdtype(type(sy), np.integer) and np.issubdtype(type(sx), np.integer):\n+                if not isinstance(filterIndex, slice) and len(filterIndex) == 1:\n+                    return array[0]\n+                result = MultibandPixel(\n+                    singles=array,\n+                    filters=filters,\n+                    bbox=Point2I(sx + self.x0, sy + self.y0)\n+                )\n+                return result\n+            # Set the bbox size based on the slices\n+            bbox = self.getBBoxFromIndices(allSlices[1:])\n+            singles = self._arrayToSingles(array, self.singleType, bbox.getMin())\n+            result = type(self)(filters, singles, array=array)\n+        else:\n+            result = type(self)(filters=filters, array=self._array[filterIndex], bbox=self.getBBox())\n+\n+        # Check that the image and array shapes agree\n+        imageShape = (\n+            len(result.filters),\n+            result.getBBox().getHeight(),\n+            result.getBBox().getWidth()\n+        )\n+        if result.array.shape != imageShape:\n+            err = (\"Something went wrong with the internal slicing mechanism, \"\n+                   \"the array shape {0} != the image shape {1}.\")\n+            raise RuntimeError(err.format(result.array.shape, imageShape))\n+        return result\n+\n+\n+class MultibandMask(MultibandImage):\n+    def __init__(self, filters, singles=None, array=None, bbox=None,\n+                 singleType=Mask, filterKwargs=None, **kwargs):\n+        \"\"\"Initialize a `MultibandMask` object\n+\n+        See `MultibandImage` for a description of the parameters..\n+        \"\"\"\n+        super().__init__(filters, singles, array, bbox, singleType, filterKwargs, **kwargs)\n+        # Set Mask specific properties\n+        refMask = self._singles[0]\n+        self._maskPlaneDict = refMask.getMaskPlaneDict()\n+        assert np.all([refMask.getMaskPlaneDict() == m.getMaskPlaneDict() for m in self.singles])\n+        self._getNumPlanesMax = refMask.getNumPlanesMax()\n+        assert np.all([refMask.getNumPlanesMax() == m.getNumPlanesMax() for m in self.singles])\n+        self._getNumPlanesUsed = refMask.getNumPlanesUsed()\n+        assert np.all([refMask.getNumPlanesUsed() == m.getNumPlanesUsed() for m in self.singles])\n+\n+    def getMaskPlane(self, key):\n+        return self.getMaskPlaneDict()[key]\n+\n+    def getNumPlanesMax(self):\n+        \"\"\"Maximum number of mask planes available\n+\n+        This is required to be the same for all of the single\n+        band `Mask` objects.\n+        \"\"\"\n+        return self._getNumPlanesMax\n+\n+    def getNumPlanesUsed(self):\n+        \"\"\"Number of mask planes used\n+\n+        This is required to be the same for all of the single\n+        band `Mask` objects.\n+        \"\"\"\n+        return self._getNumPlanesUsed\n+\n+    def getMaskPlaneDict(self):\n+        \"\"\"Dictionary of Mask Plane bit values\n+        \"\"\"\n+        return self._maskPlaneDict\n+\n+    def clearMaskPlaneDict(self):\n+        \"\"\"Reset the mask plane dictionary\n+        \"\"\"\n+        mask = self._singles[0]\n+        mask.clearMaskPlaneDict()\n+        self._maskPlaneDict = mask.getMaskPlaneDict()\n+\n+    def addMaskPlane(self, name):\n+        \"\"\"Add a mask to the mask plane\n+\n+        Parameters\n+        ----------\n+        name: str\n+            Name of the new mask plane\n+        \"\"\"\n+        mask = self._singles[0]\n+        idx = mask.addMaskPlane(name)\n+        self._maskPlaneDict = mask.getMaskPlaneDict()\n+        return idx\n+\n+    def removeMaskPlane(self, name):\n+        \"\"\"Remove a mask from the mask plane\n+\n+        Parameters\n+        ----------\n+        name: str\n+            Name of the mask plane to remove\n+        \"\"\"\n+        mask = self._singles[0]\n+        mask.removeMaskPlane(name)\n+        self._maskPlaneDict = mask.getMaskPlaneDict()\n+\n+    def clearAllMaskPlanes(self):\n+        \"\"\"Clear all the pixels\n+        \"\"\"\n+        mask = self._singles[0]\n+        mask.clearAllMaskPlanes()\n+\n+    def _getOtherMasks(self, others):\n+        \"\"\"Check if two masks can be combined\n+\n+        This method checks that `self` and `others`\n+        have the same number of bands, or if\n+        others is a single value, creates a list\n+        to use for updating all of the `singles`.\n+        \"\"\"\n+        if isinstance(others, MultibandMask):\n+            if len(self.singles) != len(others.singles) or self.filters != others.filters:\n+                msg = \"Both `MultibandMask` objects must have the same number of bands to combine\"\n+                raise ValueError(msg)\n+            result = [s for s in others.singles]\n+        else:\n+            result = [others]*len(self.singles)\n+        return result\n+\n+    def __ior__(self, others):\n+        _others = self._getOtherMasks(others)\n+        singles = list(self.singles)\n+        for n in range(len(self)):\n+            singles[n] |= _others[n]\n+        self._singles = tuple(singles)\n+        return self\n+\n+    def __iand__(self, others):\n+        _others = self._getOtherMasks(others)\n+        singles = list(self.singles)\n+        for n in range(len(self)):\n+            singles[n] &= _others[n]\n+        self._singles = tuple(singles)\n+        return self\n+\n+    def __ixor__(self, others):\n+        _others = self._getOtherMasks(others)\n+        singles = list(self.singles)\n+        for n in range(len(self)):\n+            singles[n] ^= _others[n]\n+        self._singles = tuple(singles)\n+        return self\n+\n+    def __setitem__(self, args, value):\n+        \"\"\"Set a subset of the MultibandMask\n+        \"\"\"\n+        if not isinstance(args, tuple):\n+            indices = (args,)\n+        else:\n+            indices = args\n+\n+        # Return the single band object if the first\n+        # index is not a list or slice.\n+        filters, filterIndex = self.filterToIndex(indices[0])\n+        if len(indices) > 1:\n+            sy, sx = self.imageIndicesToNumpy(indices[1:])\n+        else:\n+            sy = sx = slice(None)\n+        if isinstance(value, Mask):\n+            self._array[filterIndex, sy, sx] = value.array\n+        else:\n+            self._array[filterIndex, sy, sx] = value\n+\n+    def set(self, value, filters=None, x=None, y=None):\n+        \"\"\"Set the value of the mask\n+\n+        Parameters\n+        ----------\n+        filters: str name, index, or slice\n+            Filter(s) to set to `value`.\n+            If filters is `None` then all filters are used.\n+        value: int or array\n+            Value(s) to set for the assigned filter(s)\n+        x: int\n+            Optional x-position of a pixel to set\n+        y: int\n+            Optional y-position of a pixel to set\n+        \"\"\"\n+        if (x is None) ^ (y is None):\n+            err = \"Must specify either `x` and `y` coordinate or no coordinates\"\n+            raise IndexError(err)\n+        if x is not None:\n+            if not np.issubdtype(type(y), np.integer) or not np.issubdtype(type(x), np.integer):\n+                raise IndexError(\"x and y must be integers if they are specified\")\n+            # Temporarily adjust for XY0, since Mask does not support XY0 yet\n+            x -= self.x0\n+            y -= self.y0\n+\n+        if filters is not None:\n+            filters, filterIndex = self.filterToIndex(filters)\n+        else:\n+            filterIndex = range(len(self))\n+        if isinstance(filterIndex, slice):\n+            filterIndex = np.arange(len(self))[filterIndex]\n+\n+        if hasattr(value, \"__len__\"):\n+            for f in filterIndex:\n+                if x is None:\n+                    self.singles[f].set(value[f])\n+                else:\n+                    self.singles[f].set(x, y, value[f])\n+        else:\n+            for f in filterIndex:\n+                if x is None:\n+                    self.singles[f].set(value)\n+                else:\n+                    self.singles[f].set(x, y, value)\n+\n+\n+class MultibandTripleBase(MultibandBase):\n+    \"\"\"MultibandTripleBase class\n+\n+    This is a base class inherited by multiband classes\n+    with `image`, `mask`, and `variance` objects,\n+    such as `MultibandMaskedImage` and `MultibandExposure`.\n+    \"\"\"\n+    def __init__(self, filters, singles=None, image=None, mask=None, variance=None,\n+                 singleType=MaskedImageF, filterKwargs=None, **kwargs):\n+        \"\"\"Initialize a `MultibandTripleBase` object\n+\n+        Parameters\n+        ----------\n+        singles: list or `OrderedDict`\n+            Either a list of single band objects or an `OrderedDict` with\n+            filter names as keys and single band objects as values.\n+            If no `singles` are specified then `image`, `mask`, and `variance`\n+            must be given.\n+        filters: list\n+            List of filter names. If `singles` is an `OrderedDict`\n+            then this argument is ignored, otherwise it is required.\n+        image: list\n+            List of `Image` objects that represent the image in each band.\n+            Ignored if `singles` is not `None`.\n+        mask: list\n+            List of `Mask` objects that represent the mask in each band.\n+            Ignored if `singles` is not `None`.\n+        variance: list\n+            List of `Image` objects that represent the variance in each band.\n+            Ignored if `singles` is not `None`.\n+        singleType: class\n+            Class of single band objects.\n+        filterKwargs: dict\n+            Keyword arguments to pass to `_fullInitialize` to\n+            initialize a new instance of an inherited class\n+            that are different for each filter.\n+            The keys are the names of the arguments and the values\n+            should also be dictionaries, with filter names as keys\n+            and the value of the argument for a given filter as values.\n+        kwargs: dict\n+            Keyword arguments to pass to `_fullInitialize` to\n+            initialize a new instance of an inherited class that are the\n+            same in all bands.\n+        \"\"\"\n+        if singles is not None:\n+            # Extract the single band objects and filters\n+            super().__init__(filters, singles)\n+            image = [s.image for s in self._singles]\n+            mask = [s.mask for s in self._singles]\n+            variance = [s.variance for s in self._singles]\n+            self._setMultiband(image, mask, variance, filters)\n+        elif image is not None or mask is not None or variance is not None:\n+            if image is None or mask is None or variance is None or filters is None:\n+                err = (\"`MultibandTripleBase` must be initialized with \"\n+                       \"`singles` and `filters` or `image`, `mask`, `variance`, and `filters`\")\n+                raise ValueError(err)\n+            isMultiband = [isinstance(m, MultibandBase) for m in [image, mask, variance]]\n+            if np.any(isMultiband):\n+                if not np.all(isMultiband):\n+                    err = \"`image`, `mask`, `variance` must all be either multiband or single band\"\n+                    raise ValueError(err)\n+                self._image = image\n+                self._mask = mask\n+                self._variance = variance\n+            else:\n+                self._setMultiband(image, mask, variance, filters)\n+            self._filters = tuple(filters)\n+            self._singleType = singleType\n+        elif filters is not None:\n+            # Extract the single band objects and filters\n+            self.singleType = singleType\n+            singles = []\n+            for f in self.filters:\n+                if filterKwargs is not None:\n+                    for key, value in filterKwargs:\n+                        kwargs[key] = value[f]\n+                singles.append(self.singleType(**kwargs))\n+            super().__init__(filters, singles)\n+        else:\n+            err = (\"Either a list of `singles` and `filters`; \"\n+                   \"a set of `filters`, `image`, `mask`, and `variance`; \"\n+                   \"or a set of `kwargs` is required to initialize a `MultibandImage`.\")\n+            raise NotImplementedError(err)\n+\n+        self._singles = self._buildSingles(self._image, self._mask, self._variance)\n+\n+    def _setMultiband(self, image, mask, variance, filters):\n+        \"\"\"Set image, mask, and variance to the multiband objects\n+\n+        See `MultibandTripleBase` for parameter descriptions.\n+        \"\"\"\n+        self._image = MultibandImage(filters, image)\n+        self._mask = MultibandMask(filters, mask)\n+        self._variance = MultibandImage(filters, variance)\n+\n+    def setBBox(self, bbox):\n+        \"\"\"Set the bounding box\n+\n+        This is different than `MultibandBase.setBBox`\n+        because the multiband `image`, `mask`, and `variance` objects\n+        must all have their bounding boxes updated.\n+\n+        Parameters\n+        ----------\n+        bbox: `Box2I` or tuple of indices\n+            Bounding box to set as the current bounding box.\n+            If a tuple of indices is passed, a new bounding box\n+            is created from them and used.\n+        \"\"\"\n+        super().setBBox(bbox)\n+        self.image.setBBox(bbox)\n+        self.mask.setBBox(bbox)\n+        self.variance.setBBox(bbox)\n+\n+    def copy(self, deep=False):\n+        \"\"\"Make a copy of the current instance\n+        \"\"\"\n+        if deep:\n+            filters = tuple([f for f in self.filters])\n+            singles = tuple([self.singleType(s, deep=True) for s in self.singles])\n+            result = type(self)(filters=filters, singles=singles)\n+        else:\n+            result = type(self)(image=self.image, mask=self.mask, variance=self.variance,\n+                                filters=self.filters)\n+        return result\n+\n+    def _slice(self, filters, filterIndex, indices):\n+        \"\"\"Slice the current object and return the result\n+\n+        See `Multiband._slice` for a list of the parameters.\n+        \"\"\"\n+        image = self.image._slice(filters, filterIndex, indices)\n+        mask = self._mask._slice(filters, filterIndex, indices)\n+        variance = self._variance._slice(filters, filterIndex, indices)\n+\n+        result = type(self)(filters=filters, image=image, mask=mask, variance=variance)\n+        assert np.all([r.getBBox() == result._bbox for r in [result._mask, result._variance]])\n+        return result\n+\n+    def _verifyUpdate(self, image=None, mask=None, variance=None):\n+        \"\"\"Check that the new image, mask, or variance is valid\n+\n+        This basically means checking that the update to the\n+        property matches the current bounding box and inherits\n+        from the `MultibandBase` class.\n+        \"\"\"\n+        for prop in [image, mask, variance]:\n+            if prop is not None:\n+                if prop.getBBox() != self.getBBox():\n+                    raise ValueError(\"Bounding box does not match the current class\")\n+                if not isinstance(prop, MultibandBase):\n+                    err = \"image, mask, and variance should all inherit from the MultibandBase class\"\n+                    raise ValueError(err)\n+\n+    def getImage(self):\n+        \"\"\"Get the image\n+        \"\"\"\n+        return self._image\n+\n+    def setImage(self, image):\n+        \"\"\"Set the image\n+        \"\"\"\n+        self._verifyUpdate(image=image)\n+        self._image = image\n+        self._singles = self._buildSingles(image=image)","path":"python/lsst/afw/image/multiband.py","position":680,"original_position":680,"commit_id":"9623fc7461c14e99a159671271083282d9f14086","original_commit_id":"9623fc7461c14e99a159671271083282d9f14086","user":{"login":"TallJimbo","id":1330720,"node_id":"MDQ6VXNlcjEzMzA3MjA=","avatar_url":"https://avatars0.githubusercontent.com/u/1330720?v=4","gravatar_id":"","url":"https://api.github.com/users/TallJimbo","html_url":"https://github.com/TallJimbo","followers_url":"https://api.github.com/users/TallJimbo/followers","following_url":"https://api.github.com/users/TallJimbo/following{/other_user}","gists_url":"https://api.github.com/users/TallJimbo/gists{/gist_id}","starred_url":"https://api.github.com/users/TallJimbo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TallJimbo/subscriptions","organizations_url":"https://api.github.com/users/TallJimbo/orgs","repos_url":"https://api.github.com/users/TallJimbo/repos","events_url":"https://api.github.com/users/TallJimbo/events{/privacy}","received_events_url":"https://api.github.com/users/TallJimbo/received_events","type":"User","site_admin":false},"body":"I like the idea of making them properties, but the question of what a setter should do here (copy pixel values?  reset the \"pointer\" to the given object, even if that messes up the 3-d data cube?) is sufficiently non-obvious (and the need for a setter is sufficiently unproven) that I'm not sure we should allow them to be changed.","created_at":"2018-07-06T15:42:27Z","updated_at":"2018-07-06T15:42:27Z","html_url":"https://github.com/lsst/afw/pull/353#discussion_r200693011","pull_request_url":"https://api.github.com/repos/lsst/afw/pulls/353","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/lsst/afw/pulls/comments/200693011"},"html":{"href":"https://github.com/lsst/afw/pull/353#discussion_r200693011"},"pull_request":{"href":"https://api.github.com/repos/lsst/afw/pulls/353"}},"in_reply_to_id":199292939},"pull_request":{"url":"https://api.github.com/repos/lsst/afw/pulls/353","id":187214277,"node_id":"MDExOlB1bGxSZXF1ZXN0MTg3MjE0Mjc3","html_url":"https://github.com/lsst/afw/pull/353","diff_url":"https://github.com/lsst/afw/pull/353.diff","patch_url":"https://github.com/lsst/afw/pull/353.patch","issue_url":"https://api.github.com/repos/lsst/afw/issues/353","number":353,"state":"open","locked":false,"title":"tickets/DM-13911: Create multiband classes","user":{"login":"fred3m","id":2251900,"node_id":"MDQ6VXNlcjIyNTE5MDA=","avatar_url":"https://avatars1.githubusercontent.com/u/2251900?v=4","gravatar_id":"","url":"https://api.github.com/users/fred3m","html_url":"https://github.com/fred3m","followers_url":"https://api.github.com/users/fred3m/followers","following_url":"https://api.github.com/users/fred3m/following{/other_user}","gists_url":"https://api.github.com/users/fred3m/gists{/gist_id}","starred_url":"https://api.github.com/users/fred3m/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fred3m/subscriptions","organizations_url":"https://api.github.com/users/fred3m/orgs","repos_url":"https://api.github.com/users/fred3m/repos","events_url":"https://api.github.com/users/fred3m/events{/privacy}","received_events_url":"https://api.github.com/users/fred3m/received_events","type":"User","site_admin":false},"body":"","created_at":"2018-05-10T14:48:58Z","updated_at":"2018-07-06T15:42:27Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e2bcd2680a130476f385771c80c322a0a584012b","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/lsst/afw/pulls/353/commits","review_comments_url":"https://api.github.com/repos/lsst/afw/pulls/353/comments","review_comment_url":"https://api.github.com/repos/lsst/afw/pulls/comments{/number}","comments_url":"https://api.github.com/repos/lsst/afw/issues/353/comments","statuses_url":"https://api.github.com/repos/lsst/afw/statuses/9623fc7461c14e99a159671271083282d9f14086","head":{"label":"lsst:tickets/DM-13911","ref":"tickets/DM-13911","sha":"9623fc7461c14e99a159671271083282d9f14086","user":{"login":"lsst","id":1134504,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMzQ1MDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1134504?v=4","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"repo":{"id":23013175,"node_id":"MDEwOlJlcG9zaXRvcnkyMzAxMzE3NQ==","name":"afw","full_name":"lsst/afw","owner":{"login":"lsst","id":1134504,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMzQ1MDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1134504?v=4","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lsst/afw","description":"LSST data management: astronomical framework, including images and tables","fork":false,"url":"https://api.github.com/repos/lsst/afw","forks_url":"https://api.github.com/repos/lsst/afw/forks","keys_url":"https://api.github.com/repos/lsst/afw/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lsst/afw/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lsst/afw/teams","hooks_url":"https://api.github.com/repos/lsst/afw/hooks","issue_events_url":"https://api.github.com/repos/lsst/afw/issues/events{/number}","events_url":"https://api.github.com/repos/lsst/afw/events","assignees_url":"https://api.github.com/repos/lsst/afw/assignees{/user}","branches_url":"https://api.github.com/repos/lsst/afw/branches{/branch}","tags_url":"https://api.github.com/repos/lsst/afw/tags","blobs_url":"https://api.github.com/repos/lsst/afw/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lsst/afw/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lsst/afw/git/refs{/sha}","trees_url":"https://api.github.com/repos/lsst/afw/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lsst/afw/statuses/{sha}","languages_url":"https://api.github.com/repos/lsst/afw/languages","stargazers_url":"https://api.github.com/repos/lsst/afw/stargazers","contributors_url":"https://api.github.com/repos/lsst/afw/contributors","subscribers_url":"https://api.github.com/repos/lsst/afw/subscribers","subscription_url":"https://api.github.com/repos/lsst/afw/subscription","commits_url":"https://api.github.com/repos/lsst/afw/commits{/sha}","git_commits_url":"https://api.github.com/repos/lsst/afw/git/commits{/sha}","comments_url":"https://api.github.com/repos/lsst/afw/comments{/number}","issue_comment_url":"https://api.github.com/repos/lsst/afw/issues/comments{/number}","contents_url":"https://api.github.com/repos/lsst/afw/contents/{+path}","compare_url":"https://api.github.com/repos/lsst/afw/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lsst/afw/merges","archive_url":"https://api.github.com/repos/lsst/afw/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lsst/afw/downloads","issues_url":"https://api.github.com/repos/lsst/afw/issues{/number}","pulls_url":"https://api.github.com/repos/lsst/afw/pulls{/number}","milestones_url":"https://api.github.com/repos/lsst/afw/milestones{/number}","notifications_url":"https://api.github.com/repos/lsst/afw/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lsst/afw/labels{/name}","releases_url":"https://api.github.com/repos/lsst/afw/releases{/id}","deployments_url":"https://api.github.com/repos/lsst/afw/deployments","created_at":"2014-08-16T07:37:40Z","updated_at":"2018-07-06T12:03:07Z","pushed_at":"2018-07-06T12:03:02Z","git_url":"git://github.com/lsst/afw.git","ssh_url":"git@github.com:lsst/afw.git","clone_url":"https://github.com/lsst/afw.git","svn_url":"https://github.com/lsst/afw","homepage":"http://dm.lsst.org/","size":44799,"stargazers_count":10,"watchers_count":10,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":11,"mirror_url":null,"archived":false,"open_issues_count":10,"license":null,"forks":11,"open_issues":10,"watchers":10,"default_branch":"master"}},"base":{"label":"lsst:master","ref":"master","sha":"dd706d521f5a4b1ff378ac81e56f48f806b3eafb","user":{"login":"lsst","id":1134504,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMzQ1MDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1134504?v=4","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"repo":{"id":23013175,"node_id":"MDEwOlJlcG9zaXRvcnkyMzAxMzE3NQ==","name":"afw","full_name":"lsst/afw","owner":{"login":"lsst","id":1134504,"node_id":"MDEyOk9yZ2FuaXphdGlvbjExMzQ1MDQ=","avatar_url":"https://avatars1.githubusercontent.com/u/1134504?v=4","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lsst/afw","description":"LSST data management: astronomical framework, including images and tables","fork":false,"url":"https://api.github.com/repos/lsst/afw","forks_url":"https://api.github.com/repos/lsst/afw/forks","keys_url":"https://api.github.com/repos/lsst/afw/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lsst/afw/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lsst/afw/teams","hooks_url":"https://api.github.com/repos/lsst/afw/hooks","issue_events_url":"https://api.github.com/repos/lsst/afw/issues/events{/number}","events_url":"https://api.github.com/repos/lsst/afw/events","assignees_url":"https://api.github.com/repos/lsst/afw/assignees{/user}","branches_url":"https://api.github.com/repos/lsst/afw/branches{/branch}","tags_url":"https://api.github.com/repos/lsst/afw/tags","blobs_url":"https://api.github.com/repos/lsst/afw/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lsst/afw/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lsst/afw/git/refs{/sha}","trees_url":"https://api.github.com/repos/lsst/afw/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lsst/afw/statuses/{sha}","languages_url":"https://api.github.com/repos/lsst/afw/languages","stargazers_url":"https://api.github.com/repos/lsst/afw/stargazers","contributors_url":"https://api.github.com/repos/lsst/afw/contributors","subscribers_url":"https://api.github.com/repos/lsst/afw/subscribers","subscription_url":"https://api.github.com/repos/lsst/afw/subscription","commits_url":"https://api.github.com/repos/lsst/afw/commits{/sha}","git_commits_url":"https://api.github.com/repos/lsst/afw/git/commits{/sha}","comments_url":"https://api.github.com/repos/lsst/afw/comments{/number}","issue_comment_url":"https://api.github.com/repos/lsst/afw/issues/comments{/number}","contents_url":"https://api.github.com/repos/lsst/afw/contents/{+path}","compare_url":"https://api.github.com/repos/lsst/afw/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lsst/afw/merges","archive_url":"https://api.github.com/repos/lsst/afw/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lsst/afw/downloads","issues_url":"https://api.github.com/repos/lsst/afw/issues{/number}","pulls_url":"https://api.github.com/repos/lsst/afw/pulls{/number}","milestones_url":"https://api.github.com/repos/lsst/afw/milestones{/number}","notifications_url":"https://api.github.com/repos/lsst/afw/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lsst/afw/labels{/name}","releases_url":"https://api.github.com/repos/lsst/afw/releases{/id}","deployments_url":"https://api.github.com/repos/lsst/afw/deployments","created_at":"2014-08-16T07:37:40Z","updated_at":"2018-07-06T12:03:07Z","pushed_at":"2018-07-06T12:03:02Z","git_url":"git://github.com/lsst/afw.git","ssh_url":"git@github.com:lsst/afw.git","clone_url":"https://github.com/lsst/afw.git","svn_url":"https://github.com/lsst/afw","homepage":"http://dm.lsst.org/","size":44799,"stargazers_count":10,"watchers_count":10,"language":"C++","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":11,"mirror_url":null,"archived":false,"open_issues_count":10,"license":null,"forks":11,"open_issues":10,"watchers":10,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/lsst/afw/pulls/353"},"html":{"href":"https://github.com/lsst/afw/pull/353"},"issue":{"href":"https://api.github.com/repos/lsst/afw/issues/353"},"comments":{"href":"https://api.github.com/repos/lsst/afw/issues/353/comments"},"review_comments":{"href":"https://api.github.com/repos/lsst/afw/pulls/353/comments"},"review_comment":{"href":"https://api.github.com/repos/lsst/afw/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/lsst/afw/pulls/353/commits"},"statuses":{"href":"https://api.github.com/repos/lsst/afw/statuses/9623fc7461c14e99a159671271083282d9f14086"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"TallJimbo"}}