Skip to content

Call Spots

Bleed Matrix

view_bleed_matrix

Diagnostic to plot bleed_matrix. If config['call_spots']['bleed_matrix_method'] is 'single', a single bleed_matrix will be plotted. If it is 'separate', one will be shown for each round.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
Source code in coppafish/plot/call_spots/bleed_matrix.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(self, nb: Notebook):
    """
    Diagnostic to plot `bleed_matrix`. If `config['call_spots']['bleed_matrix_method']` is `'single'`,
    a single `bleed_matrix` will be plotted. If it is `'separate'`, one will be shown for each round.

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
    """
    color_norm = nb.call_spots.color_norm_factor[np.ix_(nb.basic_info.use_rounds,
                                                        nb.basic_info.use_channels)]
    n_use_rounds, n_use_channels = color_norm.shape
    single_bm = (color_norm == color_norm[0]).all()
    if single_bm:
        bleed_matrix = [nb.call_spots.bleed_matrix[0][np.ix_(nb.basic_info.use_channels,
                                                             nb.basic_info.use_dyes)]]
        subplot_row_columns = [1, 1]
        subplot_adjust = [0.07, 0.775, 0.095, 0.94]
        fig_size = (9, 5)
    else:
        bleed_matrix = [nb.call_spots.bleed_matrix[r][np.ix_(nb.basic_info.use_channels,
                                                             nb.basic_info.use_dyes)]
                        for r in range(n_use_rounds)]
        if n_use_rounds <= 3:
            subplot_row_columns = [n_use_rounds, 1]
        else:
            n_cols = int(np.ceil(n_use_rounds / 4))  # at most 4 rows
            subplot_row_columns = [int(np.ceil(n_use_rounds / n_cols)), n_cols]
        subplot_adjust = [0.07, 0.775, 0.095, 0.92]
        fig_size = (12, 7)
    n_use_dyes = bleed_matrix[0].shape[1]
    # different norm for each round, each has dims n_use_channels x 1 whereas BM dims is n_use_channels x n_dyes
    # i.e. normalisation just affected by channel not by dye.
    color_norm = [np.expand_dims(color_norm[r], 1) for r in range(n_use_rounds)]
    super().__init__(bleed_matrix, color_norm, subplot_row_columns, subplot_adjust=subplot_adjust,
                     fig_size=fig_size)
    self.ax[0].set_yticks(ticks=np.arange(n_use_channels), labels=nb.basic_info.use_channels)
    if nb.basic_info.dye_names is None:
        self.ax[-1].set_xticks(ticks=np.arange(n_use_dyes), labels=nb.basic_info.use_dyes)
    else:
        self.fig.subplots_adjust(bottom=0.15)
        self.ax[-1].set_xticks(ticks=np.arange(n_use_dyes),
                               labels=np.asarray(nb.basic_info.dye_names)[nb.basic_info.use_dyes], rotation=45)
    if single_bm:
        self.ax[0].set_title('Bleed Matrix')
        self.ax[0].set_ylabel('Color Channel')
        self.ax[0].set_xlabel('Dyes')
    else:
        for i in range(n_use_rounds):
            self.ax[i].set_title(f'Round {nb.basic_info.use_rounds[i]}', size=8)
            plt.suptitle("Bleed Matrices", size=12, x=(subplot_adjust[0] + subplot_adjust[1]) / 2)
        self.fig.supylabel('Color Channel', size=12)
        self.fig.supxlabel('Dyes', size=12, x=(subplot_adjust[0] + subplot_adjust[1]) / 2)
    self.change_norm()  # initialise with method = 'norm'
    plt.show()

view_bled_codes

Diagnostic to plot bleed_matrix. If config['call_spots']['bleed_matrix_method'] is 'single', a single bleed_matrix will be plotted. If it is 'separate', one will be shown for each round.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
Source code in coppafish/plot/call_spots/bleed_matrix.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(self, nb: Notebook):
    """
    Diagnostic to plot `bleed_matrix`. If `config['call_spots']['bleed_matrix_method']` is `'single'`,
    a single `bleed_matrix` will be plotted. If it is `'separate'`, one will be shown for each round.

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
    """
    color_norm = nb.call_spots.color_norm_factor[np.ix_(nb.basic_info.use_rounds,
                                                        nb.basic_info.use_channels)]
    n_use_rounds, n_use_channels = color_norm.shape
    single_bm = (color_norm == color_norm[0]).all()
    if single_bm:
        bleed_matrix = [nb.call_spots.bleed_matrix[0][np.ix_(nb.basic_info.use_channels,
                                                             nb.basic_info.use_dyes)]]
        subplot_row_columns = [1, 1]
        subplot_adjust = [0.07, 0.775, 0.095, 0.94]
        fig_size = (9, 5)
    else:
        bleed_matrix = [nb.call_spots.bleed_matrix[r][np.ix_(nb.basic_info.use_channels,
                                                             nb.basic_info.use_dyes)]
                        for r in range(n_use_rounds)]
        if n_use_rounds <= 3:
            subplot_row_columns = [n_use_rounds, 1]
        else:
            n_cols = int(np.ceil(n_use_rounds / 4))  # at most 4 rows
            subplot_row_columns = [int(np.ceil(n_use_rounds / n_cols)), n_cols]
        subplot_adjust = [0.07, 0.775, 0.095, 0.92]
        fig_size = (12, 7)
    n_use_dyes = bleed_matrix[0].shape[1]
    # different norm for each round, each has dims n_use_channels x 1 whereas BM dims is n_use_channels x n_dyes
    # i.e. normalisation just affected by channel not by dye.
    color_norm = [np.expand_dims(color_norm[r], 1) for r in range(n_use_rounds)]
    super().__init__(bleed_matrix, color_norm, subplot_row_columns, subplot_adjust=subplot_adjust,
                     fig_size=fig_size)
    self.ax[0].set_yticks(ticks=np.arange(n_use_channels), labels=nb.basic_info.use_channels)
    if nb.basic_info.dye_names is None:
        self.ax[-1].set_xticks(ticks=np.arange(n_use_dyes), labels=nb.basic_info.use_dyes)
    else:
        self.fig.subplots_adjust(bottom=0.15)
        self.ax[-1].set_xticks(ticks=np.arange(n_use_dyes),
                               labels=np.asarray(nb.basic_info.dye_names)[nb.basic_info.use_dyes], rotation=45)
    if single_bm:
        self.ax[0].set_title('Bleed Matrix')
        self.ax[0].set_ylabel('Color Channel')
        self.ax[0].set_xlabel('Dyes')
    else:
        for i in range(n_use_rounds):
            self.ax[i].set_title(f'Round {nb.basic_info.use_rounds[i]}', size=8)
            plt.suptitle("Bleed Matrices", size=12, x=(subplot_adjust[0] + subplot_adjust[1]) / 2)
        self.fig.supylabel('Color Channel', size=12)
        self.fig.supxlabel('Dyes', size=12, x=(subplot_adjust[0] + subplot_adjust[1]) / 2)
    self.change_norm()  # initialise with method = 'norm'
    plt.show()

Spot Colors

view_codes

Diagnostic to compare spot_color to bled_code of predicted gene.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'anchor'
Source code in coppafish/plot/call_spots/spot_colors.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def __init__(self, nb: Notebook, spot_no: int, method: str = 'anchor'):
    """
    Diagnostic to compare `spot_color` to `bled_code` of predicted gene.

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
    """
    color_norm = nb.call_spots.color_norm_factor[np.ix_(nb.basic_info.use_rounds,
                                                        nb.basic_info.use_channels)].transpose()
    if method.lower() == 'omp':
        page_name = 'omp'
        config = nb.get_config()['thresholds']
        spot_score = omp_spot_score(nb.omp, config['score_omp_multiplier'], spot_no)
    else:
        page_name = 'ref_spots'
        spot_score = nb.ref_spots.score[spot_no]
    self.spot_color = nb.__getattribute__(page_name).colors[spot_no][
                          np.ix_(nb.basic_info.use_rounds, nb.basic_info.use_channels)].transpose() / color_norm
    # Get spot color after background fitting
    self.background_removed = False
    self.spot_color_pb = fit_background(self.spot_color.T[np.newaxis],
                                        nb.call_spots.background_weight_shift)[0][0].T
    gene_no = nb.__getattribute__(page_name).gene_no[spot_no]

    gene_name = nb.call_spots.gene_names[gene_no]
    gene_color = nb.call_spots.bled_codes_ge[gene_no][np.ix_(nb.basic_info.use_rounds,
                                                             nb.basic_info.use_channels)].transpose()
    super().__init__([self.spot_color, gene_color], color_norm, slider_pos=[0.85, 0.2, 0.01, 0.75],
                     cbar_pos=[0.9, 0.2, 0.03, 0.75])
    self.ax[0].set_title(f'Spot {spot_no}: match {str(np.around(spot_score, 2))} '
                         f'to {gene_name}')
    self.ax[1].set_title(f'Predicted code for Gene {gene_no}: {gene_name}')
    self.ax[0].set_yticks(ticks=np.arange(self.im_data[0].shape[0]), labels=nb.basic_info.use_channels)
    self.ax[1].set_xticks(ticks=np.arange(self.im_data[0].shape[1]))
    self.ax[1].set_xticklabels(['{:.0f} ({:.2f})'.format(r, nb.call_spots.gene_efficiency[gene_no, r])
                                for r in nb.basic_info.use_rounds])
    self.ax[1].set_xlabel('Round (Gene Efficiency)')
    self.fig.supylabel('Color Channel')
    intense_gene_cr = np.where(gene_color > self.intense_gene_thresh)
    for i in range(len(intense_gene_cr[0])):
        for j in range(2):
            # can't add rectangle to multiple axes hence second for loop
            rectangle = plt.Rectangle((intense_gene_cr[1][i]-0.5, intense_gene_cr[0][i]-0.5), 1, 1,
                                      fill=False, ec="lime", linestyle=':', lw=2)
            self.ax[j].add_patch(rectangle)

    self.background_button_ax = self.fig.add_axes([0.85, 0.1, 0.1, 0.05])
    self.background_button = Button(self.background_button_ax, 'Background', hovercolor='0.275')
    self.background_button.label.set_color(self.norm_button_color)
    self.background_button.on_clicked(self.change_background)

    self.change_norm()  # initialise with method = 'norm'
    plt.show()

