genomeblocks.architecture

A CRE interaction graph stored as two tables: the vertices are the rows of a Loci (vertex i = row i), the edges are a sorted src / tgt table with edge columns in A.ep (w weight, d distance, n observed / expected, …) and vertex columns in A.vp (annot, gene, strength, …). See the Architecture guide for the workflow and Design: architecture for the layout of the edge table.

Table of contents

  1. Architecture
  2. Construction
    1. make
    2. from_edges
    3. from_frame and from_scipy
  3. Weights: add_mcool
  4. Normalize, prune, annotate
  5. Sizes and blocks
  6. Views
  7. Lookups
  8. Hubs: strength, elbow, prime_hubs, support
  9. Graph backends
  10. Tables out
  11. Set operations
  12. Copy, save and load
  13. Drawing

Architecture

import os
import genomeblocks as gb
from genomeblocks import Architecture

Architecture(loci, src=(), tgt=(), *, ep=None, vp=None, name="Architecture")

The constructor takes vertex rows src[k] / tgt[k] and dicts of edge and vertex columns; loci must be genome-sorted (what Loci.make returns, or call .sort()). The classmethods below are the usual way in.

Edges are kept in canonical order: src < tgt, cis edges grouped by chromosome in genome order, every trans edge in one final block. That order is what blocks, chrom(), cis and trans slice.

cre   = gb.Loci.make("peaks.bed", keep=True)
genes = gb.Genes.make("genes.gtf")
A = Architecture.make(cre, "loops.bedpe", r=100)
# -> [INFO] 4 loops | 4 mapped (100.0%) | loci=6, links=4 (1 trans)
A
# -> Architecture(name='Skeleton', loci=6, links=4 [3 cis · 1 trans], edge_props=[w], vertex_props=[])
list(A)                      # the edges as (src row, tgt row)
# -> [(0, 2), (1, 4), (5, 6), (0, 5)]
A.blocks
# -> {'chr1': (0, 2), 'chr2': (2, 3), 'trans': (3, 4)}

As a table an Architecture is its edge table: shape, columns, head(), describe(), to_pandas() / to_polars() / to_arrow() and the Arrow / dataframe-interchange / narwhals protocols all describe the edges (see Tables out). len(A) counts the vertices with at least one edge.


Construction

Method Signature One line
Architecture.make make(loci, bedpe, *, name="Skeleton", r=2500, dmax=1e9, trans=True, verbose=True, backend=None) Edges between every pair of CREs within r bp of the two anchor midpoints of each loop.
Architecture.from_edges from_edges(loci, src, tgt, *, name="Architecture", vp=None, **ep) From vertex rows; keyword arrays become edge columns.
Architecture.from_frame from_frame(loci, df, *, src="src", tgt="tgt", name="Architecture") From an edge table (pandas / polars / arrow) with rows in src / tgt or uids in uid1 / uid2.
Architecture.from_scipy from_scipy(loci, M, *, weight="w", name="Architecture") From a square sparse matrix over the loci rows (upper triangle read).
Architecture.load load(path, *, genome=None) Read back what save wrote.

make

loci is anything as_loci takes; bedpe is a BEDPE path, a Pairs or a frame with BEDPE columns. Cis loops longer than dmax are skipped (dmax does not apply to trans loops); trans=False drops trans loops. backend picks the interval engine for the anchor-to-CRE mapping (any intervals backend gives the same edges). The new graph has one edge column, w, set to 0 — add_mcool fills it.

A2 = Architecture.make(cre, gb.Pairs.make("loops.bedpe"), r=100, dmax=5000, trans=False, verbose=False)
A2.n_links, A2.n_trans
# -> (2, 0)

If loci is not in genome order make prints a warning and works on a sorted copy, exposed as A.loci — align vertex columns to A.loci, not to the input.

from_edges

B = Architecture.from_edges(cre, [0, 0, 1], [2, 5, 4], w=[5.0, 1.0, 3.0])
B
# -> Architecture(name='Architecture', loci=5, links=3 [2 cis · 1 trans], edge_props=[w], vertex_props=[])
list(B), B.ep["w"].tolist()          # re-sorted into canonical order, columns follow
# -> ([(0, 2), (1, 4), (0, 5)], [5.0, 3.0, 1.0])

from_frame and from_scipy

