OSCR

Chromosome-scale genome assembly of the European common cuttlefish <i>Sepia officinalis</i>.

Code ↔ Paper

40 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 40 matches · 7 of them tie a paragraph to a whole file, not to given lines: weak matches, whose lines are not tinted
  1. [1] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 81–128 · score 0.95 · DToL_31, DToL_41, DToL_44, DToL_5, MPIBR_2, MPIBR_40
  2. [2] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/wga/assembly_comparison.R, lines 176–234 · score 0.94 · DToL_31, DToL_41, DToL_44, DToL_5, MPIBR_2, MPIBR_40
  3. [3] § Materials and methods › Nuclear genome annotation ↔ analysis/gene_families/cafe5/large_families_heatmap.R, lines 50–126 · score 0.92 · Pecten maximus, Nautilus pompilius, Octopus vulgaris, Octopus bimaculoides, Doryteuthis pealeii, Euprymna scolopes
  4. [4] § Materials and methods › Nuclear genome annotation ↔ annotation/task_braker_softmasked.sh, the whole file · a weak match · score 0.91 · addUTR, prot_seq, metazoa_odb10, BUSCO lineage, softmasked, BRAKER3
  5. [5] § Results › Gene modeling and annotation ↔ analysis/gene_families/cafe5/large_families_heatmap.R, lines 50–126 · score 0.89 · Pecten maximus, Nautilus pompilius, Octopus vulgaris, Octopus bimaculoides, Doryteuthis pealeii, Euprymna scolopes
  6. [6] § Materials and methods › Bulk RNA-seq analysis ↔ alignment/task_star_rna.sh, lines 26–89 · score 0.85 · RemoveNoncanonical, SortedByCoordinate, outFilterIntronMotifs, outSAMmultNmax, outSAMtype, STAR
  7. [7] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/rna_seq/bulkRNA_Deseq2_braker.Rmd, lines 200–273 · score 0.85 · log2 fold change, DESeq2, target tissue, Wald, apeglm, binary
  8. [8] § Materials and methods › Gene family expansion analysis ↔ analysis/gene_families/cafe5/gene_repeat_overlap.py, lines 271–346 · score 0.84 · avoid double, overlap fraction, CDS length, repeat intervals, gene families, intersect
  9. [9] § Materials and methods › Bulk RNA-seq analysis ↔ alignment/task_star_scell.sh, lines 23–91 · score 0.84 · RemoveNoncanonical, SortedByCoordinate, outFilterIntronMotifs, outSAMmultNmax, outSAMtype, STAR
  10. [10] § Materials and methods › Coverage analysis ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 1143–1258 · score 0.81 · breakpoint rate, intra scaffold, trans pair, DToL, contact rates, empirical
  11. [11] § Materials and methods › Nuclear genome assembly ↔ assembly/run_scaffolding.sh, lines 44–101 · score 0.80 · JBAT, telo, motif, tool, YAHS, mapping
  12. [12] § Materials and methods › Coverage analysis ↔ analysis/genome_alignments/breakpoints/count_hic_trans_background.sh, lines 1–49 · score 0.78 · intra scaffold pairs, trans pair, scaffold lengths, product, background, Mb2
  13. [13] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/rna_seq/parse_interproscan_go.R, lines 328–409 · score 0.76 · DESeq2, clusterProfiler, DE genes, GO terms, enricher, background
  14. [14] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/extract_hic_trans_pairs.sh, lines 1–68 · score 0.76 · breakpoint scaffold pairs, scaffold_40, scaffold length, pairtools, BP1, BP3
  15. [15] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/gene_families/gene_family_expression_analysis.Rmd, lines 414–472 · score 0.73 · clusterProfiler, GO enrichment, GO annotations, GO terms, enricher, FDR
  16. [16] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/count_hic_trans_background.sh, lines 1–49 · score 0.73 · intra chromosomal contacts, intra scaffold pairs, scaffold length, background, pairtools, HiC
  17. [17] § Materials and methods › Bulk RNA-seq analysis ↔ alignment/task_star_count_braker.sh, lines 44–93 · score 0.72 · FeatureCounts, countReadPairs, gene_id, STAR, exon
  18. [18] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/rna_seq/parse_interproscan_go.R, lines 159–223 · score 0.72 · reduce noise, unique GO terms, generic, union, collapsed, enrichment
  19. [19] § Materials and methods › Coverage analysis ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 131–240 · score 0.71 · mapping quality threshold, count_coverage, pysam, querying, depth, alignments
  20. [20] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/run_repeatmasker_junctions.sh, lines 67–144 · score 0.71 · breakpoint junctions, junction window, scaffold_40, scaffold ends, BP2, RepeatMasker
  21. [21] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 1143–1258 · score 0.69 · intra scaffold, control scaffolds, background pairs, DToL, Contact rates, empirical
  22. [22] § Materials and methods › Gene family expansion analysis ↔ analysis/gene_families/cafe5/run_cafe5.sh, lines 45–100 · score 0.66 · Gamma models, log likelihood, Gene family, lambda, lowest, CAFE5
  23. [23] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/rna_seq/parse_interproscan_go.R, lines 328–409 · score 0.64 · DESeq2, clusterProfiler, GO term, enricher, tissue, RNA
  24. [24] § Materials and methods › Bulk RNA-seq analysis ↔ analysis/gene_families/gene_family_expression_analysis.Rmd, lines 414–472 · score 0.64 · clusterProfiler, GO enrichment, GO term, enricher, gene family, VST
  25. [25] § Materials and methods › Coverage analysis ↔ analysis/genome_alignments/breakpoints/extract_hic_trans_pairs.sh, lines 1–68 · score 0.64 · uninvolved scaffold, trans pairs, UU, pairtools, breakpoints, Hi
  26. [26] § Materials and methods › Gene family expansion analysis ↔ analysis/gene_families/cafe5/large_families_heatmap.R, lines 1–48 · score 0.63 · OrthoFinder, species tree, ape, ultrametric, rooted, Orthogroups
  27. [27] § Results › Gene modeling and annotation ↔ analysis/rna_seq/emapper_interproscan_LUT.R, the whole file · a weak match · score 0.63 · officinalis proteome, eggNOG, InterProScan, mapper, transcriptomic, filters
  28. [28] § Materials and methods › Bulk RNA-seq analysis ↔ alignment/task_countreads.sh, the whole file · a weak match · score 0.61 · featureCounts, countReadPairs, Subread, GTF, exon, id
  29. [29] § Materials and methods › Short-read RNA library preparation and sequencing ↔ analysis/rna_seq/bulkRNA_Deseq2_braker.Rmd, lines 48–65 · score 0.60 · NextSeq2000, library prep, mRNA, RNA seq, stranded, Illumina
  30. [30] § Materials and methods › Gene family expansion analysis ↔ analysis/gene_families/orthofinder/orthofinder_to_cafe.R, the whole file · a weak match · score 0.60 · OrthoFinder, species tree, ultrametric, rooted, proteomes, Orthogroups
  31. [31] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 739–748 · score 0.59 · Hi Fi, DToL, kb, boundary, windows, breakpoint
  32. [32] § Materials and methods › Gene family expansion analysis ↔ analysis/gene_families/gene_family_expression_analysis.Rmd, lines 722–755 · score 0.57 · birth death model, extreme copy, Gene families, likelihood, orthogroups, species
  33. [33] § Materials and methods › Nuclear genome annotation ↔ analysis/rna_seq/emapper_interproscan_LUT.R, the whole file · a weak match · score 0.56 · eggNOG, InterProScan, lookup, mapper, GO, officinalis
  34. [34] § Materials and methods › Coverage analysis ↔ analysis/genome_alignments/breakpoints/breakpoint_analysis.ipynb, lines 1488–1527 · score 0.56 · LTR, RepeatMasker, SINE, parsed, classes, unknown
  35. [35] § Results › Genome size and heterozygosity ↔ assembly/run_busco.sh, the whole file · a weak match · score 0.55 · mollusca_odb12, metazoa_odb12, BUSCO, assembly, genome
  36. [36] § Materials and methods › Nuclear genome annotation ↔ annotation/task_repeatmasker_custom_softmask.sh, lines 47–77 · score 0.55 · RepeatModeler, xsmall, gff, softmasked, genome
  37. [37] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ analysis/genome_alignments/breakpoints/extract_spanning_reads.py, lines 1–37 · score 0.53 · Hi Fi, terminal, breakpoint, boundary, windows, alignments
  38. [38] § Materials and methods › Nuclear genome annotation ↔ analysis/genome_alignments/breakpoints/run_repeatmasker_junctions.sh, lines 146–198 · score 0.51 · repeat library, RepeatMasker, xsmall, gff, genome
  39. [39] § Results › Tissue-specific expression of expanded gene families ↔ analysis/rna_seq/bulkRNA_Deseq2_braker.Rmd, lines 83–117 · score 0.50 · Bulk RNA seq, DESeq2, posterior, vertical, retina, skin
  40. [40] § Results › Comparison with another chromosome-scale Sepia officinalis assembly ↔ assembly/run_busco.sh, the whole file · a weak match · score 0.50 · mollusca_odb12, metazoa_odb12, BUSCO, assemblies

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,598 lines · 67 KB · MIT · 6 matches

  1. # %% [markdown]
  2. # # Sepia officinalis Genome Assembly — Breakpoint Analysis
  3. # Jupyter notebook consolidating all visualisation and statistical analysis scripts for the MPIBR vs DToL assembly comparison.
  4. #
  5. # **Sections**
  6. # 1. HiFi spanning reads + trans HiC contacts at breakpoints
  7. # 2. Trans HiC contact rate statistics
  8. # 3. Repeat content at junction windows
  9. #
  10. # ---
  11. # %% [markdown]
  12. # ## Shared imports and plot settings
  13. # %%
  14. import os, sys, warnings
  15. import numpy as np
  16. import pandas as pd
  17. import matplotlib
  18. import matplotlib.pyplot as plt
  19. import matplotlib.patches as mpatches
  20. from matplotlib.patches import ConnectionPatch
  21. from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec
  22. from matplotlib.ticker import MaxNLocator
  23. from scipy import stats
  24. warnings.filterwarnings("ignore")
  25. matplotlib.rcParams.update({
  26. "font.family": "sans-serif",
  27. "font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"],
  28. "font.size": 7,
  29. "axes.linewidth": 0.5,
  30. "xtick.major.width": 0.5,
  31. "ytick.major.width": 0.5,
  32. "xtick.major.size": 2,
  33. "ytick.major.size": 2,
  34. "pdf.fonttype": 42,
  35. })
  36. try:
  37. import pysam
  38. except ImportError:
  39. sys.exit("ERROR: pysam is required — pip install pysam")
  40. print("Imports OK")
  41. # %% [markdown]
  42. # ---
  43. # ## 1. HiFi spanning reads + trans HiC contacts
  44. #
  45. # Visualises HiFi read alignments at scaffold junction ends alongside trans HiC contact dots and arcs.
  46. # Each breakpoint row shows: genome-wide contact density | terminal read track | HiFi coverage.
  47. # Control rows show terminal read track and coverage only.
  48. # %% [markdown]
  49. # ### 1.1 Parameters
  50. # %%
  51. # ── Input paths ─────────────────────────────────────────────────────────────
  52. BASE = "/gpfs/scic/data/projects/CuttlefishOmics/genome/assembly/soff250801"
  53. WD = "/gpfs/scic/data/projects/CuttlefishOmics/sandbox/resubmission"
  54. SPANNING_DIR = f"{WD}/spanning_reads"
  55. TRANS_HIC_DIR = f"{WD}/hic_trans_pairs"
  56. MPIBR_BAM = f"{BASE}/bams_hifi_scaffolds/soff250801_mpibr.hic_hifi_scaffolds.bam"
  57. DTOL_BAM = f"{BASE}/bams_hifi_scaffolds/soff250801_sanger.hic_hifi_scaffolds.bam"
  58. MPIBR_FAI = (f"{BASE}/yahs/soff250801_mpibr.hic/"
  59. "soff250801_mpibr.hic_scaffolds_final.fa.fai")
  60. DTOL_FAI = (f"{BASE}/yahs/soff250801_sanger.hic/"
  61. "soff250801_sanger.hic_scaffolds_final.fa.fai")
  62. # ── Run options ──────────────────────────────────────────────────────────────
  63. VIEW_WINDOW = 200_000 # bp shown in terminal read / coverage tracks
  64. MIN_MAPQ = 10
  65. OUTPUT_SPANNING = "spanning_reads.pdf"
  66. # %% [markdown]
  67. # ### 1.2 Constants, breakpoint definitions, colours
  68. # %%
  69. # ── Font sizes (edit here to scale all labels together) ─────────────
  70. FONT = {
  71. "title": 10, # scaffold name on density track
  72. "label": 9, # axis labels (ylabel, xlabel)
  73. "tick": 8, # tick labels
  74. "annot": 7, # small annotations ("200 kb view" etc.)
  75. "gap": 9, # gap-column text (pair counts, spanning reads)
  76. "legend": 9, # figure legend
  77. "boundary": 7, # "scaffold end / start" rotated label
  78. }
  79. MB = 1_000_000
  80. COV_BIN = 1000
  81. DENS_BIN = 500_000
  82. DOT_Y = -0.7
  83. MAX_ARCS = 200
  84. BREAKPOINTS_SPANNING = [
  85. ("mpibr", "scaffold_40", "scaffold_44", "BP1_MPIBR_40-44", "MPIBR_40", "MPIBR_44"),
  86. ("dtol", "scaffold_31", "scaffold_40", "BP2_DToL_31-40", "DToL_31", "DToL_40"),
  87. ("dtol", "scaffold_41", "scaffold_46", "BP3_DToL_41-46", "DToL_41", "DToL_46"),
  88. ("dtol", "scaffold_44", "scaffold_45", "BP4_DToL_44-45", "DToL_44", "DToL_45"),
  89. ]
  90. # Correct single scaffold from the other assembly for each breakpoint
  91. # (the chromosome that the broken assembly should have kept intact)
  92. CORRECT_SCAFFOLDS = [
  93. ("dtol", "scaffold_5", "BP1", "DToL_5"), # BP1: MPIBR split → DToL kept intact
  94. ("mpibr", "scaffold_2", "BP2", "MPIBR_2"), # BP2: DToL split → MPIBR kept intact
  95. ("mpibr", "scaffold_6", "BP3", "MPIBR_6"), # BP3: DToL split → MPIBR kept intact
  96. ("mpibr", "scaffold_7", "BP4", "MPIBR_7"), # BP4: DToL split → MPIBR kept intact
  97. ]
  98. COLOURS_SPANNING = {
  99. "mpibr": "#2166AC",
  100. "dtol": "#e65c00",
  101. "spanning": "#2CA02C",
  102. "background": "#CCCCCC",
  103. "hic_contact": "#9467BD",
  104. "boundary": "#444444",
  105. "coverage": {"mpibr": "#AEC6E8", "dtol": "#F4A582"},
  106. "density": {"mpibr": "#6BAED6", "dtol": "#FC8D59"},
  107. }
  108. # %% [markdown]
  109. # ### 1.3 Data-loading helpers
  110. # %%
  111. def load_fai(fai_path):
  112. lengths = {}
  113. with open(fai_path) as fh:
  114. for line in fh:
  115. parts = line.strip().split("\t")
  116. if len(parts) >= 2:
  117. lengths[parts[0]] = int(parts[1])
  118. return lengths
  119. def find_trans_tsv(trans_dir, label, assembly):
  120. return os.path.join(trans_dir, f"{label}_{assembly}_trans.tsv")
  121. def find_ctrl_trans_tsv(trans_dir, assembly, ctrl_scaf):
  122. ctrl_n = ctrl_scaf.replace("scaffold_", "")
  123. prefix = f"Ctrl_{assembly.upper()}_{ctrl_n}-"
  124. for fname in os.listdir(trans_dir):
  125. if fname.startswith(prefix) and fname.endswith("_trans.tsv"):
  126. return os.path.join(trans_dir, fname)
  127. return ""
  128. def load_trans_pairs(tsv_path):
  129. if not tsv_path or not os.path.isfile(tsv_path):
  130. return pd.DataFrame()
  131. df = pd.read_csv(tsv_path, sep="\t")
  132. return df if not df.empty else pd.DataFrame()
  133. def compute_density(positions, scaffold_len, bin_size=DENS_BIN):
  134. n_bins = max(1, scaffold_len // bin_size)
  135. counts, edges = np.histogram(positions, bins=n_bins, range=(0, scaffold_len))
  136. mids = (edges[:-1] + edges[1:]) / 2
  137. return mids, counts.astype(float)
  138. def normalised_contact_rate(n_pairs, len_A, len_B):
  139. la, lb = len_A / MB, len_B / MB
  140. return 0.0 if la == 0 or lb == 0 else n_pairs / (la * lb)
  141. def get_reads_in_window(bam_path, scaffold, win_start, win_end,
  142. spanning_names, min_mapq=10):
  143. reads = []
  144. try:
  145. bam = pysam.AlignmentFile(bam_path, "rb")
  146. for read in bam.fetch(scaffold, win_start, win_end):
  147. if read.is_unmapped or read.is_secondary:
  148. continue
  149. if read.mapping_quality < min_mapq:
  150. continue
  151. reads.append({
  152. "read_name": read.query_name,
  153. "start": max(read.reference_start, win_start),
  154. "end": min(read.reference_end, win_end),
  155. "strand": "-" if read.is_reverse else "+",
  156. "is_spanning": read.query_name in spanning_names,
  157. })
  158. bam.close()
  159. except (ValueError, KeyError) as e:
  160. print(f" WARNING: fetch failed {scaffold}:{win_start}-{win_end}: {e}")
  161. return reads
  162. def get_coverage(bam_path, scaffold, win_start, win_end,
  163. min_mapq=10, bin_size=COV_BIN):
  164. try:
  165. bam = pysam.AlignmentFile(bam_path, "rb")
  166. cov_arrays = bam.count_coverage(
  167. scaffold, win_start, win_end,
  168. quality_threshold=0,
  169. read_callback=lambda r: (
  170. not r.is_unmapped and not r.is_secondary and
  171. r.mapping_quality >= min_mapq
  172. )
  173. )
  174. bam.close()
  175. except (ValueError, KeyError) as e:
  176. print(f" WARNING: coverage failed {scaffold}:{win_start}-{win_end}: {e}")
  177. n = (win_end - win_start) // bin_size + 1
  178. return np.zeros(n), np.zeros(n)
  179. depth = sum(np.array(a) for a in cov_arrays)
  180. n_bins = len(depth) // bin_size
  181. if n_bins == 0:
  182. return np.array([len(depth) / 2]), np.array([depth.mean()])
  183. binned = depth[:n_bins * bin_size].reshape(n_bins, bin_size).mean(axis=1)
  184. mids = np.arange(n_bins) * bin_size + bin_size / 2
  185. return mids, binned
  186. def stack_reads(reads):
  187. reads_sorted = sorted(reads, key=lambda r: (not r["is_spanning"], r["start"]))
  188. track_ends = []
  189. for read in reads_sorted:
  190. placed = False
  191. for i, end in enumerate(track_ends):
  192. if read["start"] > end + 500:
  193. track_ends[i] = read["end"]
  194. read["track"] = i
  195. placed = True
  196. break
  197. if not placed:
  198. read["track"] = len(track_ends)
  199. track_ends.append(read["end"])
  200. return reads_sorted
  201. # %% [markdown]
  202. # ### 1.4 Drawing helpers
  203. # %%
  204. def draw_density_track(ax, mids, counts, scaffold_len, assembly,
  205. global_ymax, title, view_win, boundary_x,
  206. show_boundary_side, view_win_other=None):
  207. colour = COLOURS_SPANNING["density"][assembly]
  208. line_c = COLOURS_SPANNING[assembly]
  209. ax.fill_between(mids / MB, 0, counts,
  210. color=colour, alpha=0.7, linewidth=0, step="mid", zorder=1)
  211. ax.step(mids / MB, counts, color=line_c, linewidth=0.5, where="mid", zorder=2)
  212. # Other-end window (pale gold, drawn first)
  213. if view_win_other is not None:
  214. vw2_start, vw2_end = view_win_other
  215. ax.axvspan(vw2_start / MB, vw2_end / MB, color="gold", alpha=0.4, zorder=3, linewidth=0)
  216. inner_edge2 = vw2_end / MB if vw2_start == 0 else vw2_start / MB
  217. ax.axvline(inner_edge2, color="goldenrod", linewidth=0.8, linestyle="-", zorder=6)
  218. mid_vw2 = (vw2_start + vw2_end) / 2 / MB
  219. # ax.annotate("other\nend", xy=(mid_vw2, global_ymax * 1.05),
  220. # xycoords="data", ha="center", va="bottom",
  221. # fontsize=FONT["annot"], color="goldenrod", zorder=7, annotation_clip=False)
  222. # Junction-facing window (bright gold)
  223. vw_start, vw_end = view_win
  224. ax.axvspan(vw_start / MB, vw_end / MB, color="gold", alpha=0.9, zorder=3, linewidth=0)
  225. inner_edge = vw_start / MB if boundary_x > 0 else vw_end / MB
  226. ax.axvline(inner_edge, color="goldenrod", linewidth=1.2, linestyle="-", zorder=6)
  227. mid_vw = (vw_start + vw_end) / 2 / MB
  228. # ax.annotate("junction\nend", xy=(mid_vw, global_ymax * 1.05),
  229. # xycoords="data", ha="center", va="bottom",
  230. # fontsize=FONT["annot"], color="goldenrod", zorder=7, annotation_clip=False)
  231. ax.annotate("200 kb\nview", xy=(mid_vw, global_ymax * 1.05),
  232. xycoords="data", ha="center", va="bottom",
  233. fontsize=FONT["annot"], color="goldenrod", zorder=7, annotation_clip=False)
  234. ax.axvline(boundary_x / MB, color=COLOURS_SPANNING["boundary"],
  235. linewidth=1.0, linestyle="--", zorder=5)
  236. ax.set_xlim(0, scaffold_len / MB)
  237. ax.set_ylim(0, global_ymax * 1.08)
  238. ax.yaxis.set_major_locator(MaxNLocator(nbins=3, min_n_ticks=2, integer=True))
  239. ax.tick_params(axis="both", labelsize=FONT["tick"], pad=1)
  240. ax.tick_params(axis="x", labelbottom=True)
  241. ax.spines["top"].set_visible(False)
  242. ax.spines["right"].set_visible(False)
  243. ax.set_title(title, fontsize=FONT["title"], fontweight="bold",
  244. color=COLOURS_SPANNING[assembly], pad=3)
  245. def draw_read_track(ax, reads, win_start, win_end, trans_df, pos_col,
  246. read_height=0.6):
  247. n_tracks = max((r["track"] for r in reads), default=0) + 1
  248. for read in reads:
  249. x0 = read["start"] - win_start
  250. x1 = read["end"] - win_start
  251. colour = COLOURS_SPANNING["spanning"] if read["is_spanning"] else COLOURS_SPANNING["background"]
  252. alpha = 0.9 if read["is_spanning"] else 0.4
  253. zorder = 3 if read["is_spanning"] else 1
  254. ax.barh(read["track"], x1 - x0, left=x0, height=read_height,
  255. color=colour, alpha=alpha, zorder=zorder, linewidth=0)
  256. if not trans_df.empty:
  257. in_win = trans_df[
  258. (trans_df[pos_col] >= win_start) &
  259. (trans_df[pos_col] < win_end)].copy()
  260. if not in_win.empty:
  261. ax.scatter(in_win[pos_col] - win_start, [DOT_Y] * len(in_win),
  262. s=4, color=COLOURS_SPANNING["hic_contact"],
  263. alpha=0.6, zorder=4, linewidths=0, clip_on=False)
  264. ax.set_xlim(0, win_end - win_start)
  265. ax.set_ylim(DOT_Y - 0.5, n_tracks + 1)
  266. ax.set_yticks([])
  267. ax.set_xticks([])
  268. for spine in ["top", "right", "left", "bottom"]:
  269. ax.spines[spine].set_visible(False)
  270. return n_tracks
  271. def draw_hic_arcs(fig, ax_A, ax_B, trans_df, win_A, win_B):
  272. if trans_df.empty:
  273. return
  274. win_A_start, win_A_end = win_A
  275. win_B_start, win_B_end = win_B
  276. in_win = trans_df[
  277. (trans_df["pos1"] >= win_A_start) & (trans_df["pos1"] < win_A_end) &
  278. (trans_df["pos2"] >= win_B_start) & (trans_df["pos2"] < win_B_end)]
  279. if in_win.empty:
  280. return
  281. df_plot = in_win.sample(min(MAX_ARCS, len(in_win)), random_state=42)
  282. for _, row in df_plot.iterrows():
  283. con = ConnectionPatch(
  284. xyA=(row["pos1"] - win_A_start, DOT_Y), coordsA="data", axesA=ax_A,
  285. xyB=(row["pos2"] - win_B_start, DOT_Y), coordsB="data", axesB=ax_B,
  286. color=COLOURS_SPANNING["hic_contact"],
  287. linewidth=0.35, alpha=0.25, clip_on=False, zorder=5)
  288. fig.add_artist(con)
  289. def draw_coverage_track(ax, mids, depth, win_start, win_end, assembly, global_ymax):
  290. colour = COLOURS_SPANNING["coverage"][assembly]
  291. ax.fill_between(mids, 0, depth, color=colour, alpha=0.7, linewidth=0)
  292. ax.plot(mids, depth, color=COLOURS_SPANNING[assembly], linewidth=0.5)
  293. ax.set_xlim(0, win_end - win_start)
  294. ax.set_ylim(0, global_ymax * 1.08)
  295. ax.yaxis.set_major_locator(MaxNLocator(nbins=3, min_n_ticks=2))
  296. ax.tick_params(axis="y", labelsize=FONT["tick"], pad=1)
  297. ax.spines["top"].set_visible(False)
  298. ax.spines["right"].set_visible(False)
  299. tick_pos = np.linspace(0, win_end - win_start, 5)
  300. tick_lbl = [f"{(win_start + p) / MB:.2f}" for p in tick_pos]
  301. ax.set_xticks(tick_pos)
  302. ax.set_xticklabels(tick_lbl, fontsize=FONT["tick"])
  303. ax.set_xlabel("Position (Mb)", fontsize=FONT["label"], labelpad=2)
  304. def draw_boundary_line(axes, x):
  305. for ax in axes:
  306. ax.axvline(x, color=COLOURS_SPANNING["boundary"],
  307. linewidth=1.0, linestyle="--", zorder=10)
  308. def add_boundary_label(ax, x, side):
  309. text = "scaffold end" if side == "right" else "scaffold start"
  310. ax.text(x, ax.get_ylim()[1] * 0.92, text,
  311. ha=side, va="top", fontsize=FONT["boundary"],
  312. color=COLOURS_SPANNING["boundary"], rotation=90)
  313. # %% [markdown]
  314. # ### 1.5 Build row metadata and pre-compute coverage
  315. # %%
  316. lengths = {
  317. "mpibr": load_fai(MPIBR_FAI),
  318. "dtol": load_fai(DTOL_FAI),
  319. }
  320. bams = {"mpibr": MPIBR_BAM, "dtol": DTOL_BAM}
  321. ASM_DISPLAY = {"mpibr": "MPIBR", "dtol": "DToL"}
  322. # Correct-scaffold entries — one per breakpoint, matched in order
  323. controls = [
  324. (asm, scaf, None, f"Correct_{bp_lbl}", lbl, None, True)
  325. for asm, scaf, bp_lbl, lbl in CORRECT_SCAFFOLDS
  326. ]
  327. all_rows = BREAKPOINTS_SPANNING + controls
  328. n_rows = len(all_rows)
  329. print("Pre-computing HiFi coverage...")
  330. cov_store = {}
  331. def get_cov(assembly, scaffold, ws, we, bin_size=COV_BIN):
  332. key = (assembly, scaffold, ws, we)
  333. if key not in cov_store:
  334. mids, depth = get_coverage(bams[assembly], scaffold, ws, we,
  335. min_mapq=MIN_MAPQ, bin_size=bin_size)
  336. cov_store[key] = (mids, depth)
  337. print(f" {assembly} {scaffold}:{ws}-{we} mean={depth.mean():.1f}x")
  338. return cov_store[key]
  339. # ── Trans HiC diagnostic ───────────────────────────────────────────────
  340. _hic_dir_ok = os.path.isdir(TRANS_HIC_DIR)
  341. if _hic_dir_ok:
  342. _hic_files = [f for f in os.listdir(TRANS_HIC_DIR) if f.endswith('_trans.tsv')]
  343. print(f"Trans HiC dir: {TRANS_HIC_DIR} ({len(_hic_files)} TSV files)")
  344. else:
  345. print(f"WARNING: TRANS_HIC_DIR not found: {TRANS_HIC_DIR}")
  346. print(" → Run extract_hic_trans_pairs.sh first, then re-run this cell.")
  347. row_meta = []
  348. for entry in all_rows:
  349. is_ctrl = len(entry) == 7
  350. assembly = entry[0]
  351. scaf_A = entry[1]
  352. scaf_B = entry[2]
  353. label = entry[3]
  354. lbl_A = entry[4]
  355. lbl_B = entry[5]
  356. lens = lengths[assembly]
  357. len_A = lens.get(scaf_A, 0)
  358. end_A, end_B = "end", "start"
  359. spanning_names = set()
  360. if not is_ctrl:
  361. tsv = os.path.join(SPANNING_DIR, f"{label}_{assembly}.tsv")
  362. if os.path.isfile(tsv):
  363. sdf = pd.read_csv(tsv, sep="\t")
  364. spanning_names = set(sdf["read_name"]) if not sdf.empty else set()
  365. if not sdf.empty:
  366. a_rows = sdf[sdf["primary_scaffold"] == scaf_A]
  367. b_rows = sdf[sdf["primary_scaffold"] == scaf_B]
  368. a_supp = sdf[sdf["supp_scaffold"] == scaf_A]
  369. b_supp = sdf[sdf["supp_scaffold"] == scaf_B]
  370. if not a_rows.empty: end_A = a_rows["primary_end_label"].mode()[0]
  371. elif not a_supp.empty: end_A = a_supp["supp_end_label"].mode()[0]
  372. if not b_rows.empty: end_B = b_rows["primary_end_label"].mode()[0]
  373. elif not b_supp.empty: end_B = b_supp["supp_end_label"].mode()[0]
  374. win_A = (max(0, len_A - VIEW_WINDOW), len_A) if end_A == "end" else (0, min(VIEW_WINDOW, len_A))
  375. win_A_other = (0, min(VIEW_WINDOW, len_A)) if end_A == "end" else (max(0, len_A - VIEW_WINDOW), len_A)
  376. win_B = None
  377. win_B_other = None
  378. len_B = 0
  379. if not is_ctrl:
  380. len_B = lens.get(scaf_B, 0)
  381. win_B = (0, min(VIEW_WINDOW, len_B)) if end_B == "start" else (max(0, len_B - VIEW_WINDOW), len_B)
  382. win_B_other = (max(0, len_B - VIEW_WINDOW), len_B) if end_B == "start" else (0, min(VIEW_WINDOW, len_B))
  383. get_cov(assembly, scaf_A, *win_A)
  384. get_cov(assembly, scaf_A, *win_A_other) # both ends for all rows
  385. if win_B:
  386. get_cov(assembly, scaf_B, *win_B)
  387. get_cov(assembly, scaf_B, *win_B_other)
  388. trans_df = pd.DataFrame()
  389. len_partner = 0
  390. if is_ctrl:
  391. tsv_path = find_ctrl_trans_tsv(TRANS_HIC_DIR, assembly, scaf_A)
  392. if tsv_path:
  393. fname = os.path.basename(tsv_path)
  394. partner_n = fname.split("-")[1].split("_")[0]
  395. len_partner = lens.get(f"scaffold_{partner_n}", 0)
  396. trans_df = load_trans_pairs(tsv_path)
  397. if trans_df.empty:
  398. _expected = os.path.basename(tsv_path) if tsv_path else "(no TSV path)"
  399. print(f" WARNING: no trans HiC pairs loaded for {label} "
  400. f"-- file not found or empty: {_expected}")
  401. n_trans = len(trans_df)
  402. rate = normalised_contact_rate(n_trans, len_A, len_partner) \
  403. if len_partner > 0 else 0.0
  404. print(f" {label}: {n_trans} trans HiC pairs ({rate:.2f} pairs/Mb²)")
  405. else:
  406. tsv_path = find_trans_tsv(TRANS_HIC_DIR, label, assembly)
  407. len_partner = len_B
  408. trans_df = load_trans_pairs(tsv_path)
  409. if trans_df.empty:
  410. _expected = os.path.basename(tsv_path) if tsv_path else "(no TSV path)"
  411. print(f" WARNING: no trans HiC pairs loaded for {label} "
  412. f"-- file not found or empty: {_expected}")
  413. n_trans = len(trans_df)
  414. rate = normalised_contact_rate(n_trans, len_A, len_partner) \
  415. if len_partner > 0 else 0.0
  416. print(f" {label}: {n_trans} trans HiC pairs ({rate:.2f} pairs/Mb²)")
  417. row_meta.append({
  418. "assembly": assembly, "scaf_A": scaf_A, "scaf_B": scaf_B,
  419. "label": label, "lbl_A": lbl_A, "lbl_B": lbl_B,
  420. "is_ctrl": is_ctrl, "end_A": end_A, "end_B": end_B,
  421. "len_A": len_A, "len_B": len_B,
  422. "win_A": win_A, "win_A_other": win_A_other,
  423. "win_B": win_B, "win_B_other": win_B_other,
  424. "spanning_names": spanning_names, "trans_df": trans_df,
  425. "contact_rate": rate,
  426. })
  427. def global_cov_ymax(assembly):
  428. vals = [cov_store[k][1] for k in cov_store if k[0] == assembly]
  429. return float(np.quantile(np.concatenate(vals), 0.995)) if vals else 1.0
  430. cov_ymax = {"mpibr": global_cov_ymax("mpibr"), "dtol": global_cov_ymax("dtol")}
  431. dens_ymax = {"mpibr": 1.0, "dtol": 1.0}
  432. if os.path.isdir(TRANS_HIC_DIR):
  433. for asm in ["mpibr", "dtol"]:
  434. all_counts = []
  435. for meta in row_meta:
  436. if meta["assembly"] != asm or meta["trans_df"].empty:
  437. continue
  438. df = meta["trans_df"]
  439. if meta["len_A"] > 0:
  440. _, c = compute_density(df["pos1"].values, meta["len_A"], DENS_BIN)
  441. all_counts.extend(c.tolist())
  442. if meta["len_B"] > 0:
  443. _, c = compute_density(df["pos2"].values, meta["len_B"], DENS_BIN)
  444. all_counts.extend(c.tolist())
  445. if all_counts:
  446. dens_ymax[asm] = float(np.quantile(all_counts, 0.995))
  447. print("\nMetadata ready.")
  448. # %% [markdown]
  449. # ### 1.6 Generate plot
  450. # %%
  451. # ── Breakpoints figure ───────────────────────────────────────────────
  452. bp_meta = [m for m in row_meta if not m["is_ctrl"]]
  453. ctrl_meta = [m for m in row_meta if m["is_ctrl"]]
  454. def _draw_rows(fig, outer_gs, meta_list, bams, cov_store,
  455. cov_ymax, dens_ymax):
  456. """Draw one figure worth of rows; meta_list is bp_meta or ctrl_meta."""
  457. for row, meta in enumerate(meta_list):
  458. assembly = meta["assembly"]
  459. scaf_A = meta["scaf_A"]
  460. scaf_B = meta["scaf_B"]
  461. label = meta["label"]
  462. lbl_A = meta["lbl_A"]
  463. lbl_B = meta["lbl_B"]
  464. is_ctrl = meta["is_ctrl"]
  465. end_A = meta["end_A"]
  466. end_B = meta["end_B"]
  467. len_A = meta["len_A"]
  468. len_B = meta["len_B"]
  469. win_A_start, win_A_end = meta["win_A"]
  470. spanning_names = meta["spanning_names"]
  471. trans_df = meta["trans_df"]
  472. contact_rate = meta["contact_rate"]
  473. bam = bams[assembly]
  474. ymax_cov = cov_ymax[assembly]
  475. ymax_dens = dens_ymax[assembly]
  476. if is_ctrl:
  477. inner_A = GridSpecFromSubplotSpec(
  478. 5, 1, subplot_spec=outer_gs[row, 0],
  479. height_ratios=[2, 2, 1, 2, 1], hspace=0.55)
  480. else:
  481. inner_A = GridSpecFromSubplotSpec(
  482. 5, 1, subplot_spec=outer_gs[row, 0],
  483. height_ratios=[2, 2, 1, 2, 1], hspace=0.55)
  484. inner_B = GridSpecFromSubplotSpec(
  485. 5, 1, subplot_spec=outer_gs[row, 2],
  486. height_ratios=[2, 2, 1, 2, 1], hspace=0.55)
  487. if is_ctrl:
  488. win_A_o_start, win_A_o_end = meta["win_A_other"]
  489. ax_dens_A = fig.add_subplot(inner_A[0])
  490. ax_reads_end = fig.add_subplot(inner_A[1])
  491. ax_cov_end = fig.add_subplot(inner_A[2])
  492. ax_reads_str = fig.add_subplot(inner_A[3])
  493. ax_cov_str = fig.add_subplot(inner_A[4])
  494. # Density track (full scaffold, same as breakpoints)
  495. if not trans_df.empty and len_A > 0:
  496. mids_d, counts_d = compute_density(trans_df["pos1"].values, len_A, DENS_BIN)
  497. else:
  498. mids_d, counts_d = np.array([len_A / 2]), np.array([0.0])
  499. bnd_genome_A = len_A if end_A == "end" else 0
  500. draw_density_track(
  501. ax_dens_A, mids_d, counts_d, len_A, assembly, ymax_dens,
  502. title=lbl_A, view_win=meta["win_A"], boundary_x=bnd_genome_A,
  503. show_boundary_side="right" if end_A == "end" else "left",
  504. view_win_other=meta["win_A_other"])
  505. ax_dens_A.set_ylabel("Trans HiC\ncontacts", fontsize=FONT["label"], labelpad=3)
  506. # End terminal (scaffold end — win_A)
  507. reads_end = stack_reads(get_reads_in_window(
  508. bam, scaf_A, win_A_start, win_A_end, spanning_names, MIN_MAPQ))
  509. mids_end, dep_end = cov_store[(assembly, scaf_A, win_A_start, win_A_end)]
  510. draw_read_track(ax_reads_end, reads_end, win_A_start, win_A_end,
  511. trans_df, "pos1")
  512. draw_coverage_track(ax_cov_end, mids_end, dep_end,
  513. win_A_start, win_A_end, assembly, ymax_cov)
  514. ax_cov_end.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  515. ax_cov_end.set_xlabel("")
  516. ax_cov_end.tick_params(axis="x", labelbottom=False)
  517. draw_boundary_line([ax_reads_end, ax_cov_end], win_A_end - win_A_start)
  518. # Start terminal (scaffold start — win_A_other)
  519. reads_str = stack_reads(get_reads_in_window(
  520. bam, scaf_A, win_A_o_start, win_A_o_end, spanning_names, MIN_MAPQ))
  521. mids_str, dep_str = cov_store[(assembly, scaf_A, win_A_o_start, win_A_o_end)]
  522. draw_read_track(ax_reads_str, reads_str, win_A_o_start, win_A_o_end,
  523. trans_df, "pos1")
  524. draw_coverage_track(ax_cov_str, mids_str, dep_str,
  525. win_A_o_start, win_A_o_end, assembly, ymax_cov)
  526. ax_cov_str.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  527. draw_boundary_line([ax_reads_str, ax_cov_str], 0)
  528. # Turn off right panel
  529. ax_right = fig.add_subplot(outer_gs[row, 2])
  530. ax_right.set_axis_off()
  531. else:
  532. reads_A_j = stack_reads(get_reads_in_window(
  533. bam, scaf_A, win_A_start, win_A_end, spanning_names, MIN_MAPQ))
  534. mids_A_j, depth_A_j = cov_store[(assembly, scaf_A, win_A_start, win_A_end)]
  535. win_A_o_start, win_A_o_end = meta["win_A_other"]
  536. reads_A_o = stack_reads(get_reads_in_window(
  537. bam, scaf_A, win_A_o_start, win_A_o_end, spanning_names, MIN_MAPQ))
  538. mids_A_o, depth_A_o = cov_store[(assembly, scaf_A, win_A_o_start, win_A_o_end)]
  539. ax_dens_A = fig.add_subplot(inner_A[0])
  540. ax_reads_A_j = fig.add_subplot(inner_A[1])
  541. ax_cov_A_j = fig.add_subplot(inner_A[2])
  542. ax_reads_A_o = fig.add_subplot(inner_A[3])
  543. ax_cov_A_o = fig.add_subplot(inner_A[4])
  544. if not trans_df.empty and len_A > 0:
  545. mids_d, counts_d = compute_density(trans_df["pos1"].values, len_A, DENS_BIN)
  546. else:
  547. mids_d, counts_d = np.array([len_A / 2]), np.array([0.0])
  548. bnd_genome_A = len_A if end_A == "end" else 0
  549. draw_density_track(
  550. ax_dens_A, mids_d, counts_d, len_A, assembly, ymax_dens,
  551. title=lbl_A, view_win=meta["win_A"], boundary_x=bnd_genome_A,
  552. show_boundary_side="right" if end_A == "end" else "left",
  553. view_win_other=meta["win_A_other"])
  554. ax_dens_A.set_ylabel("Trans HiC\ncontacts", fontsize=FONT["label"], labelpad=3)
  555. draw_read_track(ax_reads_A_j, reads_A_j, win_A_start, win_A_end, trans_df, "pos1")
  556. draw_coverage_track(ax_cov_A_j, mids_A_j, depth_A_j,
  557. win_A_start, win_A_end, assembly, ymax_cov)
  558. ax_cov_A_j.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  559. ax_cov_A_j.set_xlabel("")
  560. ax_cov_A_j.tick_params(axis="x", labelbottom=False)
  561. bnd_x_A_j = (win_A_end - win_A_start) if end_A == "end" else 0
  562. draw_boundary_line([ax_reads_A_j, ax_cov_A_j], bnd_x_A_j)
  563. end_A_other = "start" if end_A == "end" else "end"
  564. draw_read_track(ax_reads_A_o, reads_A_o, win_A_o_start, win_A_o_end, trans_df, "pos1")
  565. draw_coverage_track(ax_cov_A_o, mids_A_o, depth_A_o,
  566. win_A_o_start, win_A_o_end, assembly, ymax_cov)
  567. ax_cov_A_o.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  568. bnd_x_A_o = 0 if end_A_other == "start" else (win_A_o_end - win_A_o_start)
  569. draw_boundary_line([ax_reads_A_o, ax_cov_A_o], bnd_x_A_o)
  570. ax_gap = fig.add_subplot(outer_gs[row, 1])
  571. ax_gap.set_axis_off()
  572. if is_ctrl:
  573. #bp_ref = meta["label"].replace("Correct_", "")
  574. # ax_gap.text(0.5, 0.75,
  575. # f"{bp_ref}\ncorrect\nassembly",
  576. # ha="center", va="center", fontsize=FONT["gap"],
  577. # color="grey", style="italic",
  578. # transform=ax_gap.transAxes)
  579. ax_gap.text(0.5, 0.45, f"{len(trans_df):,}\ntrans HiC\npairs",
  580. ha="center", va="center", fontsize=FONT["gap"],
  581. color=COLOURS_SPANNING["hic_contact"], fontweight="bold",
  582. transform=ax_gap.transAxes)
  583. ax_gap.text(0.5, 0.20, f"{contact_rate:.2f}\npairs/Mb\u00b2",
  584. ha="center", va="center", fontsize=FONT["gap"],
  585. color=COLOURS_SPANNING["hic_contact"],
  586. transform=ax_gap.transAxes)
  587. else:
  588. win_B_start, win_B_end = meta["win_B"]
  589. win_B_o_start, win_B_o_end = meta["win_B_other"]
  590. reads_B_j = stack_reads(get_reads_in_window(
  591. bam, scaf_B, win_B_start, win_B_end, spanning_names, MIN_MAPQ))
  592. mids_B_j, depth_B_j = cov_store[(assembly, scaf_B, win_B_start, win_B_end)]
  593. reads_B_o = stack_reads(get_reads_in_window(
  594. bam, scaf_B, win_B_o_start, win_B_o_end, spanning_names, MIN_MAPQ))
  595. mids_B_o, depth_B_o = cov_store[(assembly, scaf_B, win_B_o_start, win_B_o_end)]
  596. ax_dens_B = fig.add_subplot(inner_B[0])
  597. ax_reads_B_j = fig.add_subplot(inner_B[1])
  598. ax_cov_B_j = fig.add_subplot(inner_B[2])
  599. ax_reads_B_o = fig.add_subplot(inner_B[3])
  600. ax_cov_B_o = fig.add_subplot(inner_B[4])
  601. if not trans_df.empty and len_B > 0:
  602. mids_dB, counts_dB = compute_density(trans_df["pos2"].values, len_B, DENS_BIN)
  603. else:
  604. mids_dB, counts_dB = np.array([len_B / 2]), np.array([0.0])
  605. bnd_genome_B = 0 if end_B == "start" else len_B
  606. draw_density_track(
  607. ax_dens_B, mids_dB, counts_dB, len_B, assembly, ymax_dens,
  608. title=lbl_B, view_win=meta["win_B"], boundary_x=bnd_genome_B,
  609. show_boundary_side="left" if end_B == "start" else "right",
  610. view_win_other=meta["win_B_other"])
  611. ax_dens_B.set_yticklabels([])
  612. draw_read_track(ax_reads_B_j, reads_B_j, win_B_start, win_B_end, trans_df, "pos2")
  613. draw_coverage_track(ax_cov_B_j, mids_B_j, depth_B_j,
  614. win_B_start, win_B_end, assembly, ymax_cov)
  615. ax_cov_B_j.set_yticklabels([])
  616. ax_cov_B_j.set_xlabel("")
  617. ax_cov_B_j.tick_params(axis="x", labelbottom=False)
  618. bnd_x_B_j = 0 if end_B == "start" else (win_B_end - win_B_start)
  619. draw_boundary_line([ax_reads_B_j, ax_cov_B_j], bnd_x_B_j)
  620. end_B_other = "end" if end_B == "start" else "start"
  621. draw_read_track(ax_reads_B_o, reads_B_o, win_B_o_start, win_B_o_end, trans_df, "pos2")
  622. draw_coverage_track(ax_cov_B_o, mids_B_o, depth_B_o,
  623. win_B_o_start, win_B_o_end, assembly, ymax_cov)
  624. ax_cov_B_o.set_yticklabels([])
  625. bnd_x_B_o = (win_B_o_end - win_B_o_start) if end_B_other == "end" else 0
  626. draw_boundary_line([ax_reads_B_o, ax_cov_B_o], bnd_x_B_o)
  627. if not trans_df.empty:
  628. draw_hic_arcs(fig, ax_reads_A_j, ax_reads_B_j, trans_df,
  629. meta["win_A"], meta["win_B"])
  630. n_span = len(spanning_names)
  631. ax_gap.text(0.5, 0.88, f"{len(trans_df):,}\ntrans HiC\npairs",
  632. ha="center", va="center", fontsize=FONT["gap"],
  633. color=COLOURS_SPANNING["hic_contact"], fontweight="bold",
  634. transform=ax_gap.transAxes)
  635. ax_gap.text(0.5, 0.70, f"{contact_rate:.2f}\npairs/Mb\u00b2",
  636. ha="center", va="center", fontsize=FONT["gap"],
  637. color=COLOURS_SPANNING["hic_contact"], transform=ax_gap.transAxes)
  638. ax_gap.text(0.5, 0.50, f"{n_span}\nspanning\nread{'s' if n_span != 1 else ''}",
  639. ha="center", va="center", fontsize=FONT["gap"],
  640. color=COLOURS_SPANNING["spanning"], fontweight="bold",
  641. transform=ax_gap.transAxes)
  642. handles_leg = [
  643. mpatches.Patch(color=COLOURS_SPANNING["spanning"], alpha=0.9, label="HiFi spanning read"),
  644. mpatches.Patch(color=COLOURS_SPANNING["background"], alpha=0.5, label="HiFi non-spanning read"),
  645. mpatches.Patch(color=COLOURS_SPANNING["hic_contact"], alpha=0.7, label="Trans HiC contact"),
  646. mpatches.Patch(color=COLOURS_SPANNING["coverage"]["mpibr"], alpha=0.7, label="HiFi depth (MPIBR)"),
  647. mpatches.Patch(color=COLOURS_SPANNING["coverage"]["dtol"], alpha=0.7, label="HiFi depth (DToL)"),
  648. plt.Line2D([0], [0], color=COLOURS_SPANNING["boundary"], linewidth=0.9,
  649. linestyle="--", label="Scaffold boundary"),
  650. mpatches.Patch(color="yellow", alpha=0.4, label="200 kb view window"),
  651. ]
  652. # ── Breakpoints figure ────────────────────────────────────────────────
  653. n_bp = len(bp_meta)
  654. fig_bp = plt.figure(figsize=(18.0 / 2.54, 14)) #old height:(15.0 * n_bp + 2.5) / 2.54
  655. outer_gs_bp = GridSpec(
  656. n_bp, 3, figure=fig_bp,
  657. width_ratios=[1, 0.20, 1],
  658. height_ratios=[15.0] * n_bp,
  659. hspace=0.25, wspace=0.08,
  660. left=0.11, right=0.97, top=0.97, bottom=0.07,
  661. )
  662. _draw_rows(fig_bp, outer_gs_bp, bp_meta, bams, cov_store, cov_ymax, dens_ymax)
  663. fig_bp.legend(handles=handles_leg, loc="lower center",
  664. ncol=4, fontsize=FONT["legend"], frameon=False,
  665. bbox_to_anchor=(0.5, 0.0))
  666. fig_bp.savefig(OUTPUT_SPANNING, dpi=300, bbox_inches="tight")
  667. plt.show()
  668. print(f"Saved: {OUTPUT_SPANNING}")
  669. # ── Controls figure ───────────────────────────────────────────────────
  670. output_ctrl = OUTPUT_SPANNING.replace(".pdf", "_controls.pdf")
  671. n_ctrl = len(ctrl_meta)
  672. fig_ctrl = plt.figure(figsize=(4, 13))
  673. outer_gs_ctrl = GridSpec(
  674. n_ctrl, 3, figure=fig_ctrl,
  675. width_ratios=[1, 0.20, 0.20],
  676. height_ratios=[15.0] * n_ctrl,
  677. hspace=0.25, wspace=0.08,
  678. left=0.11, right=0.97, top=0.97, bottom=0.04,
  679. )
  680. _draw_rows(fig_ctrl, outer_gs_ctrl, ctrl_meta, bams, cov_store, cov_ymax, dens_ymax)
  681. fig_ctrl.savefig(output_ctrl, dpi=300, bbox_inches="tight")
  682. plt.show()
  683. print(f"Saved: {output_ctrl}")
  684. # %%
  685. # ── Breakpoints figure ───────────────────────────────────────────────
  686. bp_meta = [m for m in row_meta if not m["is_ctrl"]]
  687. ctrl_meta = [m for m in row_meta if m["is_ctrl"]]
  688. def _draw_rows(fig, outer_gs, meta_list, bams, cov_store,
  689. cov_ymax, dens_ymax):
  690. """Draw one figure worth of rows; meta_list is bp_meta or ctrl_meta."""
  691. for row, meta in enumerate(meta_list):
  692. assembly = meta["assembly"]
  693. scaf_A = meta["scaf_A"]
  694. scaf_B = meta["scaf_B"]
  695. label = meta["label"]
  696. lbl_A = meta["lbl_A"]
  697. lbl_B = meta["lbl_B"]
  698. is_ctrl = meta["is_ctrl"]
  699. end_A = meta["end_A"]
  700. end_B = meta["end_B"]
  701. len_A = meta["len_A"]
  702. len_B = meta["len_B"]
  703. win_A_start, win_A_end = meta["win_A"]
  704. spanning_names = meta["spanning_names"]
  705. trans_df = meta["trans_df"]
  706. contact_rate = meta["contact_rate"]
  707. bam = bams[assembly]
  708. ymax_cov = cov_ymax[assembly]
  709. ymax_dens = dens_ymax[assembly]
  710. if is_ctrl:
  711. inner_A = GridSpecFromSubplotSpec(
  712. 3, 1, subplot_spec=outer_gs[row, 0],
  713. height_ratios=[2, 3, 1], hspace=0.55)
  714. else:
  715. inner_A = GridSpecFromSubplotSpec(
  716. 5, 1, subplot_spec=outer_gs[row, 0],
  717. height_ratios=[2, 2, 1, 2, 1], hspace=0.55)
  718. inner_B = GridSpecFromSubplotSpec(
  719. 5, 1, subplot_spec=outer_gs[row, 2],
  720. height_ratios=[2, 2, 1, 2, 1], hspace=0.55)
  721. if is_ctrl:
  722. reads_A = stack_reads(get_reads_in_window(
  723. bam, scaf_A, win_A_start, win_A_end, spanning_names, MIN_MAPQ))
  724. mids_A, depth_A = cov_store[(assembly, scaf_A, win_A_start, win_A_end)]
  725. ax_dens_A = fig.add_subplot(inner_A[0])
  726. ax_reads_A = fig.add_subplot(inner_A[1])
  727. ax_cov_A = fig.add_subplot(inner_A[2])
  728. if not trans_df.empty and len_A > 0:
  729. mids_d, counts_d = compute_density(trans_df["pos1"].values, len_A, DENS_BIN)
  730. else:
  731. mids_d, counts_d = np.array([len_A / 2]), np.array([0.0])
  732. bnd_genome_A = len_A if end_A == "end" else 0
  733. draw_density_track(
  734. ax_dens_A, mids_d, counts_d, len_A, assembly, ymax_dens,
  735. title=lbl_A, view_win=meta["win_A"], boundary_x=bnd_genome_A,
  736. show_boundary_side="right" if end_A == "end" else "left")
  737. ax_dens_A.set_ylabel("Trans HiC\ncontacts", fontsize=FONT["label"], labelpad=3)
  738. draw_read_track(ax_reads_A, reads_A, win_A_start, win_A_end, trans_df, "pos1")
  739. draw_coverage_track(ax_cov_A, mids_A, depth_A,
  740. win_A_start, win_A_end, assembly, ymax_cov)
  741. ax_cov_A.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  742. bnd_x_A = (win_A_end - win_A_start) if end_A == "end" else 0
  743. draw_boundary_line([ax_reads_A, ax_cov_A], bnd_x_A)
  744. else:
  745. reads_A_j = stack_reads(get_reads_in_window(
  746. bam, scaf_A, win_A_start, win_A_end, spanning_names, MIN_MAPQ))
  747. mids_A_j, depth_A_j = cov_store[(assembly, scaf_A, win_A_start, win_A_end)]
  748. win_A_o_start, win_A_o_end = meta["win_A_other"]
  749. reads_A_o = stack_reads(get_reads_in_window(
  750. bam, scaf_A, win_A_o_start, win_A_o_end, spanning_names, MIN_MAPQ))
  751. mids_A_o, depth_A_o = cov_store[(assembly, scaf_A, win_A_o_start, win_A_o_end)]
  752. ax_dens_A = fig.add_subplot(inner_A[0])
  753. ax_reads_A_j = fig.add_subplot(inner_A[1])
  754. ax_cov_A_j = fig.add_subplot(inner_A[2])
  755. ax_reads_A_o = fig.add_subplot(inner_A[3])
  756. ax_cov_A_o = fig.add_subplot(inner_A[4])
  757. if not trans_df.empty and len_A > 0:
  758. mids_d, counts_d = compute_density(trans_df["pos1"].values, len_A, DENS_BIN)
  759. else:
  760. mids_d, counts_d = np.array([len_A / 2]), np.array([0.0])
  761. bnd_genome_A = len_A if end_A == "end" else 0
  762. draw_density_track(
  763. ax_dens_A, mids_d, counts_d, len_A, assembly, ymax_dens,
  764. title=lbl_A, view_win=meta["win_A"], boundary_x=bnd_genome_A,
  765. show_boundary_side="right" if end_A == "end" else "left",
  766. view_win_other=meta["win_A_other"])
  767. ax_dens_A.set_ylabel("Trans HiC\ncontacts", fontsize=FONT["label"], labelpad=3)
  768. draw_read_track(ax_reads_A_j, reads_A_j, win_A_start, win_A_end, trans_df, "pos1")
  769. draw_coverage_track(ax_cov_A_j, mids_A_j, depth_A_j,
  770. win_A_start, win_A_end, assembly, ymax_cov)
  771. ax_cov_A_j.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  772. ax_cov_A_j.set_xlabel("")
  773. ax_cov_A_j.tick_params(axis="x", labelbottom=True)
  774. bnd_x_A_j = (win_A_end - win_A_start) if end_A == "end" else 0
  775. draw_boundary_line([ax_reads_A_j, ax_cov_A_j], bnd_x_A_j)
  776. end_A_other = "start" if end_A == "end" else "end"
  777. draw_read_track(ax_reads_A_o, reads_A_o, win_A_o_start, win_A_o_end, trans_df, "pos1")
  778. draw_coverage_track(ax_cov_A_o, mids_A_o, depth_A_o,
  779. win_A_o_start, win_A_o_end, assembly, ymax_cov)
  780. ax_cov_A_o.set_ylabel("Depth (x)", fontsize=FONT["label"], labelpad=3)
  781. bnd_x_A_o = 0 if end_A_other == "start" else (win_A_o_end - win_A_o_start)
  782. draw_boundary_line([ax_reads_A_o, ax_cov_A_o], bnd_x_A_o)
  783. ax_gap = fig.add_subplot(outer_gs[row, 1])
  784. ax_gap.set_axis_off()
  785. if is_ctrl:
  786. ax_gap.text(0.5, 0.65, f"{len(trans_df):,}\ntrans HiC\npairs",
  787. ha="center", va="center", fontsize=FONT["gap"],
  788. color=COLOURS_SPANNING["hic_contact"], fontweight="bold",
  789. transform=ax_gap.transAxes)
  790. ax_gap.text(0.5, 0.28, f"{contact_rate:.2f}\npairs/Mb\u00b2",
  791. ha="center", va="center", fontsize=FONT["gap"],
  792. color=COLOURS_SPANNING["hic_contact"], transform=ax_gap.transAxes)
  793. else:
  794. win_B_start, win_B_end = meta["win_B"]
  795. win_B_o_start, win_B_o_end = meta["win_B_other"]
  796. reads_B_j = stack_reads(get_reads_in_window(
  797. bam, scaf_B, win_B_start, win_B_end, spanning_names, MIN_MAPQ))
  798. mids_B_j, depth_B_j = cov_store[(assembly, scaf_B, win_B_start, win_B_end)]
  799. reads_B_o = stack_reads(get_reads_in_window(
  800. bam, scaf_B, win_B_o_start, win_B_o_end, spanning_names, MIN_MAPQ))
  801. mids_B_o, depth_B_o = cov_store[(assembly, scaf_B, win_B_o_start, win_B_o_end)]
  802. ax_dens_B = fig.add_subplot(inner_B[0])
  803. ax_reads_B_j = fig.add_subplot(inner_B[1])
  804. ax_cov_B_j = fig.add_subplot(inner_B[2])
  805. ax_reads_B_o = fig.add_subplot(inner_B[3])
  806. ax_cov_B_o = fig.add_subplot(inner_B[4])
  807. if not trans_df.empty and len_B > 0:
  808. mids_dB, counts_dB = compute_density(trans_df["pos2"].values, len_B, DENS_BIN)
  809. else:
  810. mids_dB, counts_dB = np.array([len_B / 2]), np.array([0.0])
  811. bnd_genome_B = 0 if end_B == "start" else len_B
  812. draw_density_track(
  813. ax_dens_B, mids_dB, counts_dB, len_B, assembly, ymax_dens,
  814. title=lbl_B, view_win=meta["win_B"], boundary_x=bnd_genome_B,
  815. show_boundary_side="left" if end_B == "start" else "right",
  816. view_win_other=meta["win_B_other"])
  817. ax_dens_B.set_yticklabels([])
  818. draw_read_track(ax_reads_B_j, reads_B_j, win_B_start, win_B_end, trans_df, "pos2")
  819. draw_coverage_track(ax_cov_B_j, mids_B_j, depth_B_j,
  820. win_B_start, win_B_end, assembly, ymax_cov)
  821. ax_cov_B_j.set_yticklabels([])
  822. ax_cov_B_j.set_xlabel("")
  823. ax_cov_B_j.tick_params(axis="x", labelbottom=True)
  824. bnd_x_B_j = 0 if end_B == "start" else (win_B_end - win_B_start)
  825. draw_boundary_line([ax_reads_B_j, ax_cov_B_j], bnd_x_B_j)
  826. end_B_other = "end" if end_B == "start" else "start"
  827. draw_read_track(ax_reads_B_o, reads_B_o, win_B_o_start, win_B_o_end, trans_df, "pos2")
  828. draw_coverage_track(ax_cov_B_o, mids_B_o, depth_B_o,
  829. win_B_o_start, win_B_o_end, assembly, ymax_cov)
  830. ax_cov_B_o.set_yticklabels([])
  831. bnd_x_B_o = (win_B_o_end - win_B_o_start) if end_B_other == "end" else 0
  832. draw_boundary_line([ax_reads_B_o, ax_cov_B_o], bnd_x_B_o)
  833. if not trans_df.empty:
  834. draw_hic_arcs(fig, ax_reads_A_j, ax_reads_B_j, trans_df,
  835. meta["win_A"], meta["win_B"])
  836. n_span = len(spanning_names)
  837. ax_gap.text(0.5, 0.88, f"{len(trans_df):,}\ntrans HiC\npairs",
  838. ha="center", va="center", fontsize=FONT["gap"],
  839. color=COLOURS_SPANNING["hic_contact"], fontweight="bold",
  840. transform=ax_gap.transAxes)
  841. ax_gap.text(0.5, 0.70, f"{contact_rate:.2f}\npairs/Mb\u00b2",
  842. ha="center", va="center", fontsize=FONT["gap"],
  843. color=COLOURS_SPANNING["hic_contact"], transform=ax_gap.transAxes)
  844. ax_gap.text(0.5, 0.50, f"{n_span}\nspanning\nread{'s' if n_span != 1 else ''}",
  845. ha="center", va="center", fontsize=FONT["gap"],
  846. color=COLOURS_SPANNING["spanning"], fontweight="bold",
  847. transform=ax_gap.transAxes)
  848. handles_leg = [
  849. mpatches.Patch(color=COLOURS_SPANNING["spanning"], alpha=0.9, label="HiFi spanning read"),
  850. mpatches.Patch(color=COLOURS_SPANNING["background"], alpha=0.5, label="HiFi non-spanning read"),
  851. mpatches.Patch(color=COLOURS_SPANNING["hic_contact"], alpha=0.7, label="Trans HiC contact"),
  852. mpatches.Patch(color=COLOURS_SPANNING["coverage"]["mpibr"], alpha=0.7, label="HiFi depth (MPIBR)"),
  853. mpatches.Patch(color=COLOURS_SPANNING["coverage"]["dtol"], alpha=0.7, label="HiFi depth (DToL)"),
  854. plt.Line2D([0], [0], color=COLOURS_SPANNING["boundary"], linewidth=0.9,
  855. linestyle="--", label="Scaffold boundary"),
  856. mpatches.Patch(color="yellow", alpha=0.4, label="200 kb view window"),
  857. ]
  858. # ── Breakpoints figure ────────────────────────────────────────────────
  859. n_bp = len(bp_meta)
  860. fig_bp = plt.figure(figsize=(18.0 / 2.54, 14)) #old height:(15.0 * n_bp + 2.5) / 2.54
  861. outer_gs_bp = GridSpec(
  862. n_bp, 3, figure=fig_bp,
  863. width_ratios=[1, 0.20, 1],
  864. height_ratios=[15.0] * n_bp,
  865. hspace=0.25, wspace=0.08,
  866. left=0.11, right=0.97, top=0.97, bottom=0.07,
  867. )
  868. _draw_rows(fig_bp, outer_gs_bp, bp_meta, bams, cov_store, cov_ymax, dens_ymax)
  869. fig_bp.legend(handles=handles_leg, loc="lower center",
  870. ncol=4, fontsize=FONT["legend"], frameon=False,
  871. bbox_to_anchor=(0.5, 0.0))
  872. fig_bp.savefig(OUTPUT_SPANNING, dpi=300, bbox_inches="tight")
  873. plt.show()
  874. print(f"Saved: {OUTPUT_SPANNING}")
  875. # ── Controls figure ───────────────────────────────────────────────────
  876. output_ctrl = OUTPUT_SPANNING.replace(".pdf", "_controls.pdf")
  877. n_ctrl = len(ctrl_meta)
  878. fig_ctrl = plt.figure(figsize=(6, 13))
  879. outer_gs_ctrl = GridSpec(
  880. n_ctrl, 3, figure=fig_ctrl,
  881. width_ratios=[1, 0.20, 1],
  882. height_ratios=[15.0] * n_ctrl,
  883. hspace=0.25, wspace=0.08,
  884. left=0.11, right=0.97, top=0.97, bottom=0.04,
  885. )
  886. _draw_rows(fig_ctrl, outer_gs_ctrl, ctrl_meta, bams, cov_store, cov_ymax, dens_ymax)
  887. fig_ctrl.savefig(output_ctrl, dpi=300, bbox_inches="tight")
  888. plt.show()
  889. print(f"Saved: {output_ctrl}")
  890. # %% [markdown]
  891. # ---
  892. # ## 2. Trans HiC contact rate statistics
  893. #
  894. # Tests whether the normalised trans contact rate (pairs per Mb²) between breakpoint scaffold pairs is significantly elevated above the background distribution of all trans scaffold pairs.
  895. #
  896. # - **Empirical p-value**: fraction of background pairs with rate ≥ observed
  897. # - **Wilcoxon rank-sum** (one-tailed): DToL only (3 breakpoints)
  898. # - **Plot A**: violin/strip plot of background, positive control, and breakpoint rates
  899. # - **Plot B**: histogram of background distribution with breakpoint lines
  900. # %% [markdown]
  901. # ### 2.1 Parameters
  902. # %%
  903. MPIBR_TRANS = f"{WD}/hic_trans_background/mpibr_trans_counts.tsv"
  904. MPIBR_INTRA = f"{WD}/hic_trans_background/mpibr_intra_counts.tsv"
  905. DTOL_TRANS = f"{WD}/hic_trans_background/dtol_trans_counts.tsv"
  906. DTOL_INTRA = f"{WD}/hic_trans_background/dtol_intra_counts.tsv"
  907. OUTPUT_STATS = "hic_trans_stats" # prefix; .pdf and _distribution.pdf produced
  908. # %% [markdown]
  909. # ### 2.2 Breakpoint definitions and colours
  910. # %%
  911. BREAKPOINTS_STATS = {
  912. "mpibr": [("BP1", "scaffold_40", "scaffold_44")],
  913. "dtol": [
  914. ("BP2", "scaffold_31", "scaffold_40"),
  915. ("BP3", "scaffold_41", "scaffold_46"),
  916. ("BP4", "scaffold_44", "scaffold_45"),
  917. ],
  918. }
  919. COLOURS_STATS = {
  920. "negative": "#AAAAAA", # grey — background trans
  921. "positive": "#4DAF4A", # green — intra-scaffold
  922. "breakpoint": {
  923. "mpibr": "#2166AC", # blue — MPIBR breakpoints
  924. "dtol": "#FF6600", # orange — DToL breakpoints
  925. },
  926. }
  927. # %% [markdown]
  928. # ### 2.3 Helper functions
  929. # %%
  930. def load_trans_bg(path):
  931. df = pd.read_csv(path, sep="\t")
  932. df.columns = df.columns.str.strip()
  933. df["rate_per_mb2"] = pd.to_numeric(df["rate_per_mb2"], errors="coerce")
  934. return df.dropna(subset=["rate_per_mb2"])
  935. def load_intra_bg(path):
  936. df = pd.read_csv(path, sep="\t")
  937. df.columns = df.columns.str.strip()
  938. df["rate_per_mb2"] = pd.to_numeric(df["rate_per_mb2"], errors="coerce")
  939. return df.dropna(subset=["rate_per_mb2"])
  940. def get_breakpoint_rates(trans_df, assembly):
  941. rows = []
  942. for label, sa, sb in BREAKPOINTS_STATS.get(assembly, []):
  943. key_a, key_b = sorted([sa, sb])
  944. match = trans_df[
  945. (trans_df["scaffold_A"] == key_a) &
  946. (trans_df["scaffold_B"] == key_b)]
  947. if match.empty:
  948. print(f" WARNING: {sa}/{sb} not found in trans table")
  949. continue
  950. rate = float(match["rate_per_mb2"].values[0])
  951. rows.append({"label": label, "scaffold_A": sa, "scaffold_B": sb,
  952. "rate_per_mb2": rate})
  953. return pd.DataFrame(rows)
  954. def empirical_pvalue(observed, background):
  955. n = len(background)
  956. count = np.sum(background >= observed)
  957. return (count + 1) / (n + 1)
  958. def wilcoxon_ranksum(bp_rates, background_rates):
  959. stat, p = stats.mannwhitneyu(bp_rates, background_rates, alternative="greater")
  960. return stat, p
  961. def jitter(n, width=0.15, seed=42):
  962. rng = np.random.default_rng(seed)
  963. return rng.uniform(-width, width, n)
  964. def draw_distribution(ax, rates, x_pos, colour, label, show_violin=True):
  965. if len(rates) > 1 and show_violin:
  966. parts = ax.violinplot([rates], positions=[x_pos],
  967. widths=0.6, showmedians=True, showextrema=False)
  968. for pc in parts["bodies"]:
  969. pc.set_facecolor(colour)
  970. pc.set_alpha(0.35)
  971. pc.set_edgecolor(colour)
  972. parts["cmedians"].set_color(colour)
  973. parts["cmedians"].set_linewidth(1.5)
  974. ax.scatter(np.full(len(rates), x_pos) + jitter(len(rates)),
  975. rates, s=8, color=colour, alpha=0.5, linewidths=0, zorder=3)
  976. ax.errorbar(x_pos, rates.mean(), yerr=rates.std(),
  977. fmt="D", color=colour, markersize=4, capsize=3,
  978. linewidth=1.0, zorder=5)
  979. def draw_breakpoint_lines(ax, bp_df, colour, x_range):
  980. y_span = ax.get_ylim()[1] - ax.get_ylim()[0]
  981. min_y_gap = y_span * 0.02
  982. nudge = y_span * 0.02
  983. sorted_df = bp_df.sort_values("rate_per_mb2")
  984. prev_y_label = -np.inf
  985. for _, row in sorted_df.iterrows():
  986. ax.axhline(row["rate_per_mb2"], color=colour, linewidth=0.5,
  987. linestyle="--", alpha=0.8, zorder=4)
  988. y_label = row["rate_per_mb2"]
  989. if y_label - prev_y_label < min_y_gap:
  990. y_label = prev_y_label + nudge
  991. ax.text(x_range[1] - 0.05, y_label, f" {row['label']}",
  992. va="center", ha="left", fontsize=8, color=colour)
  993. prev_y_label = y_label
  994. # %% [markdown]
  995. # ### 2.4 Run analysis and generate plots
  996. # %%
  997. assemblies = [("mpibr", MPIBR_TRANS, MPIBR_INTRA),
  998. ("dtol", DTOL_TRANS, DTOL_INTRA)]
  999. n_asm = len(assemblies)
  1000. fig_w = 6 #16.0 / 2.54
  1001. fig_h = 6 # 8.0 / 2.54 * n_asm
  1002. fig, axes = plt.subplots(1, n_asm, figsize=(fig_w, fig_h), sharey=False)
  1003. if n_asm == 1:
  1004. axes = [axes]
  1005. all_results = []
  1006. dist_store = {}
  1007. for ax, (assembly, trans_path, intra_path) in zip(axes, assemblies):
  1008. print(f"\n{'='*60}\nAssembly: {assembly.upper()}\n{'='*60}")
  1009. trans_df = load_trans_bg(trans_path)
  1010. intra_df = load_intra_bg(intra_path)
  1011. print(f" Negative control pairs: {len(trans_df)}")
  1012. print(f" Positive control scaffolds: {len(intra_df)}")
  1013. bp_df = get_breakpoint_rates(trans_df, assembly)
  1014. if bp_df.empty:
  1015. print(" No breakpoint rates found — skipping.")
  1016. ax.set_visible(False)
  1017. continue
  1018. bp_keys = set()
  1019. for _, sa, sb in BREAKPOINTS_STATS.get(assembly, []):
  1020. bp_keys.add(tuple(sorted([sa, sb])))
  1021. bg_mask = ~trans_df.apply(
  1022. lambda r: tuple(sorted([r["scaffold_A"], r["scaffold_B"]])) in bp_keys, axis=1)
  1023. background = trans_df[bg_mask]["rate_per_mb2"].values
  1024. intra_rates = intra_df["rate_per_mb2"].values
  1025. bp_rates = bp_df["rate_per_mb2"].values
  1026. print(f"\n Background: mean={background.mean():.3f} "
  1027. f"median={np.median(background):.3f} sd={background.std():.3f} n={len(background)}")
  1028. print(f" Intra-scaffold: mean={intra_rates.mean():.3f} "
  1029. f"median={np.median(intra_rates):.3f} sd={intra_rates.std():.3f} n={len(intra_rates)}")
  1030. emp_pvals = []
  1031. print(f"\n Breakpoint rates (n={len(background)} background pairs):")
  1032. for _, row in bp_df.iterrows():
  1033. p_emp = empirical_pvalue(row["rate_per_mb2"], background)
  1034. emp_pvals.append(p_emp)
  1035. print(f" {row['label']} ({row['scaffold_A']} / {row['scaffold_B']}): "
  1036. f"rate={row['rate_per_mb2']:.3f} empirical p={p_emp:.4f}")
  1037. all_results.append({
  1038. "assembly": assembly, "label": row["label"],
  1039. "scaffold_A": row["scaffold_A"], "scaffold_B": row["scaffold_B"],
  1040. "rate_per_mb2": row["rate_per_mb2"],
  1041. "bg_mean": background.mean(), "bg_median": np.median(background),
  1042. "bg_sd": background.std(), "bg_n": len(background),
  1043. "empirical_p": p_emp, "wilcoxon_U": np.nan, "wilcoxon_p": np.nan,
  1044. })
  1045. colour_bp = COLOURS_STATS["breakpoint"][assembly]
  1046. colour_asm = "#2166AC" if assembly == "mpibr" else "#e65c00"
  1047. x_neg, x_pos = 1.0, 2.0
  1048. x_range = (0.3, 2.9)
  1049. draw_distribution(ax, background, x_neg, COLOURS_STATS["negative"], "Background trans")
  1050. draw_distribution(ax, intra_rates, x_pos, COLOURS_STATS["positive"], "Intra-scaffold")
  1051. draw_breakpoint_lines(ax, bp_df, colour_bp, x_range)
  1052. for _, row in bp_df.iterrows():
  1053. ax.scatter(x_neg, row["rate_per_mb2"], s=40, color=colour_bp,
  1054. zorder=6, marker="*", linewidths=0)
  1055. ax.set_xlim(*x_range)
  1056. ax.set_xticks([x_neg, x_pos])
  1057. ax.set_xticklabels(["Background\ntrans pairs", "Intra-scaffold\n(positive ctrl)"],
  1058. fontsize=8)
  1059. ax.set_ylabel("Contact rate (pairs / Mb²)", fontsize=9)
  1060. ax.set_title(assembly.upper(), fontsize=10, fontweight="bold", color=colour_asm)
  1061. ax.spines["top"].set_visible(False)
  1062. ax.spines["right"].set_visible(False)
  1063. _wilcoxon_p = np.nan
  1064. if len(bp_rates) > 1:
  1065. stat, _wilcoxon_p = wilcoxon_ranksum(bp_rates, background)
  1066. print(f"\n Wilcoxon (joint) n={len(bp_rates)} vs n={len(background)}: "
  1067. f"U={stat:.1f} p={_wilcoxon_p:.4f} (one-tailed, greater)")
  1068. all_results[-1]["wilcoxon_U"] = stat
  1069. all_results[-1]["wilcoxon_p"] = _wilcoxon_p
  1070. else:
  1071. print(f"\n Wilcoxon skipped for {assembly.upper()} (single breakpoint)")
  1072. dist_store[assembly] = {
  1073. "background": background, "bp_df": bp_df,
  1074. "emp_pvals": emp_pvals, "wilcoxon_p": _wilcoxon_p,
  1075. "colour_bp": colour_bp, "colour_asm": colour_asm,
  1076. }
  1077. handles_stats = [
  1078. mpatches.Patch(color=COLOURS_STATS["negative"], alpha=0.7, label="Background trans pairs"),
  1079. mpatches.Patch(color=COLOURS_STATS["positive"], alpha=0.7, label="Intra-scaffold long-range"),
  1080. plt.Line2D([0], [0], color=COLOURS_STATS["breakpoint"]["mpibr"],
  1081. linewidth=1.0, linestyle="--", label="MPIBR breakpoint rate"),
  1082. plt.Line2D([0], [0], color=COLOURS_STATS["breakpoint"]["dtol"],
  1083. linewidth=1.0, linestyle="--", label="DToL breakpoint rate"),
  1084. plt.Line2D([0], [0], marker="*", color="w",
  1085. markerfacecolor=COLOURS_STATS["breakpoint"]["mpibr"],
  1086. markersize=8, label="Breakpoint observed"),
  1087. ]
  1088. plt.tight_layout()
  1089. fig.subplots_adjust(bottom=0.18)
  1090. fig.legend(handles=handles_stats, loc="lower center", ncol=3, fontsize=9,
  1091. frameon=False, bbox_to_anchor=(0.5, 0.01))
  1092. plot_path = f"{OUTPUT_STATS}.pdf"
  1093. fig.savefig(plot_path, dpi=300, bbox_inches="tight")
  1094. plt.show()
  1095. print(f"\nSaved: {plot_path}")
  1096. # %% [markdown]
  1097. # ### 2.5 Background distribution histogram
  1098. # %%
  1099. n_dist = len(dist_store)
  1100. fig2, axes2 = plt.subplots(1, n_dist,
  1101. figsize=(6, 3),
  1102. sharey=False)
  1103. if n_dist == 1:
  1104. axes2 = [axes2]
  1105. for ax2, (assembly, store) in zip(axes2, dist_store.items()):
  1106. bg = store["background"]
  1107. bp_df = store["bp_df"]
  1108. emp_pvals = store["emp_pvals"]
  1109. colour_bp = store["colour_bp"]
  1110. colour_asm= store["colour_asm"]
  1111. counts, _, _ = ax2.hist(bg, bins=40, color=COLOURS_STATS["negative"],
  1112. alpha=0.75, edgecolor="white", linewidth=0.3,
  1113. zorder=1, label="Background trans pairs")
  1114. y_max = counts.max() * 1.15
  1115. ax2.set_ylim(0, y_max)
  1116. bp_sorted = sorted(zip(bp_df.iterrows(), emp_pvals),
  1117. key=lambda t: t[0][1]["rate_per_mb2"])
  1118. min_x_gap = (bg.max() - bg.min()) * 0.06
  1119. label_step = y_max * 0.18
  1120. prev_x, nudge_level = -np.inf, 0
  1121. for (_, row), p_emp in bp_sorted:
  1122. x_val = row["rate_per_mb2"]
  1123. ax2.axvline(x_val, color=colour_bp, linewidth=0.5, linestyle="--", zorder=3)
  1124. p_str = f"p = {p_emp:.4f}" if p_emp >= 0.0001 else "p < 0.0001"
  1125. if x_val - prev_x < min_x_gap:
  1126. nudge_level += 1
  1127. else:
  1128. nudge_level = 0
  1129. y_label = y_max * 0.97 - nudge_level * label_step
  1130. ax2.text(x_val, y_label, f" {row['label']}\n {p_str}",
  1131. va="top", ha="left", fontsize=8, color=colour_bp,
  1132. style="italic", zorder=4)
  1133. prev_x = x_val
  1134. wilcoxon_p = store["wilcoxon_p"]
  1135. if not np.isnan(wilcoxon_p):
  1136. p_str = f"p = {wilcoxon_p:.4f}" if wilcoxon_p >= 0.0001 else "p < 0.0001"
  1137. ax2.text(0.99, 0.15, f"Wilcoxon (joint)\n{p_str}",
  1138. transform=ax2.transAxes, va="bottom", ha="right",
  1139. fontsize=8, color="#FF6600",
  1140. bbox=dict(boxstyle="round,pad=0.2", fc="white",
  1141. ec="#FF6600", linewidth=0.6, alpha=0.6))
  1142. ax2.set_xlabel("Contact rate (pairs / Mb²)", fontsize=9)
  1143. ax2.set_ylabel("Number of scaffold pairs", fontsize=9)
  1144. ax2.set_title(assembly.upper(), fontsize=10, fontweight="bold", color=colour_asm)
  1145. ax2.spines["top"].set_visible(False)
  1146. ax2.spines["right"].set_visible(False)
  1147. plt.tight_layout()
  1148. dist_path = f"{OUTPUT_STATS}_distribution.pdf"
  1149. fig2.savefig(dist_path, dpi=300, bbox_inches="tight")
  1150. plt.show()
  1151. print(f"Saved: {dist_path}")
  1152. # %% [markdown]
  1153. # ### 2.6 Results table
  1154. # %%
  1155. if all_results:
  1156. results_df = pd.DataFrame(all_results)
  1157. tsv_path = f"{OUTPUT_STATS}_results.tsv"
  1158. results_df.to_csv(tsv_path, sep="\t", index=False, float_format="%.4f")
  1159. print(f"Saved: {tsv_path}\n")
  1160. display(results_df)
  1161. # %% [markdown]
  1162. # ---
  1163. # ## 3. Repeat content at junction windows
  1164. #
  1165. # Parses RepeatMasker `.out` files for junction and control scaffold windows and produces a stacked bar chart showing repeat class fractions per window.
  1166. # %% [markdown]
  1167. # ### 3.1 Parameters
  1168. # %%
  1169. REPEAT_OUTPUT_DIR = f"{BASE}/junction_repeats"
  1170. OUTPUT_REPEATS = "junction_repeat_content" # prefix; .tsv and .pdf produced
  1171. WINDOW_SIZE = 200_000
  1172. # %% [markdown]
  1173. # ### 3.2 Repeat class colours and label metadata
  1174. # %%
  1175. CLASS_COLOURS_RM = {
  1176. "SINE": "#EE6677",
  1177. "LINE": "#377EB8",
  1178. "DNA": "#4DAF4A",
  1179. "LTR": "#984EA3",
  1180. "Simple_repeat": "#FF7F00",
  1181. "Low_complexity": "#A65628",
  1182. "Unknown": "#999999",
  1183. "Other": "#CCCCCC",
  1184. }
  1185. LABEL_META_RM = {
  1186. "BP1_MPIBR_40_end": ("mpibr", False),
  1187. "BP1_MPIBR_44_start": ("mpibr", False),
  1188. "BP2_DToL_31_end": ("dtol", False),
  1189. "BP2_DToL_40_start": ("dtol", False),
  1190. "BP3_DToL_41_end": ("dtol", False),
  1191. "BP3_DToL_46_start": ("dtol", False),
  1192. "BP4_DToL_44_end": ("dtol", False),
  1193. "BP4_DToL_45_start": ("dtol", False),
  1194. "Ctrl_MPIBR_3_end": ("mpibr", True),
  1195. "Ctrl_MPIBR_3_start": ("mpibr", True),
  1196. "Ctrl_MPIBR_5_end": ("mpibr", True),
  1197. "Ctrl_MPIBR_5_start": ("mpibr", True),
  1198. "Ctrl_DToL_1_end": ("dtol", True),
  1199. "Ctrl_DToL_1_start": ("dtol", True),
  1200. "Ctrl_DToL_2_end": ("dtol", True),
  1201. "Ctrl_DToL_2_start": ("dtol", True),
  1202. }
  1203. LABEL_ASSEMBLY_RM = {k: v[0] for k, v in LABEL_META_RM.items()}
  1204. ASSEMBLY_COLOURS_RM = {"mpibr": "#2166AC", "dtol": "#e65c00"}
  1205. ROW_ORDER_RM = [
  1206. "BP1_MPIBR_40_end", "BP1_MPIBR_44_start",
  1207. "BP2_DToL_31_end", "BP2_DToL_40_start",
  1208. "BP3_DToL_41_end", "BP3_DToL_46_start",
  1209. "BP4_DToL_44_end", "BP4_DToL_45_start",
  1210. "Ctrl_MPIBR_3_end", "Ctrl_MPIBR_3_start",
  1211. "Ctrl_MPIBR_5_end", "Ctrl_MPIBR_5_start",
  1212. "Ctrl_DToL_1_end", "Ctrl_DToL_1_start",
  1213. "Ctrl_DToL_2_end", "Ctrl_DToL_2_start",
  1214. ]
  1215. # %% [markdown]
  1216. # ### 3.3 Parsing helpers
  1217. # %%
  1218. def normalise_class_rm(repeat_class):
  1219. rc = repeat_class.upper()
  1220. if rc.startswith("SINE"): return "SINE"
  1221. if rc.startswith("LINE"): return "LINE"
  1222. if rc.startswith("DNA"): return "DNA"
  1223. if rc.startswith("LTR"): return "LTR"
  1224. if "SIMPLE" in rc: return "Simple_repeat"
  1225. if "LOW_COMPLEX" in rc: return "Low_complexity"
  1226. if rc in ("UNKNOWN", "UNSPECIFIED"): return "Unknown"
  1227. return "Other"
  1228. def parse_repeatmasker_out(out_path):
  1229. rows = []
  1230. if not os.path.isfile(out_path):
  1231. print(f" WARNING: .out file not found: {out_path}")
  1232. return pd.DataFrame()
  1233. with open(out_path) as fh:
  1234. for _ in range(3):
  1235. fh.readline()
  1236. for line in fh:
  1237. line = line.rstrip("\n")
  1238. if not line.strip():
  1239. continue
  1240. parts = line.split()
  1241. if len(parts) < 11:
  1242. continue
  1243. try:
  1244. seq_name = parts[4]
  1245. start = int(parts[5])
  1246. end = int(parts[6])
  1247. rep_class = parts[10]
  1248. rep_length = end - start + 1
  1249. rows.append({
  1250. "seq_name": seq_name,
  1251. "start": start,
  1252. "end": end,
  1253. "rep_class": normalise_class_rm(rep_class),
  1254. "rep_length": rep_length,
  1255. })
  1256. except (ValueError, IndexError):
  1257. continue
  1258. return pd.DataFrame(rows)
  1259. def window_length_from_fasta(fa_path):
  1260. lengths = {}
  1261. current, length = None, 0
  1262. with open(fa_path) as fh:
  1263. for line in fh:
  1264. line = line.strip()
  1265. if line.startswith(">"):
  1266. if current:
  1267. lengths[current] = length
  1268. current = line[1:].split()[0]
  1269. length = 0
  1270. else:
  1271. length += len(line)
  1272. if current:
  1273. lengths[current] = length
  1274. return lengths
  1275. def summarise_repeats_rm(rm_df, seq_lengths):
  1276. rows = []
  1277. all_classes = list(CLASS_COLOURS_RM.keys())
  1278. for seq_name, seq_len in seq_lengths.items():
  1279. sub = rm_df[rm_df["seq_name"] == seq_name] if not rm_df.empty else pd.DataFrame()
  1280. row = {"label": seq_name, "seq_len": seq_len}
  1281. total_masked = 0
  1282. for cls in all_classes:
  1283. cls_bp = 0 if sub.empty else int(sub[sub["rep_class"] == cls]["rep_length"].sum())
  1284. row[f"{cls}_bp"] = cls_bp
  1285. row[f"{cls}_fraction"] = cls_bp / seq_len if seq_len > 0 else 0.0
  1286. total_masked += cls_bp
  1287. row["total_masked_bp"] = total_masked
  1288. row["total_repeat_fraction"] = total_masked / seq_len if seq_len > 0 else 0.0
  1289. rows.append(row)
  1290. return pd.DataFrame(rows)
  1291. # %% [markdown]
  1292. # ### 3.4 Parse RepeatMasker output
  1293. # %%
  1294. all_summaries = []
  1295. for asm in ["mpibr", "dtol"]:
  1296. rm_dir = os.path.join(REPEAT_OUTPUT_DIR, f"wd/{asm}")
  1297. fa_path = os.path.join(REPEAT_OUTPUT_DIR, f"fa/{asm}_junction_windows.fa")
  1298. out_file = os.path.join(rm_dir, f"{asm}_junction_windows.fa.out")
  1299. print(f"\n{asm.upper()}")
  1300. if not os.path.isfile(fa_path):
  1301. print(f" WARNING: window FASTA not found: {fa_path} — skipping")
  1302. continue
  1303. seq_lengths = window_length_from_fasta(fa_path)
  1304. rm_df = parse_repeatmasker_out(out_file)
  1305. summary = summarise_repeats_rm(rm_df, seq_lengths)
  1306. summary["assembly"] = asm
  1307. all_summaries.append(summary)
  1308. print(f" {'Label':<30} {'Total%':>7} {'SINE%':>6} {'LINE%':>6} "
  1309. f"{'DNA%':>6} {'LTR%':>6} {'Simple%':>8} {'Unknown%':>9}")
  1310. for _, row in summary.iterrows():
  1311. print(f" {row['label']:<30} {row['total_repeat_fraction']*100:>7.1f} "
  1312. f"{row['SINE_fraction']*100:>6.1f} {row['LINE_fraction']*100:>6.1f} "
  1313. f"{row['DNA_fraction']*100:>6.1f} {row['LTR_fraction']*100:>6.1f} "
  1314. f"{row['Simple_repeat_fraction']*100:>8.1f} "
  1315. f"{row['Unknown_fraction']*100:>9.1f}")
  1316. combined_rm = pd.concat(all_summaries, ignore_index=True)
  1317. combined_rm["_order"] = combined_rm["label"].map(
  1318. {lbl: i for i, lbl in enumerate(ROW_ORDER_RM)})
  1319. combined_rm = combined_rm.sort_values("_order").drop(columns="_order")
  1320. tsv_rm = f"{OUTPUT_REPEATS}.tsv"
  1321. frac_cols = ["label", "assembly", "seq_len", "total_repeat_fraction"] + [f"{c}_fraction" for c in CLASS_COLOURS_RM]
  1322. combined_rm[frac_cols].to_csv(tsv_rm, sep="\t", index=False, float_format="%.4f")
  1323. print(f"\nSummary TSV saved: {tsv_rm}")
  1324. # %% [markdown]
  1325. # ### 3.5 Plot repeat content
  1326. # %%
  1327. labels_rm = combined_rm["label"].tolist()
  1328. n_rm = len(labels_rm)
  1329. classes = list(CLASS_COLOURS_RM.keys())
  1330. fig_w = 6 #14.0 / 2.54
  1331. fig_h = 4.5 #max(6.0, n_rm * 0.55 + 2.0) / 2.54
  1332. fig3, ax3 = plt.subplots(figsize=(fig_w, fig_h))
  1333. y_positions = np.arange(n_rm)
  1334. bar_height = 0.55
  1335. lefts = np.zeros(n_rm)
  1336. for cls in classes:
  1337. fracs = combined_rm[f"{cls}_fraction"].values * 100
  1338. ax3.barh(y_positions, fracs, left=lefts,
  1339. height=bar_height, color=CLASS_COLOURS_RM[cls],
  1340. label=cls, linewidth=0)
  1341. lefts += fracs
  1342. for i, row in enumerate(combined_rm.itertuples()):
  1343. pct = row.total_repeat_fraction * 100
  1344. ax3.text(lefts[i] + 0.3, i, f"{pct:.1f}%", va="center", ha="left", fontsize=8)
  1345. ax3.set_yticks(y_positions)
  1346. ax3.set_yticklabels([])
  1347. ax3.tick_params(axis="y", length=0)
  1348. first_ctrl_idx = None
  1349. for i, lbl in enumerate(labels_rm):
  1350. meta_rm = LABEL_META_RM.get(lbl, ("mpibr", False))
  1351. asm, is_ctrl = meta_rm
  1352. colour = ASSEMBLY_COLOURS_RM[asm]
  1353. style = "italic" if is_ctrl else "normal"
  1354. prefix = "" if is_ctrl else ""
  1355. ax3.text(-0.5, i, prefix + lbl.replace("_", " "),
  1356. va="center", ha="right", fontsize=8,
  1357. color=colour, style=style)
  1358. if is_ctrl and first_ctrl_idx is None:
  1359. first_ctrl_idx = i
  1360. if first_ctrl_idx is not None:
  1361. ax3.axhline(first_ctrl_idx - 0.5, color="#444444",
  1362. linewidth=0.8, linestyle="--", zorder=5)
  1363. # ax3.text(max(lefts) * 0.5, first_ctrl_idx - 0.55,
  1364. # "breakpoints above | controls below",
  1365. # va="top", ha="center", fontsize=5,
  1366. # color="#444444", style="italic")
  1367. ax3.set_xlabel("Repeat content (%)", fontsize=9)
  1368. x_max = max(lefts) * 1.15
  1369. ax3.set_xlim(-0.5, x_max)
  1370. ax3.spines["top"].set_visible(False)
  1371. ax3.spines["right"].set_visible(False)
  1372. ax3.spines["left"].set_visible(False)
  1373. handles_rm = [mpatches.Patch(color=CLASS_COLOURS_RM[c], label=c) for c in classes]
  1374. plt.tight_layout()
  1375. fig3.subplots_adjust(bottom=0.20)
  1376. fig3.legend(handles=handles_rm, loc="lower center", fontsize=9,
  1377. frameon=False, ncol=4, bbox_to_anchor=(0.5, 0.01))
  1378. plot_rm = f"{OUTPUT_REPEATS}.pdf"
  1379. fig3.savefig(plot_rm, dpi=300, bbox_inches="tight")
  1380. plt.show()
  1381. print(f"Saved: {plot_rm}")

breakpoint_analysis.ipynb at commit efe218e, under MIT · at the source

Overview

  1. Max Planck Institute for Brain Research Frankfurt am Main Germany
  2. Radboud University, Donders Institute for Brain, Cognition and Behaviour Nijmegen Netherlands
  3. Faculty of Biological Sciences, Goethe University Frankfurt am Main Germany
  4. Department of Neuroscience and Developmental Biology, University of Vienna Vienna Austria
Journal: eLife, volume 14, article RP107393
Dates: published online 28 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7554/elife.107393 · PMID 42206967 · PMCID PMC13218726 · OpenAlex W4412827090
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: other (organism), cellular / molecular (subfield)
Methods: Statistics, Preprocessing, fMRI & imaging
Keywords: Sepia officinalis, cephalopod, genome assembly, Other
MeSH: Chromosomes*, Genome*, Sepia*, Animals (* major topic)
Journal subjects: Genetics and Genomics
Topic: Cephalopods and Marine Biology (Ecology, Evolution, Behavior and Systematics, Agricultural and Biological Sciences), according to OpenAlex
Funding: European Research Council (Advanced Grant CAMOUFLAGE 101141501, Horizon 2020 European Union Research and Innovation Programme grant No. 945026)
Citations: cited by 2 papers (Europe PMC); 195 references in the paper
Research resources: R v4.4.2 RRID:SCR_001905, STAR v2.7.11b RRID:SCR_004463, InterProScan v5.73–104 RRID:SCR_005829, bedtools v2.30 RRID:SCR_006646, featureCounts (Subread v2.0.8) RRID:SCR_012919, RepeatMasker v4.1.7-p1 RRID:SCR_012954, BUSCO v5.5.0 RRID:SCR_015008, RepeatModeler v2.0.6 RRID:SCR_015027, DESeq2 v1.42.0 RRID:SCR_015687, DIAMOND2 RRID:SCR_016071, StringTie v3.0.0 RRID:SCR_016323, VecScreen RRID:SCR_016577, clusterProfiler v4.12.6 RRID:SCR_016884, GenomeScope 2.0 RRID:SCR_017014, OrthoFinder v2.5 RRID:SCR_017118, ape v5.8.1 (R package) RRID:SCR_017343, TransDecoder v5.7.0 RRID:SCR_017647, minimap2 RRID:SCR_018550, CAFE5 v5.1.1 RRID:SCR_018924, seqtk RRID:SCR_018927, mosdepth RRID:SCR_018929, BRAKER3 (incl. TSEBRA) RRID:SCR_018964, pysam v0.22.1 RRID:SCR_021017, hifiasm RRID:SCR_021069, eggNOG-mapper v2.1.12 RRID:SCR_021165, MCScanX RRID:SCR_022067, bwa-mem2 v2.3 RRID:SCR_022192, YAHS RRID:SCR_022965, pairtools v1.1.0 RRID:SCR_023038, Winnowmap2 RRID:SCR_025349, apeglm RRID:SCR_026951

Abstract

Coleoid cephalopods, a subclass of mollusks that includes octopuses, cuttlefish, and squid, exhibit sophisticated biological features, such as dynamic and neurally driven camouflage behavior, inter-individual communication, single-lens camera-like eyes, the largest brains among invertebrates, and a distinctive embryonic development. The common cuttlefish Sepia officinalis has served as a model organism in various research fields, spanning biophysics, neurobiology, behavior, evolution, ecology, and biomechanics. More recently, it has become a model to investigate the neural mechanisms underlying cephalopod camouflage, using quantitative behavioral approaches alongside molecular techniques to characterize the identity, evolution, and development of neuronal cell types. Despite significant interest in this animal, a high-quality, annotated genome of this species is still lacking. To address this, we sequenced and assembled a chromosome-scale genome for S. officinalis. Our assembly spans 5.68 billion base pairs and comprises 1n=47 repeat-rich chromosome scaffolds. This was unexpected because the haploid karyotypes of other decapods indicate 46 chromosomes. Detailed comparisons of our data to those from published decapod genome assemblies and to another recent genome assembly of S. officinalis (itself suggesting 1n=49 chromosomes) in fact revealed clear homologies between 46 scaffolds across all the datasets. In-depth comparison of datasets reveals highly repetitive regions at discordant scaffold boundaries and suggests that the true karyotype of S. officinalis is probably 1n=46 chromosomes, a likely ancestral and if true, conserved decapod karyotype. Our results include a comprehensive gene annotation and full-length transcript prediction, which we used to characterize orthologous gene families across mollusks. We identified several large-scale expansions specific to cephalopods, with many genes specific to neural or non-neural tissues of adult S. officinalis. In summary, this genome should provide a valuable resource for future research on the evolution, brain organization, information processing, development, and behavior in this important clade.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 40 matches between paragraphs and lines of code.

gitlab.mpcdf.mpg.de/mpibr/laur/cuttlefishomics/soffgenome

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: efe218e0bf3c8318c5c729cbab94478dbf5b5502, 28 April 2026
Languages: Shell (50), R (8), Perl (5), Python (2), Jupyter (1)
Size: 80 files, 66 scripts
Software Heritage: archived
Found in: the references
Holds: README, license file, environment (envs/environment.yml), 3 notebooks
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: SAMtools (15 files), tidyverse (8 files), STAR (6 files), circlize (3 files), ComplexHeatmap (3 files), BEDTools (2 files), clusterProfiler (2 files), cowplot (2 files), DESeq2 (2 files), ggplot2 (2 files), pandas (2 files), pheatmap (2 files), pysam (2 files), Subread (featureCounts) (2 files), data.table (1 file), Matplotlib (1 file), NumPy (1 file), SciPy (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
68 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 66 scripts, each with its path and the digest of its content;
  • 40 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

The genome assembly and raw data can be found at the BioProject PRJNA1091451 on NCBI. Raw sequencing reads are deposited at SRA (study accession SRP570862). The code for the genome assembly and annotation is available at https://gitlab.mpcdf.mpg.de/mpibr/laur/cuttlefishomics/soffgenome (copy archived at Tushev, 2026). Genome annotation files are deposited at https://doi.org/10.17617/3.CGO7QG.

The following datasets were generated:

RenckenS TushevG HainD CiirdaevaE SimakovO LaurentG 2025Sepia officinalis isolate:GLC-03058 (common cuttlefish)NCBI BioProjectPRJNA109145110.7554/eLife.107393PMC1321872642206967

RenckenS TushevG HainD CiirdaevaE SimakovO LaurentG 2025Chromosome-scale genome assembly of the European common cuttlefish Sepia officinalisEdmond - the Open Research Data Repository of the Max Planck Society10.17617/3.CGO7QGPMC1321872642206967

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 6 authors, 4 keywords, 4 MeSH terms, 1 funder, 183 references, 31 RRIDs.

Cite

This paper

Rencken, S. D., Tushev, G., Hain, D., Ciirdaeva, E., Simakov, O., & Laurent, G. (2026). Chromosome-scale genome assembly of the European common cuttlefish &lt;i&gt;Sepia officinalis&lt;/i&gt;. eLife, 14, RP107393. https://doi.org/10.7554/elife.107393

BibTeX

@article{rencken2026chromosome,
author = {Rencken, Simone Daniela and Tushev, Georgi and Hain, David and Ciirdaeva, Elena and Simakov, Oleg and Laurent, Gilles},
title = {{Chromosome-scale genome assembly of the European common cuttlefish \&lt;i\&gt;Sepia officinalis\&lt;/i\&gt;}},
journal = {eLife},
year = {2026},
month = may,
volume = {14},
pages = {RP107393},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/elife.107393},
url = {https://doi.org/10.7554/elife.107393},
pmid = {42206967},
pmcid = {PMC13218726}
}

RIS

TY - JOUR
AU - Rencken, Simone Daniela
AU - Tushev, Georgi
AU - Hain, David
AU - Ciirdaeva, Elena
AU - Simakov, Oleg
AU - Laurent, Gilles
TI - Chromosome-scale genome assembly of the European common cuttlefish &lt;i&gt;Sepia officinalis&lt;/i&gt;
T2 - eLife
J2 - Elife
PY - 2026
DA - 2026/05/28
VL - 14
SP - RP107393
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/elife.107393
UR - https://doi.org/10.7554/elife.107393
LA - en
ER -

CSL-JSON

{
"id": "10.7554/elife.107393",
"type": "article-journal",
"title": "Chromosome-scale genome assembly of the European common cuttlefish &lt;i&gt;Sepia officinalis&lt;/i&gt;",
"container-title": "eLife",
"author": [
{
"family": "Rencken",
"given": "Simone Daniela"
},
{
"family": "Tushev",
"given": "Georgi"
},
{
"family": "Hain",
"given": "David"
},
{
"family": "Ciirdaeva",
"given": "Elena"
},
{
"family": "Simakov",
"given": "Oleg"
},
{
"family": "Laurent",
"given": "Gilles"
}
],
"container-title-short": "Elife",
"volume": "14",
"page": "RP107393",
"DOI": "10.7554/elife.107393",
"PMID": "42206967",
"PMCID": "PMC13218726",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://doi.org/10.7554/elife.107393",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
28
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41586-026-10512-9 [code]
Astrocyte glucocorticoid receptor signalling restricts neuronal plasticity.
Journal: Nature
In common: Subread (featureCounts), STAR, pysam, 15 other tools, cellular / molecular, 4 references
[2] doi:10.21203/rs.3.rs-9927928/v1 [code]
Genome-wide and allele-resolved maps of the radial architecture of the mouse genome
Journal: Research Square (preprint)
In common: Subread (featureCounts), STAR, pysam, 13 other tools, 4 references
[3] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: Subread (featureCounts), STAR, SAMtools, 13 other tools, 4 references
[4] doi:10.1038/s41467-026-69944-6 [code]
Multi-modal dissection of cell-type specific TDP-43 pathology in the motor cortex.
Journal: Nature communications
In common: pysam, BEDTools, SAMtools, 12 other tools, 5 references
[5] doi:10.1038/s42003-026-10402-w [code]
Temporal orchestration of transcriptional and epigenomic programming underlying maternal embryonic diapause in a cricket model.
Journal: Communications biology
In common: SAMtools, DESeq2, pheatmap, 2 other tools, other, 12 references
[6] doi:10.1126/sciadv.aed2952 [code]
Activation of transposable elements is linked to a region- and cell type-specific interferon response in Parkinson's disease.
Journal: Science advances
In common: Subread (featureCounts), STAR, pysam, 12 other tools, cellular / molecular, 3 references
[7] doi:10.1016/j.celrep.2026.117073 [code]
Single-cell epigenomics uncovers heterochromatin instability and transcription factor dysfunction during mouse brain aging.
Journal: Cell reports
In common: Subread (featureCounts), pysam, BEDTools, 12 other tools, cellular / molecular, 2 references
[8] doi:10.1038/s41586-026-10629-x [code]
Whole-genome duplication shaped cell-type evolution in the vertebrate brain.
Journal: Nature
In common: BEDTools, circlize, DESeq2, 9 other tools, other, cellular / molecular, 5 references
[9] doi:10.1038/s41467-026-76675-1 [code]
Long-read proteogenomic atlas of human neuronal differentiation reveals isoform diversity informing neurodevelopmental risk mechanisms.
Journal: Nature communications
In common: STAR, pysam, BEDTools, 11 other tools, 2 references
[10] doi:10.1038/s41467-026-71790-5 [code]
Recurrent DNA break clusters drive replication-stress-induced copy number variants and genome diversification.
Journal: Nature communications
In common: pysam, BEDTools, SAMtools, 12 other tools, cellular / molecular

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.