view_spot

Diagnostic to show intensity of each color channel / round in neighbourhood of spot. Will show a grid of n_use_channels x n_use_rounds subplots.

Requires access to nb.file_names.tile_dir

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'anchor'
im_size int

Radius of image to be plotted for each channel/round.

8
Source code in coppafish/plot/call_spots/spot_colors.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def __init__(self, nb: Notebook, spot_no: int, method: str = 'anchor', im_size: int = 8):
    """
    Diagnostic to show intensity of each color channel / round in neighbourhood of spot.
    Will show a grid of `n_use_channels x n_use_rounds` subplots.

    !!! warning "Requires access to `nb.file_names.tile_dir`"

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
        im_size: Radius of image to be plotted for each channel/round.
    """
    color_norm = nb.call_spots.color_norm_factor[np.ix_(nb.basic_info.use_rounds,
                                                        nb.basic_info.use_channels)].transpose()
    if method.lower() == 'omp':
        config = nb.get_config()['thresholds']
        page_name = 'omp'
        spot_score = omp_spot_score(nb.omp, config['score_omp_multiplier'], spot_no)
    else:
        page_name = 'ref_spots'
        spot_score = nb.ref_spots.score[spot_no]
    gene_no = nb.__getattribute__(page_name).gene_no[spot_no]
    t = nb.__getattribute__(page_name).tile[spot_no]
    spot_yxz = nb.__getattribute__(page_name).local_yxz[spot_no]

    gene_name = nb.call_spots.gene_names[gene_no]
    gene_color = nb.call_spots.bled_codes_ge[gene_no][np.ix_(nb.basic_info.use_rounds,
                                                             nb.basic_info.use_channels)].transpose().flatten()
    n_use_channels, n_use_rounds = color_norm.shape
    color_norm = [val for val in color_norm.flatten()]
    spot_yxz_global = spot_yxz + nb.stitch.tile_origin[t]
    im_size = [im_size, im_size]  # Useful for debugging to have different im_size_y, im_size_x.
    # Subtlety here, may have y-axis flipped, but I think it is correct:
    # note im_yxz[1] refers to point at max_y, min_x+1, z. So when reshape and set plot_extent, should be correct.
    # I.e. im = np.zeros(49); im[1] = 1; im = im.reshape(7,7); plt.imshow(im, extent=[-0.5, 6.5, -0.5, 6.5])
    # will show the value 1 at max_y, min_x+1.
    im_yxz = np.array(np.meshgrid(np.arange(spot_yxz[0]-im_size[0], spot_yxz[0]+im_size[0]+1)[::-1],
                                  np.arange(spot_yxz[1]-im_size[1], spot_yxz[1]+im_size[1]+1), spot_yxz[2]),
                      dtype=np.int16).T.reshape(-1, 3)
    im_diameter = [2*im_size[0]+1, 2*im_size[1]+1]
    spot_colors = get_spot_colors(im_yxz, t, nb.register.transform, nb.file_names, nb.basic_info)
    spot_colors = np.moveaxis(spot_colors, 1, 2)  # put round as the last axis to match color_norm
    spot_colors = spot_colors.reshape(im_yxz.shape[0], -1)
    # reshape
    cr_images = [spot_colors[:, i].reshape(im_diameter[0], im_diameter[1]) / color_norm[i]
                 for i in range(spot_colors.shape[1])]
    subplot_adjust = [0.07, 0.775, 0.075, 0.92]
    super().__init__(cr_images, color_norm, subplot_row_columns=[n_use_channels, n_use_rounds],
                     subplot_adjust=subplot_adjust, fig_size=(13, 8))
    # set x, y coordinates to be those of the global coordinate system
    plot_extent = [im_yxz[:, 1].min()-0.5+nb.stitch.tile_origin[t, 1],
                   im_yxz[:, 1].max()+0.5+nb.stitch.tile_origin[t, 1],
                   im_yxz[:, 0].min()-0.5+nb.stitch.tile_origin[t, 0],
                   im_yxz[:, 0].max()+0.5+nb.stitch.tile_origin[t, 0]]
    for i in range(self.n_images):
        # Add cross-hair
        if gene_color[i] > self.intense_gene_thresh:
            cross_hair_color = 'lime'  # different color if expected large intensity
            linestyle = '--'
            self.ax[i].tick_params(color='lime', labelcolor='lime')
            for spine in self.ax[i].spines.values():
                spine.set_edgecolor('lime')
        else:
            cross_hair_color = 'k'
            linestyle = ':'
        self.ax[i].axes.plot([spot_yxz_global[1], spot_yxz_global[1]], [plot_extent[2], plot_extent[3]],
                             cross_hair_color, linestyle=linestyle, lw=1)
        self.ax[i].axes.plot([plot_extent[0], plot_extent[1]], [spot_yxz_global[0], spot_yxz_global[0]],
                             cross_hair_color, linestyle=linestyle, lw=1)
        self.im[i].set_extent(plot_extent)
        self.ax[i].tick_params(labelbottom=False, labelleft=False)
        # Add axis labels to subplots of far left column or bottom row
        if i % n_use_rounds == 0:
            self.ax[i].set_ylabel(f'{nb.basic_info.use_channels[int(i/n_use_rounds)]}')
        if i >= self.n_images - n_use_rounds:
            r = nb.basic_info.use_rounds[i-(self.n_images - n_use_rounds)]
            self.ax[i].set_xlabel('{:.0f} ({:.2f})'.format(r, nb.call_spots.gene_efficiency[gene_no, r]))


    self.ax[0].set_xticks([spot_yxz_global[1]])
    self.ax[0].set_yticks([spot_yxz_global[0]])
    self.fig.supylabel('Color Channel', size=14)
    self.fig.supxlabel('Round (Gene Efficiency)', size=14, x=(subplot_adjust[0] + subplot_adjust[1]) / 2)
    plt.suptitle(f'Spot {spot_no}: match {str(np.around(spot_score, decimals=2))} '
                 f'to {gene_name}', x=(subplot_adjust[0] + subplot_adjust[1]) / 2, size=16)
    self.change_norm()
    plt.show()

view_intensity

