diff --git a/bin/QC.py b/bin/QC.py index 0c05a32..5044866 100755 --- a/bin/QC.py +++ b/bin/QC.py @@ -6,18 +6,24 @@ from libs.figs import ATGC_graph from libs.figs import GC_content_plot from libs.report import make_report +from libs.reporte_maker import reporter +import plotly.graph_objects as go parser = argparse.ArgumentParser(description='Process some integers.') parser.add_argument('-q', '--fastq', action='append', dest='fastq', required=True, help='FASTQ file (necessary if no sequencing summary file), ' + 'can also be in a tar.gz archive') +parser.add_argument('-r', "--conf_params", action='append', dest="conf_params", help="config parameters") +parser.add_argument('-w', "--work_params", action='append', dest="work_params", help="workflow parameters") +parser.add_argument('-e', "--env", action='append', dest="env", help="packages versions") + +parser.add_argument('-s', "--stats", action='store', dest="stats", help="template maker stats") parser.add_argument('-p', "--positions", action='store', dest="positions", help="Number of base position to show", type=int, default=1000) parser.add_argument('-t', "--thread", action='store', dest="thread", help="Number of threads", type=int, default=2) parser.add_argument('-b', "--batch-size", action='store', dest="batch_size", help="Batch size", type=int, default=500) parser.add_argument('-n', "--project", action='store', dest="project", help="project name", type=str, default="test_project") args = parser.parse_args() - columns = ["pos", "Q_score", "GC_content", @@ -46,6 +52,50 @@ def compQC(fastq, positions, reverse=False): return df_stats, df_qc, positions, q_scores, q1, q3 +def pie_unknown(df): + labels = ['Simulated feature counts','Unknow feature counts'] + values = [int(df['Simulated UMI counts']), + int(df['Unknown transcript counts'])] + + fig = go.Figure(data=[go.Pie(labels=labels, values=values, pull=[0, 0, 0.2, 0])]) + return fig + + +def create_table(stats_data, table_width=500, table_height=400, margin=10): + data=[go.Table( + header=dict( + values=['', 'Value'], + fill_color='#EBEBEB', # Couleur de fond de l'en-tête + font=dict(color='black', size=12) # Couleur et taille du texte de l'en-tête + ), + cells=dict( + values=[ + ['Simulated Cell BC', 'Simulated Filtered-Out', 'Simulated UMI counts', 'Mean UMI per cell'], + [ + stats_data['Simulated Cell BC'], + stats_data['Simulated Filtered-Out'], + stats_data['Simulated UMI counts'], + int(int(stats_data['Simulated UMI counts']) / int(stats_data['Simulated Cell BC'])) + ] + ], + fill_color='#f4f4f4', # Couleur de fond des cellules + font=dict(color='black', size=11) # Couleur et taille du texte des cellules + ) + )] + + fig = go.Figure(data) + + fig.update_layout( + #width=table_width, + #height=table_height, + margin=dict(l=margin, + r=margin, + t=20, + b=0), + ) + return fig + + def main(): df_stats, df_qc, pos, q_scores, q1, q3 = compQC(args.fastq, args.positions) df_stats_rev, df_qc_rev, pos_rev, q_scores_rev, q1_rev, q3_rev = compQC(args.fastq, args.positions, reverse=True) @@ -77,7 +127,21 @@ def main(): gc = GC_content_plot(df_stats['GC_percentage']) - make_report(Q_over_time, atgc, gc, args.project, args.positions) + df_stats = pd.read_csv(args.stats) + pie_fig = pie_unknown(df_stats) + stats_table = create_table(df_stats) + + #make_report(Q_over_time, atgc, gc, args.project, args.positions) + reporter(stats_table, + pie_fig, + Q_over_time, + atgc, + gc, + args.conf_params, + args.work_params, + args.project, + args.env) + if __name__ == "__main__": main() diff --git a/bin/libs/css_style.py b/bin/libs/css_style.py new file mode 100644 index 0000000..b43d707 --- /dev/null +++ b/bin/libs/css_style.py @@ -0,0 +1,174 @@ +from dominate.util import raw + +def _summary(graphs): + """ + Compose the summary section of the page + :param graphs: + :return: a string with HTML code for the module list + """ + result = " \n" + return result + + +def div_summary_style(titles): + style = """ + + """ + menu = """ +
+ + {summary_list} +
+ """.format(summary_list=_summary(titles)) + html = style+menu + return html + + +def head_style(): + style = raw(f""" + body {{ font-family: Arial, sans-serif; }} + header {{ display: flex; + justify-content: space-between; + align-items: center; + background-color: #EBEBEB; + padding: 10px 20px; + border-bottom: 1px solid #ddd; }} + + h1.title {{ font-size: 14px; }} + + h1.title2 {{ + font-size: 16px; + max-width: 400px; + border-bottom: 2px solid #000000; /* Exemple avec une bordure noire de 2px */ + padding-bottom: 10px; /* Ajoute un peu d'espace entre le texte et la bordure */ + }} + p.date {{ font-size: 12px; + color: #666; }} + + .container {{ display: flex; + justify-content: space-between; + background-color: #FFFFFF}} + + .content {{ flex: 2; + padding: 20px; + max-width: 1400px; + margin-left: 0; + margin-right: auto; + }} + + .aside {{ flex: 1; border-right: 1px solid #ddd; padding: 20px; }} + .tabs {{ display: flex; + margin-top: 20px; + margin-bottom: 20px; }} + + .tab-button {{ + background-color: #f4f4f4; + padding: 10px; + cursor: pointer; + border: none; + border-radius: 4px; + }} + + .tab-button.active {{ + background-color: #052F61; + color: white; + border-radius: 10px; + }} + + .tab-content {{ display: none; }} + .tab-content.active {{ display: block; }} + """) + return style + + +def body_style(): + style = """ + body { + font-family: 'Arial', sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; + } + .table-container { + width: 80%; + margin: 50px auto; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); + } + table { + width: 100%; + font-size: 14px; + border-collapse: collapse; + background-color: #fff; + border-radius: 8px; + overflow: hidden; + } + th, td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #ddd; + } + th { + background-color: #EBEBEB; + color: #666; + + } + tr:nth-child(even) { + background-color: #f2f2f2; + } + tr:hover { + background-color: #e9f1fe; + } + """ + return style \ No newline at end of file diff --git a/bin/libs/figs.py b/bin/libs/figs.py index 2af2ef7..a4a61f4 100755 --- a/bin/libs/figs.py +++ b/bin/libs/figs.py @@ -249,33 +249,23 @@ def over_time_graph(time_series, ] ) - # Set minimal value of y axis to 0 if required y_axis_range_mode = 'tozero' if yaxis_starts_zero else 'normal' - # Update layout fig.update_layout( title=graph_name, - autosize=False, - width=1400, - height=400, + autosize=True, xaxis_title=' Position ', yaxis_title='' +yaxis_title +'', hovermode='x unified', yaxis=dict(rangemode=y_axis_range_mode) ) - # Set logarithmic scale if required + if log: fig.update_yaxes(type="log") - # Show the figure - return fig #.write_html("Qscore_over_positions.html") #fig.show() + return fig - # Dummy return values (modify as needed for your application) - #table_html = None - #div = None - #output_file = None - #return graph_name, output_file, table_html, div def ATGC_graph(time_series, percentage_G, @@ -292,16 +282,14 @@ def ATGC_graph(time_series, green_zone_starts_at=None, green_zone_color=toulligqc_colors['phred_score_over_time']): - # Apply Gaussian filter to the percentile series filtered_G = gaussian_filter1d(percentage_G[0], sigma=sigma) filtered_C = gaussian_filter1d(percentage_C[0], sigma=sigma) filtered_A = gaussian_filter1d(percentage_A[0], sigma=sigma) filtered_T = gaussian_filter1d(percentage_T[0], sigma=sigma) - # Create a Plotly figure + fig = go.Figure() - # Add the 25th percentile trace fig.add_trace(go.Scatter( x=time_series[0], y=filtered_G, @@ -311,29 +299,24 @@ def ATGC_graph(time_series, fill=None )) - # Add the 75th percentile trace fig.add_trace(go.Scatter( x=time_series[0], y=filtered_A, name="Base A Percentage", mode='lines', line=dict(color="green", width=2), - #fill='tonexty', # Fill the area between this line and the previous line - #fillcolor='rgba(0, 100, 0, 0.2)' # Semi-transparent fill )) - # Add the 75th percentile trace + fig.add_trace(go.Scatter( x=time_series[0], y=filtered_T, name="Base T Percentage", mode='lines', line=dict(color="blue", width=2), - #fill='tonexty', # Fill the area between this line and the previous line - #fillcolor='rgba(0, 100, 0, 0.2)' # Semi-transparent fill + )) - # Add the 50th percentile (median) trace fig.add_trace(go.Scatter( x=time_series[0], y=filtered_C, @@ -345,12 +328,11 @@ def ATGC_graph(time_series, ###### ### add REVERSE ##### - # Apply Gaussian filter to the percentile series filtered_G = gaussian_filter1d(percentage_G[1], sigma=sigma) filtered_C = gaussian_filter1d(percentage_C[1], sigma=sigma) filtered_A = gaussian_filter1d(percentage_A[1], sigma=sigma) filtered_T = gaussian_filter1d(percentage_T[1], sigma=sigma) - # Add the 25th percentile trace + fig.add_trace(go.Scatter( x=time_series[1].multiply(other = -1), y=filtered_G, @@ -361,7 +343,6 @@ def ATGC_graph(time_series, visible=False )) - # Add the 75th percentile trace fig.add_trace(go.Scatter( x=time_series[1].multiply(other = -1), y=filtered_A, @@ -369,11 +350,9 @@ def ATGC_graph(time_series, mode='lines', line=dict(color="green", width=2), visible=False - #fill='tonexty', # Fill the area between this line and the previous line - #fillcolor='rgba(0, 100, 0, 0.2)' # Semi-transparent fill + )) - # Add the 75th percentile trace fig.add_trace(go.Scatter( x=time_series[1].multiply(other = -1), y=filtered_T, @@ -381,11 +360,9 @@ def ATGC_graph(time_series, mode='lines', line=dict(color="blue", width=2), visible=False - #fill='tonexty', # Fill the area between this line and the previous line - #fillcolor='rgba(0, 100, 0, 0.2)' # Semi-transparent fill )) - # Add the 50th percentile (median) trace + fig.add_trace(go.Scatter( x=time_series[1].multiply(other = -1), y=filtered_C, @@ -407,7 +384,7 @@ def ATGC_graph(time_series, buttons=list([ dict( args=[{'visible': [True, True, True, True, False, False, False, False]}, - {**_xaxis('Q score', dict(visible=True)), + {**_xaxis('Base percent', dict(visible=True)), **_yaxis('Position', dict(visible=True)), 'plot_bgcolor': '#e5ecf6'}], label="Begining ", @@ -415,7 +392,7 @@ def ATGC_graph(time_series, ), dict( args=[{'visible': [False, False, False, False, True, True, True, True]}, - {**_xaxis('Q score', dict(visible=True)), + {**_xaxis('Base percent', dict(visible=True)), **_yaxis('Position', dict(visible=True)), 'plot_bgcolor': '#e5ecf6'}], label="End", @@ -431,46 +408,32 @@ def ATGC_graph(time_series, ) ] ) - # Set minimal value of y axis to 0 if required + y_axis_range_mode = 'tozero' if yaxis_starts_zero else 'normal' - # Update layout + fig.update_layout( title=graph_name, - autosize=False, - width=1400, - height=400, + autosize=True, xaxis_title=' Position ', yaxis_title= '' + yaxis_title + '', hovermode='x unified', yaxis=dict(rangemode=y_axis_range_mode) ) - # Set logarithmic scale if required if log: fig.update_yaxes(type="log") - - # Show the figure - return fig #.write_html("ATGC_graph.html") #fig.show() - - # Dummy return values (modify as needed for your application) - #table_html = None - #div = None - #output_file = None - #return graph_name, output_file, table_html, div + return fig def GC_content_plot(gc_content_percentages): - # Distribution réelle - max_gc = 100 # Pourcentage max + max_gc = 100 gc_distribution = [0] * (max_gc + 1) for gc_content in gc_content_percentages: gc_distribution[int(gc_content)] += 1 - # affichage en ligne real_distribution_smooth = np.convolve(gc_distribution, np.ones(5)/5, mode='same') - # Distribution théorique mean_gc = np.mean(gc_content_percentages) std_gc = np.std(gc_content_percentages) theoretical_distribution = [stats.norm.pdf(gc, mean_gc, std_gc) * len(gc_content_percentages) for gc in range(max_gc + 1)] @@ -480,17 +443,16 @@ def GC_content_plot(gc_content_percentages): fig.add_trace(go.Scatter(x=list(range(max_gc + 1)), y=real_distribution_smooth, mode='lines', name='Distribution GC réelle')) fig.add_trace(go.Scatter(x=list(range(max_gc + 1)), y=theoretical_distribution, mode='lines', name='Distribution GC théorique')) - # Mise en forme - fig.update_layout(title='Distribution du contenu en GC', - autosize=False, - width=1400, - height=400, - xaxis_title=' Contenu en GC (%) ', - yaxis_title=' Compte ', + fig.update_layout(title='GC content distribution', + autosize=True, + #width=1400, + #height=400, + xaxis_title=' GC content (%) ', + yaxis_title=' Count ', legend_title=' Légende ') - return fig #fig.write_html("GC_content.html") #.show() + return fig def interpolation_points(series, graph_name): diff --git a/bin/libs/reporte_maker.py b/bin/libs/reporte_maker.py new file mode 100644 index 0000000..15f9115 --- /dev/null +++ b/bin/libs/reporte_maker.py @@ -0,0 +1,185 @@ +import pandas as pd +import re +import dominate +from dominate.tags import * +from dominate.util import raw +from datetime import date, datetime +from libs.css_style import body_style, head_style, div_summary_style + +import plotly.express as px +df2 = px.data.iris() +fig = px.scatter(df2, x="sepal_width", y="sepal_length", color='petal_length') + +def list_to_dict(params): + return {"--"+k: v for k, v in (s.split(":", 1) for s in params) if v != "null"} + + +def string_to_dict(params): + params_list = params[0].split(', ') + params_list = {k: v for k, v in (s.split(":", 1) for s in params_list) if v != "null"} + return params_list + +def parse_versions(versions): + try: + versions_list = {k: v for d, k, v in (re.split(r'(Badread|SPARSim)', s) for s in versions)} + except: + versions_list = {k:k for k in versions} + return versions_list + + +def read_logo(image_path): + import base64 + with open(image_path, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode('utf-8') + return encoded_string + +def reporter(stats_table, pie_fig, Q_over_time, atgc, gc, conf_params, work_params, project, versions): + work_params = string_to_dict(work_params) + conf_params = list_to_dict(conf_params) + + versions = parse_versions(versions) + + conf_params.update(work_params) + + conf_params.update(versions) + + df = pd.DataFrame(list(conf_params.items()), columns=['Params', 'Value']) + + + Basic_inputs_df = df[df['Params'].isin(['--matrix', '--bc_counts', '--transcriptome', + '--features', '--gtf', '--sim_celltypes', '--cell_types_annotation', + '--error_model', '--fastq_model', '--ref_genome', '--trained_model', + '--qscore_model'])] + + simulation_parames_df = df[df['Params'].isin(['--full_length', '--umi_duplication', '--pcr_cycles', + '--pcr_error_rate', '--pcr_dup_rate', '--pcr_total_reads', + '--badread_identity', '--dT_LENGTH', '--ADPTER_SEQ', '--TSO_SEQ'])] + + run_params_df = df[df['Params'].isin(['container', 'runOptions', 'dsl2', 'startTime', 'threads', 'commandLine'])] + + versions_df = df[df['Params'].isin(['nextflow', 'SPARSim', 'Badread'])] + + doc = dominate.document(title='Styled Table with Tabs') + current_date = raw(str(date.today().strftime('%b-%d-%Y'))+'
'+str(datetime.now().strftime('%H:%M:%S'))) + + with doc.head: + style(head_style()) + + with doc.body: + style(body_style()) + + with doc: + with header(): + with div(): + h1('AsaruSim v0.1.0', cls='title') + p(current_date, cls='date') + img_logo = read_logo('asarusim_v2.png') + img(src=f'data:image/png;base64,{img_logo}',height='60', alt='Logo', style="float: right;") + + with div(cls='container'): + with div(): + titles = ['Simulation parameters', + 'Simulation statistics', + 'Qscore over sequence position', + 'Base content over position', + 'Per base GC content'] + raw(div_summary_style(titles)) + + with div(cls='content'): + h1('Simulation parameters', cls='title2') + with div(cls='tabs'): + button('Basic inputs', cls='tab-button active', onclick="openTab(event, 'tab1')") + button('Simulation config', cls='tab-button', onclick="openTab(event, 'tab2')") + button('Run parameters', cls='tab-button', onclick="openTab(event, 'tab3')") + button('Package versions', cls='tab-button', onclick="openTab(event, 'tab4')") + + with div(style='margin-bottom: 20px; min-height: 300px;'): + with div(id='tab1', cls='tab-content active'): + with table(): + with thead(): + with tr(): + for col in Basic_inputs_df.columns: + th(col) + with tbody(): + for _, row in Basic_inputs_df.iterrows(): + with tr(): + for cell in row: + td(cell) + + with div(id='tab2', cls='tab-content'): + with table(): + with thead(): + with tr(): + for col in simulation_parames_df.columns: + th(col) + with tbody(): + for _, row in simulation_parames_df.iterrows(): + with tr(): + for cell in row: + td(cell) + + with div(id='tab3', cls='tab-content'): + with table(): + with thead(): + with tr(): + for col in run_params_df.columns: + th(col) + with tbody(): + for _, row in run_params_df.iterrows(): + with tr(): + for cell in row: + td(cell) + with div(id='tab4', cls='tab-content'): + with table(): + with thead(): + with tr(): + for col in versions_df.columns: + th(col) + with tbody(): + for _, row in versions_df.iterrows(): + with tr(): + for cell in row: + td(cell) + + h1('Simulation statistics', cls='title2') + with div(style='display: flex; justify-content: space-between'): + with div(style='max-width: 600px; flex: 2; margin-top: 80px'): + raw(stats_table.to_html()) + with div(style='max-width: 800px; flex: 2'): + raw(pie_fig.to_html()) + + h1('Read QC', cls='title2') + with div(style='margin-bottom: 20px'): + + raw(Q_over_time.to_html()) + raw(atgc.to_html()) + raw(gc.to_html()) + + with footer(): + with div(style='font-size: 12px; padding: 10px 20px;;'): + app_url="https://alihamraoui.github.io/AsaruSim/introduction/" + p(raw(f'Generated by AsaruSim (version 0.1.0)')) + + + with doc.body: + script(raw(""" + function openTab(evt, tabName) { + var i, tabcontent, tablinks; + tabcontent = document.getElementsByClassName("tab-content"); + for (i = 0; i < tabcontent.length; i++) { + tabcontent[i].style.display = "none"; + } + tablinks = document.getElementsByClassName("tab-button"); + for (i = 0; i < tablinks.length; i++) { + tablinks[i].className = tablinks[i].className.replace(" active", ""); + } + document.getElementById(tabName).style.display = "block"; + evt.currentTarget.className += " active"; + } + """)) + out_name = project+'.html' + with open(out_name, 'w') as f: + f.write(doc.render()) + +if __name__ == '__main__': + main() diff --git a/bin/template_maker.py b/bin/template_maker.py index 85898f3..df72339 100755 --- a/bin/template_maker.py +++ b/bin/template_maker.py @@ -39,6 +39,7 @@ parser.add_argument('--adapter', type=str, default="ATGCGTAGTCAGTCATGATC", help="Adapter sequence.") parser.add_argument('--TSO', type=str, default="ATGCGTAGTCAGTCATGATC", help="TSO sequence.") parser.add_argument('--len_dT', type=str, default=15, help="Poly-dT sequence.") +parser.add_argument('--log', type=str, help="Path to the log file CSV.") class Transcriptome: @@ -274,6 +275,16 @@ def main(): os.remove(filename) count_unfiltered_bc = len(unfiltered_bc) if args.unfilteredBC else 0 + + if args.log: + log_df = { + "Simulated Cell BC": len(matrix.columns), + "Simulated Filtered-Out": count_unfiltered_bc, + "Simulated UMI counts": generator.counter, + "Unknown transcript counts": generator.unfound} + log_df = pd.DataFrame(log_df, index=[0]) + log_df.to_csv(args.log, index=False) + logging.info("Completed successfully. Have a great day!") logging.info("Stats : "+ "\nSimulated Cell BC: "+ diff --git a/bin/toolkit.py b/bin/toolkit.py index e424701..94c489e 100755 --- a/bin/toolkit.py +++ b/bin/toolkit.py @@ -103,7 +103,10 @@ def parse_gtf(gtf_file, index_by, protein_coding=False): transcript_id = attributes['transcript_id'] transcript_type = attributes.get('transcript_biotype', attributes.get('transcript_type', '')) - index_key = attributes[index_by] + if index_by in attributes: + index_key = attributes[index_by] + else: + continue start = int(columns[3]) end = int(columns[4]) diff --git a/images/asarusim_v2.png b/images/asarusim_v2.png new file mode 100644 index 0000000..c5c54ea Binary files /dev/null and b/images/asarusim_v2.png differ diff --git a/main.nf b/main.nf index 916965f..015e22f 100755 --- a/main.nf +++ b/main.nf @@ -25,6 +25,12 @@ log.info """\ """ .stripIndent() +logo_ch = channel.fromPath('./images/asarusim_v2.png') +config_params_ch = Channel.value(params) + .map { p -> p.collect { key, value -> "--conf_params $key:$value" }} + +workflow_params_ch = Channel.from(workflow).map { p -> p.collect { value -> "--work_params '$value'" }} + include { SUBSAMPLE } from './modules/errorModel.nf' include { ALIGNMENT } from './modules/errorModel.nf' include { ERROR_MODLING } from './modules/errorModel.nf' @@ -83,19 +89,22 @@ workflow { if (params.sim_celltypes) { counts_ch = COUNT_SIMULATOR(matrix_ch, cell_types_ch) - template_ch = TEMPLATE_MAKER(counts_ch, transcriptome_ch, barcodes_ch, gtf_ch, length_dist_ch) + TEMPLATE_MAKER(counts_ch, transcriptome_ch, barcodes_ch, gtf_ch, length_dist_ch) } else { - template_ch = TEMPLATE_MAKER(matrix_ch, transcriptome_ch, barcodes_ch, gtf_ch, length_dist_ch) + TEMPLATE_MAKER(matrix_ch, transcriptome_ch, barcodes_ch, gtf_ch, length_dist_ch) } + template_fa_ch = TEMPLATE_MAKER.out.template + template_log_ch = TEMPLATE_MAKER.out.logfile + if (params.pcr_cycles > 0) { - template_ch = PCR_SIMULATOR(template_ch) + template_fa_ch = PCR_SIMULATOR(template_fa_ch) } - gr_truth_ch = GROUND_TRUTH(template_ch) - error_ch = ERRORS_SIMULATOR(template_ch, error_model_ch, qscore_model_ch, identity_ch) - qc_ch = QC(error_ch) + gr_truth_ch = GROUND_TRUTH(template_fa_ch) + error_ch = ERRORS_SIMULATOR(template_fa_ch, error_model_ch, qscore_model_ch, identity_ch) + qc_ch = QC(error_ch, config_params_ch, workflow_params_ch, logo_ch, template_log_ch) } workflow.onComplete { diff --git a/modules/modules.nf b/modules/modules.nf index 0e00204..1764112 100755 --- a/modules/modules.nf +++ b/modules/modules.nf @@ -27,7 +27,8 @@ process TEMPLATE_MAKER { val length_dist output: - path "template.fa" + path "template.fa", emit: template + path "log.csv", emit: logfile script: def gtf = params.features != "transcript_id" ? "--features $params.features --gtf $gtf" : "" @@ -45,7 +46,8 @@ process TEMPLATE_MAKER { --len_dT $params.dT_LENGTH \ $full_length \ --length_dist ${length_dist.trim()} \ - -o template.fa + -o template.fa \ + --log log.csv """ } @@ -99,15 +101,28 @@ process GROUND_TRUTH { process QC { publishDir params.outdir, mode:'copy' + cache false input: path fastq + val conf_params + val work_params + path logo + path log_csv output: path "${params.projetctName}.html" script: """ - python3.11 $projectDir/bin/QC.py -q $fastq -n $params.projetctName + bedread_version=\$(echo "-e \$(badread --version | head -n 1)") + nextflow_version=\$(echo "-e \$(Rscript -e "ip <- installed.packages()[, c('Package', 'Version')]; + cat(paste(ip[,1], ip[,2], sep=': '), sep='\n')" | grep SPARSim)") + + python3.11 $projectDir/bin/QC.py -q $fastq --stats $log_csv -n $params.projetctName \ + ${conf_params.join(' ').replaceAll(/[\(\)\$]/, "")} \ + ${work_params.join('').replaceAll(/[\(\)\$]/, "")} \ + "\$bedread_version" \ + "\$nextflow_version" \ """ } \ No newline at end of file