Example #1
0
def _xmatch_mapper(qresult, tabname_to, radius, tabname_xm, n_neighbors):
    """
	    Mapper:
	    	- given all objects in a cell, make an ANN tree
	    	- load all objects in tabname_to (including neighbors), make an ANN tree, find matches
	    	- store the output into an index table
	"""
    from scikits.ann import kdtree

    db = qresult.db
    pix = qresult.pix
    table_xm = db.table(tabname_xm)

    for rows in qresult:
        cell_id = rows.info.cell_id

        join = ColGroup(
            dtype=[('_M1', 'u8'), ('_M2',
                                   'u8'), ('_DIST',
                                           'f4'), ('_NR',
                                                   'u1'), ('_LON',
                                                           'f8'), ('_LAT',
                                                                   'f8')])

        (id1, ra1, dec1) = rows.as_columns()
        (id2, ra2,
         dec2) = db.query('_ID, _LON, _LAT FROM %s' % tabname_to).fetch_cell(
             cell_id, include_cached=True).as_columns()

        if len(id2) != 0:
            # Project to tangent plane around the center of the cell. We
            # assume the cell is small enough for the distortions not to
            # matter and Euclidian distances apply
            bounds, _ = pix.cell_bounds(cell_id)
            (clon, clat) = bhpix.deproj_bhealpix(*bounds.center())
            xy1 = np.column_stack(gnomonic(ra1, dec1, clon, clat))
            xy2 = np.column_stack(gnomonic(ra2, dec2, clon, clat))

            # Construct kD-tree to find an object in table_to that is nearest
            # to an object in table_from, for every object in table_from
            tree = kdtree(xy2)
            match_idxs, match_d2 = tree.knn(xy1, min(n_neighbors, len(xy2)))
            del tree

            # Create the index table array
            join.resize(match_idxs.size)
            for k in xrange(match_idxs.shape[1]):
                match_idx = match_idxs[:, k]
                join['_M1'][k::match_idxs.shape[1]] = id1
                join['_M2'][k::match_idxs.shape[1]] = id2[match_idx]
                join['_DIST'][k::match_idxs.shape[1]] = gc_dist(
                    ra1, dec1, ra2[match_idx], dec2[match_idx])
                join['_LON'][k::match_idxs.shape[1]] = ra2[match_idx]
                join['_LAT'][k::match_idxs.shape[1]] = dec2[match_idx]
                join['_NR'][k::match_idxs.shape[1]] = k

            # Remove matches beyond the xmatch radius
            join = join[join['_DIST'] < radius]

        if len(join):
            # compute the cell_id part of the join table's
            # IDs. While this is unimportant now (as we could
            # just set all of them equal to cell_id part of
            # cell_id), if we ever decide to change the
            # pixelation of the table later on, this will
            # allow us to correctly repixelize the join table as
            # well.
            #x, y, t, _  = pix._xyti_from_id(join['_M1'])	# ... but at the spatial location given by the object table.
            #join['_ID'] = pix._id_from_xyti(x, y, t, 0)     # This will make the new IDs have zeros in the object part (so Table.append will autogen them)

            # TODO: Allow the stuff above (in Table.append)
            join['_ID'] = pix.cell_for_id(join['_M1'])

            # TODO: Debugging, remove when happy
            cid = np.unique(pix.cell_for_id(join['_ID']))
            assert len(cid) == 1, len(cid)
            assert cid[0] == cell_id, '%s %s' % (cid[0], cell_id)
            ####

            table_xm.append(join)

            yield len(id1), len(id2), len(join)
        else:
            yield len(rows), 0, 0