Diagnostic to show how intensity is computed from spot_color.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'anchor'
Source code in coppafish/plot/call_spots/spot_colors.py
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
def __init__(self, nb: Notebook, spot_no: int, method: str = 'anchor'):
    """
    Diagnostic to show how intensity is computed from `spot_color`.

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
    """
    color_norm = nb.call_spots.color_norm_factor[np.ix_(nb.basic_info.use_rounds,
                                                        nb.basic_info.use_channels)].transpose()
    if method.lower() == 'omp':
        page_name = 'omp'
        config = nb.get_config()['thresholds']
    else:
        page_name = 'ref_spots'
    intensity_saved = nb.__getattribute__(page_name).intensity[spot_no]
    intensity_thresh = get_intensity_thresh(nb)
    spot_color = nb.__getattribute__(page_name).colors[spot_no][
                     np.ix_(nb.basic_info.use_rounds, nb.basic_info.use_channels)].transpose() / color_norm
    subplot_adjust = [0.07, 0.775, 0.1, 0.91]
    super().__init__([spot_color], color_norm, subplot_adjust=subplot_adjust)
    if intensity_saved > intensity_thresh:
        color = 'w'
    else:
        color = 'r'
    spot_color_symbol = r"$\mathbf{\zeta_s}$"
    intensity_symbol = r"$\chi_s$, (median of $\max_c\zeta_{s_{rc}}$ indicated in green)"
    self.ax[0].set_title(f'Spot Color, {spot_color_symbol}, for spot {spot_no}\n'
                         f'Intensity, {intensity_symbol} = {str(np.around(intensity_saved, 3))}', color=color)
    self.ax[0].set_yticks(ticks=np.arange(self.im_data[0].shape[0]), labels=nb.basic_info.use_channels)
    self.ax[0].set_xticks(ticks=np.arange(self.im_data[0].shape[1]), labels=nb.basic_info.use_rounds)
    self.ax[0].set_xlabel('Round')
    self.fig.supylabel('Color Channel')
    # Highlight max channel in each round which contributes to intensity
    max_channels = np.argmax(self.im_data[0], axis=0)
    for r in range(len(nb.basic_info.use_rounds)):
        # can't add rectangle to multiple axes hence second for loop
        rectangle = plt.Rectangle((r-0.5, max_channels[r]-0.5), 1, 1,
                                  fill=False, ec='lime', linestyle=':', lw=4)
        self.ax[0].add_patch(rectangle)
    self.change_norm()  # initialise with method = 'norm'
    plt.show()

ColorPlotBase

This is the base class for plots with multiple subplots and with a slider to change the color axis and a button to change the normalisation. After initialising, the function change_norm() should be run to plot normalised images. This will change self.method from 'raw' to 'norm'.

Parameters:

Name Type Description Default
images List

float [n_images] Each image is n_y x n_x (x n_z). This is the normalised image. There will be a subplot for each image and if it is 3D, the first z-plane will be set as the starting data, self.im_data while the full 3d data will be saved as self.im_data_3d.

required
norm_factor Optional[Union[np.ndarray, List]]

float [n_images] norm_factor[i] is the value to multiply images[i] to give raw image. norm_factor[i] is either an integer or an array of same dimensions as image[i]. If a single norm_factor given, assume same for each image.

required
subplot_row_columns Optional[List]

[n_rows, n_columns] The subplots will be arranged into n_rows and n_columns. If not given, n_columns will be 1.

None
fig_size Optional[Tuple]

[width, height] Size of figure to plot in inches. If not given, will be set to (9, 5).

None
subplot_adjust Optional[List]

[left, right, bottom, top] The position of the sides of the subplots in the figure. I.e., we don't want subplot to overlap with cbar, slider or buttom and this ensures that. If not given, will be set to [0.07, 0.775, 0.095, 0.94].

None
cbar_pos Optional[List]

[left, bottom, width, height] Position of color axis. If not given, will be set to [0.9, 0.15, 0.03, 0.8].

None
slider_pos Optional[List]

[left, bottom, width, height] Position of slider that controls color axis. If not given, will be set to [0.85, 0.15, 0.01, 0.8].

None
button_pos Optional[List]

[left, bottom, width, height] Position of button which triggers change of normalisation. If not given, will be set to [0.85, 0.02, 0.1, 0.05].

None
Source code in coppafish/plot/call_spots/spot_colors.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def __init__(self, images: List, norm_factor: Optional[Union[np.ndarray, List]],
             subplot_row_columns: Optional[List] = None,
             fig_size: Optional[Tuple] = None, subplot_adjust: Optional[List] = None,
             cbar_pos: Optional[List] = None, slider_pos: Optional[List] = None,
             button_pos: Optional[List] = None):
    """
    This is the base class for plots with multiple subplots and with a slider to change the color axis and a button
    to change the normalisation.
    After initialising, the function `change_norm()` should be run to plot normalised images.
    This will change `self.method` from `'raw'` to `'norm'`.

    Args:
        images: `float [n_images]`
            Each image is `n_y x n_x (x n_z)`. This is the normalised image.
            There will be a subplot for each image and if it is 3D, the first z-plane will be set as the
            starting data, `self.im_data` while the full 3d data will be saved as `self.im_data_3d`.
        norm_factor: `float [n_images]`
            `norm_factor[i]` is the value to multiply `images[i]` to give raw image.
            `norm_factor[i]` is either an integer or an array of same dimensions as `image[i]`.
            If a single `norm_factor` given, assume same for each image.
        subplot_row_columns: `[n_rows, n_columns]`
            The subplots will be arranged into `n_rows` and `n_columns`.
            If not given, `n_columns` will be 1.
        fig_size: `[width, height]`
            Size of figure to plot in inches.
            If not given, will be set to `(9, 5)`.
        subplot_adjust: `[left, right, bottom, top]`
            The position of the sides of the subplots in the figure.
            I.e., we don't want subplot to overlap with cbar, slider or buttom and this ensures that.
            If not given, will be set to `[0.07, 0.775, 0.095, 0.94]`.
        cbar_pos: `[left, bottom, width, height]`
            Position of color axis.
            If not given, will be set to `[0.9, 0.15, 0.03, 0.8]`.
        slider_pos: `[left, bottom, width, height]`
            Position of slider that controls color axis.
            If not given, will be set to `[0.85, 0.15, 0.01, 0.8]`.
        button_pos: `[left, bottom, width, height]`
            Position of button which triggers change of normalisation.
            If not given, will be set to `[0.85, 0.02, 0.1, 0.05]`.
    """
    # When bled code of a gene more than this, that particular round/channel will be highlighted in plots
    self.intense_gene_thresh = 0.2
    self.n_images = len(images)
    if subplot_row_columns is None:
        subplot_row_columns = [self.n_images, 1]
    # Default positions
    if fig_size is None:
        fig_size = (9, 5)
    if subplot_adjust is None:
        subplot_adjust = [0.07, 0.775, 0.095, 0.94]
    if cbar_pos is None:
        cbar_pos = [0.9, 0.15, 0.03, 0.8]
    if slider_pos is None:
        self.slider_pos = [0.85, 0.15, 0.01, 0.8]
    else:
        self.slider_pos = slider_pos
    if button_pos is None:
        button_pos = [0.85, 0.02, 0.1, 0.05]
    if not isinstance(norm_factor, list):
        # allow for different norm for each image
        if norm_factor is None:
            self.color_norm = None
        else:
            self.color_norm = [norm_factor, ] * self.n_images
    else:
        self.color_norm = norm_factor
    self.im_data = [val for val in images]  # put in order channels, rounds
    self.method = 'raw' if self.color_norm is not None else 'norm'
    if self.color_norm is None:
        self.caxis_info = {'norm': {}}
    else:
        self.caxis_info = {'norm': {}, 'raw': {}}
    for key in self.caxis_info:
        if key == 'norm':
            im_data = self.im_data
            self.caxis_info[key]['format'] = '%.2f'
        else:
            im_data = [self.im_data[i] * self.color_norm[i] for i in range(self.n_images)]
            self.caxis_info[key]['format'] = '%.0f'
        self.caxis_info[key]['min'] = np.min([im.min() for im in im_data] + [-1e-20])
        self.caxis_info[key]['max'] = np.max([im.max() for im in im_data] + [1e-20])
        self.caxis_info[key]['max'] = np.max([self.caxis_info[key]['max'], -self.caxis_info[key]['min']])
        # have equal either side of zero so small negatives don't look large
        self.caxis_info[key]['min'] = -self.caxis_info[key]['max']
        self.caxis_info[key]['clims'] = [self.caxis_info[key]['min'], self.caxis_info[key]['max']]
        # cmap_norm is so cmap is white at 0.
        self.caxis_info[key]['cmap_norm'] = \
            matplotlib.colors.TwoSlopeNorm(vmin=self.caxis_info[key]['min'],
                                           vcenter=0, vmax=self.caxis_info[key]['max'])

    self.fig, self.ax = plt.subplots(subplot_row_columns[0], subplot_row_columns[1], figsize=fig_size,
                                     sharex=True, sharey=True)
    if self.n_images == 1:
        self.ax = [self.ax]  # need it to be a list
    elif subplot_row_columns[0] > 1 and subplot_row_columns[1] > 1:
        self.ax = self.ax.flatten()  # only have 1 ax index
    oob_axes = np.arange(self.n_images, subplot_row_columns[0] * subplot_row_columns[1])
    if oob_axes.size > 0:
        for i in oob_axes:
            self.fig.delaxes(self.ax[i])  # delete excess subplots
        self.ax = self.ax[:self.n_images]
    self.fig.subplots_adjust(left=subplot_adjust[0], right=subplot_adjust[1], bottom=subplot_adjust[2],
                             top=subplot_adjust[3])
    self.im = [None] * self.n_images
    if self.im_data[0].ndim == 3:
        # For 3D data, start by showing just the first plane
        self.im_data_3d = self.im_data.copy()
        self.im_data = [val[:, :, 0] for val in self.im_data_3d]
        if self.color_norm is not None:
            self.color_norm_3d = self.color_norm.copy()
            self.color_norm = [val[:, :, 0] for val in self.color_norm_3d]
    else:
        self.im_data_3d = None
        self.color_norm_3d = None
    # initialise plots with a zero array
    for i in range(self.n_images):
        self.im[i] = self.ax[i].imshow(np.zeros(self.im_data[0].shape[:2]), cmap="seismic", aspect='auto',
                                       norm=self.caxis_info[self.method]['cmap_norm'])
    cbar_ax = self.fig.add_axes(cbar_pos)  # left, bottom, width, height
    self.fig.colorbar(self.im[0], cax=cbar_ax)

    self.slider_ax = self.fig.add_axes(self.slider_pos)
    self.color_slider = None
    if self.color_norm is not None:
        self.norm_button_color = 'white'
        self.norm_button_color_press = 'red'
        if self.method == 'raw':
            current_color = self.norm_button_color_press
        else:
            current_color = self.norm_button_color
        self.norm_button_ax = self.fig.add_axes(button_pos)
        self.norm_button = Button(self.norm_button_ax, 'Norm', hovercolor='0.275')
        self.norm_button.label.set_color(current_color)
        self.norm_button.on_clicked(self.change_norm)

