| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858 |
- #let cr_colors = (
- dark_grey: rgb("#333333"),
- beige: rgb("#fdf0d5"),
- light_grey: rgb("#eeeeee"),
- dark_red: rgb("#780000"),
- red: rgb("#c1121f"),
- blue: rgb("#669bbc"),
- dark_blue: rgb("#003049"),
- green: rgb("#29bf12"),
- )
- #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge
- #import "@preview/metro:0.3.0": *
- #import "@preview/cetz:0.2.2"
- #import "@preview/badgery:0.1.1": *
- #import "@preview/cmarker:0.1.1"
- #set page(
- paper: "a4",
- footer: locate(loc => [
- #set text(10pt)
- #let today = datetime.today()
- #if loc.page() != 1 {
- align(right, counter(page).display("1 / 1", both: true))
- }
- #align(center, [Dr. Thomas Steimlé --- #today.display("[day] [month repr:long] [year]")])
- ]),
- )
- #show heading: set text(font: "Futura")
- #show heading.where(level: 1): it => [
- #set align(center)
- #set text(fill: cr_colors.dark_blue)
- #it.body
- #v(18pt)
- ]
- #show image: set text(font: "FreeSans")
- #set text(size: 16pt, fill: cr_colors.dark_blue)
- #let contigs = (
- "chr1",
- "chr2",
- "chr3",
- "chr4",
- "chr5",
- "chr6",
- "chr7",
- "chr8",
- "chr9",
- "chr10",
- "chr11",
- "chr12",
- "chr13",
- "chr14",
- "chr15",
- "chr16",
- "chr17",
- "chr18",
- "chr19",
- "chr20",
- "chr21",
- "chr22",
- "chrX",
- "chrY",
- )
- #let parseCustomDate(dateString) = {
- let parts = dateString.split("T")
- let datePart = parts.at(0).replace("-", "/")
- let timePart = parts.at(1).split(":")
- let hour = timePart.at(0)
- let minute = timePart.at(1)
- return datePart + " " + hour + "h" + minute
- }
- #let formatString(input) = {
- let words = input.split("_")
- let capitalizedWords = words.map(word => {
- if word.len() > 0 {
- upper(word.first()) + word.slice(1)
- } else {
- word
- }
- })
- capitalizedWords.join(" ")
- }
- #let si-fmt(val, precision: 1, sep: "\u{202F}", binary: false) = {
- let factor = if binary {
- 1024
- } else {
- 1000
- }
- let gt1_suffixes = ("k", "M", "G", "T", "P", "E", "Z", "Y")
- let lt1_suffixes = ("m", "μ", "n", "p", "f", "a", "z", "y")
- let scale = ""
- let unit = ""
- if type(val) == content {
- if val.has("text") {
- val = val.text
- } else if val.has("children") {
- val = val.children.map(content => content.text).join()
- } else {
- panic(val.children.map(content => content.text).join())
- }
- }
- // if val contains a unit, split it off
- if type(val) == str {
- unit = val.find(regex("(\D+)$"))
- val = float(val.split(unit).at(0))
- }
- if calc.abs(val) > 1 {
- for suffix in gt1_suffixes {
- if calc.abs(val) < factor {
- break
- }
- val /= factor
- scale += " " + suffix
- }
- } else if val != 0 and calc.abs(val) < 1 {
- for suffix in lt1_suffixes {
- if calc.abs(val) > 1 {
- break
- }
- val *= factor
- scale += " " + suffix
- }
- }
- let formatted = str(calc.round(val, digits: precision))
- formatted + sep + scale.split().at(-1, default: "") + unit
- }
- #let reportCoverage(prefix) = {
- image(prefix + "_global.svg", width: 100%)
- for contig in contigs {
- heading(level: 4, contig)
- let path = prefix + "_" + contig
- image(path + "_chromosome.svg")
- let data = json(path + "_stats.json")
- grid(
- columns: (1fr, 2fr),
- gutter: 3pt,
- align(left + horizon)[
- #set text(size: 12pt)
- #table(
- stroke: none, columns: (auto, 1fr), gutter: 3pt, [Mean], [#calc.round(
- data.mean,
- digits: 2,
- )], [Standard dev.], [#calc.round(
- data.std_dev,
- digits: 2,
- )], ..data.breaks_values.map(r => (
- [#r.at(0)],
- [#calc.round(r.at(1) * 100, digits: 1)%],
- )).flatten(),
- )
- ],
- align(right, image(path + "_distrib.svg", width: 100%)),
- )
- parbreak()
- }
- }
- #let reportBam(path) = {
- let data = json(path)
- table(
- gutter: 3pt, stroke: none, columns: (auto, 1fr), ..for (key, value) in (
- data
- ) {
- if key != "cramino" and key != "composition" and key != "path" and key != "modified" {
- ([ #formatString(key) ], [ #value ])
- } else if key == "modified" {
- ([ Modified Date (UTC) ], [ #parseCustomDate(value) ])
- } else if key == "composition" {
- (
- [ Run(s) ],
- [
- #for (i, v) in value.enumerate() {
- if i > 0 [ \ ]
- [#v.at(0).slice(0, 5): #calc.round(v.at(1), digits: 0)%]
- }
- ],
- )
- } else if key == "cramino" {
- for (k, v) in value {
- if k == "normalized_read_count_per_chromosome" { } else if k != "path" and k != "checksum" and k != "creation_time" and k != "file_name" {
- let k = formatString(k)
- let v = if type(v) == "integer" {
- si-fmt(v)
- } else {
- v
- }
- ([ #k ], [ #v ])
- } else {
- ()
- }
- }.flatten()
- } else {
- ()
- }
- }.flatten(),
- )
- }
- #let formatedReadCount(path) = {
- let data = json(path)
- let data = data.cramino.normalized_read_count_per_chromosome
- let res = ()
- for contig in contigs {
- res.push(data.at(contig))
- }
- res.push(data.at("chrM"))
- return res
- }
- #let printReadCount(diag_path, mrd_path) = {
- let index = 14
- let c = contigs
- c.push("chrM")
- let diag = formatedReadCount(diag_path)
- let mrd = formatedReadCount(mrd_path)
- c.insert(0, "")
- diag.insert(0, "diag")
- mrd.insert(0, "mrd")
- let arrays1 = (c.slice(0, index), diag.slice(0, index), mrd.slice(0, index))
- table(
- columns: arrays1.at(0).len(), ..arrays1
- .map(arr => arr.map(item => [#item]))
- .flatten(),
- )
- let arrays2 = (c.slice(index), diag.slice(index), mrd.slice(index))
- arrays2.at(0).insert(0, "")
- arrays2.at(1).insert(0, "diag")
- arrays2.at(2).insert(0, "mrd")
- table(
- columns: arrays2.at(0).len(), ..arrays2
- .map(arr => arr.map(item => [#item]))
- .flatten(),
- )
- }
- #let variantsFlow(path) = {
- import fletcher.shapes: diamond, parallelogram, chevron
- let data = json(path)
- set text(8pt)
- diagram(
- spacing: (8pt, 25pt),
- node-fill: gradient.radial(
- cr_colors.light_grey,
- cr_colors.blue,
- radius: 300%,
- ),
- node-stroke: cr_colors.dark_blue + 1pt,
- edge-stroke: 1pt,
- mark-scale: 70%,
- node-inset: 8pt,
- node(
- (0.2, 0),
- [Variants MRD: #num(data.vcf_stats.n_tumoral_init)],
- corner-radius: 2pt,
- extrude: (0, 3),
- name: <input_mrd>,
- ),
- node(
- (1.8, 0),
- [Variants Diag: #num(data.vcf_stats.n_constit_init)],
- corner-radius: 2pt,
- extrude: (0, 3),
- name: <input_diag>,
- ),
- node(
- (1, 1),
- align(center)[Variant in MRD ?],
- shape: diamond,
- name: <is_in_mrd>,
- ),
- edge(<input_mrd>, "s", <is_in_mrd>, "-|>"),
- edge(<input_diag>, "s", <is_in_mrd>, "-|>"),
- edge(<is_in_mrd>, <is_low_mrd>, "-|>", [Yes], label-pos: 0.8),
- node(
- (0.25, 2),
- [MRD variant depth \ < 4 ?],
- shape: diamond,
- name: <is_low_mrd>,
- ),
- edge(<is_low_mrd>, <low_mrd>, "-|>"),
- node(
- (0, 3),
- [Low MRD depth: #num(data.vcf_stats.n_low_mrd_depth)],
- shape: parallelogram,
- name: <low_mrd>,
- ),
- edge(<is_in_mrd>, <next>, "-|>", [No], label-pos: 0.8),
- node(
- (1.85, 2),
- [To BAM filters: #num(data.bam_stats.n_lasting)],
- shape: chevron,
- extrude: (-3, 0),
- name: <next>,
- stroke: cr_colors.green,
- ),
- edge(<is_low_mrd>, <homo>, "-|>"),
- node((1.5, 3), [VAF = 100% ?], shape: diamond, name: <homo>),
- edge(<homo>, <constit>, "-|>", [Yes], label-pos: 0.5, bend: -80deg),
- edge(<homo>, <chi>, "-|>", [No], label-pos: 0.6),
- node(
- (1.5, 4),
- [$#sym.chi^2$ VAF MRD vs Diag ?],
- shape: diamond,
- name: <chi>,
- ),
- edge(<chi>, <constit>, "-|>", label-pos: 0.8),
- node(
- (1, 5),
- [Constit: #num(data.vcf_stats.n_constit)],
- shape: parallelogram,
- name: <constit>,
- ),
- edge(<chi>, <loh>, "-|>", [p < 0.01], label-pos: 0.8),
- node(
- (2, 5),
- [LOH: #num(data.vcf_stats.n_loh)],
- shape: parallelogram,
- name: <loh>,
- ),
- )
- }
- #let bamFilter(path) = {
- import fletcher.shapes: diamond, parallelogram, hexagon
- let data = json(path)
- set text(8pt)
- diagram(
- spacing: (8pt, 25pt),
- node-fill: gradient.radial(
- cr_colors.light_grey,
- cr_colors.blue,
- radius: 300%,
- ),
- node-inset: 8pt,
- node-stroke: cr_colors.dark_blue + 1pt,
- mark-scale: 70%,
- edge-stroke: 1pt,
- node(
- (0.75, 0),
- [Variants not in MRD VCF: #num(data.bam_stats.n_lasting)],
- corner-radius: 2pt,
- extrude: (0, 3),
- name: <input_mrd>,
- ),
- edge(<input_mrd>, <depth>, "-|>"),
- node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: <depth>),
- edge(<depth>, <low_depth>, "-|>", [< 4]),
- node(
- (0, 2),
- [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)],
- shape: parallelogram,
- name: <low_depth>,
- ),
- edge(<depth>, <seen>, "-|>"),
- node(
- (0.75, 3),
- [Alt. base seen in MRD pileup ?],
- shape: diamond,
- name: <seen>,
- ),
- edge(<seen>, <constit>, "-|>", [Yes]),
- node(
- (0, 4),
- [Constit: #num(data.bam_stats.n_constit)],
- shape: parallelogram,
- name: <constit>,
- ),
- edge(<seen>, <is_div>, "-|>", [No]),
- node(
- (1.1, 4),
- [Sequence #sym.plus.minus 20nt \ diversity ?],
- shape: diamond,
- name: <is_div>,
- ),
- edge(<is_div>, <low_div>, "-|>", [entropy < 1.8]),
- node(
- (0.25, 5),
- [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)],
- shape: parallelogram,
- name: <low_div>,
- ),
- edge(<is_div>, <somatic>, "-|>"),
- node(
- (1.75, 5),
- [Somatic: #num(data.bam_stats.n_somatic)],
- shape: hexagon,
- extrude: (-3, 0),
- name: <somatic>,
- stroke: cr_colors.green,
- ),
- )
- }
- #let barCallers(path) = {
- import cetz.draw: *
- import cetz.chart
- let json_data = json(path).variants_stats
- let data = json_data.find(item => item.name == "callers_cat")
- let chart_data = data.counts.pairs().sorted(key: x => -x.at(1))
- set text(11pt)
- cetz.canvas(
- length: 80%,
- {
- set-style(axes: (
- bottom: (tick: (label: (angle: 45deg, anchor: "north-east"))),
- ))
- chart.columnchart(
- chart_data,
- size: (1, 0.5),
- )
- },
- )
- }
- #let truncate(text, max-length) = {
- if text.len() <= max-length {
- text
- } else {
- text.slice(0, max-length - 3) + "..."
- }
- }
- // // #let add_newlines(text, n) = {
- // // let result = ""
- // // let chars = text.clusters()
- // // for (i, char) in chars.enumerate() {
- // // result += char
- // // if calc.rem((i + 1), n == 0 and i < chars.len() - 1 {
- // // result += "\n"
- // // }
- // // }
- // // result
- // // }
- //
- // #let break_long_words(text, max_length: 20, hyphen: "") = {
- // let words = text.split(" ")
- // let result = ()
- //
- // for word in words {
- // if word.len() <= max_length {
- // result.push(word)
- // } else {
- // let segments = ()
- // let current_segment = ""
- // for char in word.clusters() {
- // if current_segment.len() + 1 > max_length {
- // segments.push(current_segment + hyphen)
- // current_segment = ""
- // }
- // current_segment += char
- // }
- // if current_segment != "" {
- // segments.push(current_segment)
- // }
- // result += segments
- // }
- // }
- //
- // result.join(" ")
- // }
- //
- #let format_sequence(text, max_length: 40, hyphen: [#linebreak()]) = {
- let words = text.split(" ")
- let result = ()
- // result.push("\n")
- for word in words {
- if word.len() <= max_length {
- result.push(word)
- } else {
- let segments = ()
- let current_segment = ""
- for char in word.clusters() {
- if current_segment.len() + 1 > max_length {
- segments.push(current_segment + hyphen)
- current_segment = ""
- }
- current_segment += char
- }
- if current_segment != "" {
- segments.push(current_segment)
- }
- result += segments
- }
- }
- result.push("")
- let sequence = result.join(" ")
- box(width: 100%, par(leading: 0.2em, sequence))
- }
- #let dna(sequence, line_length: 60) = {
- let formatted = sequence.clusters().map(c => {
- if c == "A" {
- text(fill: red)[A]
- } else if c == "T" {
- text(fill: green)[T]
- } else if c == "C" {
- text(fill: blue)[C]
- } else if c == "G" {
- text(fill: orange)[G]
- } else {
- c
- }
- })
- let lines = formatted.chunks(line_length).map(line => line.join())
- let n_lines = lines.len()
- let lines = lines.join("\n")
- if n_lines > 1 {
- parbreak()
- }
- align(
- left,
- box(
- fill: luma(240),
- inset: (x: 0.5em, y: 0.5em),
- radius: 4pt,
- align(
- left,
- text(
- font: "Fira Code",
- size: 10pt,
- lines,
- ),
- ),
- ),
- )
- if n_lines > 1 {
- parbreak()
- }
- }
- #let to-string(content) = {
- if content.has("text") {
- content.text
- } else if content.has("children") {
- content.children.map(to-string).join("")
- } else if content.has("body") {
- to-string(content.body)
- } else if content == [ ] {
- " "
- }
- }
- #let format-number(num) = {
- let s = str(num).split("").filter(item => item != "")
- let result = ""
- let len = s.len()
- for (i, char) in s.enumerate() {
- result += char
- if (i < len - 1 and calc.rem((len - i - 1), 3) == 0) {
- result += ","
- }
- }
- result
- }
- #let format_json(json_data) = {
- let format_value(value) = {
- if value == none {
- ""
- } else if type(value) == "string" {
- if value != "." {
- value.replace(";", ", ").replace("=", ": ")
- } else {
- ""
- }
- } else if type(value) == "array" {
- let items = value.map(v => format_value(v))
- "[" + items.join(", ") + "]"
- } else if type(value) == "dictionary" {
- "{" + format_json(value) + "}"
- } else {
- str(value)
- }
- }
- if type(json_data) == "dictionary" {
- let result = ()
- for (key, value) in json_data {
- let formatted_value = format_value(value)
- if formatted_value != "" {
- if key == "svinsseq" {
- formatted_value = dna(formatted_value)
- }
- result.push(upper(key) + ": " + formatted_value)
- }
- }
- result.join(", ")
- } else {
- format_value(json_data)
- }
- }
- #let card(d) = {
- set text(12pt)
- let position_fmt = format-number(d.position)
- let title_bg_color = rgb("#f9fafb00")
- let grid_content = ()
- let callers_data = json.decode(d.callers_data)
- // Title
- let alt = d.alternative
- // TODO: add that in pandora_lib_variants
- if d.callers == "Nanomonsv" and alt == "<INS>" {
- alt = d.reference + callers_data.at(0).info.Nanomonsv.svinsseq
- }
- let title = d.contig + ":" + position_fmt + " " + d.reference + sym
- .quote
- .angle
- .r
- .single + truncate(alt, 30)
- grid_content.push(
- grid.cell(
- fill: cr_colors.light_grey,
- align: center,
- block(width: 100%, title),
- ),
- )
- // Consequences
- if d.consequence != none {
- let consequences = d.consequence.replace(",", ", ").replace(
- "_",
- " ",
- ) + " " + emph(strong(d.gene))
- grid_content.push(
- grid.cell(fill: cr_colors.light_grey, align: center, consequences),
- )
- }
- // hgvs_c
- if d.hgvs_c != none {
- grid_content.push(
- grid.cell(fill: rgb("#fef08a"), align: center, truncate(d.hgvs_c, 50)),
- )
- }
- // hgvs_c
- if d.hgvs_p != none {
- grid_content.push(
- grid.cell(fill: rgb("#fecaca"), align: center, truncate(d.hgvs_p, 50)),
- )
- }
- // Content
- let content = ()
- content.push(
- badge-red("VAF: " + str(calc.round(d.m_vaf * 100, digits: 2)) + "%"),
- )
- // content.push(" ")
- if d.cosmic_n != none {
- content.push(badge-red("Cosmic: " + str(d.cosmic_n)))
- }
- if d.gnomad_af != none {
- content.push(badge-blue("GnomAD: " + str(d.gnomad_af)))
- }
- let callers_contents = ()
- for caller_data in callers_data {
- let caller = ""
- for (k, v) in caller_data.format {
- caller = k
- }
- callers_contents.push(underline(caller) + ":")
- if caller_data.qual != none {
- callers_contents.push([
- Qual: #caller_data.qual,
- ])
- }
- callers_contents.push([
- #(
- format_json(caller_data.format.at(caller)),
- format_json(caller_data.info.at(caller)),
- ).filter(v => v != "").join(", ")
- ])
- }
- content.push(
- grid(
- columns: 1,
- inset: 0.5em,
- ..callers_contents
- ),
- )
- grid_content.push(grid.cell(fill: white, content.join(" ")))
- block(
- breakable: false,
- width: 100%,
- grid(
- columns: 1,
- inset: 0.5em,
- stroke: cr_colors.dark_grey,
- ..grid_content
- ),
- )
- }
- #let variants(path, interpretation: "PATHO") = {
- let data = json(path)
- let patho = data.filter(d => d.interpretation == interpretation)
- for var in patho {
- card(var)
- }
- }
- #set heading(numbering: (..numbers) => {
- if numbers.pos().len() >= 2 and numbers.pos().len() <= 3 {
- numbering("1.1", ..numbers.pos().slice(1))
- }
- })
- #heading(level: 1, outlined: false)[Whole Genome Sequencing Report]
- #outline(title: "Table of Contents", depth: 3)
- #pagebreak()
- == Identity
- #sys.inputs.id
- == Alignement
- #grid(
- columns: (1fr, 1fr),
- gutter: 3pt,
- [
- ==== Diagnostic sample
- #set text(size: 11pt)
- #reportBam(sys.inputs.base + "/diag/" + sys.inputs.id + "_diag_hs1_info.json")
- ],
- [
- ==== MRD sample
- #set text(size: 11pt)
- #reportBam(sys.inputs.base + "/mrd/" + sys.inputs.id + "_mrd_hs1_info.json")
- #set footnote(numbering: n => {
- " "
- })
- #footnote[Values computed by #link("https://github.com/wdecoster/cramino")[cramino] v0.14.5
- ]
- ],
- )
- #pagebreak()
- === Normalized read count by chromosome
- #[
- #set text(size: 10pt)
- #printReadCount(
- sys.inputs.base + "/diag/" + sys.inputs.id + "_diag_hs1_info.json",
- sys.inputs.base + "/mrd/" + sys.inputs.id + "_mrd_hs1_info.json",
- )
- ]
- === Coverage by chromosome
- ==== Proportion at given depth by chromosome
- #reportCoverage(sys.inputs.base + "/diag/report/data/scan/" + sys.inputs.id)
- #set footnote(numbering: n => {
- " "
- })
- #footnote[Values computed by Pandora development version]
- == Variants
- === Variants calling
- #pagebreak()
- ==== VCF filters
- #pad(
- top: 0.8cm,
- align(
- center,
- scale(
- x: 100%,
- y: 100%,
- reflow: true,
- variantsFlow(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_variants_stats.json"),
- ),
- ),
- )
- ==== BAM filters
- #pad(
- top: 0.8cm,
- align(
- center,
- scale(
- x: 100%,
- y: 100%,
- reflow: true,
- bamFilter(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_variants_stats.json"),
- ),
- ),
- )
- #pagebreak()
- === Somatic variants
- ==== Callers
- #v(0.5cm)
- #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_callers.svg")
- ==== Consequences (VEP)
- #v(0.5cm)
- #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_consequences.svg")
- ==== NCBI features
- #v(0.5cm)
- #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_ncbi.svg")
- #pagebreak()
- === Selected Variants
- ==== Pathogenics
- #variants(
- sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
- interpretation: "PATHO",
- )
- ==== Likely Pathogenics
- #variants(
- sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
- interpretation: "PROBPATHO",
- )
- ==== Variants of uncertain significance
- #variants(
- sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
- interpretation: "US",
- )
- #pagebreak()
- == Conclusion
- #v(0.5cm)
- #cmarker.render(
- read(sys.inputs.base + "/diag/report/" + sys.inputs.id + "_conclusion.md"),
- )
|