Example #2
0
File: tasks.py Project: banados/lsd
def _xmatch_mapper(qresult, tabname_to, radius, tabname_xm, n_neighbors):
	"""
	    Mapper:
	    	- given all objects in a cell, make an ANN tree
	    	- load all objects in tabname_to (including neighbors), make an ANN tree, find matches
	    	- store the output into an index table
	"""
	from scikits.ann import kdtree

	db       = qresult.db
	pix      = qresult.pix
	table_xm = db.table(tabname_xm)

	for rows in qresult:
		cell_id  = rows.info.cell_id

		join = ColGroup(dtype=[('_M1', 'u8'), ('_M2', 'u8'), ('_DIST', 'f4'), ('_NR', 'u1'), ('_LON', 'f8'), ('_LAT', 'f8')])

		(id1, ra1, dec1) = rows.as_columns()
		(id2, ra2, dec2) = db.query('_ID, _LON, _LAT FROM %s' % tabname_to).fetch_cell(cell_id, include_cached=True).as_columns()

		if len(id2) != 0:
			# Project to tangent plane around the center of the cell. We
			# assume the cell is small enough for the distortions not to
			# matter and Euclidian distances apply
			bounds, _    = pix.cell_bounds(cell_id)
			(clon, clat) = bhpix.deproj_bhealpix(*bounds.center())
			xy1 = np.column_stack(gnomonic(ra1, dec1, clon, clat))
			xy2 = np.column_stack(gnomonic(ra2, dec2, clon, clat))

			# Construct kD-tree to find an object in table_to that is nearest
			# to an object in table_from, for every object in table_from
			tree = kdtree(xy2)
			match_idxs, match_d2 = tree.knn(xy1, min(n_neighbors, len(xy2)))
			del tree

			# Create the index table array
			join.resize(match_idxs.size)
			for k in xrange(match_idxs.shape[1]):
				match_idx = match_idxs[:,k]
				join['_M1'][k::match_idxs.shape[1]]   = id1
				join['_M2'][k::match_idxs.shape[1]]   = id2[match_idx]
				join['_DIST'][k::match_idxs.shape[1]] = gc_dist(ra1, dec1, ra2[match_idx], dec2[match_idx])
				join['_LON'][k::match_idxs.shape[1]]  = ra2[match_idx]
				join['_LAT'][k::match_idxs.shape[1]]  = dec2[match_idx]
				join['_NR'][k::match_idxs.shape[1]]   = k

			# Remove matches beyond the xmatch radius
			join = join[join['_DIST'] < radius]

		if len(join):
			# compute the cell_id part of the join table's
			# IDs. While this is unimportant now (as we could
			# just set all of them equal to cell_id part of
			# cell_id), if we ever decide to change the
			# pixelation of the table later on, this will
			# allow us to correctly repixelize the join table as
			# well.
			#x, y, t, _  = pix._xyti_from_id(join['_M1'])	# ... but at the spatial location given by the object table.
			#join['_ID'] = pix._id_from_xyti(x, y, t, 0)     # This will make the new IDs have zeros in the object part (so Table.append will autogen them)
			
			# TODO: Allow the stuff above (in Table.append)
			join['_ID'] = pix.cell_for_id(join['_M1'])

			# TODO: Debugging, remove when happy
			cid = np.unique(pix.cell_for_id(join['_ID']))
			assert len(cid) == 1, len(cid)
			assert cid[0] == cell_id, '%s %s' % (cid[0], cell_id)
			####

			table_xm.append(join)

			yield len(id1), len(id2), len(join)
		else:
			yield len(rows), 0, 0