Dot Product

view_score

This produces 4 plots on the first row, showing spot_color, residual, variance and weight squared (basically the normalised inverse variance).

The bottom row shows the contribution from background and genes to the variance.

The iteration as well as the alpha and beta parameters used to compute the weight can be changed through the text boxes.

If the weight plot is clicked on, the view_weight plot will open for the current iteration.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing at least the call_spots and ref_spots pages.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'omp'
g Optional[int]

Gene to view dot product calculation for. If left as None, will show the gene with the largest dot product score.

None
iter int

Iteration in OMP to view the dot product calculation for i.e. the number of genes which have already been fitted (iter=0 will have only background fit, iter=1 will have background + 1 gene etc.). The score saved as nb.ref_spots.score can be viewed with iter=0.

0
omp_fit_info Optional[List]

This is a list containing [track_info, bled_codes, dp_thresh]. It is only ever used to call this function from view_omp_fit.

None
check_weight bool

When this is True, we raise an error if weight computed here is different to that computed with get_track_info.

False
Source code in coppafish/plot/call_spots/dot_product.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def __init__(self, nb: Notebook, spot_no: int, method: str = 'omp', g: Optional[int] = None,
             iter: int = 0, omp_fit_info: Optional[List] = None, check_weight: bool = False):
    """
    This produces 4 plots on the first row, showing spot_color, residual, variance and weight squared (basically
    the normalised inverse variance).

    The bottom row shows the contribution from background and genes to the variance.

    The iteration as well as the alpha and beta parameters used to compute the weight can be changed through
    the text boxes.

    If the weight plot is clicked on, the `view_weight` plot will open for the current iteration.

    Args:
        nb: *Notebook* containing at least the *call_spots* and *ref_spots* pages.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
        g: Gene to view dot product calculation for.
            If left as `None`, will show the gene with the largest dot product score.
        iter: Iteration in OMP to view the dot product calculation for i.e. the number of genes
            which have already been fitted (`iter=0` will have only background fit,
            `iter=1` will have background + 1 gene etc.).
            The score saved as `nb.ref_spots.score` can be viewed with `iter=0`.
        omp_fit_info: This is a list containing `[track_info, bled_codes, dp_thresh]`.
            It is only ever used to call this function from `view_omp_fit`.
        check_weight: When this is `True`, we raise an error if weight computed here is different
            to that computed with `get_track_info`.
    """
    self.spot_no = spot_no
    if omp_fit_info is None:
        self.track_info, self.bled_codes, self.dp_thresh = get_track_info(nb, spot_no, method)
    else:
        self.track_info, self.bled_codes, self.dp_thresh = omp_fit_info
    self.n_genes, self.n_rounds_use, self.n_channels_use = self.bled_codes.shape
    # allow to view dot product with background
    self.bled_codes = np.append(self.bled_codes, self.track_info['background_codes'], axis=0)
    self.n_genes_all = self.bled_codes.shape[0]
    self.spot_color = self.track_info['residual'][0]

    # Get saved values if anchor method
    if method.lower() != 'omp':
        self.g_saved = nb.ref_spots.gene_no[spot_no]
        if self.track_info['gene_added'][2] < self.n_genes:
            # Possibility best gene will be background here, but impossible for saved best gene to be background
            if self.track_info['gene_added'][2] != self.g_saved and check_weight:
                raise ValueError(f"\nBest gene saved was {self.g_saved} but with parameters used here, it "
                                 f"was {self.track_info['gene_added'][2]}.\nEnsure that alpha and beta in "
                                 f"config['call_spots'] have not been changed.\n"
                                 f"Set check_weight=False to skip this error.")
        self.dp_val_saved = nb.ref_spots.score[spot_no]
        config_name = 'call_spots'
    else:
        self.g_saved = None
        self.dp_val_saved = None
        config_name = 'omp'

    # Get default dot product params
    config = nb.get_config()
    self.alpha = config[config_name]['alpha']
    self.beta = config[config_name]['beta']
    self.dp_norm_shift = nb.call_spots.dp_norm_shift
    self.check_weight = check_weight

    self.n_iter = self.track_info['residual'].shape[0] - 2  # first two indices in track is not added gene
    if iter >= self.n_iter or iter < 0:
        warnings.warn(f"Only {self.n_iter} iterations for this pixel but iter={iter}, "
                      f"setting iter = {self.n_iter - 1}.")
        iter = self.n_iter - 1
    self.iter = iter
    if g is None:
        g = self.track_info['gene_added'][2 + iter]
    self.g = g
    self.gene_names = list(nb.call_spots.gene_names) + [f'BG{i}' for i in nb.basic_info.use_channels]
    self.use_channels = nb.basic_info.use_channels
    self.use_rounds = nb.basic_info.use_rounds

    # Initialize data
    self.dp_val = None
    self.dp_weight_val = None
    self.im_data = None
    self.update_data()

    # Initialize plot
    self.title = None
    self.vmax = None
    self.get_cax_lim()
    self.fig = plt.figure(figsize=(16, 7))
    ax1 = self.fig.add_subplot(2, 4, 1)
    ax2 = self.fig.add_subplot(2, 4, 5)
    ax3 = self.fig.add_subplot(2, 4, 2)
    ax4 = self.fig.add_subplot(2, 4, 6)
    ax5 = self.fig.add_subplot(1, 4, 3)
    ax6 = self.fig.add_subplot(2, 4, 4)
    ax7 = self.fig.add_subplot(2, 4, 8)
    self.ax = [ax1, ax2, ax3, ax4, ax5, ax6, ax7]
    self.ax[0].get_shared_x_axes().join(self.ax[0], *self.ax[1:])
    self.ax[0].get_shared_y_axes().join(self.ax[0], *self.ax[1:])
    self.subplot_adjust = [0.05, 0.87, 0.05, 0.9]
    self.fig.subplots_adjust(left=self.subplot_adjust[0], right=self.subplot_adjust[1],
                             bottom=self.subplot_adjust[2], top=self.subplot_adjust[3])
    self.im = [None] * len(self.ax)
    self.set_up_plots()
    self.set_titles()
    self.add_rectangles()

    # Text boxes to change parameters
    text_box_labels = ['Gene', 'Iteration', r'$\alpha$', r'$\beta$', r'dp_shift, $\lambda_d$']
    text_box_values = [self.g, self.iter, self.alpha, self.beta, self.dp_norm_shift]
    text_box_funcs = [self.update_g, self.update_iter, self.update_alpha, self.update_beta,
                      self.update_dp_norm_shift]
    self.text_boxes = [None] * len(text_box_labels)
    for i in range(len(text_box_labels)):
        text_ax = self.fig.add_axes([self.subplot_adjust[1] + 0.05, self.subplot_adjust[3] - 0.15 * (i+1),
                                     0.05, 0.04])
        self.text_boxes[i] = TextBox(text_ax, text_box_labels[i], text_box_values[i], color='k',
                                     hovercolor=[0.2, 0.2, 0.2])
        self.text_boxes[i].cursor.set_color('r')
        # change text box title to be above not to the left of box
        label = text_ax.get_children()[0]  # label is a child of the TextBox axis
        label.set_position([0.5, 1.75])  # [x,y] - change here to set the position
        # centering the text
        label.set_verticalalignment('top')
        label.set_horizontalalignment('center')
        self.text_boxes[i].on_submit(text_box_funcs[i])

    # Make so if click on weight plot, it opens view_weight
    self.nb = nb
    self.method = method
    self.fig.canvas.mpl_connect('button_press_event', self.show_weight)
    plt.show()

