From 512971ac87a335ed6bf6cecd7dd7380f96df0885 Mon Sep 17 00:00:00 2001 From: Yossi Farjoun Date: Fri, 18 Aug 2017 21:20:15 +0300 Subject: [PATCH 1/5] an example implementation of a cnn for predicting bait performance. It doesn't work very well on the ICE sample I tried it with, probably because the baits are not equimolar, and predicting the molarity is quite difficult --- .../bait_cnn/bait_performance_cnn.py | 528 ++++++++++++++++++ api_tutorials/bait_cnn/script.sh | 9 + 2 files changed, 537 insertions(+) create mode 100644 api_tutorials/bait_cnn/bait_performance_cnn.py create mode 100644 api_tutorials/bait_cnn/script.sh diff --git a/api_tutorials/bait_cnn/bait_performance_cnn.py b/api_tutorials/bait_cnn/bait_performance_cnn.py new file mode 100644 index 0000000..1dde1ac --- /dev/null +++ b/api_tutorials/bait_cnn/bait_performance_cnn.py @@ -0,0 +1,528 @@ +# DSDE Deep Learning +# +# Train 1D Convolutional Neural Network from exome bait +# Input is a small window of reference sequence, a bait, +# +# Output is the normalized coverage of the bait +# +# July 2017 +# Yossi Farjoun +# farjoun@broadinstitute.org + +from __future__ import print_function + +import os +import math +# import h5py +import argparse +import matplotlib +import numpy as np + +matplotlib.use('Agg') +from scipy import interp +from keras import metrics +import keras.backend as K +from random import shuffle, seed +from Bio import Seq, SeqIO +from itertools import cycle +from keras.models import Model +import matplotlib.pyplot as plt +from keras.models import Sequential +from keras.optimizers import SGD, Adam +from keras.initializers import RandomNormal +from sklearn.metrics import roc_curve, auc, roc_auc_score +from keras.callbacks import ModelCheckpoint, EarlyStopping +from keras.layers.convolutional import Conv1D, MaxPooling1D +from keras.layers import Input, Dense, Dropout, Flatten, Reshape, Activation +from keras.layers.merge import Concatenate + +data_path = '/Users/farjoun/exomeData/' +reference_fasta = data_path + 'Homo_sapiens_assembly19.fasta' +bait_bed_file = data_path + 'coverage.singleton.bait.bed' + + + +def run(): + args = parse_args() + seed(0) + + if 'small' == args.model: + make_small_model(args) + elif 'large' == args.model: + make_large_model(args) + elif 'plot' == args.model: + make_plots(args) + else: + print('Unknown model argument') + + +def parse_args(): + parser = argparse.ArgumentParser() + + parser.add_argument('--model', default='small') + parser.add_argument('--window_size', default=500, type=int) + parser.add_argument('--samples', default=10000, type=int) + parser.add_argument('--reference_fasta', default=reference_fasta) + parser.add_argument('--bed_file', default=bait_bed_file) + parser.add_argument('--inputs', default={'A': 0, 'C': 1, 'T': 2, 'G': 3, 'Bait': 4}) + parser.add_argument('--annotations', default=('%gc',)) + parser.add_argument('--weights') + + args = parser.parse_args() + print('Arguments are', args) + return args + + +def make_small_model(args): + model = build_small_sequential_bait_model(args) + + train_data = load_dna_and_bait_coverage(args) + train, valid, test = split_data(train_data) + + weight_path = weight_path_from_args(args) + model = train_bait_model(model, train, valid, weight_path) + + title = weight_path_to_title(weight_path) + # plot_scatter(model, test[0], test[1], args.labels, title) + + +def make_large_model(args): + model = build_small_functional_bait_model(args) + + train_data = load_dna_and_bait_coverage(args) + train, valid, test = split_data(train_data) + + weight_path = weight_path_from_args(args) + model = train_bait_model(model, train, valid, weight_path) + + title = weight_path_to_title(weight_path) + + plot_scatter(model, [test.baitdata,test.gc], test.coverage, title) + + +def make_plots(args): + model = build_small_functional_bait_model(args) + model.load_weights(args.weights, by_name=True) + print('Loaded model weights from:', args.weights) + + train_data = load_dna_and_bait_coverage(args) + train, valid, test = split_data(train_data) + + title = weight_path_to_title(args.weights) + + plot_scatter(model, [test.baitdata, test.gc], test.coverage, title) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~ Models ~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def build_small_sequential_bait_model(args): + model = Sequential() + model.add(Conv1D(input_dim=len(args.inputs), + input_length=args.window_size, + filters=40, + filter_length=16, + border_mode='valid', + activation="relu", + init='normal')) + + model.add(MaxPooling1D(pool_length=3, stride=3)) + model.add(Conv1D(nb_filter=64, filter_length=16, activation="relu", init='normal', border_mode='valid')) + model.add(Dropout(0.2)) + model.add(MaxPooling1D(pool_length=3, stride=3)) + model.add(Flatten()) + + model.add(Dense(output_dim=32, init='normal')) + model.add(Activation('relu')) + + model.add(Dense(output_dim=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None))) + model.add(Activation(None)) + + sgd = SGD(lr=0.001, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=0.5) + adamo = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, clipnorm=1.) + my_metrics = [metrics.mean_squared_error, rmse_log] + + model.compile(loss='mean_squared_error', optimizer=sgd, metrics=my_metrics) + print('model summary:\n', model.summary()) + + return model + + +def build_small_functional_bait_model(args): + bait_shape = (args.window_size, len(args.inputs),) + annotation_shape = (len(args.annotations),) + + print(bait_shape) + input_baits = Input(name="bait", shape=bait_shape) + + x = Conv1D(activation='relu', + padding='valid', + filters=100, + kernel_size=8, + kernel_initializer='normal')(input_baits) + + x = Dropout(0.2)(x) + + x = MaxPooling1D(strides=3, pool_size=3)(x) + + x = Conv1D(kernel_initializer="normal", activation="relu", padding="valid", filters=64, kernel_size=8)(x) + x = Dropout(0.2)(x) + x = MaxPooling1D(pool_size=3, strides=3)(x) + + x = Conv1D(kernel_initializer="normal", activation="relu", padding="valid", filters=40, kernel_size=8)(x) + x = Dropout(0.2)(x) + x = MaxPooling1D(pool_size=3, strides=3)(x) + + x = Flatten()(x) + + x = Dense(units=32, kernel_initializer="normal", activation="relu")(x) + + print(annotation_shape) + + input_annotations = Input(name="annotation", shape=annotation_shape) + + xy = Concatenate(axis=-1)([x, input_annotations]) + + xy = Dense(units=32, kernel_initializer="normal", activation="relu")(xy) + + predictions = Dense(units=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None), activation=None)(xy) + + sgd = SGD(lr=0.001, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=0.5) + my_metrics = [metrics.mean_squared_error, rmse_log, gme] + + # this creates a model that includes + # the Input layer and three Dense layers + model = Model(input=[input_baits, input_annotations], output=predictions) + model.compile(loss=gme, optimizer=sgd, metrics=my_metrics) + print('model summary:\n', model.summary()) + return model + + +def train_bait_model(model, train, valid, save_weight): + # type: (Model, Data, Data, unicode) -> Model + + checkpointer = ModelCheckpoint(filepath=save_weight, verbose=1, save_best_only=True) + earlystopper = EarlyStopping(monitor='val_loss', patience=100, verbose=1) + + history = model.fit([train.baitdata,train.gc], train.coverage, + batch_size=32, epochs=150, shuffle=True, + validation_data=([valid.baitdata,valid.gc], valid.coverage), + callbacks=[checkpointer, earlystopper]) + + plot_metric_history(history, weight_path_to_title(save_weight)) + + return model + + +# data class +class Data: + def __init__(self, samples, window, depth): + self.samples = samples # type: int + self.window = window # type: int + self.depth = depth # type: int + self.baitdata = np.zeros((samples, window, depth)) # type: np.array + self.coverage = np.zeros((samples, 1)) # type: np.array + self.gc = np.zeros((samples, 1)) # type: np.array + + def __repr__(self): + return "Data(sample=%r,window=%r,depth=%r,baitdata=%r,coverage=%r,gc=%r)" % (self.samples, + self.window, + self.depth, + self.baitdata, + self.coverage, + self.gc) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~ Training Data ~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def load_dna_and_bait_coverage(args, only_labels=None): + record_dict = SeqIO.to_dict(SeqIO.parse(args.reference_fasta, "fasta")) + + baits_and_coverages = bed_file_to_coverage(args.bed_file) + print('Loaded baits and annotations from:', args.bed_file) + + train_data = Data(args.samples, args.window_size, len(args.inputs)) + + idx_offset = (args.window_size / 2) + + amiguity_codes = {'K': [0, 0, 0.5, 0.5], + 'M': [0.5, 0.5, 0, 0], + 'R': [0.5, 0, 0, 0.5], + 'Y': [0, 0.5, 0.5, 0], + 'S': [0, 0.5, 0, 0.5], + 'W': [0.5, 0, 0.5, 0], + 'B': [0, 0.333, 0.333, 0.334], + 'V': [0.333, 0.333, 0, 0.334], + 'H': [0.333, 0.333, 0.334, 0], + 'D': [0.333, 0, 0.333, 0.334], + 'X': [0.25, 0.25, 0.25, 0.25], + 'N': [0.25, 0.25, 0.25, 0.25]} + + count = 0 + while count < args.samples: + contig_key, start, end, coverage, gc = sample_from_bed(baits_and_coverages) + mid = (start + end) / 2 + contig = record_dict[contig_key] + record = contig[mid - idx_offset: mid + idx_offset] + + train_data.coverage[count, 0] = coverage + train_data.gc[count] = gc + + for i, b in enumerate(record.seq): + B = b.upper() + if B in args.inputs.keys(): + train_data.baitdata[count, i, args.inputs[B]] = 1.0 + elif B in amiguity_codes.keys(): + train_data.baitdata[count, i, 0:4] = amiguity_codes[B] + else: + print('Error! Unknown code:', b) + return + + ref_pos = i + mid - idx_offset + if start <= ref_pos <= end: + train_data.baitdata[count, i, 4] = 1 + + count += 1 + + print('Train data shape:', train_data.baitdata.shape) + print('Coverage data shape:', train_data.coverage.shape) + + # should be in the form ( 5xsize x samples tensor, 1x samples tensor) + return train_data + + +def bed_file_to_coverage(bed_file): + bed_with_cov_and_gc = {} + + total_reads = 0L # type: long + total_baits = 0L # type: long + + with open(bed_file) as f: + for line in f: + parts = line.split() + contig = parts[0] + lower = int(parts[1]) + upper = int(parts[2]) + gc = float(parts[3]) + reads = int(parts[4]) + total_reads += reads + total_baits += 1 + + if contig not in bed_with_cov_and_gc.keys(): + bed_with_cov_and_gc[contig] = ([], [], [], [],) + + bed_with_cov_and_gc[contig][0].append(lower) + bed_with_cov_and_gc[contig][1].append(upper) + bed_with_cov_and_gc[contig][2].append(reads) + bed_with_cov_and_gc[contig][3].append(gc) + + reads_per_bait = float(total_reads) / total_baits + print(reads_per_bait) + for contig in bed_with_cov_and_gc.keys(): + bed_with_cov_and_gc[contig] = ( + np.array(bed_with_cov_and_gc[contig][0]), + np.array(bed_with_cov_and_gc[contig][1]), + np.array([x / reads_per_bait for x in bed_with_cov_and_gc[contig][2]]), + bed_with_cov_and_gc[contig][3],) + print('key is:', contig, 'len ', len(bed_with_cov_and_gc[contig][0])) + + return bed_with_cov_and_gc + + +def in_bed_file(bed_dict, contig, pos): + lows = bed_dict[contig][0] + ups = bed_dict[contig][1] + + return np.any((lows <= pos) & (pos <= ups)) + + +def split_data(data, valid_ratio=0.1, test_ratio=0.4): + # type: (Data, float, float) -> (Data, Data, Data) + + samples = data.samples + indices = range(samples) + shuffle(indices) + + valid_idx = int(valid_ratio * float(samples)) + test_idx = int(test_ratio * float(samples)) + + train = Data(samples - valid_idx - test_idx, data.window, data.depth) + valid = Data(valid_idx, data.window, data.depth) + test = Data(test_idx, data.window, data.depth) + + valid.coverage = data.coverage[:valid_idx] + valid.baitdata = data.baitdata[:valid_idx] + valid.gc = data.gc[:valid_idx] + + test.coverage = data.coverage[valid_idx:(valid_idx + test_idx)] + test.baitdata = data.baitdata[valid_idx:(valid_idx + test_idx)] + test.gc = data.gc[valid_idx:(valid_idx + test_idx)] + + train.coverage = data.coverage[(valid_idx + test_idx):] + train.baitdata = data.baitdata[(valid_idx + test_idx):] + train.gc = data.gc[(valid_idx + test_idx):] + + return train, valid, test + + +# TODO: make more random (this gives too much power to the small contigs) +def sample_from_fasta(record_dict): + c_idx = str(np.random.randint(1, 20)) + contig = record_dict[c_idx] + p_idx = np.random.randint(len(contig)) + return c_idx, p_idx + + +# TODO: make more random (this gives too much power to the small contigs) +def sample_from_bed(bed_dict): + contig_key = "" + str(np.random.randint(1, 20)) + lowers = bed_dict[contig_key][0] + uppers = bed_dict[contig_key][1] + coverage = bed_dict[contig_key][2] + gcs = bed_dict[contig_key][3] + + idx = np.random.randint(len(lowers)) + return contig_key, lowers[idx], uppers[idx], coverage[idx], gcs[idx] + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~ Metrics ~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def gme(y_true, y_pred): + """calculates the root (geometric) mean squared error of the values.""" + return K.exp(K.mean(K.log(K.abs(np.divide(y_true + .001, y_pred + .001)-1)))) + + +def rmse_log(y_true, y_pred): + """calculates the root mean squared error of the log (base e) of the values.""" + return K.sqrt(K.mean(K.square(K.log(np.divide(y_true + 1, y_pred + 1))))) + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~ Plots ~~~~~~~~~~~~~~~~~~~ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def plot_scatter(model, test_data, truth_data, title): + y_pred = model.predict(test_data, verbose=1) + + # Compute ROC curve and ROC area for each class + + plt.figure(figsize=[4, 4]) + fig, ax = plt.subplots() + ax.plot(truth_data,y_pred, color='darkorange', linestyle='', marker='.') + max_xy=max(ax.get_xlim()[1],ax.get_ylim()[1]) + ax.plot([0, max_xy],[0, max_xy], ls="--", c=".3") + ax.set_aspect('equal') + ax.set_xlabel('True (normalized) Coverage ') + ax.set_ylabel('Predicted (normalized) Coverage') + ax.spines['left'].set_position('zero') + ax.spines['bottom'].set_position('zero') + ax.set_title("scatter" + str(title)) + + fig.savefig("./scatter_" + title + ".jpg") + + + +def plot_history(history, title): + # list all data in history + print(history.history.keys()) + # summarize history for accuracy + plt.plot(history.history['categorical_accuracy']) + plt.plot(history.history['val_categorical_accuracy']) + plt.title('Accuracy: ' + title) + plt.ylabel('accuracy') + plt.xlabel('epoch') + plt.legend(['train', 'test'], loc='upper left') + plt.show() + # summarize history for loss + plt.plot(history.history['loss']) + plt.plot(history.history['val_loss']) + plt.title('Loss: ' + title) + plt.ylabel('loss') + plt.xlabel('epoch') + plt.legend(['train', 'test'], loc='upper left') + plt.savefig("./plot_history_" + title + ".jpg") + + +def plot_metric_history(history, title): + # list all data in history + print(history.history.keys()) + + row = 0 + col = 0 + num_plots = len(history.history) / 2.0 # valid and train plot together + rows = 4 + cols = int(math.ceil(num_plots / float(rows))) + + f, axes = plt.subplots(rows, cols, sharex=True, figsize=(36, 24)) + + if cols > 1: + for k in history.history.keys(): + + if 'val' not in k: + axes[row, col].plot(history.history[k]) + axes[row, col].plot(history.history['val_' + k]) + + axes[row, col].set_ylabel(str(k)) + axes[row, col].legend(['train', 'valid'], loc='upper left') + axes[row, col].set_xlabel('epoch') + + row += 1 + if row == rows: + row = 0 + col += 1 + if row * col >= rows * cols: + break + + axes[0, 1].set_title(title) + else: + for k in history.history.keys(): + + if 'val' not in k: + axes[row].plot(history.history[k]) + axes[row].plot(history.history['val_' + k]) + + axes[row].set_ylabel(str(k)) + axes[row].legend(['train', 'valid'], loc='upper left') + axes[row].set_xlabel('epoch') + + row += 1 + axes[0].set_title(title) + + plt.savefig("./metric_history_" + title + ".jpg") + + +def weight_path_from_args(args): + save_weight = './bait_performance_cnn_model' + + ignore = ['inputs', 'labels', 'bed_file', 'reference_fasta'] + for arg in vars(args): + if arg in ignore: + continue + + attr = getattr(args, arg) + + if os.path.isdir(str(attr)) or os.path.isfile(str(attr)): + continue + + if os.path.isabs(str(attr)): + attr = os.path.splitext(os.path.basename(attr))[0] + + save_weight += '__' + str(arg) + '_' + str(attr) + + save_weight += '.hd5' + print('save weight path:', save_weight) + + return save_weight + + +def weight_path_to_title(wp): + return wp.split('/')[-1].replace('__', '-') + + +if '__main__' == __name__: + run() diff --git a/api_tutorials/bait_cnn/script.sh b/api_tutorials/bait_cnn/script.sh new file mode 100644 index 0000000..f5eb25e --- /dev/null +++ b/api_tutorials/bait_cnn/script.sh @@ -0,0 +1,9 @@ +grep -v '^@' whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.targets.interval_list | awk 'BEGIN{FS="\t";OFS="\t"}{print $1, $2-1,$3}' > whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.targets.bed +grep -v '^@' whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.baits.interval_list | awk 'BEGIN{FS="\t";OFS="\t"}{print $1, $2-1,$3}' > whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.baits.bed + +bedtools slop -i whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.baits.bed -b 250 -g hg19.genome > sloppy.baits.bed +bedtools coverage -counts -a whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.targets.bed -b sloppy.baits.bed | awk '$4==1' > singleton.targets.bed +bedtools intersect -a whole_exome_illumina_coding_v1.Homo_sapiens_assembly19.baits.bed -b singleton.targets.bed > singleton.baits.bed + +tail -n+2 NexPond-647561.per_target_coverage | awk 'BEGIN{FS="\t";OFS="\t"}{print $1, $2-1,$3,$6,$14}' > coverage.bed +bedtools intersect -a coverage.bed -b singleton.baits.bed > coverage.singleton.bait.bed From 8ae847d36eca04e445cd4eb9123a960273faa4ab Mon Sep 17 00:00:00 2001 From: Yossi Farjoun Date: Sat, 19 Aug 2017 10:08:53 +0300 Subject: [PATCH 2/5] changed the optimizer and the loss function. performance is somewhat improved --- api_tutorials/bait_cnn/bait_performance_cnn.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api_tutorials/bait_cnn/bait_performance_cnn.py b/api_tutorials/bait_cnn/bait_performance_cnn.py index 1dde1ac..ef0a453 100644 --- a/api_tutorials/bait_cnn/bait_performance_cnn.py +++ b/api_tutorials/bait_cnn/bait_performance_cnn.py @@ -194,7 +194,8 @@ def build_small_functional_bait_model(args): # this creates a model that includes # the Input layer and three Dense layers model = Model(input=[input_baits, input_annotations], output=predictions) - model.compile(loss=gme, optimizer=sgd, metrics=my_metrics) + adamo = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, clipnorm=1.) + model.compile(loss=metrics.mean_squared_error, optimizer=adamo, metrics=my_metrics) print('model summary:\n', model.summary()) return model @@ -409,7 +410,7 @@ def rmse_log(y_true, y_pred): def plot_scatter(model, test_data, truth_data, title): y_pred = model.predict(test_data, verbose=1) - # Compute ROC curve and ROC area for each class + # Compute metrics: plt.figure(figsize=[4, 4]) fig, ax = plt.subplots() From 1457e126b9905b771806ddf757fffc2f376e60da Mon Sep 17 00:00:00 2001 From: Yossi Farjoun Date: Sun, 20 Aug 2017 17:39:03 +0300 Subject: [PATCH 3/5] responding to review: - removed unused imports - changed dropout from .2 to .3 - removed first MaxPooling layer - added informative labels to print commands - made division floating point --- .../bait_cnn/bait_performance_cnn.py | 114 ++++++++---------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/api_tutorials/bait_cnn/bait_performance_cnn.py b/api_tutorials/bait_cnn/bait_performance_cnn.py index ef0a453..0d52595 100644 --- a/api_tutorials/bait_cnn/bait_performance_cnn.py +++ b/api_tutorials/bait_cnn/bait_performance_cnn.py @@ -10,30 +10,28 @@ # farjoun@broadinstitute.org from __future__ import print_function +from __future__ import division -import os -import math -# import h5py import argparse +import math +import os + import matplotlib import numpy as np matplotlib.use('Agg') -from scipy import interp from keras import metrics import keras.backend as K from random import shuffle, seed -from Bio import Seq, SeqIO -from itertools import cycle +from Bio import SeqIO from keras.models import Model import matplotlib.pyplot as plt from keras.models import Sequential from keras.optimizers import SGD, Adam from keras.initializers import RandomNormal -from sklearn.metrics import roc_curve, auc, roc_auc_score from keras.callbacks import ModelCheckpoint, EarlyStopping from keras.layers.convolutional import Conv1D, MaxPooling1D -from keras.layers import Input, Dense, Dropout, Flatten, Reshape, Activation +from keras.layers import Input, Dense, Dropout, Flatten, Activation from keras.layers.merge import Concatenate data_path = '/Users/farjoun/exomeData/' @@ -41,7 +39,6 @@ bait_bed_file = data_path + 'coverage.singleton.bait.bed' - def run(): args = parse_args() seed(0) @@ -83,7 +80,8 @@ def make_small_model(args): model = train_bait_model(model, train, valid, weight_path) title = weight_path_to_title(weight_path) - # plot_scatter(model, test[0], test[1], args.labels, title) + + plot_scatter(model, test.baitdata, test.coverage, title) def make_large_model(args): @@ -97,7 +95,7 @@ def make_large_model(args): title = weight_path_to_title(weight_path) - plot_scatter(model, [test.baitdata,test.gc], test.coverage, title) + plot_scatter(model, [test.baitdata, test.gc], test.coverage, title) def make_plots(args): @@ -127,24 +125,23 @@ def build_small_sequential_bait_model(args): activation="relu", init='normal')) - model.add(MaxPooling1D(pool_length=3, stride=3)) - model.add(Conv1D(nb_filter=64, filter_length=16, activation="relu", init='normal', border_mode='valid')) - model.add(Dropout(0.2)) - model.add(MaxPooling1D(pool_length=3, stride=3)) + model.add(Dropout(0.3)) + model.add(Conv1D(filters=64, kernel_size=16, activation="relu", init='normal', padding='valid')) + model.add(Dropout(0.3)) + model.add(MaxPooling1D(pool_size=3, strides=3)) model.add(Flatten()) - model.add(Dense(output_dim=32, init='normal')) + model.add(Dense(units=32, kernel_initializer='normal')) model.add(Activation('relu')) - model.add(Dense(output_dim=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None))) - model.add(Activation(None)) + model.add(Dense(units=1, kernel_initializer=RandomNormal(mean=1.0, stddev=0.5, seed=None))) + model.add(Activation("relu")) - sgd = SGD(lr=0.001, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=0.5) adamo = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, clipnorm=1.) my_metrics = [metrics.mean_squared_error, rmse_log] - model.compile(loss='mean_squared_error', optimizer=sgd, metrics=my_metrics) - print('model summary:\n', model.summary()) + model.compile(loss='mean_squared_error', optimizer=adamo, metrics=my_metrics) + model.summary() return model @@ -153,7 +150,7 @@ def build_small_functional_bait_model(args): bait_shape = (args.window_size, len(args.inputs),) annotation_shape = (len(args.annotations),) - print(bait_shape) + print("Bait shape: %s" % (bait_shape,)) input_baits = Input(name="bait", shape=bait_shape) x = Conv1D(activation='relu', @@ -162,23 +159,21 @@ def build_small_functional_bait_model(args): kernel_size=8, kernel_initializer='normal')(input_baits) - x = Dropout(0.2)(x) - - x = MaxPooling1D(strides=3, pool_size=3)(x) + x = Dropout(0.3)(x) x = Conv1D(kernel_initializer="normal", activation="relu", padding="valid", filters=64, kernel_size=8)(x) - x = Dropout(0.2)(x) + x = Dropout(0.3)(x) x = MaxPooling1D(pool_size=3, strides=3)(x) x = Conv1D(kernel_initializer="normal", activation="relu", padding="valid", filters=40, kernel_size=8)(x) - x = Dropout(0.2)(x) + x = Dropout(0.3)(x) x = MaxPooling1D(pool_size=3, strides=3)(x) x = Flatten()(x) x = Dense(units=32, kernel_initializer="normal", activation="relu")(x) - print(annotation_shape) + print("Annotation shape: %s" % (annotation_shape,)) input_annotations = Input(name="annotation", shape=annotation_shape) @@ -186,9 +181,8 @@ def build_small_functional_bait_model(args): xy = Dense(units=32, kernel_initializer="normal", activation="relu")(xy) - predictions = Dense(units=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None), activation=None)(xy) + predictions = Dense(units=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None), activation="relu")(xy) - sgd = SGD(lr=0.001, decay=1e-6, momentum=0.9, nesterov=True, clipnorm=0.5) my_metrics = [metrics.mean_squared_error, rmse_log, gme] # this creates a model that includes @@ -196,7 +190,7 @@ def build_small_functional_bait_model(args): model = Model(input=[input_baits, input_annotations], output=predictions) adamo = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, clipnorm=1.) model.compile(loss=metrics.mean_squared_error, optimizer=adamo, metrics=my_metrics) - print('model summary:\n', model.summary()) + model.summary() return model @@ -206,9 +200,9 @@ def train_bait_model(model, train, valid, save_weight): checkpointer = ModelCheckpoint(filepath=save_weight, verbose=1, save_best_only=True) earlystopper = EarlyStopping(monitor='val_loss', patience=100, verbose=1) - history = model.fit([train.baitdata,train.gc], train.coverage, + history = model.fit([train.baitdata, train.gc], train.coverage, batch_size=32, epochs=150, shuffle=True, - validation_data=([valid.baitdata,valid.gc], valid.coverage), + validation_data=([valid.baitdata, valid.gc], valid.coverage), callbacks=[checkpointer, earlystopper]) plot_metric_history(history, weight_path_to_title(save_weight)) @@ -243,11 +237,11 @@ def load_dna_and_bait_coverage(args, only_labels=None): record_dict = SeqIO.to_dict(SeqIO.parse(args.reference_fasta, "fasta")) baits_and_coverages = bed_file_to_coverage(args.bed_file) - print('Loaded baits and annotations from:', args.bed_file) + print('Loaded baits and annotations from: %s' % (args.bed_file,)) train_data = Data(args.samples, args.window_size, len(args.inputs)) - idx_offset = (args.window_size / 2) + idx_offset = (args.window_size // 2) amiguity_codes = {'K': [0, 0, 0.5, 0.5], 'M': [0.5, 0.5, 0, 0], @@ -265,8 +259,11 @@ def load_dna_and_bait_coverage(args, only_labels=None): count = 0 while count < args.samples: contig_key, start, end, coverage, gc = sample_from_bed(baits_and_coverages) - mid = (start + end) / 2 + mid = (start + end) // 2 contig = record_dict[contig_key] + + # print("%s" % ((contig_key, start, end, coverage, gc, mid, contig),)) + record = contig[mid - idx_offset: mid + idx_offset] train_data.coverage[count, 0] = coverage @@ -288,8 +285,8 @@ def load_dna_and_bait_coverage(args, only_labels=None): count += 1 - print('Train data shape:', train_data.baitdata.shape) - print('Coverage data shape:', train_data.coverage.shape) + print('Train data shape: %s' % (train_data.baitdata.shape,)) + print('Coverage data shape: %s' % (train_data.coverage.shape,)) # should be in the form ( 5xsize x samples tensor, 1x samples tensor) return train_data @@ -303,12 +300,12 @@ def bed_file_to_coverage(bed_file): with open(bed_file) as f: for line in f: - parts = line.split() + parts = line.split() contig = parts[0] - lower = int(parts[1]) - upper = int(parts[2]) - gc = float(parts[3]) - reads = int(parts[4]) + lower = int(parts[1]) + upper = int(parts[2]) + gc = float(parts[3]) + reads = int(parts[4]) total_reads += reads total_baits += 1 @@ -321,14 +318,14 @@ def bed_file_to_coverage(bed_file): bed_with_cov_and_gc[contig][3].append(gc) reads_per_bait = float(total_reads) / total_baits - print(reads_per_bait) + print("Reads per bait: %s" % reads_per_bait) for contig in bed_with_cov_and_gc.keys(): bed_with_cov_and_gc[contig] = ( np.array(bed_with_cov_and_gc[contig][0]), np.array(bed_with_cov_and_gc[contig][1]), np.array([x / reads_per_bait for x in bed_with_cov_and_gc[contig][2]]), bed_with_cov_and_gc[contig][3],) - print('key is:', contig, 'len ', len(bed_with_cov_and_gc[contig][0])) + print('key is: %s len %s' % ( contig, len(bed_with_cov_and_gc[contig][0]))) return bed_with_cov_and_gc @@ -369,17 +366,12 @@ def split_data(data, valid_ratio=0.1, test_ratio=0.4): return train, valid, test -# TODO: make more random (this gives too much power to the small contigs) -def sample_from_fasta(record_dict): - c_idx = str(np.random.randint(1, 20)) - contig = record_dict[c_idx] - p_idx = np.random.randint(len(contig)) - return c_idx, p_idx +def sample_from_bed(bed_dict): + contig_sizes = {key: len(bed_dict[key]) for key in bed_dict.keys()} + total_size = sum(contig_sizes.values()) + contig_key = np.random.choice(bed_dict.keys(), 1, p=[x / total_size for x in contig_sizes.values()])[0] -# TODO: make more random (this gives too much power to the small contigs) -def sample_from_bed(bed_dict): - contig_key = "" + str(np.random.randint(1, 20)) lowers = bed_dict[contig_key][0] uppers = bed_dict[contig_key][1] coverage = bed_dict[contig_key][2] @@ -388,14 +380,13 @@ def sample_from_bed(bed_dict): idx = np.random.randint(len(lowers)) return contig_key, lowers[idx], uppers[idx], coverage[idx], gcs[idx] - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~ Metrics ~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def gme(y_true, y_pred): """calculates the root (geometric) mean squared error of the values.""" - return K.exp(K.mean(K.log(K.abs(np.divide(y_true + .001, y_pred + .001)-1)))) + return K.exp(K.mean(K.log(K.abs(np.divide(y_true + .001, y_pred + .001) - 1)))) def rmse_log(y_true, y_pred): @@ -414,9 +405,9 @@ def plot_scatter(model, test_data, truth_data, title): plt.figure(figsize=[4, 4]) fig, ax = plt.subplots() - ax.plot(truth_data,y_pred, color='darkorange', linestyle='', marker='.') - max_xy=max(ax.get_xlim()[1],ax.get_ylim()[1]) - ax.plot([0, max_xy],[0, max_xy], ls="--", c=".3") + ax.plot(truth_data, y_pred, color='darkorange', linestyle='', marker='.') + max_xy = max(ax.get_xlim()[1], ax.get_ylim()[1]) + ax.plot([0, max_xy], [0, max_xy], ls="--", c=".3") ax.set_aspect('equal') ax.set_xlabel('True (normalized) Coverage ') ax.set_ylabel('Predicted (normalized) Coverage') @@ -427,10 +418,9 @@ def plot_scatter(model, test_data, truth_data, title): fig.savefig("./scatter_" + title + ".jpg") - def plot_history(history, title): # list all data in history - print(history.history.keys()) + print("History keys: " + history.history.keys()) # summarize history for accuracy plt.plot(history.history['categorical_accuracy']) plt.plot(history.history['val_categorical_accuracy']) @@ -451,7 +441,7 @@ def plot_history(history, title): def plot_metric_history(history, title): # list all data in history - print(history.history.keys()) + print("History keys: " + history.history.keys()) row = 0 col = 0 From 51c7a33d71dcc1893f2cdb3dfc27f8299d6d3de2 Mon Sep 17 00:00:00 2001 From: Yossi Farjoun Date: Mon, 21 Aug 2017 23:23:35 +0300 Subject: [PATCH 4/5] adding TensorBoard capability, tried to visualized the filters, but failed misrably...anyone care to see what I did wrong ? --- .../bait_cnn/bait_performance_cnn.py | 130 +++++++++++++++--- 1 file changed, 108 insertions(+), 22 deletions(-) diff --git a/api_tutorials/bait_cnn/bait_performance_cnn.py b/api_tutorials/bait_cnn/bait_performance_cnn.py index 0d52595..9a2175c 100644 --- a/api_tutorials/bait_cnn/bait_performance_cnn.py +++ b/api_tutorials/bait_cnn/bait_performance_cnn.py @@ -22,17 +22,17 @@ matplotlib.use('Agg') from keras import metrics import keras.backend as K -from random import shuffle, seed from Bio import SeqIO from keras.models import Model import matplotlib.pyplot as plt from keras.models import Sequential from keras.optimizers import SGD, Adam from keras.initializers import RandomNormal -from keras.callbacks import ModelCheckpoint, EarlyStopping +from keras.callbacks import ModelCheckpoint, EarlyStopping, TensorBoard from keras.layers.convolutional import Conv1D, MaxPooling1D from keras.layers import Input, Dense, Dropout, Flatten, Activation from keras.layers.merge import Concatenate +# import tensorflow as tf data_path = '/Users/farjoun/exomeData/' reference_fasta = data_path + 'Homo_sapiens_assembly19.fasta' @@ -41,7 +41,6 @@ def run(): args = parse_args() - seed(0) if 'small' == args.model: make_small_model(args) @@ -81,7 +80,9 @@ def make_small_model(args): title = weight_path_to_title(weight_path) - plot_scatter(model, test.baitdata, test.coverage, title) + plot_scatter(model, {"test": ([test.baitdata, test.gc], test.coverage), + "validation": ([valid.baitdata, valid.gc], valid.coverage), + "train": ([train.baitdata, train.gc], train.coverage)}, title) def make_large_model(args): @@ -95,7 +96,9 @@ def make_large_model(args): title = weight_path_to_title(weight_path) - plot_scatter(model, [test.baitdata, test.gc], test.coverage, title) + plot_scatter(model, {"test": ([test.baitdata, test.gc], test.coverage), + "validation": ([valid.baitdata, valid.gc], valid.coverage), + "train": ([train.baitdata, train.gc], train.coverage)}, title) def make_plots(args): @@ -108,7 +111,9 @@ def make_plots(args): title = weight_path_to_title(args.weights) - plot_scatter(model, [test.baitdata, test.gc], test.coverage, title) + plot_scatter(model, {"test": ([test.baitdata, test.gc], test.coverage), + "validation": ([valid.baitdata, valid.gc], valid.coverage), + "train": ([train.baitdata, train.gc], train.coverage)}, title) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -157,7 +162,7 @@ def build_small_functional_bait_model(args): padding='valid', filters=100, kernel_size=8, - kernel_initializer='normal')(input_baits) + kernel_initializer='normal', name='conv1')(input_baits) x = Dropout(0.3)(x) @@ -181,13 +186,26 @@ def build_small_functional_bait_model(args): xy = Dense(units=32, kernel_initializer="normal", activation="relu")(xy) - predictions = Dense(units=1, init=RandomNormal(mean=1.0, stddev=0.5, seed=None), activation="relu")(xy) - + predictions = Dense(units=1, kernel_initializer=RandomNormal(mean=1.0, stddev=0.5, seed=None), activation="relu")( + xy) my_metrics = [metrics.mean_squared_error, rmse_log, gme] # this creates a model that includes # the Input layer and three Dense layers - model = Model(input=[input_baits, input_annotations], output=predictions) + model = Model(inputs=[input_baits, input_annotations], outputs=predictions) + + # # add some TensorBoard annotations + # conv1d_1 = filter(lambda y: y.name == "conv1d_1", model.layers)[0] + # conv1d_1_shape = map(lambda x: x.value, conv1d_1.kernel.get_shape()) + # conv1d_1_shape.insert(0, 1) + # + # reshaped = tf.reshape(conv1d_1.kernel, conv1d_1_shape) + # + # filters=put_kernels_on_grid(reshaped, 2) + # + # merged = tf.summary.merge_all() + # train_writer = tf.summary.FileWriter("./log/" + '/train') + # adamo = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, clipnorm=1.) model.compile(loss=metrics.mean_squared_error, optimizer=adamo, metrics=my_metrics) model.summary() @@ -199,11 +217,12 @@ def train_bait_model(model, train, valid, save_weight): checkpointer = ModelCheckpoint(filepath=save_weight, verbose=1, save_best_only=True) earlystopper = EarlyStopping(monitor='val_loss', patience=100, verbose=1) + tensorboard = TensorBoard(log_dir='./logs/' + save_weight, histogram_freq=1, write_graph=False, write_images=False) history = model.fit([train.baitdata, train.gc], train.coverage, batch_size=32, epochs=150, shuffle=True, validation_data=([valid.baitdata, valid.gc], valid.coverage), - callbacks=[checkpointer, earlystopper]) + callbacks=[checkpointer, earlystopper, tensorboard]) plot_metric_history(history, weight_path_to_title(save_weight)) @@ -257,13 +276,12 @@ def load_dna_and_bait_coverage(args, only_labels=None): 'N': [0.25, 0.25, 0.25, 0.25]} count = 0 + np.random.seed(0) while count < args.samples: contig_key, start, end, coverage, gc = sample_from_bed(baits_and_coverages) mid = (start + end) // 2 contig = record_dict[contig_key] - # print("%s" % ((contig_key, start, end, coverage, gc, mid, contig),)) - record = contig[mid - idx_offset: mid + idx_offset] train_data.coverage[count, 0] = coverage @@ -325,7 +343,7 @@ def bed_file_to_coverage(bed_file): np.array(bed_with_cov_and_gc[contig][1]), np.array([x / reads_per_bait for x in bed_with_cov_and_gc[contig][2]]), bed_with_cov_and_gc[contig][3],) - print('key is: %s len %s' % ( contig, len(bed_with_cov_and_gc[contig][0]))) + print('key is: %s len %s' % (contig, len(bed_with_cov_and_gc[contig][0]))) return bed_with_cov_and_gc @@ -340,9 +358,11 @@ def in_bed_file(bed_dict, contig, pos): def split_data(data, valid_ratio=0.1, test_ratio=0.4): # type: (Data, float, float) -> (Data, Data, Data) + np.random.seed(0) + samples = data.samples indices = range(samples) - shuffle(indices) + np.random.shuffle(indices) valid_idx = int(valid_ratio * float(samples)) test_idx = int(test_ratio * float(samples)) @@ -380,6 +400,7 @@ def sample_from_bed(bed_dict): idx = np.random.randint(len(lowers)) return contig_key, lowers[idx], uppers[idx], coverage[idx], gcs[idx] + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # ~~~~~~~ Metrics ~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -398,16 +419,20 @@ def rmse_log(y_true, y_pred): # ~~~~~~~ Plots ~~~~~~~~~~~~~~~~~~~ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -def plot_scatter(model, test_data, truth_data, title): - y_pred = model.predict(test_data, verbose=1) +def plot_scatter(model, data_dict, title): + # type: (Model, dict, str) -> None + + fig, ax = plt.subplots() + plt.figure(figsize=[4, 4]) # Compute metrics: + for key in data_dict.keys(): + y_pred = model.predict(data_dict[key][0], verbose=1) + y_truth = data_dict[key][1] - plt.figure(figsize=[4, 4]) - fig, ax = plt.subplots() - ax.plot(truth_data, y_pred, color='darkorange', linestyle='', marker='.') - max_xy = max(ax.get_xlim()[1], ax.get_ylim()[1]) - ax.plot([0, max_xy], [0, max_xy], ls="--", c=".3") + ax.plot(y_truth, y_pred, linestyle='', marker='.', label=key) + + ax.legend() ax.set_aspect('equal') ax.set_xlabel('True (normalized) Coverage ') ax.set_ylabel('Predicted (normalized) Coverage') @@ -415,6 +440,9 @@ def plot_scatter(model, test_data, truth_data, title): ax.spines['bottom'].set_position('zero') ax.set_title("scatter" + str(title)) + max_xy = max(ax.get_xlim()[1], ax.get_ylim()[1]) + ax.plot([0, max_xy], [0, max_xy], ls="--", c=".3") + fig.savefig("./scatter_" + title + ".jpg") @@ -514,6 +542,64 @@ def weight_path_from_args(args): def weight_path_to_title(wp): return wp.split('/')[-1].replace('__', '-') +# +# # from gist: https://gist.github.com/kukuruza/03731dc494603ceab0c5 +# from math import sqrt +# +# +# def put_kernels_on_grid(kernel, pad=1): +# """Visualize conv. filters as an image (mostly for the 1st layer). +# Arranges filters into a grid, with some paddings between adjacent filters. +# Args: +# kernel: tensor of shape [Y, X, NumChannels, NumKernels] +# pad: number of black pixels around each filter (between them) +# Return: +# Tensor of shape [1, (Y+2*pad)*grid_Y, (X+2*pad)*grid_X, NumChannels]. +# """ +# +# # get shape of the grid. NumKernels == grid_Y * grid_X +# def factorization(n): +# for i in range(int(sqrt(float(n))), 0, -1): +# if n % i == 0: +# if i == 1: print('Who would enter a prime number of filters') +# return i, int(n / i) +# +# (grid_Y, grid_X) = factorization(kernel.get_shape()[3].value) +# print('grid: %d = (%d, %d)' % (kernel.get_shape()[3].value, grid_Y, grid_X)) +# +# x_min = tf.reduce_min(kernel) +# x_max = tf.reduce_max(kernel) +# kernel = (kernel - x_min) / (x_max - x_min) +# +# # pad X and Y +# x = tf.pad(kernel, tf.constant([[pad, pad], [pad, pad], [0, 0], [0, 0]]), mode='CONSTANT') +# +# # X and Y dimensions, w.r.t. padding +# Y = kernel.get_shape()[0] + 2 * pad +# X = kernel.get_shape()[1] + 2 * pad +# +# channels = kernel.get_shape()[2] +# +# # put NumKernels to the 1st dimension +# x = tf.transpose(x, (3, 0, 1, 2)) +# # organize grid on Y axis +# x = tf.reshape(x, tf.stack([grid_X, Y * grid_Y, X, channels])) +# +# # switch X and Y axes +# x = tf.transpose(x, (0, 2, 1, 3)) +# # organize grid on X axis +# x = tf.reshape(x, tf.stack([1, X * grid_X, Y * grid_Y, channels])) +# +# # back to normal order (not combining with the next step for clarity) +# x = tf.transpose(x, (2, 1, 3, 0)) +# +# # to tf.image_summary order [batch_size, height, width, channels], +# # where in this case batch_size == 1 +# x = tf.transpose(x, (3, 0, 1, 2)) +# +# # scaling to [0, 255] is not necessary for tensorboard +# return x +# if '__main__' == __name__: run() From 3da4c6c26700041dd7eae31f7a23f5c4dce0d9de Mon Sep 17 00:00:00 2001 From: Yossi Farjoun Date: Wed, 23 Aug 2017 12:01:50 +0300 Subject: [PATCH 5/5] added perfomance plot. network not really working, but learning a little... --- api_tutorials/bait_cnn/hg19.genome | 85 ++++++++++++++++++ .../scatter_bait_performance_cnn_model.jpg | Bin 0 -> 49487 bytes api_tutorials/bait_cnn/script.sh | 3 + 3 files changed, 88 insertions(+) create mode 100644 api_tutorials/bait_cnn/hg19.genome create mode 100644 api_tutorials/bait_cnn/scatter_bait_performance_cnn_model.jpg diff --git a/api_tutorials/bait_cnn/hg19.genome b/api_tutorials/bait_cnn/hg19.genome new file mode 100644 index 0000000..3a3bdee --- /dev/null +++ b/api_tutorials/bait_cnn/hg19.genome @@ -0,0 +1,85 @@ +1 249250621 +2 243199373 +3 198022430 +4 191154276 +5 180915260 +6 171115067 +7 159138663 +8 146364022 +9 141213431 +10 135534747 +11 135006516 +12 133851895 +13 115169878 +14 107349540 +15 102531392 +16 90354753 +17 81195210 +18 78077248 +19 59128983 +20 63025520 +21 48129895 +22 51304566 +X 155270560 +Y 59373566 +MT 16569 +GL000207.1 4262 +GL000226.1 15008 +GL000229.1 19913 +GL000231.1 27386 +GL000210.1 27682 +GL000239.1 33824 +GL000235.1 34474 +GL000201.1 36148 +GL000247.1 36422 +GL000245.1 36651 +GL000197.1 37175 +GL000203.1 37498 +GL000246.1 38154 +GL000249.1 38502 +GL000196.1 38914 +GL000248.1 39786 +GL000244.1 39929 +GL000238.1 39939 +GL000202.1 40103 +GL000234.1 40531 +GL000232.1 40652 +GL000206.1 41001 +GL000240.1 41933 +GL000236.1 41934 +GL000241.1 42152 +GL000243.1 43341 +GL000242.1 43523 +GL000230.1 43691 +GL000237.1 45867 +GL000233.1 45941 +GL000204.1 81310 +GL000198.1 90085 +GL000208.1 92689 +GL000191.1 106433 +GL000227.1 128374 +GL000228.1 129120 +GL000214.1 137718 +GL000221.1 155397 +GL000209.1 159169 +GL000218.1 161147 +GL000220.1 161802 +GL000213.1 164239 +GL000211.1 166566 +GL000199.1 169874 +GL000217.1 172149 +GL000216.1 172294 +GL000215.1 172545 +GL000205.1 174588 +GL000219.1 179198 +GL000224.1 179693 +GL000223.1 180455 +GL000195.1 182896 +GL000212.1 186858 +GL000222.1 186861 +GL000200.1 187035 +GL000193.1 189789 +GL000194.1 191469 +GL000225.1 211173 +GL000192.1 547496 +NC_007605 171823 diff --git a/api_tutorials/bait_cnn/scatter_bait_performance_cnn_model.jpg b/api_tutorials/bait_cnn/scatter_bait_performance_cnn_model.jpg new file mode 100644 index 0000000000000000000000000000000000000000..67e0dcf42a2f5bc910df8265add5e1f29d92c5cb GIT binary patch literal 49487 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
% zE5O&I+e9396;Ptrk(1Z<(c*h>%4Wi-TG5E^4$U#IiFz%kc^9XcXxT&O-il-#ce3Eh z%gc+~oAw()fl{}bw>U%ChNk{-LOA0rBB5|25o193VO)?p(g&492I-L42-|U|&V>p$ z!l$Oj&~TB9Cg*8-tw(mhwt*^vnm-J7zmrT2Y?oq(sCE%k3w<8wH(Yb;lf4}Q?4CZ? zls{*i>)uu}wi5X3Fw^e-mjRt(h5j)wx5pyBJU=OEtZ6VTm(iRVOW?B>fC?LSnc1Ed zh2&%(b`I-sY!q1My5Lc3g!RyHr#Z&V+KX9}eA3|S7Yl~b^3$3%t*kbzAB+oZ(I0de zaHcSO={l*uvQzO~ZC;c)ox2OiCegN0h9DGp6%Bu5v%5IR_uF~3;I~R&lY)3(xYt$& z$H7_*oR^)`F(T-{NF9i`U1mAc92j$-;bb*h_8l`Wl#eG6LtC|*2*|c=X`a-sF0h|M zr_7^&JPXuX%eQ!+A)cW-k34}2>0M9(a+xR42JA$W7|?RY*m7!-Fu257Q`b2NG&RG-4Oy`aHXk56Wpp;j@d6o+LShHDBB`xr0SVEaAaU+8Xrz%YZZ}ZWp^4EX6jG zjg5L^Pa}2%P4?{4m+TjYwrAYvntIl}e;(h?s|X6t9f%ZmKh3Z;D&Vn_#!f;`!X3BTA%K92(ue+Zt$Rjej?>Z?+F)Mh^USGN7seiiwRNgJ8vXKn*!M+`cUx3%Bd9(aB0C*NyQl+r6`$Lj zTI__?jl{3Bbf5QN_mgND!$DOS=x%~8go9*>2Fh7$E7r$DDc!Fi?DW5&jUOIR-(GVe z<5Gy+c(A&j?O}kqmPW2Jv3wrvX$Dl6a>k6yQX-+ssNfaTCOYwqS%K8;i~{k&Ch-qy zkE}~G`Y$Ok)hWoe}6kJSKl(WrD|k2!>a=611o9_Ej1l z*S;A2EFkB=%>+n7U4JJx*DPijM!Nu?S!SG`G!*qA>2w!W9gRg;89RQMY;$?NDwNL` zUu2bQ-&!0>)_i^bI!77yH;M9K zMTx9|uEi{#KoEEqv#A@A0vU!%5(~sjZ8h|xPfI<&xFzs~8uGlD?=5&{@?cIQ4;Yy2Wr%$q+!jUw|<&ZpIz5a12N%>j=z$H&!Z0+?WhsuU~yv5%flDU(&Y~ z;iLXJ3-U&Tj&Ui8*W@aqr{lnS51&<{NB@*g7DaLd9hj$pTjP$CRh8>)r>cw+IRh^( zHD6~&!tYmo(8-hY-dHN_m^|T_#r4NK4MGPq>0J!weGcY>GyH82OKqfkXef2Lh1SIQ zwF4PZBjg6>!0N$HBS^mORspxW2vzR9)-8-a8`mL@J_YoVsvSE9tQHF-g#2hM94=|XzWg$DWuv|Fjq)JMcCy_2^4xHTW}OmVKVa_F6R6BMo>Yvl zu_$2buOMss@KpNK53~WtNgYE)ynu&%S%X_eQBzy~<+g!VfH39J$Ln zvA_X5)sJcTWtEWJjZ8FZ+%DPw`T1yMv5hM|zq(?6sO&gjl~e5L^=Pk@d%I_5EgTVc zy6X?@1dM1=WLOU|4k^eo0-+q8tMtu5E&&86;zj>^<{pSZlbW z$uEd^igrq??ak-cyTT4w>`HI5idyd+cz_0@#9eUU*>${w8(8jvOK@a3pVGamQTmr; zIZ~6_Xy*z~o9E4q&5dH6VPRi4*Ako^?gYcH3G<7upY>}1lbAX4+zw>S9UBN`siVPk z{BhVknkj`DV=!)fQD;f_EdJI|JR;E3!QFpyge`2q1@FUy^)gEmzgX6Pn3x3?u5kZY zeMkE4ikQ&oH`!|;fC1Z|LJZF}yb^mKXdK^sPE-_oaW7BqcL75dP-S{@#^E*2 z-8uxL0T|732aUr{Org-Yw6H zf0PmuCBX0ixM)3#Bbo3pEI&&+XNyvhf>u?|dlOt3O3jR_ba6e1(p*dn%AeiyaM2FP zsJXNou+LWCfh1>hKI4BTXA|5we`wh|5S@xAV3(}Ieu4d=4X+pANOo}VK)(5B1Kv6W zXHL!{^CXshf~3Ro59Brk?PZSbGQpBVahV0My3Z|B*{O5ASig3yAVhTcCT0Lue>9s8KLy}vAj*x*AkN@uwHM(M!A@n)ocDoq zh~HJ~;#I6t3G6G~je!xox(SF_>dLMi-ho(O2QhnEF->*{qHloh5eFf^p9L@i57^BU zxv*RMCl!%((=5q!hCRE$Bub|j8&8nURGIW2_j`K}NT(1}BZne*Gcp#bgp#M%`Kiyy zj4+49(zEn0z|Z;nw|rbWPFfH8>@q zMly{!1IqFfEDfeH%P{+NyxRnZ_{P&cILbAVQ6BMjZ~n?A!zD~xP9@PnEwf%kL}+W{ z-#5=cX)8_}y%!YzGN%*Ah0*V!Gs#BOrLrL91GUXVLk2Ez86jvE+ z-_z$034EreFG5a%pPc@Lr1c`}tJ(Vp7+}`yj_O3RK92sgyWG6(%#y5Q=zeBt(bKzb zJ9jdz>PNDtx2N&4T1n+7Y}0aEDy8{GzxUKqzNISl+uq8h>5$^dcM(hl%+NF#=!Xla zwiPVd(O=NS!l-1en}-ns!71BLKYXsHyxkY{=y?D_OYZz9zuix_H-1-)fDOdI>A&=! z>;xgOAA=dR2v^fYEv3BMfhe&g0GC+5sufqYLeo!GRj487sW4|mj@8$g%g-mm$^yLu zk`8Fd+nu)_7}WiSZ8V~q(!zW2HOoC4vt?u*mgH+hhOz`rxhhmP84Y{EvN4vaAXlKS z6b-f4OsuazdpKBCt70~(SbM(r9Uo!7;6HURWew0vmO)!xaRp*JarKIraaf)9ecE;p z5;iq{wt%$$i1Ncw9vm`2U1x@aiD8pkUx}u_z7#~&4Qt+b$D@Zkj#G<`T!hYINdeh6 ztCL&R(@mg^cgRG{zPf2NpDd$z0y`VU$j_af8-DOWo-z03W6=TaWX!jVT~=Gr|8P4U z{gn+IAX0tapkt=tK`|o<5d##Wlx%DENY02VeKQxT>>ElS#elbzO?}0kIDVmMZF| z^G(xkvXf_$+MJxcP?~6T_J3+ug%U8>-}S+CBSo>kirF*j-Nd^7LX3p_u)!E`;?hL8 z&f=CIZcX%H;YyIYv!wUg>w5!dxD&19@XWu7+XU`YJsWVEej%P_Nm}`Vrxnc!hsy-t zPHKM@d^^9KyMI(okLWpKIBt=$E^abh-n=Gc{U+j{JmH@r*6xMynRLdLW_EF#DpP%! zHed#!sr}JhkopX7tj2yuT9+thn*8QO;dJse#f>(U;;B6wyzzIKme?*stamj5Pm#v= zRcB)POP2eN{z1JpQ~PXUTmC(2S|%>vEqZxeJM?QyiiB0x#TC74{I)(Il<{ww#qysN z$+d>u)gjI@0z_$ZR=_D*a<>yfHmM#=LP5s1PkofyV$#KgB|h5d>-wwZSgYlFYS1G% zf`2`T39Ql2I01M%k|Y^Sz^CYVzoSdnp_-hMZ708F+S_j!e19|b>HAvN5%i3^Slk|P z_V=HzsfG#WkuC$QH;hm^&>Gdd0n#D3tA3eAogXc*Oyl&p;m2)16~6Jf8kRGNy0|%^ zuPb&zEh8+v;M?&Qay6iirG}G;NgYCfDH(b9^JP>3dAj!~edO-XbPw;}-ebSMHMce! zS*LCeeOvf)pC>;-@Muc+Yd(;k^q+41U(N7b4~Op_o+^sm2cS8~L|!xf=)s^yB$Dlv$GO$7bBKD<#UU?8Y>x`32)%w+!lb##y?;Zq8-d8!6ZxIr>b+1n~9j0|V{GaUiPZfj`oZ1c==nwlI z)(0ULQli5y?hreWSqbFGU70ehVBgq*Txu^|(ERN*bRBClW*(NXbR{$Y$n`{$qlYs` zu!SiRg0CG&hI_Y%llCVr_k;_iAQBA@SDo9mu20?PANaDdJt*^wzV>$S(2;urUhjC* zy8kI7{!`UOjK`Alk#Do_V5ET|(x-IV@pS{86b;`JeTC_?29I-Fjxsmu8}Hi8y(`yF z&Q+}&Xtb|+SWC(Y(FebxR)-4aMBW7&KGp05W)S=bXu63HWp3_-oA5hZSZ~g%naYGF z=kpV{o~Y^-*#0)ZkHgTC)RYfGAEg}sVKx7>=s0L1ya~}i0^&pB2H8BQWRTuKU+rSJ z7SALt_j2}!i?HJ_Tol~$XKL3O%<=1PS}fo1a{*MrbyczNg53=^%?Yl6kwFW9$dOf+ z@8|GeILY9`+V2&I)^e$0#4p3rNlW{!o2)n&ou8^y{Db-a*-fjMmY_?3ECw(Z%z-Ts ztqvXtOK^5wuQ*U7gG1E-c&FO>2`!ZX-@EH$Q!6K8-gn*SxGY{fBX;B1o1R5WYBtk^ zg$3YVG)ancPeZui4?Q=t>=%Kq#Z%er$5Y>Z;;CalHhZ+1&+eD9803C@lhU^QpT;(* zIr|ab7Xd~eT+-!Pth~3^rPiq$Kc(kLcOo4I-wnq)v1S0 zrmiYq%40rHtNO>{2=?z))MtL&nj~nQ5C6r_N(rZ*S={bgEsk+)u1y-T%Rs$zs;`|0 zC94fc&*wEv+?q}?g5{$gi(R{MtoquhfvsYXu~u2K$?@l|KmOdpg(x*7Cg!#FiPr1B zx|C*MCoO)7PY9%U^P!L|NyGqFstwE3o%clh0Jw70kozb`1GU;y)?3(DT--jn_9l6hhu(0uD4>TY+AYvz%J)Z995RsWJOj2PUAhd^II6q z_~oEm(~|C8;a?w_R%xvGAJzb z29s?Zdtqrk967SZ0@^-2ztwx=;CV849OT!^K^~CS5(Ed-ovEQr_YY(yF&insUxp=z zsMmfrEY?R(7*tt|jdnt>>S${3YYx18a9;Abg-M=TVLJsj17jScBdB#DOsNo99Wi;N zdrlGcbey#>l;;%R(=3GH{zQsjzD93L?uezthJP&cVeTr3ge8Nh8O?TM2G2MLq{I70 z;^6!isg(2)Xh-_1)otZ{6XEL{RKm^2RUZqYBpN;zEoMuebG5tw+^hSb{l`Ee%@PNw z%O;!cKSOuu!Pnh1j%7EcNV+$SQPGIkJ*B5!J@1ZevE8qoc-Lw__sO$7-?&=S{^Cfq zjdUkvIrcrnY8lKpoDb^@+XK=uvU}Mv=hOSAJR$gooXtgQJ~TY^7Ja3F!5B4{I`UlY z^cnqg38m;yZPN+ZKpkn!tW727BT)&gZws*sXjw8;4hXj!H!-h7O|70u>$b73TF?kB zwAFVmTvE7n%RKP2nA?d0#pJ&TU_2UxjPLqkdXcc7Bm6;lDKnvl4?yfw?R4^Cwtg?6 zq$tMkWkRM||Flz-F2aP*$9DAGdZoo>32PnNHcL{Up+L*-O2^L!7f+;(5v9Ynu#^#{?ov#o%&Z!ubsFS$N3L4dGyX zjQ~sM;Tqa8BpPJd_LXIlY3rpgFJLA@Ktd8=`HU4s`Kk zI$kXoxK#s(BTH~}Va~~G*cm6V2|6dy_;m;3L|iX~Y=MIy>@F6Rj8DMG(zcVXY8wn2 z5A-l@F%-gzVl`zLZ=WA5m$EPA%alIp=m-V}9`05o;M@vhaaQoavF05JWd#l~C7`uN zOS;u^u0K8LWvwVQ^7RduX;RE|KN#E3??5u~w2OE!3DpN=w85EffQ?flt*;^Blba)r za=FI`qkd|SOzk0^$B81izP*IBxt5_n$Idst)E7XC%l93!K3Y^4b!*$%;bXu51KQ<5 zxuDY*Bi*d~jWVuQZ}aCwR9%Z&0Po z=k&@OLDSa^ZWwJCN$q=X)!pJ}&a*r+8}e5jb{pB-e`MEm%UCv}25>R;{(*6{JuDTZ z$SPTDu4z7r7O<(3kk(<9I-dt{|O?}F= zt&ga6cT=l2mVLj5CEdaCv4m->jNP>D^tR)RFj?e^Yt^UfkcycQqPU@l>)402hW67( z-nz_x8RAQ)p?icZ#*PfFhXwLb5&!G%`TlQ;ZaVlx1pYtx&`R;AdnSh|O8e@IF9HM6 zw`lHN5x2Kgs~=Ez?|CeC>4vU{xff8!Ia30nn{O+yKTPVmTFj;Fxc=LhG=+|t#fzGR zG95iQ{n*`WhE*Z1<3Fv}(JmBI{(}ocJrY-TJ#Eg*;@#NtI_X_0rN?H+DUX_4hU}pc z21JJkidFh~a~=q{$1S}*CT~J{{sj6MPs+z{hI;Pbfz-tT;Fc?2Z%11EMN({#zmHZ+ zfJ3HEG-&gQZO`h`!2j_%rRp>@H8c+Zr}c1z!)a$IxHy(zKp85104ZXohM>%QAH9Wh zh>1pPzZI09Vb!Gi65@qAe6ENTGCe!60O$_9J?w0bF6(wOqma&sUucDa zr(IR4bruu!X=-ekC~ay=L)4x0O*=NZUsog2XBdKBVuoP4K!(0i9}YNZ*#*Cgg#PPL z%9NSzR9iq4J%G}b@sr4RdEj+p-i!0KuL_sECGzo;>sLo-8hx`4KZ)|ISlIxnQ$bVk ztJyO>6iaFoc?Tlt8aQ2IOl$jdUZINp&Q`R%fal}Qi^6gGn|JlcRjY2f!aMPax1s&R z5sdV4v{tq}kbUy)s9|VW$d9Ffaq8FR`~||X`H$CBK180lFH!bzq~MFARVO`)dWQ)| zQNm#!sN}Bger)RS%ce~2t8KQg2t&92+*WaEL-2JQ7FnpCH*87CY4JuFaBWUgL&-|Q zjPvZ5Xt0(DhSjNe5(QSLIkMomK7&L@c4qU!poEayQHUXFM^}hCxB^C`^mnny z-oiHzO-@m?VtQ{UHN0Lq%saR*D};S6dfhkc$VTnad(XnXpM)J}l*1>Rm@ngWG4eZ* z563@asiL7fkZ1XgtCD0KE4NsH(>X=_qY+3O=*1_O7*6WcBX*&P=O#7`24-KmT}BQZ zo_l;h*-KZx-ezy|llvoO}_<-`>dhp6l zDru2Nt}nWJEd-qSs{j#97-tM8f)ChG0%=Q06|%jiIxg)+E;M|+VY}bY$(pPK<~E`F zh?bC-_n%)G%&6ji{VuasqYBQ2?XtytZ=_vB#i36CMHM09D0@Q$U>&ZB=BsLNEYdE% z!I-`Uk_+Kl+WBWsghoYgK6O#eT=cuaH4nf+qvwG=uw6SqB*A8JWJdC$t<_*8O!hlR zq^5CFJ0szdNP&It{@C8)xrfUp-|9{Hn5Jkhz;O{NhPDQfv=dz%MY?RdzYE&2`n$4x zLtXonugJRcI?BgITTcHS&pw|5pD)uzE?~?*1rLCw!UW9JQLE&rzPeIRmLaY9Q#HkY zF+BOt@QY7(APprbaFUt+s!HdLmepNn?!6tTq_bHkdsM1>K~_LDHHv0)X}nEGX;?2dnaysxFH@|^Q}D}t-NomxLaOc{<1P@stO`yZw`@8ri}dF zjf&xPgH6u!&8emT{SYioF2FYq+-Y-Tx(zU8S$edrD$2XnP~&JcmLj_QyJZmBaC0PY z&)8kTM{`Rv8A^h@zDYt5CVozzJdlMC2GKq5?BrIN7K+w6qIpX}KS=SSc4?U$I_G_` z?6F>6@h1|mZ|l(oGsdSP#$|SH$8OF5UKC_aB`BO<-LtI|Rk5n@pgV-^y7tMH^6e^* zT5{P*2nhnBNOe$J#sNcsf@heeBqgS0n~_x~Tx}U>G%LpEYLuPSkR7gn8$kO%5NAAFX^KGJe?mz_aRN@E~3=v%CwAnBFd;f5S{L$ZtWbf~n);$; zQdTCVS_q{uVmob6-PjYsuwJ| zUbkypfOB&`(cUgc&K8Is82x%zP6X8XzLWbwVnP^97)WMx)Uh);;wYFMJ@eO#wmP)M z^v|NctnqI9=+Gt2v76EJw2q5!{dn)^^H~Nlk79=Yw!1`Lca$)xEHN{&YQ5Bu^CBwp z+O}-ewuG*a+SYrU!A_+oHOK7uY?vmTk4TW{>xUCuKpBw=;#J2&BegEZT3&p#9KEii zz5hnvQsL>ba~8MWd;>opHSyOI|3CCbPur)QFUUF=*b)WJ;(dx$DSV5pGf9J+)bC>` zpNm<7D)pOY<4X(e&GmVuOvjF|*6C0~6nGHFKMkxVpxCanpllg#=AofW8Lx5{LY+*H%~;mm&cF3oC5iX#p6LqK86c5_P3YjAGE--r zr+Z{2k~g3~#*wS8pUSSOG}Shy3V+bd`f}*?UF#?O=zoe9ZD+{YYh}!%oT=OXr7u~# z=}VK0r%!?vE)hDiD}iwP(dxA)&5sph;)7nzbsfKJulPG6LH9#VV-4Z@+9tLleA^as z1r2Y_8X(`wjM1yHx_LP2N_tC*SotxYaz=F#I7TG`hYsX1{n7qY97#)B&hjal!mF)G zx#-A!*O%P}4p}()?dovA>F(jC?jE-Y0bgdz&J zLM9U>$y4=v-H?)9bBH65u#L^4aHVyEcV=p{ad7nd2|Nl)AHcw z98_Mh<0Gd_*IwjZe#QIx@f`%igxvrSf@k7+u>SZv;Hmtc@OTbn+5|}xL4lN){_XDR zvYyu6#e1fv?)@P67l%ZP#CWhZv%!LI(_~N|laaE{zMvj%hFtJu( z&30L2r2z%_Ug(1gac|#o#CQr$!W9Qfq0pY z{gx4xEMzw2rsq*yugtWgoHo+of83Wo;;zRsuJ>Nb%?0OZ2h@8`xFHn2uD%Nb&bucBK&79fI3h|UxXdmzRDGN_k{F2HbMMQCRMjjV#a>wn zo{o-6dH^fg#1sr}AoX-CI^<3Rc=kn(fa)PFXO>P!E=JMLoN6CKYbbi0TAJ^Q^bviK zdCTPSd&)7f@{E2S88z`lrR{Y1*W=_3rce_ncj%0uJ7?8+nU@JYI|L&Onoe!%q}kD&6VR_P-nO9o7{n16@@WQf+R$k zQD(zo`L-QMn!<$af^9(pHSMk4=9<_353$V&5pm9Ya~#GxcsavFKDd7|k3Egph2VAz z%qMhIo0YvbxUgvx8KGYgkYg=;V)4W!iPNtjSA>d4BjEAo07lGsq{m^>bkC|Nzfy!t z^JFBprd%sBO1~gl>0!Rf<3r2A+R`U^q-XyZoJQcEfsv499bkVotH?POOC^~d+JUUZ zR6CE>|6ketzw?`mf}`1+x-H72GL?XL7XjG_ikx9@tbxZo%Hs1##axi+E5e&9(}dux zk!a<@aAj%p0KI2d`Q9F0gn>N9HB)%MP6|$#vkNnVlkZRgK23M-K;$~q-O;Bf4B3{> z?P)p!rV9 zt){>#OGhz?;8FF1_N$H1@L1M3eF16s!}*AoHCO^nw~+{j`W3#v7*=yIeS>w=ut`k2XXHm*3`ak3!{ig7X|4= znsiX46N!p|h!E*bh>A)R5Tr^VD$<*Rz>*fFw-70zOQZ{kNS7cb2}MdIK@E|V_06@< z-QTnKKI@!&_PO`$`v>F^5R#eSe9L&p7%!w=Cr~UxzM-IYfoV%u>O+7fyHh3*KGQpi{|<_nB$&x6fC zG-RaT>$F)O<6({UUYzapDwVUSutZ(=o7tOjln!Id@s`1~M|vJ=lBJk%c=bp7*DWya z3W_z=2nbw!-ZyX)+<3(~ed|%{zT~X?HDBZ|9{`-!0SMTuJhol*oH;}+6 zN;L&WlMiGR*HD4Bv4Ib-$vlgj{WNtTf%-dJBJzj24~(XRVCk68vt$vUFlC5k;O3@H z2iTk!MFi3S@cH6QHUM)w{mw?TkH!)dnLMseEIxo=pXRtK^#AIYI7qsBC1X$lBiAT= zZ?kK~;c03?u3xlKYH{4WnbksUgR`gFx&NzvMZanY6{I1;TR&SAMe9RPR)$E z3&mXCXWll_QBCp!1)5N`_~9f#j$wt_Pnq|uwA?hzcD8)-UgimG=M8qK@-@9S_RNO`MWtDQ?m>#i(=DD;UrRIUZoiuw zPLMJo;Yc5mx1POae(Z-D-g+el@_pR8!n)i1r*jMTKh4>gKZV8E03hg}{-6L_#7xFr z#OIy<5waBHO8Z8%9?7fw4z?NF0BQ0^V6cr;I8f!XM(Kgi8~2LBa~s#^|EPPrb-K=M zf{t~`z%^;Abnp_xu5+9v4X_~!DU-lpp|Goec#cn%jCPIR-ctfS+vC>4EybAUplv65j+nQ3q+GHTQDeAi zfP#c^os3SL*rOpB%4}M<%hiaj16uQ9`JruE5-y3w_R=@5J=JNFsu|@VO<21TM z%=jV~faQBZL|K!FW7=u1K&By^CFs^vyLewSRap5nVBqZNBWL!Yh}|$F=)Cmfzt4@! zF>H)r%HBXw4M>tTgcOVsLEQb#co>X$@M?%zu@;%%nX#Q_OPBWOUt0Tx*s{C(I~y`E z`d@pC{&E9^vTsJ9hd2titC?@YnLn*%uhD+6t-AK*%B#vG z=c5vp_g?HnFOp$T;b*Hh^VSm>nibAVez}$pP#!KW^0u&#^|gNTA4;@Nn>uhLom3KH z%Jqu#ry!zGKKsIPT_x3>!G0n$IhI1X4W+kqB1PdJFDluzW(plB;QsV)k?yY%?{6Oe z1!GC}UI30Q0rVa$6~<9!D%gtxS?>RlW4H$R|DjR6z{t_3E8(U_i9a8!x;fvC#OHNK zVRSjQMpfbl_nY^SvoRysCV{85hWmZ!Zg-fX9)5e|{3 zUl_tCahrr4OH2Y?2lrE*WIU?s|H9)~ebv*veJ5h>09qu%go+20pJqz~v6sl}sZHn; zi<_7Imjg*Pbe zmyDDNT^q%ZAP`0*-4sf9iun?TK)y< zC9-mUW6bC9b3fxb|J;uUUF6)VUdp9wL#_Zag=5AiXglv@)G6LPxC54$J zflVq*mfcde0)^&1s*N-XCFQ${pT+lWi052P?QkYX*Cn&j0Ac7ndGZQ*iIhZPA6NoR z?6!!Saj*lL434sS5I^v%X1k=Crx^CR;e6_(_~-EYQSX$y=X2etlh7*wzzK+IWh0AZxQCqznCGNI8KQadb z>s2Wj7fghxN@@9op7p$|Bk`HQn#49YI_Htbb!&9BF0;#AxQo*t)krJlGd$G#)>}hI zO#N2IwJA@GbKb`D%U{laJZyQiO(y)P3h6(6dGJ3*2$;@e@IRT_~ zO&lk2CTW?jd8E873J;6#5~qL-g`P+5$y$z@IFNrn&(}{?ZqB60T}pBk!arzi@~dMNltv0*sLIDA#@Qe_}H8#}o&h1H5)XjZ00YnD@~^p9869KLOKhpSlen z1R*dnFTQB2f(E@Dew%(Np!eRMhQqY5MK3$!{0p`W>JYOO>n(5se!ZC)2f2k(iGoV` zy271U>J--Nik8n{nbP-tJaHNwUaeVvN2YEcJVJl>V&AV9XEl>u=4w*2X<{?wSs%|s zav)_YYf6b6HnTtAq~_k7k!@LE2UaNYHROzMBFSB+@Zttu`&4uXM%6A{kPKeBoXCE4 zOY}%t{sZA_LMQau9Mk^O^ZUmT^j~p?&j8_=n-+=>1WrZ$UaB@o4DK_@l5*Y{@vO^` z@H;xFp7Y%JB_goug3eU1q1w!1XsMr?>FkHJLOs4YG@v573rd6uA?}T(VL7ngJAk&a zX|IO|#c47FzOuyo0~W^;pYd0u7}xVzz|K z7rP!Xvmjmw9I^)?!4mUDHftub1Zl8~^|V1ERt#?NhOC*gqmzO?v9&0Z;ohaYrO=;U zZR+n=zO-q5FiF}oph57cRVn)q@nqHYA?!V9s<;4Y55MzyYY^G!P?U{d5{r1yC*Jb3 zGZK_;ONd&TaIZ;a(}1Xw;o7o|pfEd9CXg z^e^R|Nb&woxSeTwFXO>X9m&6GE3UOIUF!y%^ZwcHpRWWC0k6&MceY^_>|HGVyZgP03N_B>oliu zUS%djJkh+c4VD_kIG4I}lwpB=^E_mSdqa&R0hN5pk++fDP^_$AZB$S(ZhVMLt-@<^ z_(+S)f?Xof9virOCuSe66(_cc?8v*%WJ%_{4ghZX#PwK))E&I$F&gK9^qE%5o#7O} zb1DKNE?>H-ao|IpS(a^0sV&13wt%ByZoSBK>oAhY-aWZHdK=3$D14ge51M*Vp zPxSGPr2_-ant>ttYvhlZTjO7k$(&L>mb?4K!t)fMN!b3R?R!;+sz^$~FX52ToOQxd zFJykJ8^#IddPNKWNI6PD^d1TIejTysltZP)yeI^ zq;oqPLqUrYvNXPwkP?! z7L2>h_5iFuGy~GOFx!JNxH9#B3pp>!c@ z6h8d%Iiot-jT+BP1qk%Ghkgh^T?T2G?HvOxZRj5BE?bHi>uz=}m$bf7nUNeG_G_|JwPIkn(eMpKXO17-w7ko`vij^GXg;41NOG^Gb3s5_%v9+~Ml9o39uKZz**sfK9#^qyPY zu=$YLwrTOzh?jqQ>hI<>{~5RTUogX2LAEIN@UasUwx?@os{I~D6L6jKxh?TiZxOYA z_?T`XS4Xs0wU&F}9hmHeySJV-vK=$~X#R(3AgUG)JTQcg_j*XtfUv_M#mtAp@ct46 zr*yF2GKsz0SpUi1fNSUe50O1R+@*0-BeR)n(C`Nlu3D?C{(CORhc#hW5Z&&irVha9 z51@c<{{$U@Br4`Qk~h8EWp=y9qN@uL*5YuzA|Ed5iB7tHJMVeAGE&|i(in{;?Q!fV(Y+#?lGkU0;Ea84vy}a1^gEPS&m&E z$>)O(Y#DArgzBD22yiqD76k7 zv!~}|?cDMH+X_+b!$pDY!@}p;oc#W&&Ia`7sR;e&{OwQw+4=vEzQ;KVBRh>c;fsJK z;Lvx<+G|Qp&lZZNcKvG-kxzcJJuPd&2a0ko!tE5Pn{jZ9sn>cP00ue>JZJ1?9~lS&5S@e&&#w7{~Ad04;MACk-}^|3DvX_ zP&~7Yy!hk*U(J_64rKOyc}Ks`9|E{_&l~R)|G`u10x?_F5Pm!H5b2l`5pv5;3?V4t zir}4{kHS5~+ilhH=lE2W2YfXZUUtWUeQ=&hv6wpBoA*2hl zRvS9^Isof=mJmev1@^(y0zut*ceBH*%TxCY+c*9Fq-!(otn)V>SIb+9NnJgX5S3hN~WSa?8y6brgz|{E=cM&9D*5eiy6H?{fFL$d@G3DL#X_8 zKPyziqAa8q5Y{BTOdVBTu77cQ;%Vt-b5?4g=p1Y@T9AqovT06NCX~ zt)OCt3NuZ6%gAj<#P@;E;z?S>Ir(ZX9Ff8Es&v3j)~+TAOB@}TB?mJ{=h z&I0=d+8Mf3#6u%M@GrJ#<%Nb@w6&&o<5jh<-TuwI1D@5YR=<4HNY4WvHcd{i9zdj4agZK3FeI7&TA#)ZOPWrEA7~jNxa#0Q@0A zhZus@zTwpN8=01AmdrkE|c)L{xN9ouyHIeD1|?d}p~ zefXTG+}cTZEQ?wfa~sWBA9|CLNZg$m6GNO{rs(xxaa6>Sh$n-9drMc!9kd6H;BVKP zR|MF68_ip3ntJyPmW=rGt#&Nj^B<$W*0w!!Qt)x?R>(EZWY!QcLH9~z9i(9gfpgfU z#_$Z3hF_$;taKhDq&-}^V0CIhB1+;xpqH1|?S>CG;kCbU@Tqqm0Hp5X!d+E$Tcahktbqrc-lKRo z2VM;gxkLo2@XD{77Z%fdS2y3Yjm=;APHg}J7!zQ`Z#&`~0G3+P&zIV0!H8X=%|)=X z_v@##g8c;}x(>txk>&&E6@x>u0G>Ox44IQ#Zq6sW;eReL7clulLD5y4xmM~QUq`A2 z7Ogod#)m5;in70|1n8-txp4g0b}TtyWO=G0L+7?mOJ1LJ1gp-V>$j1e^lBr+2ArSqD_ zKT%M%s@i!vR3yNq^9+8|(AuUN=^vO9T?Yf{i(AexL;-twAAoUZ zO#wSfD^RR*djgMmLk|9ga-Lhmdio&BeQkX`ueG=57$y26njHKkwtHpG=VdaE9N1g< z(;{-1GM3b7usp`;8>2!Ad+SxUk#oYBN^(SS*0bie zwH43Ij-bnweER2VPTQttPXct0;%7a7hmicy*{cGReP5IvakhUa&M>2Q>2o;2JkB3} znI)THLUBs;ntIwmyL+(h4|^uQ{3geXa>1pX@7E8pDUu#U(=WrX(VSTam?kDZq2`<()qw`QJ#Y{PAQ}&=CS{QX zB5SwApBCL&L?cd0hkp4M^CP1vimkfjvDF{l7$9&&){pgABH|#t>UVSDqv3Ua;X7U5 zUApdr=Q|QMKlX)ueZ?Wk{R8(;&$jh> zKC3PMT^CQsTZoOnlub5~xHR}dNPB6{jVj5E!}{$!nOWjOc!^5%Lj-jS15GD>GA##O zb6$tPmKRr8Wz3#7jnZD0D?`tfKIgqxzNw=Sn|%^=3sBOt*}w`s&XEU!tS~1>eMy0m zT9&q`y3HB^YPuP+GhMg4f3>YtF^lJQVM}nmiCC+zB6Me?MRp+sCuZwCA|v;~ zYW&g2^Jft!morW=6VPBUfO*?X+o6jtFl;HXlupIvw{krm3goz@v`&+tT3=V+P0FUt zb?0+GC(rfw`Dx^?o39(SrYzW`(w!x$LV{eYUUrnn9xC60pVupx7XDpW=cs)O_SR*m4ku_CCRT1bsUKDlkSt_ zMV!C09sZpy=A(G{_D`VEsi#>*`w=348f8dF$eB{C@R>4_VW`kDobY22MHxYR5k<8* zeRgi+i9c;tZ9Lvyw{o(r=`6(ju+Z#Q*yS~rPCt=cX5p%`|FC(9F#jt~$i_MU?`Kp_ z$uZ+GO0?$`A|9R0IvUf5kOC`~)3z*U)SHmUH=RUIMRuO!{e0H2)I~X1R=#}su)~1E ztO{GqvneA`Ds&-GhJ%=}Jh{uk86NBU^!e=B?b6>I)ijQ9tD z@XYiaG()`OQUV&w=MszT zF%2!)iBF{h+V^Yvg~+dLI%^_*E5;$qwJ@?Z0HrK7>%H@j>JNi7m_K5jWI&tVO!{l* zqY#%H+^x4R$J(W@?O8TWX&l!Lw({Mhm}vYEv4dND&t z@@0R!Wj=K8XUuaVQiL|r<7%C`2Xx2|{iV7m?$5hlw+IRexRY$&dF}PdcyGqKh{6%h z4Rs*9f{gyqVNBz(DMjbRcLoqR1Ys$KAzO4NrSA<~Z3Z)X4i1r8(Yv*a(>>}u7nA1v#BpaPEkYcL#xd4Qq(iB{#%2e&F zE8_N5So&_aLLkYf!w&Lez%{H5H{@DYj}RfpCAG{f`ay~LSp3ipiE z2?@GURu{o0)K607HHmn><4V^I>*N`^+tT6DIv)I%FV|ki!R^mP7+gzJNk1K6L3f@f zI?fp{Mo*smKvW+&Ep)oE=N8d@s=Q3Qc(yT{#Tkv;S)F@ToUFPCK3$wlfuumyP_|0yu*@5AB0S&sZG04^Z^b(?`zDNX}R_1Kio zv*Z9iSE5r})>XZSF^{X63D>5YN~cgT`}!v2hm7I2*2$S~i>LUXT4>#Tv1_aioF%^v z5B-@g8HOg!2AI!#(5lGvo+TdRGjMCl;kL?%^=`xT$G3_krZs++O=$ndv0t(X@TC1G zHLko0jk}jlbyN`;N0MDYNlQl*{oVR-Xn;C{K60y$WW~bx`a58RFGDxa)1KM}Ibz8-`Nna!% zgQphfv1y}yObvQyPrQ~qa0w6Y_f9%+`>I^V^|K*j$tkwth7hFKo~ ziNLvOVu1LFY@H^&z&PH`ZSg=X!r#&2;7fOtgXiDdj4g(KG&A*&`2-O~86RYvnvPLg z^v>LrgF4Ts>X*)~S;(h|KlyCr^nkZO+VJ3gps_d#ykU07pb3Tn=$mAJ`7Uf%8)nL1 zGcd)qHmLT}_6=mGMkCt=_5cIkBV)Zmf#UbSnON6xilkD7S1pFsFWSAZ+Y_3wD4!~v za+?|bVLTfbVU&1wXkzf@{i4NG-^w9_0TrvLkLnBB1r7z76C2VMbk3MhSV4$4EWSOb zks@IgOEUDRRF|D1`7~xSSY=-A7z{tE1*|!+J4Mbjf=eN3m04_I=<>ZL-Xo>vd(*Q+XR9v&iTG zI4e@c?qd*tA;*OWLs8n_UVC{7EmD10dCta~3}^;5Y1~*g<^!z3izHR@6>_{2;^WVc zs#MQfXpYyAT$i|lE?g~5=FT}|;Qmh1VdwKaHHgMVS7GSnFYk0)Swz@rZU4jP8VdsJSkQLeNl{_x znLa8x_vflx=2p{*0q&oHn2?z1^4KOL_v#kpb*59#MAenjMcLx=0@;;f99A1g^IYGW zEBXd~?)1_7R`q0!HFunzP1TU;kfNW-ud>yR1(5Z)sql`p=>L-P{xf2qzy9q1IHewr zr{4rLn;2v&us;f5=rtj?^K0QcR?v5gA{4U}1YipEgt`1Y)TJr%7(t*OpW{66Qsr15 zyhY7msLqMX#YAG4OCgPpuK=E(h9C0aHVU7zP z?3Y?tJ}>GuWH;T0uJis&mB@dv7hqB3gu+r9V#aVVk}8?CKKSN6Wpp_OA?Dmwm;NZ% z?ykc3$Je+{R)`vGxWm#q1DCP!U4m0xCjq0em|Q%Q)2uOb$7$y0+lxk)MiL-ajIrCEBfu2Plit02)Zs!u2)icSI%H_J$fVzjP+ZO>InebPcEH9He@eyGPp@Xdr$Zno z7TkPumV7@EraaH|?8>Jl=ADOTudzw~SU4{w@@@<_OSNZKtV|J@c?chL!qU;DOU~8} zU56=!>2>!Gbuda!V{)LXh}=`aj=a_8vrPmDu7>y&(YVy@4<(%Z1hOj>Q%9+@zc7)1 zd7$u}kv(rw!84JQ&E`*8Ll7aB8jXj#16adY5HfJmvxIL+6^zsMl-nMPS?4G3VIceq z7SRfolbY7KLb?-b<7njJTj|%{jCOpmtu;c!C>Kzu<^Fl+K2z35n_s^2|_v+c@B z-`R(b56hXkpg3GpP@MXUg6%J+rwJ(3;>f<=7-ab=NK%g00hD6BG$Z`45YC2Hazd{F z--gy@$>X)939bpWMw(bG|%qyC-Z7EBn?&C_MX z;Gl3ajxR0eb|cy55qJG?i0&PMu^Ly48!{q$Jms#a{+{-g%kz}NJ_EKJ{vPLQsCZ^W zjC9NpjIT7`jdH~`BHy*C5UlB~E*%zWuXI+c`k5e4)vs1X_M)KfN25x%@5_?WNoj#z zB%ul(S4uWCRDnupyXh}c5H{7hTgc0Qi&+M#o{SkSbJ#5{6Zg4dn~f6FT`8(EbJg?A zVy4B&$9!JY1nNX7RpLx6f6p+VaA#WD zsSX4o;x@83*OK?%$s%oF-I?@h-JSCvUZt{28@s;)82}YYT7=Oc@-Wsf0&Z6x;pCWm zk>W9GHqtNnEut3v45YR28#^Xe)^MegM~PjA3!OF+)V3!;i!#^lnLs6{1fw7*3)||- z2bM)Pwmug9=6gZVrMHAUU&VRP%KLzKXVAj+E{v~!w9FNdgEdCk@qUJEfXemi>q8$M zoBV66cM(@;Zq$u&V~vp-MC8Fmkl4CsUpAkPl9u z$8>ef7=%3(&F2zfWJ`m-8Y=ghY?{$Eu{OOdm8dq-#2aujfJHt00G1J>%`CxsFC`$w z)w}e7&DO%R)R&Qg<B859tUn?g<@%)}9)k6;2| z69R;rRvK{y+7N-g(vXbTrc+Z>*|rZd-f<|j;9G8TXLf}jrdYcd-i}BOL)aEDf6aX? zgoLHQ$17CL5CSS|whgwcPA~HG&%Spm>;D-Hj^#@%QBx@s>zGPanF-pfws}#;q14e- zn%@~&=G}dE4T(!&aYxc`(EwYzu~fv}u_r~!u!M110Bf^n@G6=5Qt|=L{NC6xQ~3`P zo;MFZ7dv0;B*_c%U;^9ld;!xIM7J^PINXqSmOM-KhaUBfktJg)lDbYsq(hDkG<@;6 zKBN|WCOexoeqUVdg4rGC;a`!Wx3Iu6a-hX+Sj@(gjPS^wvADcCBq4GAW^Kd(u#+{c z>*NioMW5#{1Plt3zKeL{beGdQ9(0D%&v(Dx%IV_Izu20&WchSZdthBLs`2s_Wl3JG zRIf~~9S^BM;qSFbsl<`s586S`Lx{Gin^iqF!3}x$J;oMXcPHCda}~-)A~cOu@r!J| z_qWe}*-P{~Vku%MeBD6B)Iw2dG8J!KhVpAKXvk<>o3X{1?{kvn`VnMnfWtHY0NXQ` z>J-j0XpKtQI>+t2YI){?{MGM?rQVM+P0hm2>=?zw7+(~|av2|s!99eLDm0TBPT@6t zCw*s+`L@^M9{cgs9K5qjyv*p~es=!yqf@f>+3uKMKbO&|9tX(KzIo7bhH-OFuUk}q zYs;+vE#`{eF^vh6dW|+-`^UR!eV$^%`#ZoQ+6%Oo6ai|;M5hnMc+ujg#U<8N(y>8> zZSECwnQLqukKXD3dc?>&2oXqG;bk7z=W+yTS8+p(6$ z+Ha=N0XpGX+s*B_l|BgS?u2O!vq`)-P>>mYKn_1Px7F(*!Yl?l4jy$&0-ag7Mv-q{ zanw^tIrrmOexiS`q(5%hkfg?5s(GPaKi;@)R}#Oem>b*o7VB~LEH*-lXLCH zddEEP+(>Kk0!zL6$u>K6r@%dfSSN^1!EgErB&Au{rJxk_Epu$(RGXBP7ZQ8&NY&;B zrxxj(J--=zz?j2^xjVhiKs%b(5bJR)2P5 zcMk;_wR5E2^C#_|0s7o?LEB6Gz^03xLJ+y3!YB%->Ln55GB)v%(x!S<9HnQ4SfA>apbv4&r zwe#i2F)Ka4j?O?uUrbg)U;Pez{!j77e*vEOS1l#`Kq!c&2&`6^b)qFf z`blm8Q~zuzVFcxaNkIx-higv2Z1QbLcq2RmwQeq4{;9L_- z%Z59V{d)3VQOe%F#ip8-;OBjS4Uo55Y~Pqiz7@a$Yp7*MT2IRJ-feGJVt-CEphJM| z1BIRE^qwNTcAj_$k~Ok}AV}kv%R79yjhtP})Ie`iiRs-zF*zN8^WS~ZA1o6@SELmY z2Mj}}d)&;0@**4WI3;htKc+Zx@L&L3PfY2nEyI+xtvCtCZ-KepagO8<9owOuf^4r+I0-;aXNOPdN5qO_m}7}*47rkx zJ=b&jsLmRHW;Q$cxiNWS{h_i=)1%A1>>i`Q^EyfsWMGC#kli+<2eXh-9YQyXau^lO`z)o@SB>8abvHx(MpLAk=M^DXy+V(UdjKeDNr zS;%K1xbWMK63YFz*6lbTK4VJ3)@9`H)|e~ljTIrkc82q;V4(^-vS4qDX6#uFv2+J> zDa#Y9Ru)R*S%rqr;#96Y-NbR3Y3L}3czH<$6x{wb-$=3U`QD3=HWvGu7x)BC@;IkE zu+X~qEm0` zy|+i^<$VqX%G{bb(o=&q)nHR!t$WZC^lQzcG+QqH;*liXav^>#C~-(9znbh} z@rtI?p`UvWODYidks6cbkXW;Zcb^QD$+uwONMT3eF^$Vi@MN2Udz*_>xO?>vnYSw zu=_sdd;K>nz1|p!$xh7*J_z)pg;Dv-Uy-nmGe1$>G)%%s18qzo*CaD5s4K}pzed?e zb>7LlnH05kYHqjFZcx&)ueq0amYbG*#vko(YukzJSB1n`)KvPG+?fqX!6zhZ!;a7j zL*0Y>%QA+QB`e#!ouBC)KwgcV+IQSb2GEFzS%SQ)!q`*NT8rEGy*m;);Rl*?zLefK z&_B5>F!CtvL$F%%Vf&TC*USk5huZ=Cv`euamJ}3`;xbK=)08a#=JuI5C%Y{dAnHE` zwa)%7R_Ah2H2GzAeu2-B=0jJV7L^P)y-iX%3f)QGOMh_H+Ue19SM08O>9n<{{yOLJTJ z^b88IfB)NSq93q8tUtWmujqIRQ+CiQ&qpD7aT%KU!FyCz<@92ibB4MbvV0j%x#giM zXcj6b3AQh@bxd_PR~^W{tod-lW4zA!YssDRrO>{`6G2d3{A%HB|IO2(K{~HhL>~&n zrq_>@7rZSk+ir6UA<+j`)^y#2u*~%9%W2q91_vi8 zy1h=Sla;V9o`1iKca|!ji*4ZyY+ytI*TsW1ffa_E5@Yd8Nv@{VeyDJr98@^Ma41Y6 zxM$HJP3v{Sd#3%t`;u_X44ju)3%M;Y_d6T)TGuClO;Qp?P{p!u@>E=(+iE6p;I1!N zsOBfF>{3?sa+ME5Uc7MLEq!v1hk6tELx~YQL~a+F9RW7i1Ul{k8at&$&Y;Bg%N#{<*PJT&Bi*vU!_V5a2c=}L!T7R# zVXw(6aGMXU;b#U2ewJcaIu+upO7DK{QLcVb#cZm=$gdCoa@|9#J7pod4Y11@j*v!y z6z#DLh$P&$9hvrHD1iTE`PW(bjTuY%yC=6@bt21f6Axu~B+p!%-ZK@ZRP-Zg>J{7b zM4zi(JJxum>i(!_ zve$0sq~L)ORW{MvsmDs$-=7*x#oicu8ly|wMoRuvhuWpzeVe@d{c;kKey)!~>OJOk z-x0^75p&D>$Y@NE9Y}IO^ECc4e#;@-*$%Dut?M#2+CZu5iVbwZdJD53Q<2bX&U55u zL}uv5C#qIWLTdENs&;{VR#x)UA03Y0TVR33$=@~S1*ln**xMO%>oD5)stAv0Z|^Pc z3*~MfWWTTF-Hty!aTMP4Wx4UjGXDX6&L6zzxLd0!B^DmGJ_-wO1nZ755GFfRa1eP9rJi>D&%x#;vZRs z|M+j=6+{d4Dewk?Bv>QHs^r1#Zj|LsqEWtge7-v7Yx7IuahW{*a`_{r-m%rqPKTJQ z^xjz|6nT0zuJ2-lMQhXc^kn1Pyv>f^ZoQ%O->fjRDw7Ak;^f#K4f0&up(zaZVsVrB zifz2cV>--FWKVo@tI@J#-WON-wxg+DS=2t~##q67`8&2B9GrPG+Oe0%O?}e)M?k4X zvNkpk!kZj2f0#w^N@_8L!P>W1UZEn5hW6h8L$+sI{{nH(Ux(U%dF-+RV!^ntp?|_Q zG?Kp}kT)e1?bd%ZA8fH|y=i9f(Jpy^xa%zNGhchXG`C^5_wMwX6Qgun=9oY)zmOi? zoP$OZS`>OCYIM;&?%qFNh!6_9ymWONdzV$GuciL-nF`BpbFZ3O+(7BMp7R7b(5mPf zh#?Hz7|;o-0-Aw|%Ud0F$Enu=iLDO}m{Z?!%mplPpU+-l$xmYVf?-@PO*^L_r}g)D z>CP8DIH(RNAzlT1mP z;7kU8M4%OU0Qjl_*U*C6$Kc(TFwZYDWf;`-_Ljewzx;&WLb|-#;8zYa)#GLbcPs@? zRe3M`So39l-^NVn`8-^Rm#y{Eiyis3V4%OzQKb-4Hk|da;I-x#IFztEqr8 zZ}pI0D=)&-!;~HqA4wgPj9egr~Kkq>FZTm)y-jySH~*AyPT4HV(UG4 zivIq$c>aPK`^H=a{kGB4I$Ua?V*7@_V|&da?AhJ9)a9y)pr>V`Q5K6rCCbJQV~##{ z$?f3*t}YASANF&4KKh&OOBpW(z66g*Mx`Hiexwys-c?2uqsxv_;&F6yn$PlxzsnDl z%uk+_{6z3fp~$o3ckl^|0@+lr7Pi?pG4CN{D*+z3Cqih;157?ugP$X*bj zzY@l?^xrE_pnqFybu3Zgf2%(XC&#&%ed8a)=|~4YG2BonLw~OTR1h44WcvA>9!qMQ zS9bZ!8M)MDUX|3Wcl&wYAvAN;&oSvsi;6R(;)zA!oVmqcM^nzfjMuRZest=S2s@jk z(}rRnh^F9_7J}v5fRRz1K6h$-i$Y>u4CDPhasBYdqX%d<9bwki!RJ&HD4L9;o_4cf zn-dikF`vSkb)6h9TA3++N*&`^>u!_jd!CRGbKlKRW%*`?4jA%I5^<&Z%&iUUJx1Cc~`aT&oJZfs}u)rE2@_8KTBiETe#@A{G*`F46f3sV#QFnQhS&|uCXqd76 zU0L@_bAW1}u^?*@A%gXWvPB@TF$+RM{6I&2DK5UVZ~THvk84}-DQeM>G8r>g7&ea#ndG9~b?OL;iWD`RKkf z{V;1_22pR`ql!(iB>L#kN1@KqW-R)4(WRRoe8fwGzTIMTp8aFt7x^@2i_?~9Aa9kY zRx84r;5#BnZ;8a%m`)@=wY9}|jEK@ZXHsbD^%UpM39)a=cSGO!ak-XDqqqhIZ0$+n z-$NC>cr-#!F|)=*&D}=WFyZh&Bp$$8OiciRAq^6k>{MJmMR+Hkp-y}+Gphrv2jhe6 z6>t0kY-C;yC9b?UD+catUb{2!bDo+p5I8r#98Th^_`HFTFc#U6R!F$odKTzfabd4< z3q$Vnk~g#6e7{`_YrQ6Vlp{3H?0G3I$^!FAn@?kLU|?WDeY>`H+4*%#oRErH;wu%E zN;aiG%&7Ml&mQRg+$GtwI`LZBzH~=<*y{V7jzlSApHw}w-fJvYq zZFVO0W_VL}Cg7{T`ua!X2mKUK9&~HM?)8z2+WA?*7OKH>UrRs@*_jiIZlUIVfUW;h z8XOXdhlqNgcvzQPdH&Oq4=%1@l%vn9+E8ER-M*kxW$jdftIu3Xu}y06q|?WhqRSL6#Z2kYvd+VvI>;%RSP#YnJDCboImED zd5!zNUDtJA>*w=+BN;UG(Up|X3G3M}DtsGJaqBOVMLQMFw|vj|U~QUX-{5RJZ~5N! zg1Ajp<&L?SL;C$8Qoip$zBnV9kkd5(sBrk<*w+Di;2a!nhGaNy{!#^pw&8bI50|TW4NXqCYo9LDR9kV{YPT3UmT`0U-ZU3R zkg3PP@@ST`_4#3bN$M+7hRVDj7ZI7 zXF+6eA zSNq-Pm+9`QdtCb&ih(M}4ml*B<5gav);GrI|6UzcaOKhq4IqOI(9uJ~iQt6|WtA-3 zFp?wAjp4Pp2jKIwcUXrk-UTRpc8d3saf#T!EmJe$t7}C$$;i6U>GSr!2q9(U1^xiN z?(^~>#|`aIL$waZ$-2-|;sXQ2%KQ`tW5|1yeww-`gFNj<8Cr6)q+Ot1CP(u(U!r%Q z_dQQZFxU+>8_}x*|5WjnT~RWwNh&YPB&=iV+OJ=y>xTC9DdzQj2E5&!W|kZ;LYuAR zT-Guf{LwYIjj1Pw#mt7i&6y)4WS_b-`-yYx`%H!3vErCYGn2Z?r9CavokPtIA%b#Q zI1ATZjt-zk?i1SfQ~d`41>;upQ1dZxpqBAc>|n>L0M_T!tClA7Eh7$Z>Y8=xf2ti13gEWfVjtliBU$qv zH=kvQiBI{m*&7BwZdcowFO+z{k5}G&E98d@SXXZ;0?#eYe~*+#T}c(d1=aCLpa5c> zm9!!ACuP1v?tYEB`R%kw->c4RGrbcq4FF?Jy+`9 z+|C+Y&_#d2vqNUaSLNbXCcH(T_P#dVxcW-y`jMktbo>!QvZiwdu5kb5M=f9a4u$#K z+10BH1xL{RcayHnV_s0t-ZwLb>XBLJi-@D0)|9<{_UYw&PnnE7Tin-s~;w zc++9+hf5m)R(D+4#6q|K4C8(77_hX9>{T;BFB;ng@Afy@oFdAU>o~AG{kpBNQVk@? z-=&~G=BN;MBuzkw(uRA^6NXcmc}tN7DaMG6LkW;6@-7W(qh(6$;>9z ztKWYyM~W|VHVP>(I%4@m_v-fWc?XZKqoEBI<-GY!+lQn5PlZ&N z|HhObEqxX~&$8>5`jPW~YC1|bg*r<_SFfhvg%_!t=&ce9wD9MGcF!jyctuh#R9>f7 zbl!RV;B~W+i=f}dT?eSkIiOa*0fO+AVtMqBhm2%n(S? zK9HOsaHO|aZA|ZP%$SCsh;P7HeRj}Th+JctDWC`u3%V@K*Axc47_)!O>@z{wwt zBWz7xu_igek1$*DmjJTE%>RvWEKaCRmEV>9@b5f}y=ryFfh~hYz93Kb-?Wl?nI{+i zLJ|F||Gc?_NpD3?c{4~|sC(vO5SlBk$?W3iFjwO?OpuAX#ZcCr`9PC))XpW7gj31L znD)-@#r9#HwXJ7r4DP&2dnCBhrv;#c|6)=9yPMtq$H({^1*hN_+TUFm^Tx~t5@t^f z5du;-bA?9;vt=kHbqK&4cVmk#j)i4{2d25Z^UOIGcZY=@VFRoa)ox`lrLJms3mrJ8 z8Y95@BLO6M{JS>s|4Jzf{51N9OMgS$CJGP&Q{&N6{6nyDHxW8VOqEdxzKvI%^2JLw z#-#naM1-*Zc|wMR!D2YFHdFk#?t=2eny?wFpbux zb;Q);srH@o8OMdE9=QOt^uKE_k5aVyb#Sx8iUZoL0% z=ThakWbbN@i>5;Y2kn%z1vp;+P@VskV*Gm{ zI%p|yD!Ty|lt%=>#* zm@u>FA6KlC4#k|mY7U|-{-&%1{%gg!33R7mNc^9%WtYIXY2kZ|7(?*p*>}(e6Uk!u zuRYAz&ZWqTQ0J6sU9yNO>up1Fh{2SVl!~3a{mYcY$KQq*>PkhS{*R-kbPz*)T0SuS7&Wgs4DG*ln_@aFaFdZRVNmcF*@s!z~;Qw=F}_7c2* zjzDu{Yrs^F8LaX3h#_$|*I@jHn2PJuNrh>Tg>NTh*vmN|J$gnA*NCy^%3KG)UONiN zfJMmV3@}KCqXf+NV6)D_vB7hw%^xQEHOfMYdM?YvKa01htkBYLcwiUc`5{ha^O@m& z0t$qe8!I&oy-7gP8{uejm4^5T_}iRKGI@29+R->by~p3ZkXdhB_w#&ZhWGCAZ{+EL zT73((?0uv1X1yMD-6@uvzHhW5Qs`n-Kk7a6J@9uZ_Ch+RgQ7Vyw(5`_>2vp-`~jA| zx((TA+EKM{YM8Q>*}LK@%Kh4?a=AP)~Jtp{51^J_3 zdG-w2xX-l%)9f~iN}y=B=T24%aZg=IAw6#Is|&>!u4c-*)HmF{k|}>Be)#->&kqiw zg28e=YYwrT#xd__NivMVIm;Y1j=yh|?0Yy$*b1UXhwX9I(baTO`N=W;Th z1~dLcZdyM5fXicix4AJS+lrd>?m^10gN}w92;Z4oT9p(FoeM8`&Xpdkb%^Y8d*A1N ze0aj|Rz^z=x%}W$@h&OTqL`V{{o!HZmv(-h%Ua=}?h<@?1OO)Y!Y*>GstIn$Yzl$Ay+i*mV89TAQZ{qw%{cBw1ecFLW2n(cdc_r9z`d8A{cqj+oKl*5{?rh1+$ zW5P0XXS4AnU9grOZ(`i$D7=7_54#ZDd>O+CG)GF)-5+>2jAqgd8`G`}&_;w+1+M!F zmKB1;@pe3lKP`ci29aP-@o#3tc$s_+t^?~@#q3h5B=2FXjYW#7gNb58#h&?x&hNEj zR*igH&&^z)Tn<^P%QLKvWh|p5aG2tYSkEm(S!dptlNa)LE$rL-?P>~RHr>|pH)CP_ z*Q|LFm#ZDfEYxhFxil}P*_FA3LDN#%+dxiD7h1fFA>$~vlqU{5JjcXHrTc~Y1edz* zbujv3cA49&R!>t)zF(AEi{AwTHZobCxsrv0e32!NQDp4)*G39b!vgCKrb zF&7|iT9~PbHXm=M={A|8YT2DVA=*_$jSHW}r)`1{S#*8Ir=4Z$ zEoU(LQzr>?ns?NA#M%l=C%7PrsV6d6JVB#_tqa4Df}Y|DkCR*69;ah^H6-0?U;c1z zt$ATmfxq+LfHA^vQFSP;lm&-3)MpoPHWj|k>~}_G zQu-%8+XbomWvZGam7x!7%;IsOo@Y6cuwW0NHmjR>7g?R0dY2rTx@iVG!k0uR+?rdc zt^@nsB881r!=G}3A6;o2o$2aSuRm-QA?)Xbc?ExGX);QZn`K$&zaOI9 zE1H;s6S@&w93wGvISwZfPj>#9w(Do=qkO;_@aWCW*H;!7qfdTgj;ER%i#c$8IBGZ> z*fUeVm7R{|e?I=e-^=008mcZ z2|mFrVmtBn261P3MesU83O=>b5#C5j7-QSWW|~;7^%zq64ZQOR8fs6nmCZUNE^mkl zNPk4u)vv@B;5$)U4Us8%+VFm`d!XP;)6I=6U5cL-(-`~YVbnc5`87zl#Ms4%)uYg( zh@`%XYrl&np-E2&voG<78i&AGl#7%zSNwwB!Ht+}iJU`pAa`NCJ4_sJHw%|7jr5kz z_{oMGuTj6hD+ilxeg4o%8AI(&m(z~#N$o*R#kQh^ZqPTmEr5ZY>&Fb1!u=lO%2k|2 z2`#Xz7wcbI24%?}s%m_7m$oh6&TE&Q_LqX+kR4t){NFy@iJ>4I&!yjJ*A-VZHJf%dQ^iVQ#<$l?zl^u)#1m)nm`g ze=w<)DO3waK(wL|4a*$gES51(tbCw3i4bHL}n+vKUrLGwRQqux(RvY3%f=>snJUm}poh zwNq=!aWZHSY(+=&#Xt{oS-+kUo`D+$1t%P}lV%rlr|Lv8k(A6WtM((b+*H|)08 z=4?COlK}jFG(C3f=&#C<=q+@`zXm>HZls@2L9)BcI8HeK!r;}*!HrJm$kQuLAAT>C z@fNCf*yn6n^Vum%kzw4Ww;2}#doTupp+ofh$M-nnxq+P{7v34}8_EbO|9aWA;2}n! z|J9yTV?dyK+ggjMH6#*NY~Vwaz2zkVYv60)v?zwS%jP7zxo zl1c4oSwb72LtajeM~Rxbd$SG%+Rtqyeka5yX2jT`T3sdvzFW{!X@DcjxeD2FX_ zf>3QOgp1X+!&C-xeAzxRNA#%IO{=cy#>di0`L=SUp1y)(X6|6%5oj2p`svBw-%xiD zywQmSak8Ue&ofklAytGLt1?m^Qy`K}|1?TYv?G3W)~VL$9^Q%_Y=ViMGf2oKhK!Vh zYh3O*4%g<$lfq0nB~#MmW?N6Yi|q5~wH+^Mee36Mx11fE>|jP^rIHc%kuQb2Kw<;U z#vOWJ3XWPps>%5m3pKRGfy9+YvwmcboHi%fb9l($hom*@XkYyZs+OI`Tur6d zY;+PPjKGN8KesB6A8dTqgk3p&Ew!V^sil^9YE5Rf@I$=&+qLkI7M2_*A;KiM_gRpm znF!j~YAgo2H;1~b&Kiu>H3a(vG-4_aC6qx*Iy_-koEfPq-C3=w9onInb|drKk2As& z89Q9QJcm_h^Qu_x>=Irf_3EVKau-Xo^)y!)`bvsqX{`1r?IPOO){hpydz&hg;cfO@ zb+j#g&aZjttlDPNa~ckBU!Ws@z2cB2VEB`I^k}Bt$Z8H30mp+^RUAfjux~95GCh=r z$LQxrDoJ|Uj928f%4d&??7gp}B6r(t-?l{p)U+thoL8_yM#9>k6PumQ1#p)FbQ9R-Cu_KhZ0p zob#{``#9`Vg0o?1c2FhZY6qXLpEfZ!f)QAAQw>VXXqZ-QEjd|iWuSKZj%sD?wNj^0 zPj9HCRml|Y1x?Htj^~`@T0*Bt#n@nmPqYqh6ZCRPA)t=6cxYx@m`>>Vx?QE;RIe!n zroVmSY-GG$j{CldW6!nVELG@xs{G^! zoDzgeDle&Od>MZOK6^S(R5bnCNcpE`XKck4i3}AN32Su$UfA(}I=C?AWq^)$gO^QF z@FyUUUEqi}wG$}~=d5`3Eb}jng>iTn-B)IQtl_6WuD+(v2h(!uQf>d2!Mgt`UgCcf z*!!P<%L<-~WfP$KZuEV(l7uj-64&ZoaI+?3zgU5=m zQ~u*4zf(TkOl|szisl=FPzgma^y!#y#ZMr&n%T|K;|P*`9Y3n*dd0OVrfk>XE=*(K z{8&p!aYv|jhzH$%&;6v0KP0sE86xs2zoE5n5~t=N2tA>Xsjd&e{#R*-t)uQc|Z#!xlJH*BsH2`hC++ zLn~|qSMnP>8%_f<+HDQ2@h(z~%Z$SK4FHc&(CKY}C}l*gWVNW2fUaf;&3D zFy^dX)vNeJBe>p!;X$}rdUCSEN%+foX;uU{#+l7W)s}!&y=>2n0cg z^h6V7I5aRaP^Z?!>|Iyn(^DIvCv~5qFm-ERhXUVIqTXpr6BwkGU``O%1nz*A7=tU( z`CR?Rdbn-2#rqu8#5I3Ot$*0jSWNVhURoHBS^RXGAnD$#p<5{Pmw=hj@5;920_N?7 zR1-5KC{1zD_A?hzFOhBZDSI?TY`bX~oUF7L(s;;N6{7hmlps79RL1TIj-)8jgLbSaeP6X1EdRVEe{#d zB}qk)3>kXSrKCHid+C)mws~-Q>)R&*6Kh?gLnogN|Grlu?P6HTidHdx2uVh@QE8uT zp{UyM83W#J+|{|q++!~gwU%Mu}H8*61KY6~i!Ctdv8|^Pp zrR4ZiPR%zorMsSdTX5-NU$`1uj6(N@%Q3> z8^P=Q{(Mv~b7(O_)_IJ!+o?^b_JOVX$SSV?m2~^QxWowUb6CW356M4o>#zQd--9O0 z5KBPsgMHXRP^?|i^;LOnyIKOrc8)7(6kNKn=~MC4#~it~%s%1PecJ?hrwb|IDC_PE zZX`^CkZkV}Wq9x04RGMrOaL@RzR(EQUA^#>nMq^!i{Pueg{lvvA`Kqn>nETaj~{R3DvTa@Q3oKw${4p5Qn!Q3 z1z@Nm7C_CmY_cSJ?L7Xf%2j;p102bCLH_z4>^1 z;^F;LsfSdX19yIp6Nq|Yg^%Q_g39QDUz})x+e&aZ>PK+sK!`aTxY5be^niNWsk^ZH zVTz_2X}SUUUeb?bt)Mltrc!lm&cLO49Zlc?1su8b@@npc1e zgQ(q3IQ6VE5UMLMXl4uaHVrcE4zK4+jRdPk)Q$#$-6di6sr&@oog>kglYe`|axmx? zhlgXgIuWnKY8j_J zZD;_f)8PEPpW&f@B5OLf=;wD!tHw*+`ojTBO+?M+n#;q{3<6DdHaHRIb2AX4Gv9OF zS_U#>dUqBTEuM9=3M@NpV86TSM`%>a7$ZW}%QfhagZ=1}L&ufQvruef*pT&*d6tWT z(=bub9e@b_e=9aXUae_QxHD z4>1Gvda^fq^y2B_B!GJ`C5d5oqT)<=6(8`@BSF8}n}vIcUa;?5*rK14^U!QwRpe8k zw&IsQur2rt9z< zBEN-nUyz}B&n71av$bJ&65wtcC+UGTfVj?JkDkaRP-&(0gbAb!)5hK|4jb2Q-R1qD zCt|Pn-VFN#;`y5qB*6PQ_{)vs4yB$Mt(~@MNA98Q#~!zV`cFc_3Cd7OoQ}?T*Da+# zYjnzu->_3yeRn^7yvYs>)YZH$w_{ToG{P5o0#e33AI=_A?3mGE-5A1|!H6g*#|ksW zarRqxuN$G+$FryQVP4*tFlt;*3Vx`+yOLg5nvY1YM9rovi4Swp&{Pjn6!=_IlV(Jz zFtA|uGd|zXvC8U*2x%*JFI4>yFuLykEMbk|744HPA^Z5Hb(2LUZMiuVG_fDU&8L}( zdWka#UKw8t*6eX$m?99^`tCe&>abTMD~kM>pyF|o#ypkXTPgi4_x?Ng)`icF1-9%$ zo-lB9{9vi_Md-71B`i>NORS@4nse0q&F=ZIj+y#%nA)MFUgu%cNi#uZ*ZoY&GO7`g zJ2~7})BbW$@xw{)G~?_}h{%<+Z$s?>q*7tt6UrWJ^*Yx8)?>=D4ozUv6O(PuH{Slx zUt+OVsB+T@Fw>saO?Q>AjQ`n$EGf<3d5C?FN90TLN2$VwgSY95T~F%3#RhwAey*QyV%q$yL*r-81PGav67zT@6kKrkId zicn{B@d~iyT#g2Of^&-N{?1ISc!~6Ufi4n2UJ_(*T$RFeSD1QN#(Vv*grly?dWnlz z9VhDe*@GK@gGHk_2DsmN55ryngaXL=^7-Z55es)EtEH^bHe5zOjef%TT~0Qu5_>aM z&{uU@LD)dtKC(h;(>EjtO`px8Z?Dyho6!KOYWKN~&eTMTGRv_&XEm0#PLLqKkgXrR zVH%%#jeKy@b;?Ra-PPE-tieI`NW|WryTB~)lDeX$w^9UAnZ{ix5uDqQ84?O^%bv+9 zEa@i3(YC(2sYSO8#z?0SWnv$3v_ouE?ILasMCiNUIJ(W2Cj;C7H(@Xv-SQOzTjL}@ z8KMZmACZ_He7hNi+fYEXqgR%M-yL4pexu`kt(tR_Y(ZCV@~E}lT singleton.baits.bed tail -n+2 NexPond-647561.per_target_coverage | awk 'BEGIN{FS="\t";OFS="\t"}{print $1, $2-1,$3,$6,$14}' > coverage.bed +tail -n+2 NexPond-647561.coverage | awk 'BEGIN{FS="\t";OFS="\t"}{print $1, $2-1,$3,$6,$7}' > coverage2.bed bedtools intersect -a coverage.bed -b singleton.baits.bed > coverage.singleton.bait.bed +bedtools intersect -a coverage2.bed -b singleton.baits.bed > temp +bedtools sort -i temp > coverage2.singleton.bait.bed