Example #3
0
File: smf.py Project: banados/lsd
def _obj_det_match(cells, db, obj_tabname, det_tabname, o2d_tabname, radius, explist=None, _rematching=False):
	"""
	This kernel assumes:
	   a) det_table and obj_table have equal partitioning (equally
	      sized/enumerated spatial cells)
	   b) both det_table and obj_table have up-to-date neighbor caches
	   c) temporal det_table cells within this spatial cell are stored
	      local to this process (relevant for shared-nothing setups)
	   d) exposures don't stretch across temporal cells

	Algorithm:
	   - fetch all existing static sky objects, including the cached ones (*)
	   - project them to tangent plane around the center of the cell
	     (we assume the cell is small enough for the distortions not to matter)
	   - construct a kD tree in (x, y) tangent space
	   - for each temporal cell, in sorted order (++):
	   	1.) Fetch the detections, including the cached ones (+)
	   	2.) Project to tangent plane

	   	3.) for each exposure, in sorted order (++):
		    a.) Match agains the kD tree of objects
		    b.) Add those that didn't match to the list of objects 

		4.) For newly added objects: store to disk only those that
		    fall within this cell (the others will be matched and
		    stored in their parent cells)

		5.) For matched detections: Drop detections matched to cached
		    objects (these will be matched and stored in the objects'
		    parent cell). Store the rest.


	   (+) It is allowed (and necessary to allow) for a cached detection
		    to be matched against an object within our cell.  This
		    correctly matches cases when the object is right inside
		    the cell boundary, but the detection is just to the
		    outside.

	   (++) Having cells and detections sorted ensures that objects in overlapping
	        (cached) regions are seen by kernels in different cells in the same
	        order, thus resulting in the same matches. Note: this may fail in
	        extremely crowded region, but as of now it's not clear how big of
	        a problem (if any!) will this pose.

	   (*) Cached objects must be loaded and matched against to guard against
	       the case where an object is just outside the edge, while a detection
	       is just inside. If the cached object was not loaded, the detection
	       would not match and be proclamed to be a new object. However, in the
	       cached object's parent cell, the detection would match to the object
	       and be stored there as well.
	       
	       The algorithm above ensures that such a detection will matched to
	       the cached object in this cell (and be dropped in step 5), preventing
	       it from being promoted into a new object.

	   TODO: The above algorithm ensures no detection is assigned to more than
	   	one object. It also ensures that each detection links to an object.
	   	Implement a consistency check to verify that.
	"""

	from scikits.ann import kdtree

	# Input is a tuple of obj_cell, and det_cells falling under that obj_cell
	obj_cell, det_cells = cells
	det_cells.sort()
	assert len(det_cells)

	# Fetch the frequently used bits
	obj_table = db.table(obj_tabname)
	det_table = db.table(det_tabname)
	o2d_table = db.table(o2d_tabname)
	pix = obj_table.pix

	# locate cell center (for gnomonic projection)
	(bounds, tbounds)  = pix.cell_bounds(obj_cell)
	(clon, clat) = bhpix.deproj_bhealpix(*bounds.center())

	# fetch existing static sky, convert to gnomonic
	objs  = db.query('_ID, _LON, _LAT FROM %s' % obj_tabname).fetch_cell(obj_cell, include_cached=True)
	xyobj = np.column_stack(gnomonic(objs['_LON'], objs['_LAT'], clon, clat))
	nobj  = len(objs)	# Total number of static sky objects
	tree  = None
	nobj_old = 0

	# for sanity checks/debugging (see below)
	expseen = set()

	## TODO: Debugging, remove when happy
	assert (np.unique(sorted(det_cells)) == sorted(det_cells)).all()
	##print "Det cells: ", det_cells

	# Loop, xmatch, and store
	if explist is not None:
		explist = np.asarray(list(explist), dtype=np.uint64)	# Ensure explist is a ndarray
	det_query = db.query('_ID, _LON, _LAT, _EXP, _CACHED FROM %s' % det_tabname)
	for det_cell in sorted(det_cells):
		# fetch detections in this cell, convert to gnomonic coordinates
		# keep only detections with _EXP in explist, unless explist is None
		detections = det_query.fetch_cell(det_cell, include_cached=True)

		# if there are no preexisting static sky objects, and all detections in this cell are cached,
		# there's no way we'll get a match that will be kept in the end. Just continue to the
		# next one if this is the case.
		cachedonly = len(objs) == 0 and detections._CACHED.all()
		if cachedonly:
#			print "Skipping cached-only", len(cached)
			yield (None, None, None, None, None, None) # Yield just to have the progress counter properly incremented
			continue;

		if explist is not None:
			keep = np.in1d(detections._EXP, explist)
			if not np.all(keep):
				detections = detections[keep]
			if len(detections) == 0:
				yield (None, None, None, None, None, None) # Yield just to have the progress counter properly incremented
				continue
		_, ra2, dec2, exposures, cached = detections.as_columns()
		detections.add_column('xy', np.column_stack(gnomonic(ra2, dec2, clon, clat)))

		# prep join table
		join  = ColGroup(dtype=o2d_table.dtype_for(['_ID', '_M1', '_M2', '_DIST', '_LON', '_LAT']))
		njoin = 0;
		nobj0 = nobj;

		##print "Cell", det_cell, " - Unique exposures: ", set(exposures)

		# Process detections exposure-by-exposure, as detections from
		# different exposures within a same temporal cell are allowed
		# to belong to the same object
		uexposures = set(exposures)
		for exposure in sorted(uexposures):
			# Sanity check: a consistent table cannot have two
			# exposures stretching over more than one cell
			assert exposure not in expseen
			expseen.add(exposure);

			# Extract objects belonging to this exposure only
			detections2 = detections[exposures == exposure]
			id2, ra2, dec2, _, _, xydet = detections2.as_columns()
			ndet = len(xydet)

			if len(xyobj) != 0:
				# Construct kD-tree and find the object nearest to each
				# detection from this cell
				if tree is None or nobj_old != len(xyobj):
					del tree
					nobj_old = len(xyobj)
					tree = kdtree(xyobj)
				match_idx, match_d2 = tree.knn(xydet, 1)
				match_idx = match_idx[:,0]		# First neighbor only

				####
				#if np.uint64(13828114484734072082) in id2:
				#	np.savetxt('bla.%d.static=%d.txt' % (det_cell, pix.static_cell_for_cell(det_cell)), objs.as_ndarray(), fmt='%s')

				# Compute accurate distances, and select detections not matched to existing objects
				dist       = gc_dist(objs['_LON'][match_idx], objs['_LAT'][match_idx], ra2, dec2)
				unmatched  = dist >= radius
			else:
				# All detections will become new objects (and therefore, dist=0)
				dist       = np.zeros(ndet, dtype='f4')
				unmatched  = np.ones(ndet, dtype=bool)
				match_idx  = np.empty(ndet, dtype='i4')