Weight

view_weight

This produces at least 5 plots which show how the weight used in the dot product score was calculated.

The iteration as well as the alpha and beta parameters used to compute the weight can be changed with the text boxes.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing at least the call_spots and ref_spots pages.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'omp'
iter int

Iteration in OMP to view the dot product calculation for i.e. the number of genes which have already been fitted (iter=0 will have only background fit, iter=1 will have background + 1 gene etc.). The score saved as nb.ref_spots.score can be viewed with iter=0.

0
score_info Optional[List]

This is a list containing [track_info, bled_codes, weight_vmax]. It is only ever used to call this function from view_score.

None
check_weight bool

When this is True, we raise an error if weight computed here is different to that computed with get_track_info.

True
Source code in coppafish/plot/call_spots/weight.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def __init__(self, nb: Notebook, spot_no: int, method: str = 'omp',
             iter: int = 0, alpha: Optional[float] = None, beta: Optional[float] = None,
             score_info: Optional[List] = None, check_weight: bool = True):
    """
    This produces at least 5 plots which show how the weight used in the dot product score was calculated.

    The iteration as well as the alpha and beta parameters used to compute the weight can be changed
    with the text boxes.

    Args:
        nb: *Notebook* containing at least the *call_spots* and *ref_spots* pages.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
        iter: Iteration in OMP to view the dot product calculation for i.e. the number of genes
            which have already been fitted (`iter=0` will have only background fit,
            `iter=1` will have background + 1 gene etc.).
            The score saved as `nb.ref_spots.score` can be viewed with `iter=0`.
        score_info: This is a list containing `[track_info, bled_codes, weight_vmax]`.
            It is only ever used to call this function from `view_score`.
        check_weight: When this is `True`, we raise an error if weight computed here is different
            to that computed with `get_track_info`.
    """
    self.spot_no = spot_no
    if score_info is None:
        self.track_info, self.bled_codes = get_track_info(nb, spot_no, method)[:2]
        self.weight_vmax = None
    else:
        self.track_info, self.bled_codes, self.weight_vmax = score_info
    self.n_genes, self.n_rounds_use, self.n_channels_use = self.bled_codes.shape
    # allow to view dot product with background
    self.bled_codes = np.append(self.bled_codes, self.track_info['background_codes'], axis=0)
    self.n_genes_all = self.bled_codes.shape[0]
    self.spot_color = self.track_info['residual'][0]

    if method.lower() == 'omp':
        config_name = 'omp'
    else:
        config_name = 'call_spots'
    # Get default params
    config = nb.get_config()
    if alpha is None:
        alpha = config[config_name]['alpha']
    if beta is None:
        beta = config[config_name]['beta']
    self.alpha = alpha
    self.beta = beta
    self.check_weight = check_weight

    self.n_iter = self.track_info['residual'].shape[0] - 2  # first two indices in track is not added gene
    if iter >= self.n_iter or iter < 0:
        warnings.warn(f"Only {self.n_iter} iterations for this pixel but iter={iter}, "
                      f"setting iter = {self.n_iter - 1}.")
        iter = self.n_iter - 1
    self.iter = iter
    self.gene_names = list(nb.call_spots.gene_names) + [f'BG{i}' for i in nb.basic_info.use_channels]
    self.use_channels = nb.basic_info.use_channels
    self.use_rounds = nb.basic_info.use_rounds

    # Initialize data
    self.n_plots = self.n_plots_top_row + self.n_iter
    self.update_data()

    # Initialize plots
    self.vmax = None
    self.get_cax_lim()
    n_rows = 2
    n_cols = int(np.max([self.n_plots_top_row, self.n_iter]))
    self.fig = plt.figure(figsize=(16, 7))
    self.ax = []
    for i in range(self.n_plots):
        if i >= self.n_plots_top_row:
            # So goes to next row
            self.ax += [self.fig.add_subplot(n_rows, n_cols, n_cols + i + 1 - self.n_plots_top_row)]
        else:
            self.ax += [self.fig.add_subplot(n_rows, n_cols, i + 1)]
    # Y and X axis are the same for all plots hence share
    self.ax[0].get_shared_x_axes().join(self.ax[0], *self.ax[1:])
    self.ax[0].get_shared_y_axes().join(self.ax[0], *self.ax[1:])
    self.subplot_adjust = [0.05, 0.87, 0.07, 0.9]
    self.fig.subplots_adjust(left=self.subplot_adjust[0], right=self.subplot_adjust[1],
                             bottom=self.subplot_adjust[2], top=self.subplot_adjust[3])
    self.im = [None] * self.n_plots
    self.variance_cbar = None
    self.set_up_plots()
    self.set_titles()
    self.add_rectangles()

    # Text boxes to change parameters
    text_box_labels = ['Iteration', r'$\alpha$', r'$\beta$']
    text_box_values = [self.iter, self.alpha, self.beta]
    text_box_funcs = [self.update_iter, self.update_alpha, self.update_beta]
    self.text_boxes = [None] * len(text_box_labels)
    for i in range(len(text_box_labels)):
        text_ax = self.fig.add_axes([self.subplot_adjust[1] + 0.05, self.subplot_adjust[3] - 0.15 * (i+1),
                                     0.05, 0.04])
        self.text_boxes[i] = TextBox(text_ax, text_box_labels[i], text_box_values[i], color='k',
                                     hovercolor=[0.2, 0.2, 0.2])
        self.text_boxes[i].cursor.set_color('r')
        # change text box title to be above not to the left of box
        label = text_ax.get_children()[0]  # label is a child of the TextBox axis
        label.set_position([0.5, 1.75])  # [x,y] - change here to set the position
        # centering the text
        label.set_verticalalignment('top')
        label.set_horizontalalignment('center')
        self.text_boxes[i].on_submit(text_box_funcs[i])

    self.nb = nb
    self.method = method
    self.fig.canvas.mpl_connect('button_press_event', self.show_background)
    plt.show()

Background

view_background

This shows how the background coefficients were calculated.

The weighted dot product is equal to weight multiplied by dot product. Coefficient for background gene c is the sum over all rounds of weighted dot product in channel c.

Also shows residual after removing background.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing at least the call_spots and ref_spots pages.

required
spot_no int

Spot of interest to be plotted.

required
method str

'anchor' or 'omp'. Which method of gene assignment used i.e. spot_no belongs to ref_spots or omp page of Notebook.

'omp'
check bool

When this is True, we raise an error if background coefs computed here is different to that computed with get_track_info.

True
track_info Optional[List]

To use when calling from view_weight.

