Merge branch 'direct_rna' into 'dev'

Re-add ability to do direct RNA

Closes CW-566

See merge request epi2melabs/workflow-containers/wf-isoforms!46
This commit is contained in:
Neil Horner 2022-04-05 18:04:17 +00:00
commit 63c4b5c7a5
11 changed files with 202 additions and 204 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
## Added
- --direct_rna option.
## [v0.1.1] ## [v0.1.1]
## Fixed ## Fixed
- Incorrect numbers and of transcripts caused by merging gff files with same gene and transcript ids - Incorrect numbers and of transcripts caused by merging gff files with same gene and transcript ids

View File

@ -8,9 +8,9 @@ It has been adapted from two existing Snakemake pipelines:
* https://github.com/nanoporetech/pipeline-nanopore-denovo-isoforms * https://github.com/nanoporetech/pipeline-nanopore-denovo-isoforms
--- ---
## Overview ## Overview
* cDNA or direct RNA reads are initially and optionally preprocessed by [pychopper](https://github.com/nanoporetech/pychopper) * cDNA reads are initially preprocessed by [pychopper](https://github.com/nanoporetech/pychopper)
for identification of full-length reads, as well as trimming and orientation correction. <br> for identification of full-length reads, as well as trimming and orientation correction (This step is omited for
direct RNA reads)
Reference-based approach Reference-based approach
* Full length reads are mapped to a supplied reference genome using [minimap2](https://github.com/lh3/minimap2) <br> * Full length reads are mapped to a supplied reference genome using [minimap2](https://github.com/lh3/minimap2) <br>
@ -22,11 +22,12 @@ using [gffcompare](http://ccb.jhu.edu/software/stringtie/gffcompare.shtml)
de novo-based approach (experimental!) de novo-based approach (experimental!)
* Sequence clusters are generated using [isONclust2](https://github.com/nanoporetech/isONclust2) * Sequence clusters are generated using [isONclust2](https://github.com/nanoporetech/isONclust2)
* If a reference genome is supplied, cluster quality metrics are determined by comparing * If a reference genome is supplied, cluster quality metrics are determined by comparing
with clusters generated from a minimap2 alignment with clusters generated from a minimap2 alignment.
* A consensus sequence for each cluster is generated using [spoa](https://github.com/rvaser/spoa) * A consensus sequence for each cluster is generated using [spoa](https://github.com/rvaser/spoa)
* Three rounds of polishing using racon and minimap2 to give a final polished CDS for each gene. * Three rounds of polishing using racon and minimap2 to give a final polished CDS for each gene.
* Full-length reads are then mapped to these polished CDS * Full-length reads are then mapped to these polished CDS.
* Transcripts are assembled by stringtie as for the reference-based approach * Transcripts are assembled by stringtie as for the reference-based approach.
* Note: This is currently not supported with direct RNA reads.
For both approaches: For both approaches:
@ -67,7 +68,7 @@ to see the options for the workflow.
- Optional reference annotation in GFF2/3 format - Optional reference annotation in GFF2/3 format
**Example execution of a workflow for reference-based transcript assembly** **Example execution of a workflow for reference-based transcript assembly**
This uses a synthetic SIRV dataset so we need to tell minimap2 about the non-canonical spplice junctions with This uses a synthetic SIRV dataset, so we need to tell minimap2 about the non-canonical splice junctions with
--minimap2_opts '-uf --splice-flank=no' --minimap2_opts '-uf --splice-flank=no'
``` ```
OUTPUT=~/output; OUTPUT=~/output;
@ -94,6 +95,8 @@ or at the command line:<br>
- Threshold for including isoforms into interactive table `transcript_table_cov_thresh = 50` - Threshold for including isoforms into interactive table `transcript_table_cov_thresh = 50`
- Run the denovo pipeline `denovo = true` (default false) - Run the denovo pipeline `denovo = true` (default false)
- To run the workflow with direct RNA reads `--direct_rna` (skips the pychopper step).
Pychopper and minimap2 can take options via `minimap2_opts` and `pychopper_opts` Pychopper and minimap2 can take options via `minimap2_opts` and `pychopper_opts`
<br> <br>
@ -107,7 +110,7 @@ For example:
- pychopper can use one of two available backends for identifying primers in the raw reads - pychopper can use one of two available backends for identifying primers in the raw reads
- nhmmscan `pychopper opts = '-m phmm'` - nhmmscan `pychopper opts = '-m phmm'`
- edlib `pychopper opts = '-m edlib'` - edlib `pychopper opts = '-m edlib'`
<br>Note: edlib is used in the configs as it's quite a lot faster. However it may be less sensitive than nhmmscan. <br>Note: edlib is set by default in the config as it's quite a lot faster. However it may be less sensitive than nhmmscan.
--- ---
## Workflow outputs ## Workflow outputs
@ -130,8 +133,6 @@ Each sample will also have it's own directory containing the following (dependen
- A transcriptome derived from the query reads - A transcriptome derived from the query reads
* {sample_id}_merged_transcriptome.fas * {sample_id}_merged_transcriptome.fas
- A transcriptome derived from the query reads + reference annotation - A transcriptome derived from the query reads + reference annotation
* merged_transcriptome.fas
-A transcriptome made from the combined query reads and reference annotation
## Useful links ## Useful links

View File

@ -21,8 +21,7 @@ def main():
"Warning: sample sheet contains both 'alias' and " "Warning: sample sheet contains both 'alias' and "
'sample_id, using the former.') 'sample_id, using the former.')
samples['sample_id'] = samples['alias'] samples['sample_id'] = samples['alias']
if 'barcode' not in samples.columns \ if not set(['sample_id', 'barcode']).intersection(samples.columns):
or 'sample_id' not in samples.columns:
raise IOError() raise IOError()
except Exception: except Exception:
raise IOError( raise IOError(

View File

@ -15,8 +15,9 @@ from matplotlib import pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages from matplotlib.backends.backend_pdf import PdfPages
import pandas as pd import pandas as pd
import pysam import pysam
from sklearn.metrics.cluster import adjusted_rand_score, completeness_score,\ from sklearn.metrics.cluster import (
homogeneity_score, v_measure_score adjusted_rand_score, completeness_score,
homogeneity_score, v_measure_score)
matplotlib.use('Agg') matplotlib.use('Agg')
@ -368,14 +369,26 @@ def get_cluster_information(clusters, classes):
print("MIXED:", "Tot classes containing both:", len( print("MIXED:", "Tot classes containing both:", len(
set(clustered_classes.keys()) & set(not_clustered_classes.keys()))) set(clustered_classes.keys()) & set(not_clustered_classes.keys())))
print("Total number of classes (unique gene ID):", total_nr_classes) print("Total number of classes (unique gene ID):", total_nr_classes)
return (total_nr_classes - len(singleton_classes), len(singleton_classes), return (
min_class_size, max_class_size, mean_class_size, median_class_size, total_nr_classes - len(singleton_classes),
total_nr_clusters, len(singleton_clusters) len(singleton_classes),
+ len(omitted_from_output_singletons), min_class_size,
min_cluster_size, max_cluster_size, mean_cluster_size, max_class_size,
median_cluster_size, len(unaligned_but_nontrivially_clustered), mean_class_size,
upper_75_class_size, upper_75_cluster_size, e_class_size, median_class_size,
n50_class_size, e_cluster_size, n50_cluster_size) total_nr_clusters,
len(singleton_clusters) + len(omitted_from_output_singletons),
min_cluster_size,
max_cluster_size,
mean_cluster_size,
median_cluster_size,
len(unaligned_but_nontrivially_clustered),
upper_75_class_size,
upper_75_cluster_size,
e_class_size,
n50_class_size,
e_cluster_size,
n50_cluster_size)
def main(args): def main(args):
@ -400,14 +413,16 @@ def main(args):
v_score, compl_score, homog_score, clustered_but_unaligned, ari = \ v_score, compl_score, homog_score, clustered_but_unaligned, ari = \
compute_V_measure(clusters, classes) compute_V_measure(clusters, classes)
nr_non_singleton_classes, singleton_classes, min_class_size, \ (
max_class_size, mean_class_size, median_class_size, total_nr_clusters,\ nr_non_singleton_classes, singleton_classes, min_class_size,
singleton_clusters, min_cluster_size, max_cluster_size, \ max_class_size, mean_class_size, median_class_size, total_nr_clusters,
mean_cluster_size, median_cluster_size, \ singleton_clusters, min_cluster_size, max_cluster_size,
unaligned_but_nontrivially_clustered, \ mean_cluster_size, median_cluster_size,
upper_75_class_size, upper_75_cluster_size, e_class_size, \ unaligned_but_nontrivially_clustered,
n50_class_size, e_cluster_size, n50_cluster_size = \ upper_75_class_size, upper_75_cluster_size, e_class_size,
get_cluster_information(clusters, classes) n50_class_size, e_cluster_size,
n50_cluster_size
) = get_cluster_information(clusters, classes)
outfile = open(args.outfile, "w") outfile = open(args.outfile, "w")
@ -519,10 +534,10 @@ def main(args):
upper_75_class_size, upper_75_class_size,
median_cluster_size, median_cluster_size,
median_class_size]}).set_index('Statistic') median_class_size]}).set_index('Statistic')
dfs2 = pd.DataFrame({'Statistic': ['N50ClsSize', 'N50ClassSize'], dfs2 = pd.DataFrame(
'Value': [ {'Statistic': ['N50ClsSize', 'N50ClassSize'],
n50_cluster_size, 'Value': [n50_cluster_size, n50_class_size]
n50_class_size]}).set_index('Statistic') }).set_index('Statistic')
rdo = Path(args.raw_data_out) rdo = Path(args.raw_data_out)
dfc.to_csv(rdo / 'v_ari_com_hom.csv') dfc.to_csv(rdo / 'v_ari_com_hom.csv')