#			x, y, t = pix._xyt_from_cell_id(det_cell)
#			print "det_cell %s, MJD %s, Exposure %s  ==  %d detections, %d objects, %d matched, %d unmatched" % (det_cell, t, exposure, len(detections2), nobj, len(unmatched)-unmatched.sum(), unmatched.sum())

			# Promote unmatched detections to new objects
			_, newra, newdec, _, _, newxy = detections2[unmatched].as_columns()
			nunmatched = unmatched.sum()
			reserve_space(objs, nobj+nunmatched)
			objs['_LON'][nobj:nobj+nunmatched] = newra
			objs['_LAT'][nobj:nobj+nunmatched] = newdec
			dist[unmatched]                    = 0.
			match_idx[unmatched] = np.arange(nobj, nobj+nunmatched, dtype='i4')	# Set the indices of unmatched detections to newly created objects

			# Join objects to their detections
			reserve_space(join, njoin+ndet)
			join['_M1'][njoin:njoin+ndet]   = match_idx
			join['_M2'][njoin:njoin+ndet]   =       id2
			join['_DIST'][njoin:njoin+ndet] =      dist
			# TODO: For debugging; remove when happy
			join['_LON'][njoin:njoin+ndet]  =       ra2
			join['_LAT'][njoin:njoin+ndet]  =      dec2
			njoin += ndet

			# Prep for next loop
			nobj  += nunmatched
			xyobj  = np.append(xyobj, newxy, axis=0)

			# TODO: Debugging: Final consistency check (remove when happy with the code)
			dist = gc_dist( objs['_LON'][  join['_M1'][njoin-ndet:njoin]  ],
					objs['_LAT'][  join['_M1'][njoin-ndet:njoin]  ], ra2, dec2)
			assert (dist < radius).all()

		# Truncate output tables to their actual number of elements
		objs = objs[0:nobj]
		join = join[0:njoin]
		assert len(objs) >= nobj0

		# Find the objects that fall outside of cell boundaries. These will
		# be processed and stored by their parent cells. Also leave out the objects
		# that are already stored in the database
		(x, y) = bhpix.proj_bhealpix(objs['_LON'], objs['_LAT'])
		in_    = bounds.isInsideV(x, y)
		innew  = in_.copy();
		innew[:nobj0] = False											# New objects in cell selector

		ids = objs['_ID']
		nobjadded = innew.sum()
		if nobjadded:
			# Append the new objects to the object table, obtaining their IDs.
			assert not _rematching, 'cell_id=%s, nnew=%s\n%s' % (det_cell, nobjadded, objs[innew])
			ids[innew] = obj_table.append(objs[('_LON', '_LAT')][innew])

		# Set the indices of objects not in this cell to zero (== a value
		# no valid object in the database can have). Therefore, all
		# out-of-bounds links will have _M1 == 0 (#1), and will be removed
		# by the np1d call (#2)
		ids[~in_] = 0

		# 1) Change the relative index to true obj_id in the join table
		join['_M1'] = ids[join['_M1']]

		# 2) Keep only the joins to objects inside the cell
		join = join[ np.in1d(join['_M1'], ids[in_]) ]

		# Append to the join table, in *dec_cell* of obj_table (!important!)
		if len(join) != 0:
			# compute the cell_id part of the join table's
			# IDs.While this is unimportant now (as we could
			# just set all of them equal to cell_id part of
			# cell_id), if we ever decide to change the
			# pixelation of the table later on, this will
			# allow us to correctly split up the join table as
			# well.
			#_, _, t    = pix._xyt_from_cell_id(det_cell)	# This row points to a detection in the temporal cell ...
			#x, y, _, _ = pix._xyti_from_id(join['_M1'])	# ... but at the spatial location given by the object table.
			#join['_ID'][:] = pix._id_from_xyti(x, y, t, 0) # This will make the new IDs have zeros in the object part (so Table.append will autogen them)
			join['_ID'][:] = det_cell

			o2d_table.append(join)

		assert not cachedonly or (nobjadded == 0 and len(join) == 0)

		# return: Number of exposures, number of objects before processing this cell, number of detections processed (incl. cached),
		#         number of newly added objects, number of detections xmatched, number of detection processed that weren't cached
		# Note: some of the xmatches may be to newly added objects (e.g., if there are two 
		#       overlapping exposures within a cell; first one will add new objects, second one will match agains them)
		yield (len(uexposures), nobj0, len(detections), nobjadded, len(join), (cached == False).sum())