None
Source code in coppafish/plot/call_spots/background.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def __init__(self, nb: Notebook, spot_no: int, method: str = 'omp', check: bool = True,
             track_info: Optional[List] = None):
    """
    This shows how the background coefficients were calculated.

    The weighted dot product is equal to weight multiplied by dot product.
    Coefficient for background gene c is the sum over all rounds of weighted dot product in channel c.

    Also shows residual after removing background.

    Args:
        nb: *Notebook* containing at least the *call_spots* and *ref_spots* pages.
        spot_no: Spot of interest to be plotted.
        method: `'anchor'` or `'omp'`.
            Which method of gene assignment used i.e. `spot_no` belongs to `ref_spots` or `omp` page of Notebook.
        check: When this is `True`, we raise an error if background coefs computed here is different
            to that computed with `get_track_info`.
        track_info: To use when calling from `view_weight`.
    """
    self.spot_no = spot_no
    if track_info is None:
        self.track_info = get_track_info(nb, spot_no, method)[0]
        self.color_vmax = None
    else:
        self.track_info, self.color_vmax = track_info
    self.n_genes_all = self.track_info['coef'][0].size
    self.spot_color = self.track_info['residual'][0]
    self.n_rounds_use, self.n_channels_use = self.spot_color.shape
    self.n_genes = self.n_genes_all - self.n_channels_use
    self.use_channels = nb.basic_info.use_channels
    self.use_rounds = nb.basic_info.use_rounds

    self.background_weight_shift = nb.call_spots.background_weight_shift
    self.check = check

    # Initialize data
    self.im_data = None
    self.update_data()
    hi = 5

    # Initialize plots
    self.vmax = None
    self.weight_dp_max_initial = None
    self.get_cax_lim()
    self.fig = plt.figure(figsize=(16, 7))
    self.ax = []
    ax1 = self.fig.add_subplot(2, 5, 1)
    ax2 = self.fig.add_subplot(2, 5, 6)
    ax3 = self.fig.add_subplot(1, 5, 2)
    ax4 = self.fig.add_subplot(2, 5, 3)
    ax5 = self.fig.add_subplot(2, 5, 8)
    ax6 = self.fig.add_subplot(2, 5, 4)
    ax7 = self.fig.add_subplot(2, 5, 9)
    ax8 = self.fig.add_subplot(2, 5, 5)
    ax9 = self.fig.add_subplot(2, 5, 10)
    self.ax = [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]
    self.ax[0].get_shared_y_axes().join(self.ax[0], *self.ax[1:])
    # Coef plots have different x-axis
    self.ax[self.coef_plot_ind[0]].get_shared_x_axes().join(self.ax[self.coef_plot_ind[0]],
                                                            self.ax[self.coef_plot_ind[1]])
    # All other plots have same axis
    self.ax[0].get_shared_x_axes().join(self.ax[0], *self.ax[1:self.coef_plot_ind[0]])
    self.ax[0].get_shared_x_axes().join(self.ax[0], *self.ax[self.coef_plot_ind[1]+1:])
    self.subplot_adjust = [0.05, 0.97, 0.07, 0.9]
    self.fig.subplots_adjust(left=self.subplot_adjust[0], right=self.subplot_adjust[1],
                             bottom=self.subplot_adjust[2], top=self.subplot_adjust[3])
    self.im = [None] * len(self.ax)
    self.set_up_plots()
    self.set_titles()

    weight_plot_pos = self.ax[self.weight_plot_ind].get_position()
    box_x = np.mean([weight_plot_pos.x0, weight_plot_pos.x1])
    box_y = weight_plot_pos.y0
    text_ax = self.fig.add_axes([box_x, box_y - 0.15, 0.05, 0.04])
    self.text_box = TextBox(text_ax, r'background_shift, $\lambda_b$', self.background_weight_shift, color='k',
                                     hovercolor=[0.2, 0.2, 0.2])
    self.text_box.cursor.set_color('r')
    label = text_ax.get_children()[0]  # label is a child of the TextBox axis
    label.set_position([0.5, 1.75])  # [x,y] - change here to set the position
    # centering the text
    label.set_verticalalignment('top')
    label.set_horizontalalignment('center')
    self.text_box.on_submit(self.update_background_weight_shift)

    plt.show()

Gene Counts

gene_counts

This shows the number of reference spots assigned to each gene which pass the quality thresholding based on the parameters score_thresh and intensity_thresh.

If nb has the OMP page, then the number of omp spots will also be shown, where the quality thresholding is based on score_omp_thresh, score_omp_multiplier and intensity_thresh.

There will also be a second reference spots histogram, the difference with this is that the spots were allowed to be assigned to some fake genes with bled_codes specified through fake_bled_codes.

Note

fake_bled_codes have dimension n_fake x nbp_basic.n_rounds x nbp_basic.n_channels not n_fake x len(nbp_basic.use_rounds) x len(nbp_basic.use_channels).

Parameters:

Name Type Description Default
nb Notebook

Notebook containing at least call_spots page.

required
fake_bled_codes Optional[np.ndarray]

float [n_fake_genes x n_rounds x n_channels]. colors of fake genes to find dot product with Will find new gene assignment of anchor spots to new set of bled_codes which include these in addition to bled_codes_ge. By default, will have a fake gene for each round and channel such that it is \(1\) in round r, channel \(c\) and 0 everywhere else.

None
fake_gene_names Optional[List[str]]

str [n_fake_genes]. Can give name of each fake gene. If None, fake gene \(i\) will be called FAKE:\(i\).

None
score_thresh Optional[float]

Threshold for score for ref_spots. Can be changed with text box.

None
intensity_thresh Optional[float]

Threshold for intensity. Can be changed with text box.

None
score_omp_thresh Optional[float]

Threshold for score for omp_spots. Can be changed with text box.

None
score_omp_multiplier Optional[float]

Can specify the value of score_omp_multiplier to use to compute omp score. If None, will use value in config file. Can be changed with text box.

