Skill Tracer
Skills › Scientific Agent Skills › alphagenome › atlas_link.py

atlas_link.py

202 lines · skills/alphagenome/scripts/atlas_link.py at 5f2bc03

1#!/usr/bin/env python3
2"""Build deep links into the AlphaGenome Atlas website. Standard library only.
3
4The portal at https://deepmind.google.com/science/alphagenome/atlas is the
5no-code face of the Atlas: AVI score, per-modality heatmaps across every
6biosample, and motif maps. Linking a scored variant to it lets a reader check
7the prediction tracks behind a number. Positions are 1-based, intervals are
81-based closed, and rsIDs are not accepted by the site.
9
10Examples:
11 python atlas_link.py variant "chr9:128225994:G>A"
12 python atlas_link.py variant "chr9:128225994:G>A" --biosample K562 --modalities RNA_SEQ,DNASE,CHIP_TF --tf GATA1
13 python atlas_link.py locus chr11:5225727-5226575 --modalities RNA_SEQ,DNASE
14 python atlas_link.py gene HBB
15 python atlas_link.py motifs chr11:5225727-5226575
16 python atlas_link.py variant "chr9:128225994:G>A" --markdown
17"""
19from __future__ import annotations
21import argparse
22import sys
23import urllib.parse
24from collections.abc import Sequence
26import _common as common
28MODES = ("variant", "locus", "gene", "motifs")
30#: Layout sections the portal renders (``lItems=section:<MODALITY>``).
31SECTION_MODALITIES: tuple[str, ...] = (
32 "RNA_SEQ",
33 "DNASE",
34 "ATAC",
35 "CHIP_TF",
36 "CHIP_HISTONE",
37 "CAGE",
38 "PROCAP",
39 "POLYADENYLATION",
40 "SPLICE_JUNCTIONS",
41 "SPLICE_SITE_USAGE",
42 "SPLICE_SITES",
43 "CONTACT_MAPS",
46#: ``f=SCORER_MODALITY:<display name>`` filter values used by the site.
47MODALITY_FILTER_NAMES: dict[str, str] = {
48 "RNA_SEQ": "RNA-seq",
49 "DNASE": "DNase",
50 "ATAC": "ATAC-seq",
51 "CHIP_TF": "ChIP-TF",
52 "CHIP_HISTONE": "ChIP-Histone",
53 "CAGE": "CAGE",
54 "PROCAP": "PRO-cap",
55 "POLYADENYLATION": "Polyadenylation",
56 "SPLICE_JUNCTIONS": "Splice junctions",
57 "SPLICE_SITE_USAGE": "Splice site usage",
60DEFAULT_MODALITIES: tuple[str, ...] = ("RNA_SEQ", "DNASE", "CHIP_TF")
63def parse_modalities(text: str | Sequence[str] | None) -> list[str]:
64 if not text:
65 return []
66 items = text.split(",") if isinstance(text, str) else list(text)
67 result = []
68 for item in items:
69 name = item.strip().upper()
70 if not name:
71 continue
72 if name not in SECTION_MODALITIES:
73 raise ValueError(f"unknown modality {item!r}; choose from {', '.join(SECTION_MODALITIES)}")
74 result.append(name)
75 return result
78def build_filter(
79 biosample: str | None = None,
80 modalities: Sequence[str] = (),
81 tfs: Sequence[str] = (),
82 histone_marks: Sequence[str] = (),
83) -> str | None:
84 """The ``f=`` predicate list.
86 Biosample predicates AND together; assay predicates (modality, TF, histone
87 mark) OR together. RNA-seq and DNase tracks carry no TF code, so a
88 TF-only assay filter hides them; when TFs are given, RNA-seq and DNase
89 are added so the expression and accessibility context stays visible.
90 """
91 parts: list[str] = []
92 if biosample:
93 parts.append(f"BIOSAMPLE_NAME:{biosample}")
94 active = list(modalities)
95 if tfs:
96 for required in ("RNA_SEQ", "DNASE"):
97 if required not in active:
98 active.append(required)
99 for modality in active:
100 display = MODALITY_FILTER_NAMES.get(modality.upper())
101 if display:
102 parts.append(f"SCORER_MODALITY:{display}")
103 for tf in tfs:
104 parts.append(f"ASSAY_TRANSCRIPTOR_FACTOR:{tf}")
105 for mark in histone_marks:
106 parts.append(f"ASSAY_HISTONE_MARK:{mark}")
107 return ",".join(parts) if parts else None
110def build_layout(include_avi: bool = True, modalities: Sequence[str] = ()) -> str:
111 items = ["avi"] if include_avi else []
112 items.extend(f"section:{modality.upper()}" for modality in modalities)
113 return ",".join(items) if items else "avi"
116def build_url(
117 mode: str,
118 query: str,
119 *,
120 biosample: str | None = None,
121 modalities: Sequence[str] = DEFAULT_MODALITIES,
122 tfs: Sequence[str] = (),
123 histone_marks: Sequence[str] = (),
124 include_avi: bool = True,
125 zoom: str | None = None,
126) -> str:
127 """Assemble a portal URL.
129 ``mode`` is ``variant`` (``q`` = ``chr:pos:ref>alt``), ``locus`` (``q`` =
130 1-based closed ``chr:start-end``), ``gene`` (``q`` = symbol or Ensembl
131 ID, portal mode ``entity``), or ``motifs`` (``q`` = interval, motif view).
132 """
133 if mode not in MODES:
134 raise ValueError(f"mode must be one of {MODES}")
135 if mode == "variant":
136 spec = common.parse_variant_string(query)
137 q = str(spec)
138 portal_mode = "variant"
139 elif mode in {"locus", "motifs"}:
140 chromosome, start0, end = common.parse_interval_string(query)
141 q = f"{chromosome}:{start0 + 1}-{end}"
142 portal_mode = "locus" if mode == "locus" else "motifs"
143 else:
144 q = query.strip()
145 if not q:
146 raise ValueError("gene query is empty")
147 portal_mode = "entity"
149 params: dict[str, str] = {"q": q, "m": portal_mode, "lItems": build_layout(include_avi, modalities)}
150 if zoom:
151 chromosome, start0, end = common.parse_interval_string(zoom)
152 params["i"] = f"{chromosome}:{start0 + 1}-{end}"
153 filter_string = build_filter(biosample, modalities, tfs, histone_marks)
154 if filter_string:
155 params["f"] = filter_string
156 return f"{common.ATLAS_BASE_URL}?" + urllib.parse.urlencode(params, quote_via=urllib.parse.quote, safe=":,-")
159def build_parser() -> argparse.ArgumentParser:
160 parser = argparse.ArgumentParser(
161 prog="atlas_link.py",
162 description="Build AlphaGenome Atlas website deep links (no network, no key).",
163 epilog=__doc__.split("Examples:", 1)[-1],
164 formatter_class=argparse.RawDescriptionHelpFormatter,
165 )
166 parser.add_argument("mode", choices=MODES, help="what the query is")
167 parser.add_argument("query", help="chr:pos:ref>alt, chr:start-end (1-based closed), or a gene symbol / Ensembl ID")
168 parser.add_argument("--biosample", metavar="NAME", help="biosample name filter, e.g. K562 or 'heart left ventricle'")
169 parser.add_argument("--modalities", default=",".join(DEFAULT_MODALITIES), metavar="A,B,C", help=f"layout sections (default {','.join(DEFAULT_MODALITIES)})")
170 parser.add_argument("--tf", nargs="+", metavar="TF", default=(), help="ChIP-TF transcription factor filters")
171 parser.add_argument("--histone-mark", nargs="+", metavar="MARK", default=(), help="ChIP-Histone mark filters, e.g. H3K27ac")
172 parser.add_argument("--no-avi", action="store_true", help="omit the AVI track from the layout")
173 parser.add_argument("--zoom", metavar="CHR:START-END", help="viewport interval (1-based closed); needed for motif rendering")
174 parser.add_argument("--markdown", action="store_true", help="print a Markdown link instead of a bare URL")
175 return parser
178def main(argv: Sequence[str] | None = None) -> int:
179 args = build_parser().parse_args(argv)
180 try:
181 url = build_url(
182 args.mode,
183 args.query,
184 biosample=args.biosample,
185 modalities=parse_modalities(args.modalities),
186 tfs=tuple(args.tf),
187 histone_marks=tuple(args.histone_mark),
188 include_avi=not args.no_avi,
189 zoom=args.zoom,
190 )
191 except ValueError as error:
192 print(f"error: {error}", file=sys.stderr)
193 return 1
194 if args.markdown:
195 print(f"[{args.query} on AlphaGenome Atlas]({url})")
196 else:
197 print(url)
198 return 0
201if __name__ == "__main__":
202 sys.exit(main())