Example #4
0
def _obj_det_match(cells,
                   db,
                   obj_tabname,
                   det_tabname,
                   o2d_tabname,
                   radius,
                   explist=None,
                   _rematching=False):
    """
	This kernel assumes:
	   a) det_table and obj_table have equal partitioning (equally
	      sized/enumerated spatial cells)
	   b) both det_table and obj_table have up-to-date neighbor caches
	   c) temporal det_table cells within this spatial cell are stored
	      local to this process (relevant for shared-nothing setups)
	   d) exposures don't stretch across temporal cells

	Algorithm:
	   - fetch all existing static sky objects, including the cached ones (*)
	   - project them to tangent plane around the center of the cell
	     (we assume the cell is small enough for the distortions not to matter)
	   - construct a kD tree in (x, y) tangent space
	   - for each temporal cell, in sorted order (++):
	   	1.) Fetch the detections, including the cached ones (+)
	   	2.) Project to tangent plane

	   	3.) for each exposure, in sorted order (++):
		    a.) Match agains the kD tree of objects
		    b.) Add those that didn't match to the list of objects 

		4.) For newly added objects: store to disk only those that
		    fall within this cell (the others will be matched and
		    stored in their parent cells)

		5.) For matched detections: Drop detections matched to cached
		    objects (these will be matched and stored in the objects'
		    parent cell). Store the rest.


	   (+) It is allowed (and necessary to allow) for a cached detection
		    to be matched against an object within our cell.  This
		    correctly matches cases when the object is right inside
		    the cell boundary, but the detection is just to the
		    outside.

	   (++) Having cells and detections sorted ensures that objects in overlapping
	        (cached) regions are seen by kernels in different cells in the same
	        order, thus resulting in the same matches. Note: this may fail in
	        extremely crowded region, but as of now it's not clear how big of
	        a problem (if any!) will this pose.

	   (*) Cached objects must be loaded and matched against to guard against
	       the case where an object is just outside the edge, while a detection
	       is just inside. If the cached object was not loaded, the detection
	       would not match and be proclamed to be a new object. However, in the
	       cached object's parent cell, the detection would match to the object
	       and be stored there as well.
	       
	       The algorithm above ensures that such a detection will matched to
	       the cached object in this cell (and be dropped in step 5), preventing
	       it from being promoted into a new object.

	   TODO: The above algorithm ensures no detection is assigned to more than
	   	one object. It also ensures that each detection links to an object.
	   	Implement a consistency check to verify that.
	"""

    from scikits.ann import kdtree

    # Input is a tuple of obj_cell, and det_cells falling under that obj_cell
    obj_cell, det_cells = cells
    det_cells.sort()
    assert len(det_cells)

    # Fetch the frequently used bits
    obj_table = db.table(obj_tabname)
    det_table = db.table(det_tabname)
    o2d_table = db.table(o2d_tabname)
    pix = obj_table.pix

    # locate cell center (for gnomonic projection)
    (bounds, tbounds) = pix.cell_bounds(obj_cell)
    (clon, clat) = bhpix.deproj_bhealpix(*bounds.center())

    # fetch existing static sky, convert to gnomonic
    objs = db.query('_ID, _LON, _LAT FROM %s' % obj_tabname).fetch_cell(
        obj_cell, include_cached=True)
    xyobj = np.column_stack(gnomonic(objs['_LON'], objs['_LAT'], clon, clat))
    nobj = len(objs)  # Total number of static sky objects
    tree = None
    nobj_old = 0

    # for sanity checks/debugging (see below)
    expseen = set()

    ## TODO: Debugging, remove when happy
    assert (np.unique(sorted(det_cells)) == sorted(det_cells)).all()
    ##print "Det cells: ", det_cells

    # Loop, xmatch, and store
    if explist is not None:
        explist = np.asarray(list(explist),
                             dtype=np.uint64)  # Ensure explist is a ndarray
    det_query = db.query('_ID, _LON, _LAT, _EXP, _CACHED FROM %s' %
                         det_tabname)
    for det_cell in sorted(det_cells):
        # fetch detections in this cell, convert to gnomonic coordinates
        # keep only detections with _EXP in explist, unless explist is None
        detections = det_query.fetch_cell(det_cell, include_cached=True)

        # if there are no preexisting static sky objects, and all detections in this cell are cached,
        # there's no way we'll get a match that will be kept in the end. Just continue to the
        # next one if this is the case.
        cachedonly = len(objs) == 0 and detections._CACHED.all()
        if cachedonly:
            #			print "Skipping cached-only", len(cached)
            yield (
                None, None, None, None, None, None
            )  # Yield just to have the progress counter properly incremented
            continue

        if explist is not None:
            keep = np.in1d(detections._EXP, explist)
            if not np.all(keep):
                detections = detections[keep]
            if len(detections) == 0:
                yield (
                    None, None, None, None, None, None
                )  # Yield just to have the progress counter properly incremented
                continue
        _, ra2, dec2, exposures, cached = detections.as_columns()
        detections.add_column('xy',
                              np.column_stack(gnomonic(ra2, dec2, clon, clat)))

        # prep join table
        join = ColGroup(dtype=o2d_table.dtype_for(
            ['_ID', '_M1', '_M2', '_DIST', '_LON', '_LAT']))
        njoin = 0
        nobj0 = nobj

        ##print "Cell", det_cell, " - Unique exposures: ", set(exposures)

        # Process detections exposure-by-exposure, as detections from
        # different exposures within a same temporal cell are allowed
        # to belong to the same object
        uexposures = set(exposures)
        for exposure in sorted(uexposures):
            # Sanity check: a consistent table cannot have two
            # exposures stretching over more than one cell
            assert exposure not in expseen
            expseen.add(exposure)

            # Extract objects belonging to this exposure only
            detections2 = detections[exposures == exposure]
            id2, ra2, dec2, _, _, xydet = detections2.as_columns()
            ndet = len(xydet)

            if len(xyobj) != 0:
                # Construct kD-tree and find the object nearest to each
                # detection from this cell
                if tree is None or nobj_old != len(xyobj):
                    del tree
                    nobj_old = len(xyobj)
                    tree = kdtree(xyobj)
                match_idx, match_d2 = tree.knn(xydet, 1)
                match_idx = match_idx[:, 0]  # First neighbor only

                ####
                #if np.uint64(13828114484734072082) in id2:
                #	np.savetxt('bla.%d.static=%d.txt' % (det_cell, pix.static_cell_for_cell(det_cell)), objs.as_ndarray(), fmt='%s')

                # Compute accurate distances, and select detections not matched to existing objects
                dist = gc_dist(objs['_LON'][match_idx],
                               objs['_LAT'][match_idx], ra2, dec2)
                unmatched = dist >= radius
            else:
                # All detections will become new objects (and therefore, dist=0)
                dist = np.zeros(ndet, dtype='f4')
                unmatched = np.ones(ndet, dtype=bool)
                match_idx = np.empty(ndet, dtype='i4')