None
Source code in coppafish/plot/call_spots/gene_counts.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def __init__(self, nb: Notebook, fake_bled_codes: Optional[np.ndarray] = None,
             fake_gene_names: Optional[List[str]] = None,
             score_thresh: Optional[float] = None, intensity_thresh: Optional[float] = None,
             score_omp_thresh: Optional[float] = None, score_omp_multiplier: Optional[float] = None):
    """
    This shows the number of reference spots assigned to each gene which pass the quality thresholding based on
    the parameters `score_thresh` and `intensity_thresh`.

    If `nb` has the *OMP* page, then the number of omp spots will also be shown, where the quality thresholding is
    based on `score_omp_thresh`, `score_omp_multiplier` and `intensity_thresh`.

    There will also be a second reference spots histogram, the difference with this is that the spots
    were allowed to be assigned to some fake genes with `bled_codes` specified through `fake_bled_codes`.

    !!! note
        `fake_bled_codes` have dimension `n_fake x  nbp_basic.n_rounds x nbp_basic.n_channels` not
        `n_fake x len(nbp_basic.use_rounds) x len(nbp_basic.use_channels)`.

    Args:
        nb: *Notebook* containing at least `call_spots` page.
        fake_bled_codes: `float [n_fake_genes x n_rounds x n_channels]`.
            colors of fake genes to find dot product with
            Will find new gene assignment of anchor spots to new set of `bled_codes` which include these in
            addition to `bled_codes_ge`.
            By default, will have a fake gene for each round and channel such that it is $1$ in round r,
            channel $c$ and 0 everywhere else.
        fake_gene_names: `str [n_fake_genes]`.
            Can give name of each fake gene. If `None`, fake gene $i$ will be called FAKE:$i$.
        score_thresh: Threshold for score for ref_spots. Can be changed with text box.
        intensity_thresh: Threshold for intensity. Can be changed with text box.
        score_omp_thresh: Threshold for score for omp_spots. Can be changed with text box.
        score_omp_multiplier: Can specify the value of score_omp_multiplier to use to compute omp score.
            If `None`, will use value in config file. Can be changed with text box.
    """
    # Add fake genes
    if fake_bled_codes is None:
        # Default have binary fake gene for each used round and channel
        n_fake = len(nb.basic_info.use_rounds) * len(nb.basic_info.use_channels)
        fake_bled_codes = np.zeros((n_fake, nb.basic_info.n_rounds, nb.basic_info.n_channels))
        i = 0
        # Cluster fake genes by channel because more likely to be a faulty channel
        for c in nb.basic_info.use_channels:
            for r in nb.basic_info.use_rounds:
                fake_bled_codes[i, r, c] = 1
                i += 1
        fake_gene_names = [f'r{r}c{c}' for c in nb.basic_info.use_channels for r in nb.basic_info.use_rounds]
    n_fake = fake_bled_codes.shape[0]
    if fake_gene_names is None:
        fake_gene_names = [f'FAKE:{i}' for i in range(n_fake)]
    self.gene_names = nb.call_spots.gene_names.tolist() + fake_gene_names
    self.n_genes = len(self.gene_names)
    self.n_genes_real = self.n_genes - n_fake

    # Do new gene assignment for anchor spots when fake genes are included
    spot_colors_pb, background_var = background_fitting(nb, 'anchor')[1:]  # Fit background before dot product score
    grc_ind = np.ix_(np.arange(self.n_genes), nb.basic_info.use_rounds, nb.basic_info.use_channels)
    # Bled codes saved to Notebook should already have L2 norm = 1 over used_channels and rounds
    bled_codes = np.append(nb.call_spots.bled_codes_ge, fake_bled_codes, axis=0)
    bled_codes = bled_codes[grc_ind]
    # Ensure L2 norm of 1 for each gene
    norm_factor = np.expand_dims(np.linalg.norm(bled_codes, axis=(1, 2)), (1, 2))
    norm_factor[norm_factor == 0] = 1  # For genes with no dye in use_dye, this avoids blow up on next line
    bled_codes = bled_codes / norm_factor
    score, gene_no = get_dot_product_score(spot_colors_pb, bled_codes, None, nb.call_spots.dp_norm_shift,
                                           background_var)

    # Add quality thresholding info and gene assigned to for method with no fake genes and method with fake genes
    self.intensity = [nb.ref_spots.intensity.astype(np.float16)] * 2
    self.score = [nb.ref_spots.score.astype(np.float16), score.astype(np.float16)]
    self.gene_no = [nb.ref_spots.gene_no, gene_no]

    # Add current thresholds
    config = nb.get_config()['thresholds']
    if config['intensity'] is None:
        config['intensity'] = nb.call_spots.gene_efficiency_intensity_thresh
    if score_thresh is None:
        score_thresh = config['score_ref']
    if intensity_thresh is None:
        intensity_thresh = config['intensity']
    self.score_thresh = [score_thresh] * 2
    self.intensity_thresh = intensity_thresh

    # Add omp gene assignment if have page
    if nb.has_page('omp'):
        if score_omp_multiplier is None:
            score_omp_multiplier = config['score_omp_multiplier']
        self.score_multiplier = score_omp_multiplier
        self.nbp_omp = nb.omp
        if score_omp_thresh is None:
            score_omp_thresh = config['score_omp']
        self.score_thresh += [score_omp_thresh]
        self.score += [omp_spot_score(self.nbp_omp, self.score_multiplier).astype(np.float16)]
        self.intensity += [self.nbp_omp.intensity.astype(np.float16)]
        self.gene_no += [self.nbp_omp.gene_no]
        self.omp = True
    else:
        self.omp = False
    self.n_plots = len(self.score)
    self.use = None
    self.update_use()

    # Initialise plot
    self.fig, self.ax = plt.subplots(1, 1, figsize=(16, 7))
    self.subplot_adjust = [0.07, 0.85, 0.12, 0.93]
    self.fig.subplots_adjust(left=self.subplot_adjust[0], right=self.subplot_adjust[1],
                             bottom=self.subplot_adjust[2], top=self.subplot_adjust[3])
    self.ax.set_ylabel(r"Number of Spots")
    self.ax.set_xlabel('Gene')
    self.ax.set_title(f"Number of Spots assigned to each Gene")

    # record min and max for textbox input
    self.score_min = np.around(self.score[0].min(), 2)  # Min score of score[1] cannot be less than this
    self.score_max = np.around(self.score[1].max(), 2)  # Max score of score[0] cannot be more than this
    self.intensity_min = np.around(self.intensity[0].min(), 2)
    self.intensity_max = np.around(self.intensity[0].max(), 2)
    self.plots = [None] * self.n_plots
    default_colors = plt.rcParams['axes.prop_cycle']._left
    default_colors = default_colors[:2] + default_colors[3:4]  # default 3 is better than default 2 for omp plot
    for i in range(self.n_plots):
        self.plots[i], = self.ax.plot(np.arange(self.n_genes),
                                      np.histogram(self.gene_no[i][self.use[i]],
                                                   np.arange(self.n_genes + 1) - 0.5)[0],
                                      color=default_colors[i]['color'])
        if i == 1:
            self.plots[i].set_visible(False)
    self.ax.set_ylim(0, None)
    self.ax.set_xticks(np.arange(self.n_genes))
    self.ax.set_xticklabels(self.gene_names, rotation=90, size=7)
    self.ax.set_xlim(-0.5, self.n_genes_real - 0.5)

    # Add text box to change score multiplier
    text_box_labels = [r'Score, $\Delta_s$' + '\nThreshold', r'Intensity, $\chi_s$' + '\nThreshold']
    text_box_values = [np.around(self.score_thresh[0], 2), np.around(self.intensity_thresh, 2)]
    text_box_funcs = [self.update_score_thresh, self.update_intensity_thresh]
    if self.omp:
        text_box_labels += [r'OMP Score, $\gamma_s$' + '\nThreshold', 'Score\n' + r'Multiplier, $\rho$']
        text_box_values += [np.around(self.score_thresh[2], 2), np.around(self.score_multiplier, 2)]
        text_box_funcs += [self.update_score_omp_thresh, self.update_score_multiplier]
    self.text_boxes = [None] * len(text_box_labels)
    for i in range(len(text_box_labels)):
        text_ax = self.fig.add_axes([self.subplot_adjust[1] + 0.05,
                                     self.subplot_adjust[2] + 0.15 * (len(text_box_labels) - i - 1), 0.05, 0.04])
        self.text_boxes[i] = TextBox(text_ax, text_box_labels[i], text_box_values[i], color='k',
                                     hovercolor=[0.2, 0.2, 0.2])
        self.text_boxes[i].cursor.set_color('r')
        label = text_ax.get_children()[0]  # label is a child of the TextBox axis
        label.set_position([0.5, 2.75])
        # centering the text
        label.set_verticalalignment('top')
        label.set_horizontalalignment('center')
        self.text_boxes[i].on_submit(text_box_funcs[i])

    # Add buttons to add/remove score_dp histograms
    self.buttons_ax = self.fig.add_axes([self.subplot_adjust[1] + 0.02, self.subplot_adjust[3] - 0.25, 0.15, 0.3])
    plt.axis('off')
    self.button_labels = ["Ref Spots",
                          "Ref Spots - Fake Genes"]
    label_checked = [True, False]
    if self.omp:
        self.button_labels += ["OMP Spots"]
        label_checked += [True]
    self.buttons = CheckButtons(self.buttons_ax, self.button_labels, label_checked)

    for i in range(self.n_plots):
        self.buttons.labels[i].set_fontsize(7)
        self.buttons.labels[i].set_color(default_colors[i]['color'])
        self.buttons.rectangles[i].set_color('w')
    self.buttons.on_clicked(self.choose_plots)
    plt.show()

Score Calculation

background_fitting(nb, method)

Computes background using parameters in config file. Then removes this from the spot_colors.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing call_spots page

required
method str

'omp' or 'anchor', indicating which spot_colors to use.

required

Returns:

Type Description
np.ndarray

spot_colors - float [n_spots x n_rounds_use x n_channels_use]. spot_color after normalised by color_norm_factor but before background fit.

np.ndarray

spot_colors_pb - float [n_spots x n_rounds_use x n_channels_use]. spot_color after background removed.

np.ndarray

background_var - float [n_spots x n_rounds_use x n_channels_use]. inverse of the weighting used for dot product score calculation.