from_frame reads what edges_frame() writes: every numeric column other than src, tgt, uid1, uid2, chrom1, chrom2, cis becomes an edge column. A table with neither rows nor uids raises a ValueError naming the accepted columns.

df = B.edges_frame()
df["score"] = [1, 2, 3]
Architecture.from_frame(cre, df).ep.keys()
# -> dict_keys(['w', 'score'])
Architecture.from_frame(cre, df[["uid1", "uid2", "w"]]).n_links
# -> 3
Architecture.from_scipy(cre, B.to_scipy("w")).ep["w"].tolist()
# -> [5.0, 3.0, 1.0]
Architecture.from_frame(cre, df[["w"]])
# -> ValueError: an edge table needs vertex rows in 'src' / 'tgt' or uids in 'uid1' / 'uid2'
#    (what edges_frame() writes); got columns ['w']

Weights: add_mcool

A.add_mcool(mcool, *, resolution=None, name="w", verbose=True) -> Architecture

Edge weight = the Hi-C count of the (bin, bin) pixel that holds the two CREs, shared equally among the edges that fall in the same pixel. Pixels are read one chromosome block at a time, so memory stays bounded and trans pixels are found in the same pass. Pass resolution= for a multi-resolution .mcool; a single-resolution .cool takes none. Both mistakes raise a ValueError that lists the resolutions available.

A.add_mcool("hic.cool")
# -> [INFO] Set distributed weights for 4/4 edges from cooler. [w]
A.ep["w"].tolist()
# -> [6.0, 3.0, 4.0, 9.0]
A.add_mcool("hic.cool", resolution=1000)
# -> ValueError: hic.cool is a single-resolution cooler: call add_mcool without resolution=

Normalize, prune, annotate

Method Signature One line
A.normalize normalize(*, source="w", name="n", verbose=True) Observed / expected: a power law on distance for cis edges, the mean trans weight for trans edges; writes ep.d and ep[name], stores the fit in A.fit. When the fit cannot run it warns and sets the cis ep[name] to 0.
A.prune prune(*, dist_prop="d", verbose=True) Drop zero-distance (co-located) cis edges; trans edges (d = inf) stay.
A.annotate annotate(genes, *, key="n", name="gene", verbose=True) vp.annot = region label per CRE; vp[name] = the nearest-TSS gene for promoter CREs, else the gene of the highest-key promoter neighbour (cis or trans).

Every one of these returns self, so they chain.

A.normalize()
# -> [INFO] Power-law fit: alpha=0.802, C=4.143e+03 on 3 cis edges; 1 trans edges use the mean trans weight → ep.n
list(A.ep), A.ep["d"].tolist()
# -> (['w', 'd', 'n'], [4000.0, 9000.0, 4500.0, inf])
A.prune()
# -> [INFO] prune: removed 0 zero-distance edges → 4 edges.
A.annotate(genes)
# -> [INFO] Annotated 6 loci: 3 promoter CREs | 2/3 non-promoter CREs assigned to a top-'n' promoter gene → vp.gene.
A.vp["annot"].tolist()
# -> ['Promoter-TSS', 'Promoter-TSS', '3UTR', 'Promoter-TSS', 'Promoter-TSS', 'Intergenic', 'Intergenic']
A.vp["gene"].tolist()
# -> ['GENE_A', 'GENE_A', 'GENE_A', 'GENE_B', 'GENE_B', 'GENE_A', '']

prune reads ep.d and annotate ranks promoter contacts by ep.n, so both need normalize() first; they raise a ValueError naming the missing column otherwise. A fit needs at least three cis edges with a positive weight and distance, at two distinct distances or more; with fewer, normalize issues a UserWarning that names the usable cis-edge count, A.fit holds nan and the cis n values are 0.

Vertex columns are one value per Loci row, so unlinked CREs get a label too (annot) and an empty gene (''). genes is a Genes table; a TSS is the gene’s 5’-most base, [t, t+1).


Sizes and blocks