#			x, y, t = pix._xyt_from_cell_id(det_cell)
#			print "det_cell %s, MJD %s, Exposure %s  ==  %d detections, %d objects, %d matched, %d unmatched" % (det_cell, t, exposure, len(detections2), nobj, len(unmatched)-unmatched.sum(), unmatched.sum())

# Promote unmatched detections to new objects
            _, newra, newdec, _, _, newxy = detections2[unmatched].as_columns()
            nunmatched = unmatched.sum()
            reserve_space(objs, nobj + nunmatched)
            objs['_LON'][nobj:nobj + nunmatched] = newra
            objs['_LAT'][nobj:nobj + nunmatched] = newdec
            dist[unmatched] = 0.
            match_idx[unmatched] = np.arange(
                nobj, nobj + nunmatched, dtype='i4'
            )  # Set the indices of unmatched detections to newly created objects

            # Join objects to their detections
            reserve_space(join, njoin + ndet)
            join['_M1'][njoin:njoin + ndet] = match_idx
            join['_M2'][njoin:njoin + ndet] = id2
            join['_DIST'][njoin:njoin + ndet] = dist
            # TODO: For debugging; remove when happy
            join['_LON'][njoin:njoin + ndet] = ra2
            join['_LAT'][njoin:njoin + ndet] = dec2
            njoin += ndet

            # Prep for next loop
            nobj += nunmatched
            xyobj = np.append(xyobj, newxy, axis=0)

            # TODO: Debugging: Final consistency check (remove when happy with the code)
            dist = gc_dist(objs['_LON'][join['_M1'][njoin - ndet:njoin]],
                           objs['_LAT'][join['_M1'][njoin - ndet:njoin]], ra2,
                           dec2)
            assert (dist < radius).all()

        # Truncate output tables to their actual number of elements
        objs = objs[0:nobj]
        join = join[0:njoin]
        assert len(objs) >= nobj0

        # Find the objects that fall outside of cell boundaries. These will
        # be processed and stored by their parent cells. Also leave out the objects
        # that are already stored in the database
        (x, y) = bhpix.proj_bhealpix(objs['_LON'], objs['_LAT'])
        in_ = bounds.isInsideV(x, y)
        innew = in_.copy()
        innew[:nobj0] = False  # New objects in cell selector

        ids = objs['_ID']
        nobjadded = innew.sum()
        if nobjadded:
            # Append the new objects to the object table, obtaining their IDs.
            assert not _rematching, 'cell_id=%s, nnew=%s\n%s' % (
                det_cell, nobjadded, objs[innew])
            ids[innew] = obj_table.append(objs[('_LON', '_LAT')][innew])

        # Set the indices of objects not in this cell to zero (== a value
        # no valid object in the database can have). Therefore, all
        # out-of-bounds links will have _M1 == 0 (#1), and will be removed
        # by the np1d call (#2)
        ids[~in_] = 0

        # 1) Change the relative index to true obj_id in the join table
        join['_M1'] = ids[join['_M1']]

        # 2) Keep only the joins to objects inside the cell
        join = join[np.in1d(join['_M1'], ids[in_])]

        # Append to the join table, in *dec_cell* of obj_table (!important!)
        if len(join) != 0:
            # compute the cell_id part of the join table's
            # IDs.While this is unimportant now (as we could
            # just set all of them equal to cell_id part of
            # cell_id), if we ever decide to change the
            # pixelation of the table later on, this will
            # allow us to correctly split up the join table as
            # well.
            #_, _, t    = pix._xyt_from_cell_id(det_cell)	# This row points to a detection in the temporal cell ...
            #x, y, _, _ = pix._xyti_from_id(join['_M1'])	# ... but at the spatial location given by the object table.
            #join['_ID'][:] = pix._id_from_xyti(x, y, t, 0) # This will make the new IDs have zeros in the object part (so Table.append will autogen them)
            join['_ID'][:] = det_cell

            o2d_table.append(join)

        assert not cachedonly or (nobjadded == 0 and len(join) == 0)

        # return: Number of exposures, number of objects before processing this cell, number of detections processed (incl. cached),
        #         number of newly added objects, number of detections xmatched, number of detection processed that weren't cached
        # Note: some of the xmatches may be to newly added objects (e.g., if there are two
        #       overlapping exposures within a cell; first one will add new objects, second one will match agains them)
        yield (len(uexposures), nobj0, len(detections), nobjadded, len(join),
               (cached == False).sum())