View File

@ -60,8 +60,8 @@ def main(args):
assert os.path.isdir(args.output_dir) assert os.path.isdir(args.output_dir)
if args.annotation: if args.annotation:
os.path.isfile(args.annotation) os.path.isfile(args.annotation)
generate_tracking_summary(args.tracking, output_dir=args.output_dir, generate_tracking_summary(
annotations=args.annotation) args.tracking, output_dir=args.output_dir, annotations=args.annotation)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -23,52 +23,6 @@ import pandas as pd
import sigfig import sigfig
def _vbar(x, top, title, **kwargs):
"""Vertical bar chart."""
fig = figure(title=title)
fig.vbar(x, top=top, **kwargs)
return fig
def _hbar(y, right, title='', fig_height=300, fig_width=300,
bar_height=0.1, **kwargs):
"""Horizontal bar chart."""
fig = figure(title=title, height=fig_height, width=fig_width)
yn = list(range(len(y)))
fig.hbar(yn,
right=right,
height=bar_height,
**kwargs)
# Overide the numerical labels with cate
fig.yaxis.ticker = yn
mapper = {k: v for (k, v) in zip(yn, y)}
fig.yaxis.major_label_overrides = mapper
return fig
def simple_hbar(df, y, right, title="", color=Colors.cerulean,
fig_kwargs={}, plot_kwargs={}):
"""Create a simple barplot.
:param groups: the grouping variable (the x-axis values).
:param values: the data for bars are drawn (the y-axis values).
:param kwargs: kwargs for bokeh figure.
"""
defaults = {
'output_backend': 'webgl',
'height': 300, 'width': 600}
defaults.update(fig_kwargs)
p = figure(y_range=df[y], height=250, title=title,
toolbar_location=None, tools="")
plot_kwargs.update({'height': 0.2})
p.hbar(y=df[y], right=df[right], **plot_kwargs)
return p
def _parse_stat_line(sl): def _parse_stat_line(sl):
"""Parse a stats line.""" """Parse a stats line."""
res = {} res = {}
@ -288,7 +242,8 @@ def grouped_bar(df, title="", tilted_xlabs=False):
df = df.reset_index(drop=True) df = df.reset_index(drop=True)
source = ColumnDataSource(data=df) source = ColumnDataSource(data=df)
p = figure(x_range=df['x_groups'], y_range=yrange, height=250, title=title, p = figure(
x_range=df['x_groups'], y_range=yrange, height=250, title=title,
toolbar_location=None, tools="") toolbar_location=None, tools="")
i = 0 i = 0
# Use the dodge method to plot groups of bars # Use the dodge method to plot groups of bars
@ -304,7 +259,8 @@ def grouped_bar(df, title="", tilted_xlabs=False):
for col in df.columns: for col in df.columns:
num_colors = df.shape[1] - 1 num_colors = df.shape[1] - 1
colors = list(zip(*[[Category10_10[x]] * (len(df.columns) - 1) colors = list(zip(
*[[Category10_10[x]] * (len(df.columns) - 1)
for x in range(num_colors)])) for x in range(num_colors)]))
colors = [item for sublist in colors for item in sublist] colors = [item for sublist in colors for item in sublist]
@ -314,9 +270,9 @@ def grouped_bar(df, title="", tilted_xlabs=False):
i += 1 i += 1
width = df.size / 60 width = df.size / 60
p.vbar(x=dodge('x_groups', current_dodge, range=p.x_range), top=col, p.vbar(
width=width, source=source, color=color, x=dodge('x_groups', current_dodge, range=p.x_range), top=col,
legend_label=col) width=width, source=source, color=color, legend_label=col)
current_dodge += dodge_increment current_dodge += dodge_increment
p.x_range.range_padding = 0.1 p.x_range.range_padding = 0.1
@ -341,8 +297,7 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
# Plot overview panel: # Plot overview panel:
section = report.add_section() section = report.add_section()
# TODO: update this based on current version gffcompare_md = ('''
section.markdown('''
### Annotation summary ### Annotation summary
The following plots summarize some of the output from The following plots summarize some of the output from
@ -370,19 +325,26 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
stats, _, miss, novel, total = \ stats, _, miss, novel, total = \
parse_gffcmp_stats(dir_ / 'str_merged.stats') parse_gffcmp_stats(dir_ / 'str_merged.stats')
if not any([x.empty for x in [stats, miss, novel, total]]):
bar_totals = grouped_bar(total, title="Totals") bar_totals = grouped_bar(total, title="Totals")
bar_performance = grouped_bar(stats, title="Performance", bar_performance = grouped_bar(
tilted_xlabs=True) stats, title="Performance", tilted_xlabs=True)
bar_missed = grouped_bar(miss, title="Missed") bar_missed = grouped_bar(miss, title="Missed")
bar_novel = grouped_bar(novel, title="Novel") bar_novel = grouped_bar(novel, title="Novel")
tabs.append(Panel( tabs.append(Panel(
child=gridplot( child=gridplot(
[bar_totals, bar_performance, bar_missed, bar_novel], [bar_totals, bar_performance, bar_missed, bar_novel],
ncols=2, width=350, height=260), title=id_)) ncols=2, width=350, height=260), title=id_))
cover_panel = Tabs(tabs=tabs) cover_panel = Tabs(tabs=tabs)
section.markdown(gffcompare_md)
section.plot(cover_panel) section.plot(cover_panel)
else:
gffcompare_md = ('''
__Warning__: Gffcompare summary cannot be shown.
This could be due to incompatible reference fasta and gff
files.''')
section.markdown(gffcompare_md)
names = { names = {
'=': 'ExactMatch:=', '=': 'ExactMatch:=',
@ -421,9 +383,10 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
print(gffcompare_outdirs) print(gffcompare_outdirs)
track_files = [x / 'str_merged.tracking' for x in gffcompare_outdirs] track_files = [x / 'str_merged.tracking' for x in gffcompare_outdirs]
df_tracking = load_sample_data(track_files, sample_ids, df_tracking = load_sample_data(
read_func=lambda x: track_files, sample_ids,
pd.read_csv(x, sep="\t", header=None, read_func=lambda x: pd.read_csv(
x, sep="\t", header=None,
usecols=[0, 3], usecols=[0, 3],
names=['Count', 'Overlaps'])) names=['Count', 'Overlaps']))
@ -433,9 +396,10 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
tracking.Overlaps = tracking.Overlaps.map(names) tracking.Overlaps = tracking.Overlaps.map(names)
tracking['Percent'] = tracking.Count * 100 / tracking.Count.sum() tracking['Percent'] = tracking.Count * 100 / tracking.Count.sum()
tracking = tracking.sort_values("Overlaps") tracking = tracking.sort_values("Overlaps")
track_bar = _hbar( track_bar = bars.simple_hbar(
tracking['Overlaps'].values.tolist(), tracking['Overlaps'].values.tolist(),
tracking['Percent'].values.tolist(), title="{}".format(id_)) tracking['Percent'].values.tolist(),
colors=Colors.cerulean, title=id_)
tracking_dfs.append(tracking) tracking_dfs.append(tracking)
@ -448,13 +412,12 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
tracking.drop(columns=['sample_id', 'Overlaps'], inplace=True) tracking.drop(columns=['sample_id', 'Overlaps'], inplace=True)
tracking = tracking[['Code', 'Description', 'Count', 'Percent']] tracking = tracking[['Code', 'Description', 'Count', 'Percent']]
cols = [TableColumn(field=Ci, title=Ci, width=100) cols = [TableColumn(
for Ci in tracking.columns] field=Ci, title=Ci, width=100) for Ci in tracking.columns]
track_table = DataTable(columns=cols, track_table = DataTable(
source=ColumnDataSource(tracking), columns=cols, source=ColumnDataSource(tracking),
index_position=None, index_position=None, width=500)
width=500)
tabs.append(Panel( tabs.append(Panel(
child=gridplot([track_bar, track_table], ncols=2), title=id_) child=gridplot([track_bar, track_table], ncols=2), title=id_)
) )
@ -462,13 +425,12 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
cover_panel = Tabs(tabs=tabs) cover_panel = Tabs(tabs=tabs)
section.plot(cover_panel) section.plot(cover_panel)
def plot_isoforms_per_tpm_bin(df_code, class_code, sample_id, def plot_isoforms_per_tpm_bin(
geomspace=False): df_code, class_code, sample_id, geomspace=False):
"""Make plots of number of isoforms per TPM coverage bin.""" """Make plots of number of isoforms per TPM coverage bin."""
max_ = int(sigfig.round(df_code.TPM.max(), 2)) max_ = int(sigfig.round(df_code.TPM.max(), 2))
if geomspace: if geomspace:
bins = [math.ceil(x) for x in bins = [math.ceil(x) for x in np.geomspace(10, max_, num=15)]
np.geomspace(10, max_, num=15)]
else: else:
bins = np.linspace(10, max_, 15) bins = np.linspace(10, max_, 15)
bins = np.unique(bins) # Low max_ can end up with duplicated bins bins = np.unique(bins) # Low max_ can end up with duplicated bins
@ -527,11 +489,11 @@ def gff_compare_plots(report, gffcompare_outdirs: Path, sample_ids):
return df_tmap return df_tmap
def pychopper_plots(report, df): def pychopper_plots(report, pychop_report):
"""Make plots from pychopper.cdna_classifier.py. """Make plots from pychopper.cdna_classifier.py.
:param report: aplanat WFReport :param report: aplanat WFReport
:param df: DataFrame of Pychopper stats :param pychop_report: path to pychopper stats file
""" """
section = report.add_section() section = report.add_section()
section.markdown(''' section.markdown('''
@ -548,12 +510,15 @@ def pychopper_plots(report, df):
''') ''')
plots = [] plots = []
df = pd.read_csv(pychop_report, sep='\t', index_col=0)
for id_, df in df.groupby('sample_id'): for id_, df in df.groupby('sample_id'):
df1 = df.set_index('Name', drop=True) df1 = df.set_index('Name', drop=True)
df1 = df1.T[['Primers_found', 'Rescue', 'Unusable']] df1 = df1.T[['Primers_found', 'Rescue', 'Unusable']]
df1.rename(columns={'Primers_found': 'Pr.found', df1.rename(columns={
'Primers_found': 'Pr.found',
'Rescue': 'Resc', 'Rescue': 'Resc',
'Unsable': 'Un'}, inplace=True) 'Unusable': 'Un'}, inplace=True)
df2 = df[df.index == 'Strand'] df2 = df[df.index == 'Strand']
bar_chop = bars.simple_bar( bar_chop = bars.simple_bar(
df1.columns.values.tolist() + df2.Name.values.tolist(), df1.columns.values.tolist() + df2.Name.values.tolist(),
@ -562,8 +527,8 @@ def pychopper_plots(report, df):
colors=Colors.cerulean) colors=Colors.cerulean)
plots.extend([bar_chop]) plots.extend([bar_chop])
grid = gridplot(plots, ncols=4, grid = gridplot(
width=300, height=300) plots, ncols=4, width=300, height=300)
section.plot(grid) section.plot(grid)
@ -602,8 +567,7 @@ def cluster_quality(cluster_qc_dir, report, sample_ids):
tabs = [] tabs = []
for id_, cluster_dir in zip(sample_ids, cluster_qc_dir): for id_, cluster_dir in zip(sample_ids, cluster_qc_dir):
plots = [] plots = []
for fn in [ for fn in ['v_ari_com_hom.csv', 'sing_nonsing.csv']:
'v_ari_com_hom.csv', 'sing_nonsing.csv']:
# Skip the next two plots for now # Skip the next two plots for now
# 'class_sizes1.csv', 'class_sizes2.csv']: # 'class_sizes1.csv', 'class_sizes2.csv']:
df = pd.read_csv(Path(cluster_dir) / fn) df = pd.read_csv(Path(cluster_dir) / fn)
@ -629,8 +593,9 @@ def transcript_table(report, df_tmaps, covr_threshold):
# all in single table and sample_id column? Currently it's the latter # all in single table and sample_id column? Currently it's the latter
# drop some columns for the big table and do some filtering # drop some columns for the big table and do some filtering
df = df_tmaps.drop(columns=['FPKM', 'qry_gene_id', 'major_iso_id', df = df_tmaps.drop(
'ref_match_len', 'TPM']) columns=[
'FPKM', 'qry_gene_id', 'major_iso_id', 'ref_match_len', 'TPM'])
df.sort_values('cov', ascending=True, inplace=True) df.sort_values('cov', ascending=True, inplace=True)
counts = list(range(len(df))) counts = list(range(len(df)))
@ -672,7 +637,7 @@ def transcript_table(report, df_tmaps, covr_threshold):
section.table(df, index=False) section.table(df, index=False)
def tanscriptome_summary(report, gffs, sample_ids, denovo=False): def transcriptome_summary(report, gffs, sample_ids, denovo=False):
""" """
Plot transcriptome summaries. Plot transcriptome summaries.
@ -745,12 +710,12 @@ def tanscriptome_summary(report, gffs, sample_ids, denovo=False):
if not denovo: if not denovo:
x, y = zip(*sorted(exons_per_transcript.items())) x, y = zip(*sorted(exons_per_transcript.items()))
ept_bar = _vbar(x, list(y), fig = figure(title="Exons per transcript")
title="Exons per transcript", fig.vbar(
color=Colors.cerulean) x, top=list(y), color=Colors.cerulean)
ept_bar.xaxis.major_label_orientation = math.pi / 2.8 fig.xaxis.major_label_orientation = math.pi / 2.8
plots.append(ept_bar) plots.append(fig)
df_sum = pd.DataFrame.from_dict( df_sum = pd.DataFrame.from_dict(
{'Total genes': [num_genes], {'Total genes': [num_genes],
@ -760,12 +725,11 @@ def tanscriptome_summary(report, gffs, sample_ids, denovo=False):
df_sum.reset_index(drop=False, inplace=True) df_sum.reset_index(drop=False, inplace=True)
df_sum.columns = [' ', 'count'] df_sum.columns = [' ', 'count']
cols = [TableColumn(field=Ci, title=Ci, width=80) cols = [TableColumn(
for Ci in df_sum.columns] field=Ci, title=Ci, width=80) for Ci in df_sum.columns]
data_table = DataTable(columns=cols, data_table = DataTable(
source=ColumnDataSource(df_sum), columns=cols, source=ColumnDataSource(df_sum),
index_position=None, index_position=None, width=180)
width=180)
plots.append(data_table) plots.append(data_table)
tabs.append(Panel( tabs.append(Panel(
@ -808,7 +772,6 @@ def main():
parser.add_argument( parser.add_argument(
"--commit", default='unknown', "--commit", default='unknown',
help="git commit of the executed workflow") help="git commit of the executed workflow")
parser.add_argument( parser.add_argument(
"--alignment_stats", required=False, default=None, nargs='*', "--alignment_stats", required=False, default=None, nargs='*',
help="TSV summary file of alignment statistics") help="TSV summary file of alignment statistics")
@ -819,7 +782,7 @@ def main():
"--gffcompare_dir", required=False, default=None, nargs='*', "--gffcompare_dir", required=False, default=None, nargs='*',
help="gffcompare outout dir") help="gffcompare outout dir")
parser.add_argument( parser.add_argument(
"--pychop_report", required=True, nargs='+', "--pychop_report", required=False, default=None,
help="TSV summary file of pychopper statistics") help="TSV summary file of pychopper statistics")
parser.add_argument( parser.add_argument(
"--sample_ids", required=True, nargs='+', "--sample_ids", required=True, nargs='+',
@ -860,8 +823,8 @@ def main():
section.table(df_aln_stats) section.table(df_aln_stats)
# workflow-specific plotting # workflow-specific plotting
tanscriptome_summary(report, args.gff_annotation, sample_ids, transcriptome_summary(
denovo=args.denovo) report, args.gff_annotation, sample_ids, denovo=args.denovo)
df_tmaps = gff_compare_plots( df_tmaps = gff_compare_plots(
report, report,
@ -870,17 +833,8 @@ def main():
report.write(args.report) report.write(args.report)
pc_df = pd.DataFrame() if args.pychop_report is not None:
for id_, pyc in zip(sample_ids, args.pychop_report): pychopper_plots(report, args.pychop_report)
try:
p = pd.read_csv(pyc, sep='\t', index_col=0)
except pd.errors.EmptyDataError:
continue
p['sample_id'] = id_
pc_df = pd.concat([pc_df, p])
if len(pc_df) > 0:
pychopper_plots(report, pc_df)
if df_tmaps is not None: if df_tmaps is not None:
transcript_table(report, df_tmaps, args.transcript_table_cov_thresh) transcript_table(report, df_tmaps, args.transcript_table_cov_thresh)

View File

@ -24,8 +24,8 @@ class Node:
def __repr__(self): def __repr__(self):
"""Get string repr of a node.""" """Get string repr of a node."""
return "Node:{} Level: {} File: {} Done: {} Left: {} Right: " \ return "Node:{} Level: {} File: {} Done: " \
"{} Parent: {}".format( "{} Left: {} Right: {} Parent: {}".format(
self.Id, self.Level, self.Id, self.Level,
self.File, self.Done, self.Left.Id if self.File, self.Done, self.Left.Id if
self.Left is not None else None, self.Left is not None else None,
@ -47,7 +47,9 @@ def build_job_tree():
"""Build a job tree of nodes.""" """Build a job tree of nodes."""
JOB_TREE = OrderedDict() JOB_TREE = OrderedDict()
batches = glob("batches/isONbatch_*.cer") batches = glob("batches/isONbatch_*.cer")
batch_ids = [int(re.search('batches/isONbatch_(.*)\\.cer$', x).group(1)) batch_ids = [
int(re.search(
'batches/isONbatch_(.*)\\.cer$', x).group(1))
for x in batches] for x in batches]
LEVELS = OrderedDict() LEVELS = OrderedDict()
LEVELS[0] = [] LEVELS[0] = []
@ -93,12 +95,14 @@ def main():
Path('clusters').mkdir(exist_ok=True) Path('clusters').mkdir(exist_ok=True)
job_tree, levels = build_job_tree() job_tree, levels = build_job_tree()
init_template = 'isONclust2 cluster -x {} -v -Q -l batches/' \ init_template = (
'isONbatch_{}.cer -o clusters/isONcluster_{}.cer {}; ' \ 'isONclust2 cluster -x {} -v -Q -l batches/isONbatch_{}.cer '
'sync;\n' '-o clusters/isONcluster_{}.cer {}; '
template = 'isONclust2 cluster -x {} -v -Q -l clusters/isONcluster_{}' \ 'sync;\n')
'.cer -r clusters/isONcluster_{}.cer -o clusters/isONcluster' \ template = (
'_{}.cer {}; sync\n' 'isONclust2 cluster -x {} -v -Q -l clusters/isONcluster_{}.cer '
'-r clusters/isONcluster_{}.cer -o clusters/isONcluster_{}.cer '
'{}; sync\n')
for nr, l in levels.items(): for nr, l in levels.items():
jobs_out = 'jobs_level_{}.sh'.format(nr) jobs_out = 'jobs_level_{}.sh'.format(nr)
@ -109,14 +113,15 @@ def main():
jr = init_template.format('sahlin', n.Id, n.Id, purge) jr = init_template.format('sahlin', n.Id, n.Id, purge)
fh.write(jr) fh.write(jr)
else: else:
jr = template.format('sahlin', n.Left.Id, jr = template.format(
n.Right.Id, n.Id, purge) 'sahlin', n.Left.Id, n.Right.Id, n.Id, purge)
fh.write(jr) fh.write(jr)
# Run a level in parallel # Run a level in parallel
cmd = "parallel < {}".format(jobs_out) cmd = "parallel < {}".format(jobs_out)
sub.call(cmd, shell=True) sub.call(cmd, shell=True)
sub.call("ln -s `realpath clusters/isONcluster_{}" sub.call((
".cer` isONcluster_ROOT.cer".format(n.Id), shell=True) "ln -s `realpath clusters/isONcluster_{}.cer` "
"isONcluster_ROOT.cer".format(n.Id)), shell=True)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -6,7 +6,7 @@ channels:
- defaults - defaults
dependencies: dependencies:
- python==3.8.* - python==3.8.*
- aplanat >=0.6.2 - aplanat >=0.6.4
- epi2melabs - epi2melabs
- minimap2 ==2.24 - minimap2 ==2.24
- samtools ==1.14 - samtools ==1.14

36
main.nf
View File

@ -84,12 +84,16 @@ process preprocess_reads {
tuple val(sample_id), path(input_reads) tuple val(sample_id), path(input_reads)
output: output:
tuple val(sample_id), path("${sample_id}_full_length_reads.fq"), emit: full_len_reads tuple val(sample_id), path("${sample_id}_full_length_reads.fq"), emit: full_len_reads
tuple val(sample_id), path('*.tsv'), emit: report path '*.tsv', emit: report
script: script:
""" """
cdna_classifier.py -t ${params.threads} ${params.pychopper_opts} ${input_reads} ${sample_id}_full_length_reads.fq cdna_classifier.py -t ${params.threads} ${params.pychopper_opts} ${input_reads} ${sample_id}_full_length_reads.fq
mv cdna_classifier_report.tsv ${sample_id}_cdna_classifier_report.tsv mv cdna_classifier_report.tsv ${sample_id}_cdna_classifier_report.tsv
generate_pychopper_stats.py --data ${sample_id}_cdna_classifier_report.tsv --output . generate_pychopper_stats.py --data ${sample_id}_cdna_classifier_report.tsv --output .
# Add sample id column
sed "1s/\$/\tsample_id/; 1 ! s/\$/\t${sample_id}/" ${sample_id}_cdna_classifier_report.tsv > tmp
mv tmp ${sample_id}_cdna_classifier_report.tsv
""" """
} }
@ -266,11 +270,11 @@ process makeReport {
path versions path versions
path "params.json" path "params.json"
val denovo val denovo
path pychopper_report
tuple val(sample_ids), tuple val(sample_ids),
path(seq_summaries), path(seq_summaries),
path(aln_stats), path(aln_stats),
path(gffcmp_dir), path(gffcmp_dir),
path(cdna_class_report),
path(gff_annotation) path(gff_annotation)
output: output:
path("wf-isoforms-*.html"), emit: report path("wf-isoforms-*.html"), emit: report
@ -280,12 +284,13 @@ process makeReport {
def report_name = "wf-isoforms-report.html" def report_name = "wf-isoforms-report.html"
def OPT_ALN = denovo ? '' : "--alignment_stats ${aln_stats}" def OPT_ALN = denovo ? '' : "--alignment_stats ${aln_stats}"
def OPT_DENOVO = denovo ? "--denovo" : '' def OPT_DENOVO = denovo ? "--denovo" : ''
def OPT_PC_REPORT = pychopper_report.name.startsWith('OPTIONAL_FILE') ? '' : "--pychop_report ${pychopper_report}"
""" """
report.py --report $report_name \ report.py --report $report_name \
--versions $versions \ --versions $versions \
--params params.json \ --params params.json \
$OPT_ALN \ $OPT_ALN \
--pychop_report $cdna_class_report \ $OPT_PC_REPORT \
--sample_ids $sids \ --sample_ids $sids \
--summaries $seq_summaries \ --summaries $seq_summaries \
--gffcompare_dir $gffcmp_dir \ --gffcompare_dir $gffcmp_dir \
@ -345,16 +350,25 @@ workflow pipeline {
software_versions = getVersions() software_versions = getVersions()
workflow_params = getParams() workflow_params = getParams()
if (!params.direct_rna){
preprocess_reads(summariseConcatReads.out.input_reads) preprocess_reads(summariseConcatReads.out.input_reads)
full_len_reads = preprocess_reads.out.full_len_reads
pychopper_report = preprocess_reads.out.report.collectFile(keepHeader: true)
}
else{
full_len_reads = summariseConcatReads.out.input_reads
pychopper_report = file("$projectDir/data/OPTIONAL_FILE")
}
if (params.denovo){ if (params.denovo){
println("Doing de novo assembly") println("Doing de novo assembly")
m = denovo_assembly(preprocess_reads.out.full_len_reads, ref_genome) m = denovo_assembly(full_len_reads, ref_genome)
} else { } else {
build_minimap_index(ref_genome) build_minimap_index(ref_genome)
println("Doing reference based transcript analysis") println("Doing reference based transcript analysis")
m = reference_assembly(build_minimap_index.out.index, ref_genome, preprocess_reads.out.full_len_reads) m = reference_assembly(build_minimap_index.out.index, ref_genome, full_len_reads)
} }
split_bam(m.bam) split_bam(m.bam)
@ -380,10 +394,10 @@ workflow pipeline {
software_versions, software_versions,
workflow_params, workflow_params,
params.denovo, params.denovo,
pychopper_report,
summariseConcatReads.out.summary summariseConcatReads.out.summary
.join(m.stats) .join(m.stats)
.join(run_gffcompare.out.gffcmp_dir) .join(run_gffcompare.out.gffcmp_dir)
.join(preprocess_reads.out.report)
.join(merge_gff_bundles.out.gff) .join(merge_gff_bundles.out.gff)
.toList().transpose().toList()) .toList().transpose().toList())
@ -395,9 +409,8 @@ workflow pipeline {
.join(seq_for_transcriptome_build)) .join(seq_for_transcriptome_build))
if (use_ref_ann){ if (use_ref_ann){
results = preprocess_reads.out.report results = run_gffcompare.output.gffcmp_dir
.concat( .concat(
run_gffcompare.output.gffcmp_dir,
m.stats, m.stats,
get_transcriptome.out.flatMap(map_sample_ids_cls)) get_transcriptome.out.flatMap(map_sample_ids_cls))
.map {it -> it[1]} .map {it -> it[1]}
@ -405,8 +418,8 @@ workflow pipeline {
} }
if (!use_ref_ann && !params.denovo){ if (!use_ref_ann && !params.denovo){
results = preprocess_reads.out.report results = m.stats
.concat(m.stats, .concat(
get_transcriptome.out.flatMap(map_sample_ids_cls)) get_transcriptome.out.flatMap(map_sample_ids_cls))
.map {it -> it[1]} .map {it -> it[1]}
.concat(makeReport.out.report) .concat(makeReport.out.report)
@ -418,7 +431,8 @@ workflow pipeline {
seq_for_transcriptome_build, seq_for_transcriptome_build,
get_transcriptome.out.flatMap(map_sample_ids_cls), get_transcriptome.out.flatMap(map_sample_ids_cls),
merge_gff_bundles.out.gff, merge_gff_bundles.out.gff,
m.opt_qual_ch.flatMap {it -> m.opt_qual_ch.flatMap {
it ->
l = [] l = []
for (x in it[1..-1]){ for (x in it[1..-1]){
l.add(tuple(it[0], x)) l.add(tuple(it[0], x))

View File

@ -33,7 +33,8 @@ params {
show_hidden_params = false show_hidden_params = false
schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion' schema_ignore_params = 'show_hidden_params,validate_params,monochrome_logs,aws_queue,aws_image_prefix,wfversion'
// Process cDNA reads using pychopper, turn off for direct RNA:
direct_rna = true
// Options passed to pychopper: // Options passed to pychopper:
pychopper_opts = "-m edlib" pychopper_opts = "-m edlib"

View File

@ -66,6 +66,11 @@
"description": "Extra pychopper opts", "description": "Extra pychopper opts",
"default": "-m edlib" "default": "-m edlib"
}, },
"direct_rna": {
"type": "boolean",
"description": "Set to true for direct RNA sequencing. Omits the pychopper step.",
"default": false
},
"bundle_min_reads": { "bundle_min_reads": {
"type": "integer", "type": "integer",
"description": "Minimum size of bam bundle for parallel processing." "description": "Minimum size of bam bundle for parallel processing."