Name Type Meaning
A.loci Loci The vertex table (vertex i = row i).
A.src, A.tgt int32 arrays Edge endpoints, src < tgt, canonical order.
A.ep, A.vp Props Edge / vertex columns; a dict with attribute access (A.ep.w is A.ep["w"]).
A.name str Shown in repr and plot titles.
A.n_links int Number of edges.
A.n_loci, len(A) int Vertices with at least one edge.
A.n_trans int Number of trans edges.
A.degree int array, one per Loci row Edges per vertex (0 for unlinked rows).
A.is_cis bool array, one per edge Both ends on one chromosome.
A.blocks dict {chrom: (lo, hi)} edge ranges of each cis block plus 'trans'.
A.block_counts() DataFrame chrom x chrom edge counts (cis on the diagonal).
A.fit dict {'alpha', 'C'} of the last normalize() power-law fit.
A.n_links, A.n_trans, A.n_loci, A.degree.tolist()
# -> (4, 1, 6, [2, 1, 1, 0, 1, 2, 1])
A.is_cis.tolist()
# -> [True, True, True, False]
A.block_counts()
# ->       chr1  chr2
#    chr1     2     1
#    chr2     1     1

Views

A view shares the Loci and the vertex columns with its parent and selects edges; slices (chrom, cis, trans) copy nothing.

Method Signature One line
A.chrom chrom(chrom) Cis edges of one chromosome.
A.cis property All cis edges.
A.trans property All trans edges.
A.chroms chroms() Iterate (chrom, view) over the cis blocks.
A.region region(region, start=None, end=None, *, both=True) Edges inside a region; both=False keeps edges with one end inside (trans partners too).
A.subgraph subgraph(rows=None, *, mask=None, vp=None, values=None, name=None) Edges whose two ends are both selected: by rows, a boolean mask, or vp[vp] in values.
A.chrom("chr1").n_links, A.cis.n_links, A.trans.n_links
# -> (2, 3, 1)
for chrom, view in A.chroms():
    print(chrom, view.n_links)
# -> chr1 2
#    chr2 1
A.region("chr1:0-6,000")
# -> Architecture(name='Skeleton:chr1:0-6,000', loci=2, links=1 [1 cis · 0 trans], ...)
A.region("chr1", 0, 6000, both=False)
# -> Architecture(name='Skeleton:chr1:0-6000', loci=5, links=3 [2 cis · 1 trans], ...)
A.subgraph(rows=[0, 2]).n_links
# -> 1
A.subgraph(vp="annot", values="Promoter-TSS")       # after annotate()
# -> Architecture(name='Skeleton_sub', loci=2, links=1 [1 cis · 0 trans], ...)

Region strings accept thousands separators and units: 'chr1:1,000-2,000', 'chr8:127.7-128.1 Mb', 'chr2:5kb-12kb'; a (chrom, start, end) tuple or a Locus works too.


Lookups

A vertex is named by its row number, its uid ('chr1:900-1100(+)') or a Locus read from the table; all three work wherever a vertex is expected.

Method Signature One line
A.neighbor_rows neighbor_rows(x) (partner rows, edge ids) of one CRE, cis and trans, as arrays.
A.neighbors neighbors(x) The same as a DataFrame: row, uid, chrom, cis + every edge column.
A[uid] __getitem__ {neighbour uid: w}.
A[uid1, uid2] __getitem__ That edge’s columns as a dict, or None when there is no edge.
uid in A __contains__ True when the uid is a vertex with at least one edge.
A.near_rows near_rows(key, start=None, end=None, *, r=0, mode="overlap", linked=True) Row numbers of the CREs near a window (key is a Locus, a region string or a chromosome with start / end); r widens the window on each side.
A.near near(key, start=None, end=None, *, r=0, mode="overlap") The same CREs as a Loci.

mode='overlap' keeps CREs that intersect the window, 'center' only those whose midpoint is in it.

A.neighbor_rows(0)
# -> (array([2, 5], dtype=int32), array([0, 3]))
A.neighbors(0)
# ->    row                uid chrom    cis    w       d         n
#    0    2  chr1:4900-5100(+)  chr1   True  6.0  4000.0  1.124534
#    1    5    chr2:500-600(+)  chr2  False  9.0     inf  1.000000
A[cre.uid[0]]
# -> {'chr1:4900-5100(+)': 6.0, 'chr2:500-600(+)': 9.0}
A[cre.uid[0], cre.uid[2]]
# -> {'w': 6.0, 'd': 4000.0, 'n': 1.1245336516005273}
A[cre.uid[0], cre.uid[1]]
# -> None
cre.uid[0] in A, cre.uid[3] in A
# -> (True, False)
A.near_rows("chr1:0-3,000")
# -> array([0, 1])
A.near(genes["GENE_A"].tss, r=2000)           # TSS ± 2 kb
# -> Loci(n=2, chroms=1, cols=[name, score])