Source code in coppafish/plot/call_spots/score_calc.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def background_fitting(nb: Notebook, method: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """
    Computes background using parameters in config file. Then removes this from the `spot_colors`.
    Args:
        nb: Notebook containing call_spots page
        method: 'omp' or 'anchor', indicating which `spot_colors` to use.

    Returns:
        `spot_colors` - `float [n_spots x n_rounds_use x n_channels_use]`.
            `spot_color` after normalised by `color_norm_factor` but before background fit.
        `spot_colors_pb` - `float [n_spots x n_rounds_use x n_channels_use]`.
            `spot_color` after background removed.
        `background_var` - `float [n_spots x n_rounds_use x n_channels_use]`.
            inverse of the weighting used for dot product score calculation.
    """
    rc_ind = np.ix_(nb.basic_info.use_rounds, nb.basic_info.use_channels)
    if method.lower() == 'omp':
        spot_colors = np.moveaxis(np.moveaxis(nb.omp.colors, 0, -1)[rc_ind], -1, 0)
        config = nb.get_config()['omp']
    else:
        spot_colors = np.moveaxis(np.moveaxis(nb.ref_spots.colors, 0, -1)[rc_ind], -1, 0)
        config = nb.get_config()['call_spots']
    alpha = config['alpha']
    beta = config['beta']
    spot_colors = spot_colors / nb.call_spots.color_norm_factor[rc_ind]
    spot_colors_pb, background_coef, background_codes = \
        fit_background(spot_colors, nb.call_spots.background_weight_shift)
    background_codes = background_codes.reshape(background_codes.shape[0], -1)
    background_var = background_coef ** 2 @ background_codes ** 2 * alpha + beta ** 2
    return spot_colors, spot_colors_pb, background_var

get_dot_product_score(spot_colors, bled_codes, spot_gene_no, dp_norm_shift, background_var)

Finds dot product score for each spot_color given to the gene indicated by spot_gene_no.

Parameters:

Name Type Description Default
spot_colors np.ndarray

float [n_spots x n_rounds_use x n_channels_use]. colors of spots to find score of.

required
bled_codes np.ndarray

float [n_genes x n_rounds_use x n_channels_use]. colors of genes to find dot product with.

required
spot_gene_no Optional[np.ndarray]

int [n_spots]. Gene that each spot was assigned to. If None, will set spot_gene_no[s] to gene for which score was largest.

required
dp_norm_shift float

Normalisation constant for single round used for dot product calculation. I.e. nb.call_spots.dp_norm_shift.

required
background_var Optional[np.ndarray]

float [n_spots x n_rounds_use x n_channels_use]. inverse of the weighting used for dot product score calculation.

required

Returns:

Type Description
np.ndarray

spot_score - float [n_spots]. Dot product score for each spot.

np.ndarray

spot_gene_no - will be same as input if given, otherwise will be the best gene assigned.

Source code in coppafish/plot/call_spots/score_calc.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def get_dot_product_score(spot_colors: np.ndarray, bled_codes: np.ndarray, spot_gene_no: Optional[np.ndarray],
                          dp_norm_shift: float, background_var: Optional[np.ndarray]) -> Tuple[np.ndarray, np.ndarray]:
    """
    Finds dot product score for each `spot_color` given to the gene indicated by `spot_gene_no`.

    Args:
        spot_colors: `float [n_spots x n_rounds_use x n_channels_use]`.
            colors of spots to find score of.
        bled_codes: `float [n_genes x n_rounds_use x n_channels_use]`.
            colors of genes to find dot product with.
        spot_gene_no: `int [n_spots]`.
            Gene that each spot was assigned to. If None, will set `spot_gene_no[s]` to gene for which
            score was largest.
        dp_norm_shift: Normalisation constant for single round used for dot product calculation.
            I.e. `nb.call_spots.dp_norm_shift`.
        background_var: `float [n_spots x n_rounds_use x n_channels_use]`.
            inverse of the weighting used for dot product score calculation.

    Returns:
        `spot_score` - `float [n_spots]`.
            Dot product score for each spot.
        `spot_gene_no` - will be same as input if given, otherwise will be the best gene assigned.
    """
    n_spots, n_rounds_use = spot_colors.shape[:2]
    n_genes = bled_codes.shape[0]
    dp_norm_shift = dp_norm_shift * np.sqrt(n_rounds_use)
    if background_var is None:
        weight = None
    else:
        weight = 1 / background_var
    scores = np.asarray(dot_product_score(spot_colors.reshape(n_spots, -1),
                                          bled_codes.reshape(n_genes, -1), dp_norm_shift, weight))
    if spot_gene_no is None:
        spot_gene_no = np.argmax(scores, 1)
    spot_score = scores[np.arange(n_spots), spot_gene_no]
    return spot_score, spot_gene_no

Scaled K Means

view_scaled_k_means(nb, r=0, check=False)

Plot to show how scaled_k_means was used to compute the bleed matrix. There will be upto 3 columns, each with 2 plots.

The vector for dye \(d\) in the bleed_matrix is computed from all the spot round vectors whose dot product to the dye \(d\) vector was the highest. The boxplots in the first row show these dot product values for each dye.

The second row then shows the bleed matrix at each stage of the computation.

The first column shows the initial bleed matrix. The second column shows the bleed matrix after running scaled_k_means once with a score threshold of 0. The third column shows the final bleed_matrix after running scaled_k_means a second time with score_thresh for dye \(d\) set to the median of the scores assigned to dye \(d\) in the first run. Third column only present if config['call_spots']['bleed_matrix_anneal']==True.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing experiment details. Must have run at least as far as call_reference_spots.

required
r int

Round of bleed matrix to view. Only relevant if config['call_spots']['bleed_matrix_method'] = 'separate'.

0
check bool

If True, will raise error if bleed_matrix computed here is different to that saved in notebook

False
Source code in coppafish/plot/call_spots/scaled_k_means.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def view_scaled_k_means(nb: Notebook, r: int = 0, check: bool = False):
    """
    Plot to show how `scaled_k_means` was used to compute the bleed matrix.
    There will be upto 3 columns, each with 2 plots.

    The vector for dye $d$ in the `bleed_matrix` is computed from all the spot round vectors
    whose dot product to the dye $d$ vector was the highest.
    The boxplots in the first row show these dot product values for each dye.

    The second row then shows the bleed matrix at each stage of the computation.

    The first column shows the initial bleed matrix. The second column shows the bleed matrix after running
    `scaled_k_means` once with a score threshold of 0. The third column shows the final `bleed_matrix` after running
    `scaled_k_means` a second time with `score_thresh` for dye $d$ set to the median of the scores assigned to
    dye $d$ in the first run. Third column only present if `config['call_spots']['bleed_matrix_anneal']==True`.

    Args:
        nb: Notebook containing experiment details. Must have run at least as far as `call_reference_spots`.
        r: Round of bleed matrix to view. Only relevant if `config['call_spots']['bleed_matrix_method'] = 'separate'`.
        check: If True, will raise error if `bleed_matrix` computed here is different to that saved in notebook
    """
    # Fit background to spot_colors as is done in call_reference_spots before bleed_matrix calc
    spot_colors = background_fitting(nb, 'ref')[1]

    # Get bleed matrix and plotting info
    rcd_ind = np.ix_(nb.basic_info.use_rounds, nb.basic_info.use_channels, nb.basic_info.use_dyes)
    initial_bleed_matrix = nb.call_spots.initial_bleed_matrix[rcd_ind]
    config = nb.get_config()['call_spots']
    if config['bleed_matrix_method'].lower() == 'separate':
        r_ind = int(np.where(np.asarray(nb.basic_info.use_rounds) == r)[0])
        title_start = f"Bleed matrix for round {r} "
    else:
        r_ind = 0
        title_start = "Bleed matrix "
    debug_info = get_bleed_matrix(spot_colors[nb.ref_spots.isolated], initial_bleed_matrix,
                                  config['bleed_matrix_method'], config['bleed_matrix_score_thresh'],
                                  config['bleed_matrix_min_cluster_size'], config['bleed_matrix_n_iter'],
                                  config['bleed_matrix_anneal'], r_ind)[1]
    if check:
        if np.abs(nb.call_spots.bleed_matrix[rcd_ind][r_ind]-debug_info['bleed_matrix'][-1]).max() > 1e-3:
            raise ValueError("Bleed Matrix saved to Notebook is different from that computed here "
                             "with get_bleed_matrix.\nMake sure that the background and bleed_matrix"
                             "parameters in the config file have not changed.")

    # Make it so all bleed matrices have same L2 norm as final one
    bm_norm = np.linalg.norm(debug_info['bleed_matrix'][-1])
    bleed_matrix = [bm * bm_norm / np.linalg.norm(bm) for bm in debug_info['bleed_matrix']]
    vmax = np.max(bleed_matrix)

    # Set up plot
    n_plots, n_use_channels, n_use_dyes = debug_info['bleed_matrix'].shape
    fig, ax = plt.subplots(2, n_plots, figsize=(11, 7), sharex=True)
    ax[0, 0].get_shared_y_axes().join(ax[0, 0], ax[0, 1])
    subplot_adjust = [0.075, 0.92, 0.08, 0.88]
    fig.subplots_adjust(left=subplot_adjust[0], right=subplot_adjust[1], bottom=subplot_adjust[2],
                        top=subplot_adjust[3])
    titles = ['Initial Bleed Matrix', 'Bleed Matrix after Scaled K Means 1', 'Bleed Matrix after Scaled K Means 2']
    if not config['bleed_matrix_anneal']:
        titles[1] = 'Bleed Matrix after Scaled K Means'
    for i in range(n_plots):
        box_data = [debug_info['cluster_score'][i][debug_info['cluster_ind'][i] == d] for d in range(n_use_dyes)]
        bp = ax[0, i].boxplot(box_data, notch=0, sym='+', patch_artist=True)
        for d in range(n_use_dyes):
            ax[0, i].text(d + 1, np.percentile(box_data[d], 25), "{:.1e}".format(len(box_data[d])),
                          horizontalalignment='center', color=bp['medians'][d].get_color(), size=5, clip_on=True)
            im = ax[1, i].imshow(bleed_matrix[i], extent=[0.5, n_use_dyes + 0.5, n_use_channels - 0.5, -0.5],
                                 aspect='auto', vmin=0, vmax=vmax)
        if nb.basic_info.dye_names is None:
            ax[1, i].set_xticks(ticks=np.arange(1, n_use_dyes + 1), labels=nb.basic_info.use_dyes)
        else:
            subplot_adjust[2] = 0.15
            fig.subplots_adjust(bottom=subplot_adjust[2])
            ax[1, i].set_xticks(ticks=np.arange(1, n_use_dyes + 1),
                                labels=np.asarray(nb.basic_info.dye_names)[nb.basic_info.use_dyes], rotation=45)
        ax[1, i].set_yticks(ticks=np.arange(n_use_channels), labels=nb.basic_info.use_channels)
        ax[0, i].set_title(titles[i], fontsize=10)
        if i == 0:
            ax[0, i].set_ylabel('Dot Product to Best Dye Vector')
            ax[1, i].set_ylabel('Channel')
    fig.supxlabel('Dye', size=12)
    mid_point = (subplot_adjust[2] + subplot_adjust[3]) / 2
    cbar_ax = fig.add_axes([subplot_adjust[1] + 0.01, subplot_adjust[2],
                            0.005, mid_point - subplot_adjust[2] - 0.04])  # left, bottom, width, height
    fig.colorbar(im, cax=cbar_ax)
    plt.suptitle(f'{title_start}at {n_plots} different stages\n Box plots showing the dot product of spot round '
                 f'vectors with the dye vector they best matched to in the bleed matrix', size=11)
    ax[1, 1].set_title('Bleed Matrix where each dye column was computed from all vectors assigned to that dye '
                       'in the boxplot above',
                       size=11)
    plt.show()