diff --git a/FlowCal/excel_ui.py b/FlowCal/excel_ui.py index 09de4f7..16f48ca 100644 --- a/FlowCal/excel_ui.py +++ b/FlowCal/excel_ui.py @@ -453,7 +453,7 @@ def process_beads_table(beads_table, channels=sc_channels) # Density gating try: - beads_sample_gated, __, gate_contour = FlowCal.gate.density2d( + density_gate_output = FlowCal.gate.density2d( data=beads_sample_gated, channels=sc_channels, gate_fraction=beads_row['Gate Fraction'], @@ -461,6 +461,8 @@ def process_beads_table(beads_table, yscale='logicle', sigma=5., full_output=True) + beads_sample_gated = density_gate_output.gated_data + gate_contour = density_gate_output.contour except ValueError as ve: raise ExcelUIException(ve.message) @@ -870,13 +872,15 @@ def process_samples_table(samples_table, sc_channels + report_channels) # Density gating try: - sample_gated, __, gate_contour = FlowCal.gate.density2d( + density_gate_output = FlowCal.gate.density2d( data=sample_gated, channels=sc_channels, gate_fraction=sample_row['Gate Fraction'], xscale='logicle', yscale='logicle', full_output=True) + sample_gated = density_gate_output.gated_data + gate_contour = density_gate_output.contour except ValueError as ve: raise ExcelUIException(ve.message) diff --git a/FlowCal/gate.py b/FlowCal/gate.py index cb6299a..c46d9b5 100644 --- a/FlowCal/gate.py +++ b/FlowCal/gate.py @@ -24,6 +24,32 @@ import skimage.measure import collections +### +# Gate Classes +### + +# Output namedtuples returned by gate functions +StartEndGateOutput = collections.namedtuple( + typename='StartEndGateOutput', + field_names=('gated_data', + 'mask')) +HighLowGateOutput = collections.namedtuple( + typename='HighLowGateOutput', + field_names=('gated_data', + 'mask')) +EllipseGateOutput = collections.namedtuple( + typename='EllipseGateOutput', + field_names=('gated_data', + 'mask', + 'contour')) +Density2dGateOutput = collections.namedtuple( + typename='Density2dGateOutput', + field_names=('gated_data', + 'mask', + 'contour', + 'bin_edges', + 'bin_mask')) + ### # Gate Functions ### @@ -78,9 +104,6 @@ def start_end(data, num_start=250, num_end=100, full_output=False): gated_data = data[mask] if full_output: - StartEndGateOutput = collections.namedtuple( - 'StartEndGateOutput', - ['gated_data', 'mask']) return StartEndGateOutput(gated_data=gated_data, mask=mask) else: return gated_data @@ -143,9 +166,6 @@ def high_low(data, channels=None, high=None, low=None, full_output=False): gated_data = data[mask] if full_output: - HighLowGateOutput = collections.namedtuple( - 'HighLowGateOutput', - ['gated_data', 'mask']) return HighLowGateOutput(gated_data=gated_data, mask=mask) else: return gated_data @@ -232,9 +252,6 @@ def ellipse(data, channels, cntr = [ci] # Build output namedtuple - EllipseGateOutput = collections.namedtuple( - 'EllipseGateOutput', - ['gated_data', 'mask', 'contour']) return EllipseGateOutput( gated_data=data_gated, mask=mask, contour=cntr) else: @@ -247,6 +264,7 @@ def density2d(data, xscale='logicle', yscale='logicle', sigma=10.0, + bin_mask=None, full_output=False): """ Gate that preserves events in the region with highest density. @@ -298,6 +316,10 @@ def density2d(data, Standard deviation for Gaussian kernel used by `scipy.ndimage.filters.gaussian_filter` to smooth 2D histogram into a density. + bin_mask : 2D numpy array of bool, optional + A 2D mask array that selects the 2D histogram bins permitted by the + gate. Corresponding bin edges should be specified via `bins`. If + `bin_mask` is specified, `gate_fraction` and `sigma` are ignored. full_output : bool, optional Flag specifying to return additional outputs. If true, the outputs are given as a namedtuple. @@ -310,8 +332,14 @@ def density2d(data, Boolean gate mask used to gate data such that ``gated_data = data[mask]``. contour : list of 2D numpy arrays, only if ``full_output==True`` - List of 2D numpy array(s) of x-y coordinates tracing out - the edge of the gated region. + List of 2D numpy array(s) of x-y coordinates tracing out the edge of + the gated region. If `bin_mask` is specified, `contour` is None. + bin_edges : 2-tuple of numpy arrays, only if ``full_output==True`` + X-axis and y-axis bin edges used by the np.histogram2d() command that + bins events (bin_edges=(x_edges,y_edges)). + bin_mask : 2D numpy array of bool, only if ``full_output==True`` + A 2D mask array that selects the 2D histogram bins permitted by the + gate. Raises ------ @@ -355,22 +383,12 @@ def density2d(data, if data_ch.ndim == 1: data_ch = data_ch.reshape((-1,1)) - # Check gating fraction - if gate_fraction < 0 or gate_fraction > 1: - raise ValueError('gate fraction should be between 0 and 1, inclusive') - # Check dimensions if data_ch.ndim < 2: raise ValueError('data should have at least 2 dimensions') if data_ch.shape[0] <= 1: raise ValueError('data should have more than one event') - # Build output namedtuple if necessary - if full_output: - Density2dGateOutput = collections.namedtuple( - 'Density2dGateOutput', - ['gated_data', 'mask', 'contour']) - # If ``data_ch.hist_bins()`` exists, obtain bin edges from it if # necessary. if hasattr(data_ch, 'hist_bins') and \ @@ -405,6 +423,8 @@ def density2d(data, # Make 2D histogram H,xe,ye = np.histogram2d(data_ch[:,0], data_ch[:,1], bins=bins) + xe = np.array(xe, dtype=np.float) + ye = np.array(ye, dtype=np.float) # Map each event to its histogram bin by sorting events into a 2D array of # lists which mimics the histogram. @@ -454,84 +474,111 @@ def density2d(data, zip(event_indices, x_bin_indices, y_bin_indices): H_events[x_bin_idx, y_bin_idx].append(event_idx) - # Determine number of events to keep. Only consider events which have not - # been thrown out as outliers. - n = int(np.ceil(gate_fraction*float(len(event_indices)))) - - # n = 0 edge case (e.g. if gate_fraction = 0.0); incorrectly handled below - if n == 0: - mask = np.zeros(shape=data_ch.shape[0], dtype=bool) - gated_data = data[mask] + # Create bin mask if necessary + contours = None + if bin_mask is None: + # Check gating fraction + if gate_fraction < 0 or gate_fraction > 1: + msg = "gate fraction should be between 0 and 1, inclusive" + raise ValueError(msg) + + # Determine number of events to keep. Only consider events which have + # not been thrown out as outliers. + n = int(np.ceil(gate_fraction*float(len(event_indices)))) + + # n = 0 edge case (e.g. if gate_fraction = 0.0); incorrectly handled + # below + if n == 0: + mask = np.zeros(shape=data_ch.shape[0], dtype=bool) + gated_data = data[mask] + if full_output: + return Density2dGateOutput( + gated_data=gated_data, + mask=mask, + contour=[], + bin_edges=(xe,ye), + bin_mask=np.zeros_like(H, dtype=bool)) + else: + return gated_data + + # Smooth 2D histogram + sH = scipy.ndimage.filters.gaussian_filter( + H, + sigma=sigma, + order=0, + mode='constant', + cval=0.0, + truncate=6.0) + + # Normalize smoothed histogram to make it a valid probability mass + # function + D = sH / np.sum(sH) + + # Sort bins by density + vD = D.ravel(order='C') + vH = H.ravel(order='C') + sidx = np.argsort(vD)[::-1] + svH = vH[sidx] # linearized counts array sorted by density + + # Find minimum number of accepted bins needed to reach specified + # number of events + csvH = np.cumsum(svH) + Nidx = np.nonzero(csvH >= n)[0][0] # we want to include this index + + # Get indices of accepted histogram bins + accepted_bin_indices = sidx[:(Nidx+1)] + + # Convert accepted bin indices to bin mask array + bin_mask = np.zeros_like(H, dtype=bool) + v_bin_mask = bin_mask.ravel(order='C') + v_bin_mask[accepted_bin_indices] = True + bin_mask = v_bin_mask.reshape(H.shape, order='C') + + # Determine contours if necessary if full_output: - return Density2dGateOutput( - gated_data=gated_data, mask=mask, contour=[]) - else: - return gated_data - - # Smooth 2D histogram - sH = scipy.ndimage.filters.gaussian_filter( - H, - sigma=sigma, - order=0, - mode='constant', - cval=0.0, - truncate=6.0) - - # Normalize smoothed histogram to make it a valid probability mass function - D = sH / np.sum(sH) - - # Sort bins by density - vD = D.ravel() - vH = H.ravel() - sidx = np.argsort(vD)[::-1] - svH = vH[sidx] # linearized counts array sorted by density - - # Find minimum number of accepted bins needed to reach specified number - # of events - csvH = np.cumsum(svH) - Nidx = np.nonzero(csvH >= n)[0][0] # we want to include this index - - # Get indices of events to keep - vH_events = H_events.ravel() - accepted_indices = vH_events[sidx[:(Nidx+1)]] - accepted_indices = np.array([item # flatten list of lists - for sublist in accepted_indices - for item in sublist]) - accepted_indices = np.sort(accepted_indices) - - # Convert list of accepted indices to boolean mask array + # Use scikit-image to find the contour of the gated region + # + # To find the contour of the gated region, values in the 2D + # probability mass function ``D`` are used to trace contours at + # the level of the probability associated with the last accepted + # bin, ``vD[sidx[Nidx]]``. + + # find_contours() specifies contours as collections of row and + # column indices into the density matrix. The row or column index + # may be interpolated (i.e. non-integer) for greater precision. + contours_ij = skimage.measure.find_contours(D, vD[sidx[Nidx]]) + + # Map contours from indices into density matrix to histogram x and + # y coordinate spaces (assume values in the density matrix are + # associated with histogram bin centers). + xc = (xe[:-1] + xe[1:]) / 2.0 # x-axis bin centers + yc = (ye[:-1] + ye[1:]) / 2.0 # y-axis bin centers + + contours = [np.array([np.interp(contour_ij[:,0], + np.arange(len(xc)), + xc), + np.interp(contour_ij[:,1], + np.arange(len(yc)), + yc)]).T + for contour_ij in contours_ij] + + accepted_data_indices = H_events[bin_mask] + accepted_data_indices = np.array([item # flatten list of lists + for sublist in accepted_data_indices + for item in sublist], + dtype=np.int) + + # Convert list of accepted data indices to boolean mask array mask = np.zeros(shape=data.shape[0], dtype=bool) - mask[accepted_indices] = True + mask[accepted_data_indices] = True gated_data = data[mask] if full_output: - # Use scikit-image to find the contour of the gated region - # - # To find the contour of the gated region, values in the 2D probability - # mass function ``D`` are used to trace contours at the level of the - # probability associated with the last accepted bin, ``vD[sidx[Nidx]]``. - - # find_contours() specifies contours as collections of row and column - # indices into the density matrix. The row or column index may be - # interpolated (i.e. non-integer) for greater precision. - contours_ij = skimage.measure.find_contours(D, vD[sidx[Nidx]]) - - # Map contours from indices into density matrix to histogram x and y - # coordinate spaces (assume values in the density matrix are associated - # with histogram bin centers). - xc = (xe[:-1] + xe[1:]) / 2.0 # x-axis bin centers - yc = (ye[:-1] + ye[1:]) / 2.0 # y-axis bin centers - - contours = [np.array([np.interp(contour_ij[:,0], - np.arange(len(xc)), - xc), - np.interp(contour_ij[:,1], - np.arange(len(yc)), - yc)]).T - for contour_ij in contours_ij] - - return Density2dGateOutput( - gated_data=gated_data, mask=mask, contour=contours) + return Density2dGateOutput(gated_data=gated_data, + mask=mask, + contour=contours, + bin_edges=(xe,ye), + bin_mask=bin_mask) else: return gated_data diff --git a/examples/analyze_mef.py b/examples/analyze_mef.py index c52c3b0..21b5bc5 100644 --- a/examples/analyze_mef.py +++ b/examples/analyze_mef.py @@ -121,14 +121,17 @@ # events. Since bead populations form a very narrow cluster in these # channels, we use a smoothing factor (``sigma``) lower than the default # (10). Finally, setting ``full_output=True`` instructs the function to - # return two additional outputs. The last one (``gate_contour``) is a curve - # surrounding the gated region, which we will use for plotting later. - beads_sample_gated, __, gate_contour = FlowCal.gate.density2d( + # return additional outputs in the form of a named tuple. ``gate_contour`` + # is a curve surrounding the gated region, which we will use for plotting + # later. + density_gate_output = FlowCal.gate.density2d( data=beads_sample_gated, channels=['FSC','SSC'], gate_fraction=0.3, sigma=5., full_output=True) + beads_sample_gated = density_gate_output.gated_data + gate_contour = density_gate_output.contour # Plot forward/side scatter 2D density plot and 1D fluorescence histograms print("Plotting density plot and histogram...") @@ -237,11 +240,13 @@ # Apply density gating on the forward/side scatter channels. Preserve # 50% of the events. Return also a contour around the gated region. - sample_gated, __, gate_contour = FlowCal.gate.density2d( + density_gate_output = FlowCal.gate.density2d( data=sample_gated, channels=['FSC','SSC'], gate_fraction=0.5, full_output=True) + sample_gated = density_gate_output.gated_data + gate_contour = density_gate_output.contour # Plot forward/side scatter 2D density plot and 1D fluorescence # histograms diff --git a/examples/analyze_no_mef.py b/examples/analyze_no_mef.py index 770048c..cfb1739 100644 --- a/examples/analyze_no_mef.py +++ b/examples/analyze_no_mef.py @@ -109,14 +109,16 @@ # interest (i.e. debris). # We use the forward and side scatter channels, and preserve 50% of the # events. Finally, setting ``full_output=True`` instructs the function - # to return two additional outputs. The last one (``gate_contour``) is - # a curve surrounding the gated region, which we will use for plotting - # later. - sample_gated, __, gate_contour = FlowCal.gate.density2d( + # to return additional outputs in the form of a named tuple. + # ``gate_contour`` is a curve surrounding the gated region, which we + # will use for plotting later. + density_gate_output = FlowCal.gate.density2d( data=sample_gated, channels=['FSC','SSC'], gate_fraction=0.5, full_output=True) + sample_gated = density_gate_output.gated_data + gate_contour = density_gate_output.contour # Plot forward/side scatter 2D density plot and 1D fluorescence # histograms diff --git a/test/test_gate.py b/test/test_gate.py index f0434f2..9556f81 100644 --- a/test/test_gate.py +++ b/test/test_gate.py @@ -628,6 +628,51 @@ def test_pyramid_1_mask(self): 1,1,1,1,1,1], dtype=bool) ) + def test_pyramid_1_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=11.0/31, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_pyramid_1_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=11.0/31, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool)) + + def test_pyramid_1_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 1, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[1,2], + [2,1], + [2,2], + [2,3], + [3,2], + [2,2], + [2,2], + [2,1], + [1,2], + [2,3], + [3,2]])) + def test_pyramid_2_gated_data_1(self): bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] np.testing.assert_array_equal( @@ -663,6 +708,43 @@ def test_pyramid_2_mask(self): 1,1,0,0,0,0], dtype=bool) ) + def test_pyramid_2_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/31, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_pyramid_2_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/31, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool)) + + def test_pyramid_2_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,2], + [2,2]])) + def test_slope_1_gated_data_1(self): bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] np.testing.assert_array_equal( @@ -700,6 +782,44 @@ def test_slope_1_mask(self): 0,0,0,1,1], dtype=bool) ) + def test_slope_1_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=4.0/30, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_slope_1_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=4.0/30, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1]], + dtype=bool)) + + def test_slope_1_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[4,4], + [4,4], + [4,4], + [4,4]])) + def test_slope_2_gated_data_1(self): bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] np.testing.assert_array_equal( @@ -755,6 +875,53 @@ def test_slope_2_mask(self): 1,1,1,1,1], dtype=bool) ) + def test_slope_2_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/30, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_slope_2_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/30, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1]], + dtype=bool)) + + def test_slope_2_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 1, 1], + [0, 0, 0, 1, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[3,3], + [3,4], + [4,3], + [4,4], + [3,3], + [3,4], + [4,3], + [4,4], + [3,3], + [3,4], + [4,3], + [4,4], + [4,4]])) + def test_slope_3_gated_data_1(self): bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] np.testing.assert_array_equal( @@ -830,6 +997,63 @@ def test_slope_3_mask(self): 1,1,1,1,1], dtype=bool) ) + def test_slope_3_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=23.0/30, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_slope_3_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=23.0/30, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + dtype=bool)) + + def test_slope_3_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1], + [0, 0, 1, 1, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,3], + [2,4], + [3,2], + [3,3], + [3,4], + [4,2], + [4,3], + [4,4], + [2,2], + [2,3], + [2,4], + [3,2], + [3,3], + [3,4], + [4,2], + [4,3], + [4,4], + [3,3], + [3,4], + [4,3], + [4,4], + [4,4]])) + ### # Test edge cases ### @@ -863,6 +1087,41 @@ def test_gate_fraction_1_mask(self): 1,1,1,1,1,1], dtype=bool) ) + def test_gate_fraction_1_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=1.0, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_gate_fraction_1_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=1.0, sigma=0.0, + full_output=True).bin_mask, + np.array([[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + dtype=bool)) + + def test_gate_fraction_1_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + self.pyramid) + # Confirm nothing gets through with 0.0 gate_fraction def test_gate_fraction_2_gated_data_1(self): @@ -892,6 +1151,41 @@ def test_gate_fraction_2_mask(self): 0,0,0,0,0,0], dtype=bool) ) + def test_gate_fraction_2_bin_edges(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=0.0, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_gate_fraction_2_bin_mask_1(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=0.0, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool)) + + def test_gate_fraction_2_bin_mask_2(self): + bins = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.empty((0,2), dtype=self.pyramid.dtype)) + # Test error when gate_fraction outside [0,1] def test_gate_fraction_2_error_negative_gate_fraction(self): @@ -958,6 +1252,39 @@ def test_implicit_gating_1_mask(self): 1,1,0,0,0,0], dtype=bool) ) + def test_implicit_gating_1_bin_edges(self): + bins = [0.5, 1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/15, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_implicit_gating_1_bin_mask_1(self): + bins = [0.5, 1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/15, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0], + [0, 1, 0], + [0, 0, 0]], + dtype=bool)) + + def test_implicit_gating_1_bin_mask_2(self): + bins = [0.5, 1.5, 2.5, 3.5] + bin_mask = np.array([[0, 0, 0], + [0, 1, 0], + [0, 0, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,2], + [2,2]])) + def test_implicit_gating_2_gated_data_1(self): bins = [0.5, 1.5, 2.5, 3.5] np.testing.assert_array_equal( @@ -1009,6 +1336,47 @@ def test_implicit_gating_2_mask(self): 1,1,1,1,1,1], dtype=bool) ) + def test_implicit_gating_2_bin_edges(self): + bins = [0.5, 1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=11.0/15, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_implicit_gating_2_bin_mask_1(self): + bins = [0.5, 1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=11.0/15, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], + dtype=bool)) + + def test_implicit_gating_2_bin_mask_2(self): + bins = [0.5, 1.5, 2.5, 3.5] + bin_mask = np.array([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[1,2], + [2,1], + [2,2], + [2,3], + [3,2], + [2,2], + [2,2], + [2,1], + [1,2], + [2,3], + [3,2]])) + def test_implicit_gating_3_gated_data_1(self): bins = [1.5, 2.5, 3.5] np.testing.assert_array_equal( @@ -1044,6 +1412,37 @@ def test_implicit_gating_3_mask(self): 1,0,0,0,0], dtype=bool) ) + def test_implicit_gating_3_bin_edges(self): + bins = [1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=3.0/9, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_implicit_gating_3_bin_mask_1(self): + bins = [1.5, 2.5, 3.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=3.0/9, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0], + [0, 1]], + dtype=bool)) + + def test_implicit_gating_3_bin_mask_2(self): + bins = [1.5, 2.5, 3.5] + bin_mask = np.array([[0, 0], + [0, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[3,3], + [3,3], + [3,3]])) + def test_implicit_gating_4_gated_data_1(self): bins = [0.5, 1.5, 2.5, 3.5, 4.5] np.testing.assert_array_equal( @@ -1079,6 +1478,41 @@ def test_implicit_gating_4_mask(self): 1,1,0,0,0,0], dtype=bool) ) + def test_implicit_gating_4_bin_edges(self): + bins = [0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/22, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_implicit_gating_4_bin_mask_1(self): + bins = [0.5, 1.5, 2.5, 3.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=3.0/22, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]], + dtype=bool)) + + def test_implicit_gating_4_bin_mask_2(self): + bins = [0.5, 1.5, 2.5, 3.5, 4.5] + bin_mask = np.array([[0, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,2], + [2,2]])) + # Test sub-binning (multiple values per bin) def test_sub_bin_1_gated_data_1(self): @@ -1136,6 +1570,47 @@ def test_sub_bin_1_mask(self): 1,1,1,1,1], dtype=bool) ) + def test_sub_bin_1_bin_edges(self): + bins = [0.5, 2.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/30, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_sub_bin_1_bin_mask_1(self): + bins = [0.5, 2.5, 4.5] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/30, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0], + [0, 1]], + dtype=bool)) + + def test_sub_bin_1_bin_mask_2(self): + bins = [0.5, 2.5, 4.5] + bin_mask = np.array([[0, 0], + [0, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[3,3], + [3,4], + [4,3], + [4,4], + [3,3], + [3,4], + [4,3], + [4,4], + [3,3], + [3,4], + [4,3], + [4,4], + [4,4]])) + # Test bins edge case (when bin edges = values) # # Again, the expected behavior is that density2d() should mimic @@ -1180,6 +1655,42 @@ def test_bins_edge_case_1_mask(self): 0,0,0,0,0,0], dtype=bool) ) + def test_bins_edge_case_1_bin_edges(self): + bins = list(range(5)) + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=4.0/31, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_bins_edge_case_1_bin_mask_1(self): + bins = list(range(5)) + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=4.0/31, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 1]], + dtype=bool)) + + def test_bins_edge_case_1_bin_mask_2(self): + bins = list(range(5)) + bin_mask = np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[3,3], + [3,4], + [4,3], + [4,4]])) + def test_bins_edge_case_2_gated_data_1(self): bins = list(range(5)) np.testing.assert_array_equal( @@ -1235,6 +1746,51 @@ def test_bins_edge_case_2_mask(self): 1,1,0,0,1,1], dtype=bool) ) + def test_bins_edge_case_2_bin_edges(self): + bins = list(range(5)) + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=13.0/31, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_bins_edge_case_2_bin_mask_1(self): + bins = list(range(5)) + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.pyramid, bins=bins, gate_fraction=13.0/31, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 1], + [0, 0, 1, 1]], + dtype=bool)) + + def test_bins_edge_case_2_bin_mask_2(self): + bins = list(range(5)) + bin_mask = np.array([[0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 1, 1], + [0, 0, 1, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.pyramid, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,3], + [2,4], + [3,2], + [3,3], + [3,4], + [4,2], + [4,3], + [4,4], + [2,2], + [2,2], + [2,3], + [3,2]])) + def test_bins_edge_case_3_gated_data_1(self): bins = [1,2,3] np.testing.assert_array_equal( @@ -1282,6 +1838,43 @@ def test_bins_edge_case_3_mask(self): 1,0,0,0,0], dtype=bool) ) + def test_bins_edge_case_3_bin_edges(self): + bins = [1,2,3] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=9.0/14, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_bins_edge_case_3_bin_mask_1(self): + bins = [1,2,3] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=9.0/14, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 0], + [0, 1]], + dtype=bool)) + + def test_bins_edge_case_3_bin_mask_2(self): + bins = [1,2,3] + bin_mask = np.array([[0, 0], + [0, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[2,2], + [2,3], + [3,2], + [3,3], + [2,2], + [2,3], + [3,2], + [3,3], + [3,3]])) + def test_bins_edge_case_4_gated_data_1(self): bins = [1,2,3] np.testing.assert_array_equal( @@ -1337,5 +1930,46 @@ def test_bins_edge_case_4_mask(self): 1,0,0,0,0], dtype=bool) ) + def test_bins_edge_case_4_bin_edges(self): + bins = [1,2,3] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/14, sigma=0.0, + full_output=True).bin_edges, + (bins,bins)) + + def test_bins_edge_case_4_bin_mask_1(self): + bins = [1,2,3] + np.testing.assert_array_equal( + FlowCal.gate.density2d( + self.slope, bins=bins, gate_fraction=13.0/14, sigma=0.0, + full_output=True).bin_mask, + np.array([[0, 1], + [1, 1]], + dtype=bool)) + + def test_bins_edge_case_4_bin_mask_2(self): + bins = [1,2,3] + bin_mask = np.array([[0, 1], + [1, 1]], + dtype=bool) + np.testing.assert_array_equal( + FlowCal.gate.density2d(self.slope, + bins=bins, + bin_mask=bin_mask), + np.array([[1,2], + [1,3], + [2,1], + [2,2], + [2,3], + [3,1], + [3,2], + [3,3], + [2,2], + [2,3], + [3,2], + [3,3], + [3,3]])) + if __name__ == '__main__': unittest.main()