Hubs: strength, elbow, prime_hubs, support

Method Signature One line
A.strength strength(key="n", name="strength", *, verbose=True) vp[name] = sum of incident ep[key] per vertex (node strength).
A.elbow elbow(key, *, verbose=True) Slope-1 knee on the sorted vp[key] curve; returns (cutoff, uids sorted by value, descending).
A.prime_hubs prime_hubs(key="n", gene="gene", *, verbose=True) Hubs above the knee of node strength and the genes they carry.
A.support support(genes, *, r=5000, mode="overlap", uids=True, rows=False, linked=True, backend=None) {gene_name: [uid, ...]} of the linked CREs within TSS ± r of each gene.

prime_hubs needs vp.annot and vp[gene] (run annotate first); it computes the strength column when it is missing. Its dict has the keys prime_genes, promoter_genes, enhancer_genes, hub_uids, cutoff, promoter_uids, enhancer_uids. The knee is the same slope-1 rule as se.knee, so super-enhancers and hubs are cut the same way.

A.strength()
# -> [INFO] Summed ep.n → vp.strength (node strength).
A.elbow("strength")
# -> [INFO] Slope-1 on vp.strength (value≈1.12 at cutoff): cutoff at 3/6 (50.0%)
#    (3, ['chr1:900-1100(+)', 'chr2:500-600(+)', 'chr1:4900-5100(+)', 'chr1:1900-2100(-)', ...])
hubs = A.prime_hubs(verbose=False)
hubs["hub_uids"], hubs["prime_genes"]
# -> (['chr1:900-1100(+)', 'chr2:500-600(+)', 'chr1:4900-5100(+)'], {'GENE_A'})

support computes the TSS ± r window for every gene at once; rows=True returns CRE row arrays instead of uids (what array work wants), mode='center' keeps only CREs whose midpoint is in the window, linked=False includes CREs without edges.

A.support(genes, r=5000)
# -> {'GENE_A': ['chr1:900-1100(+)', 'chr1:1900-2100(-)', 'chr1:4900-5100(+)'], 'GENE_B': ['chr1:10900-11100(-)']}
A.support(genes, r=5000, rows=True)
# -> {'GENE_A': array([0, 1, 2]), 'GENE_B': array([4])}

Graph backends

Graph algorithms run through the graph backend family (graph-tool when installed, else scipy; igraph and networkx on request — see Backends). Results are numpy arrays aligned to the Loci rows and are identical across engines. A requested engine that is not installed raises ImportError with the install command.

Method Signature One line
A.graph graph(vprops=(), *, backend=None) The graph as the engine’s own object (vertex i = row i, every edge, edge columns as properties, vprops copied too).
A.components components(name="component", *, backend=None) Connected component per vertex (-1 for unlinked rows), numbered by first row; stored as vp[name].
A.pagerank pagerank(weight=None, *, damping=0.85, name="pagerank", backend=None) PageRank per vertex (weight = an edge column or None); stored as vp[name].
A.to_graph_tool to_graph_tool(vprops=()) graph(vprops, backend="graph-tool").
A.to_networkx to_networkx(vprops=(), *, loci_cols=True) networkx.Graph; loci_cols adds chrom / start / end node attributes.
A.to_igraph to_igraph(vprops=(), *, loci_cols=True) igraph.Graph, same attributes.
A.to_scipy to_scipy(weight="w") Symmetric CSR adjacency over the Loci rows (weight=None: 1 per edge).
A.to_anndata to_anndata(*, weights=None) AnnData with the vertices as obs and one sparse adjacency per edge column in obsp (the layout scanpy’s graph tools read).

The graph-tool graph is built once and cached; its edge columns are re-synced on every graph() call. to_networkx and to_scipy sum parallel edges.

A.components().tolist()
# -> [0, 1, 0, -1, 1, 0, 0]
A.pagerank("w").round(4).tolist()
# -> [0.2438, 0.1626, 0.1073, 0.0244, 0.1626, 0.2179, 0.0814]
A.components(backend="igraph").tolist() == A.components(backend="scipy").tolist()
# -> True
A.components(backend="graph-tool")          # on a machine without it
# -> ImportError: the 'graph-tool' graph backend is not installed: conda install -c conda-forge graph-tool

g = A.to_networkx()
g.number_of_edges(), g[0][2], g.nodes[0]
# -> (4, {'w': 6.0, 'd': 4000.0, 'n': 1.1245336516005273}, {'chrom': 'chr1', 'start': 900, 'end': 1100})
ig = A.to_igraph(vprops=["strength"])
ig.vs.attributes(), ig.es.attributes()
# -> (['strength', 'chrom', 'start', 'end'], ['w', 'd', 'n'])
A.to_scipy("w")[0, 2]
# -> 6.0
A.to_anndata()
# -> AnnData object with n_obs × n_vars = 7 × 0
#        obs: 'chrom', 'start', 'end', 'strand', 'name', 'score', 'annot', 'gene', 'strength', 'component', 'pagerank'
#        uns: 'genomeblocks'
#        obsp: 'w', 'd', 'n'

Tables out

Method Returns One line
A.edges_frame() DataFrame One row per edge: src, tgt, uid1, uid2, chrom1, chrom2, cis + every edge column.
A.to_pandas() DataFrame Same as edges_frame().
A.to_polars() polars.DataFrame The edge table through Arrow.
A.to_arrow() pyarrow.Table The edge table.
A.columns list Column names of the edge table.
A.shape (n_links, n_columns)  
A.head(n=5), A.tail(n=5) DataFrame First / last edges.
A.describe() (alias summary()) DataFrame Name, loci, linked loci, edges (cis / trans / blocks), edge and vertex columns.
A.vertices_frame(linked=True) DataFrame One row per vertex with links (linked=False: every row): coordinates, uid and every vertex column, indexed by row.
A.to_frame() DataFrame Vertex table for linked vertices: uid + vertex columns.
A.block_counts() DataFrame chrom x chrom edge counts.
iter(A)   Yields (src row, tgt row) per edge.
A._repr_html_()   Notebook summary: the cis blocks and the trans block.

The Arrow C stream, __dataframe__ and narwhals protocols come from the same to_arrow(), so pl.DataFrame(A), duckdb.sql("select * from A"), seaborn, plotly and altair take an Architecture as it is.

A.shape, A.columns
# -> ((4, 10), ['src', 'tgt', 'uid1', 'uid2', 'chrom1', 'chrom2', 'cis', 'w', 'd', 'n'])
A.to_polars()
# -> shape: (4, 10)
#    ┌─────┬─────┬───────────────────┬─────────────────────┬───┬───────┬─────┬────────┬──────────┐
#    │ src ┆ tgt ┆ uid1              ┆ uid2                ┆ … ┆ cis   ┆ w   ┆ d      ┆ n        │
#    ╞═════╪═════╪═══════════════════╪═════════════════════╪═══╪═══════╪═════╪════════╪══════════╡
#    │ 0   ┆ 2   ┆ chr1:900-1100(+)  ┆ chr1:4900-5100(+)   ┆ … ┆ true  ┆ 6.0 ┆ 4000.0 ┆ 1.124534 │
#    │ 1   ┆ 4   ┆ chr1:1900-2100(-) ┆ chr1:10900-11100(-) ┆ … ┆ true  ┆ 3.0 ┆ 9000.0 ┆ 1.077749 │
#    │ 5   ┆ 6   ┆ chr2:500-600(+)   ┆ chr2:5000-5100(+)   ┆ … ┆ true  ┆ 4.0 ┆ 4500.0 ┆ 0.823993 │
#    │ 0   ┆ 5   ┆ chr1:900-1100(+)  ┆ chr2:500-600(+)     ┆ … ┆ false ┆ 9.0 ┆ inf    ┆ 1.0      │
#    └─────┴─────┴───────────────────┴─────────────────────┴───┴───────┴─────┴────────┴──────────┘
A.describe()
# ->                                                                value
#    name                                                        Skeleton
#    loci (vertices)                                                    6
#    loci with links                                                    6
#    edges                                                              4
#    cis edges                                                          3
#    trans edges                                                        1
#    cis blocks (chromosomes)                                           2
#    edge columns                                                 w, d, n
#    vertex columns            annot, gene, strength, component, pagerank
A.to_frame()
# ->                    uid         annot    gene  strength  component  pagerank
#    0     chr1:900-1100(+)  Promoter-TSS  GENE_A  2.124534          0  0.243820
#    1    chr1:1900-2100(-)  Promoter-TSS  GENE_A  1.077749          1  0.162602
#    2    chr1:4900-5100(+)          3UTR  GENE_A  1.124534          0  0.107289
#    ...

Set operations

Both graphs must be built on the same Loci object.

Expression Result
A \| B Union of the edge sets. An edge in both keeps A’s values; the edge columns are A’s in order, then B’s new ones, 0 on the edges of the graph that lacks them.
A & B The edges of A that are also in B, with A’s edge columns (a view; vertex columns are not carried).
U = A | B
list(U), U.ep["w"].tolist()
# -> ([(0, 2), (1, 4), (5, 6), (0, 5)], [6.0, 3.0, 4.0, 9.0])
list(A & B)
# -> [(0, 2), (1, 4), (0, 5)]

Copy, save and load

Method Signature One line
A.copy()   Deep copy of edges, edge columns and vertex columns (the Loci is shared).
A.save save(path) Writes a directory: vertices.parquet (Loci + vp: columns), edges.parquet (src, tgt, ep: columns), meta.json.
Architecture.load load(path, *, genome=None) Reads it back; genome re-codes the chromosomes into an existing Genome.
A.save("arch")
sorted(os.listdir("arch"))
# -> ['edges.parquet', 'meta.json', 'vertices.parquet']
back = Architecture.load("arch")
list(back) == list(A), back.loci.equals(A.loci)
# -> (True, True)

Drawing

A.draw(region, **kw) calls genomeblocks.architecture_draw.draw with the graph. Plain matplotlib on top of the tables: the CREs of a region become nodes (merged when closer than merge_distance), the edges among them become lines, and the positions come from the graph backend’s layout. Nothing here needs graph-tool.

from genomeblocks.architecture_draw import draw

draw(A, region, *,
     layout="spring",                 # 'spring' | 'circular' | 'genomic'
     backend=None,                    # graph backend for the spring layout
     merge_distance=None,             # merge CREs whose centres are within this many bp
     vertex_size_by=None,             # vertex column name or array (one value per Loci row)
     edge_width_by="w",               # edge column that scales line widths
     vertex_size_range=(40, 400),
     edge_width_range=(0.6, 4.0),
     vertex_color=None,               # a colour, a vertex column name, or an array
     cmap="viridis",                  # for numeric vertex colours
     edge_color="#b8b8b8",
     figsize=(8, 6),
     show_labels=True,
     label_prop="gene",               # vertex column used for node labels
     font_size=8,
     linked_only=True,                # skip CREs without edges
     seed=0,
     title=True,
     ax=None) -> matplotlib.axes.Axes
Argument Meaning
region 'chr1:1,000-2,000', 'chr8:127.5-128.5 Mb', (chrom, start, end) or a Locus.
layout 'spring': the graph backend’s force-directed layout (spectral for scipy); 'circular'; 'genomic': nodes on a line at their genomic position, edges as arcs — a browser-like view.
merge_distance CREs whose centres are within this many bp become one node; edge weights and size values are summed.
vertex_size_by Default: the number of CREs in the node.
vertex_color Numbers are mapped through cmap; strings get one colour per value and a legend.
label_prop Unique non-empty values of that vertex column, joined by /; nodes without one are labelled with their coordinates.

Raises ValueError when the region holds no (linked) CREs.

ax = A.draw("chr1:0-12 kb")
ax.get_title()
# -> 'Skeleton: chr1:0-12,000\n4 nodes, 2 edges'
ax = A.draw(("chr1", 0, 12_000), layout="genomic", merge_distance=1500,
            vertex_size_by="strength", vertex_color="annot")
ax.get_title()
# -> 'Skeleton: chr1:0-12,000\n3 nodes (4 CREs merged at 1,500 bp), 2 edges'
A.draw("chr1:0-12kb", layout="circular", backend="networkx", edge_width_by="n", label_prop="gene")
A.draw("chr2:7,000-8,000")
# -> ValueError: no CREs with links in chr2:7,000-8,000

For a region view with signal tracks and gene models see browser; for a shareable interactive page of the whole graph see View and igv_html.


Copyright © 2024–2026 Umut Berkay Altintas. MIT Licensed.

This site uses Just the Docs, a documentation theme for Jekyll.