Optimize nupack service startup and compute
This commit is contained in:
commit
e1b3156754
25 changed files with 9435 additions and 0 deletions
191
service/design-guide.html
Normal file
191
service/design-guide.html
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>NUPACK Design 使用笔记</title>
|
||||
<style>
|
||||
:root {
|
||||
--paper: #f7f2e8;
|
||||
--ink: #162033;
|
||||
--muted: #64708a;
|
||||
--line: #ddd4c4;
|
||||
--accent: #1f6f5f;
|
||||
--accent-2: #b65b2a;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #fbf7ee 0%, #edf5f0 100%);
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next", "Noto Sans SC", sans-serif;
|
||||
line-height: 1.65;
|
||||
}
|
||||
main {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 36px 20px 72px;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
line-height: 1.25;
|
||||
}
|
||||
h1 {
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
letter-spacing: -0.04em;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 34px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
code, pre {
|
||||
font-family: "Inconsolata", "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
pre {
|
||||
overflow: auto;
|
||||
background: #15221f;
|
||||
color: #f4ead7;
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
}
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
padding: 20px;
|
||||
margin: 18px 0;
|
||||
box-shadow: 0 18px 50px rgba(42, 31, 20, 0.08);
|
||||
}
|
||||
.toc {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 22px 0;
|
||||
}
|
||||
a.button {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 9px 14px;
|
||||
color: var(--accent);
|
||||
background: #fffaf0;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.warn {
|
||||
border-left: 4px solid var(--accent-2);
|
||||
padding-left: 14px;
|
||||
color: #5b3828;
|
||||
}
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: rgba(255,255,255,0.75);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #efe6d6;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<p class="muted">NUPACK replica / Design workflow notes</p>
|
||||
<h1>Design 使用笔记</h1>
|
||||
<p>这份笔记只讲当前网页里已经接入的功能:Design domains、target complexes、target tubes、hard constraints、soft constraints 和 defect weights。</p>
|
||||
|
||||
<nav class="toc">
|
||||
<a class="button" href="#quick-start">快速跑通</a>
|
||||
<a class="button" href="#targets">Targets 怎么填</a>
|
||||
<a class="button" href="#hard">Hard constraints</a>
|
||||
<a class="button" href="#soft">Soft constraints</a>
|
||||
<a class="button" href="#speed">为什么会慢</a>
|
||||
<a class="button" href="/#design-targets">跳到主界面 Targets</a>
|
||||
<a class="button" href="/#design-hard">跳到主界面 Hard</a>
|
||||
<a class="button" href="/#design-soft">跳到主界面 Soft</a>
|
||||
<a class="button" href="/#history">跳到历史记录</a>
|
||||
</nav>
|
||||
|
||||
<section id="quick-start" class="card">
|
||||
<h2>1. 快速跑通</h2>
|
||||
<p>主界面选择 <strong>Design</strong>,点击 <strong>载入示例</strong>,会得到一个可直接运行的双链设计:</p>
|
||||
<pre><code>domain:
|
||||
a = N10
|
||||
|
||||
strands:
|
||||
A = a
|
||||
B = ~a
|
||||
|
||||
target complex:
|
||||
AB_target = A+B
|
||||
structure = (10+)10
|
||||
|
||||
target tube:
|
||||
AB_target = 1 uM
|
||||
off-target max size = 2</code></pre>
|
||||
<p><code>~a</code> 表示 domain <code>a</code> 的反向互补,因此 <code>(10+)10</code> 这个全双链目标结构是合法的。</p>
|
||||
</section>
|
||||
|
||||
<section id="targets" class="card">
|
||||
<h2>2. Targets 怎么填</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>区域</th><th>含义</th><th>示例</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Design Domains</td><td>给每段可设计区域命名,并写 IUPAC 约束。</td><td><code>a = N10</code></td></tr>
|
||||
<tr><td>链输入</td><td>Design 模式下,序列框可以写 domain composition。</td><td><code>A = a</code>, <code>B = ~a</code></td></tr>
|
||||
<tr><td>Target Complex</td><td>指定哪些链组成目标复合物,以及目标二级结构。</td><td><code>A+B</code>, <code>(10+)10</code></td></tr>
|
||||
<tr><td>Target Tube</td><td>Tube design 才需要。指定 on-target 浓度,off-target 由 max size 自动补全。</td><td><code>AB_target = 1 uM</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="hard" class="card">
|
||||
<h2>3. Hard constraints 怎么用</h2>
|
||||
<p>Hard constraint 是“必须满足”的约束,写错会让设计空间为空,或直接报错。</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>类型</th><th>必填字段</th><th>当前推荐示例</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Match</td><td>左侧、右侧</td><td><code>a</code> 与 <code>a</code></td></tr>
|
||||
<tr><td>Complementarity</td><td>左侧、右侧</td><td><code>A</code> 与 <code>B</code></td></tr>
|
||||
<tr><td>Similarity</td><td>scope、reference、上下限</td><td><code>scope=a</code>, <code>reference=R10</code></td></tr>
|
||||
<tr><td>Window</td><td>scope、source sequences</td><td><code>scope=a</code>, 每行一个 10 nt source</td></tr>
|
||||
<tr><td>Pattern</td><td>patterns;scope 可空表示全局</td><td><code>AAAA, UUUU</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="warn">如果看到 “Constraint scope cannot be empty”,就是当前 constraint 类型需要填写 scope,但该字段为空。新版界面会在提交前拦截这类错误。</p>
|
||||
</section>
|
||||
|
||||
<section id="soft" class="card">
|
||||
<h2>4. Soft constraints 和 weights</h2>
|
||||
<p>Soft constraint 不会替代 ensemble defect,只是给优化目标增加加权惩罚。权重越大,设计器越偏向满足它,但也可能变慢。</p>
|
||||
<p>Defect weights 用来告诉设计器哪些 domain、strand、complex 或 tube 更重要。初学时建议先不填,确认 target 能跑通后再逐步添加。</p>
|
||||
</section>
|
||||
|
||||
<section id="speed" class="card">
|
||||
<h2>5. 为什么 Design 会慢</h2>
|
||||
<p>NUPACK design 是优化问题,不是一次性的结构分析。影响耗时的主要因素:</p>
|
||||
<ul>
|
||||
<li><strong>off-target max size</strong> 越大,需要考虑的非目标复合物越多。</li>
|
||||
<li><strong>f_stop</strong> 越小,停止条件越严格,通常越慢。</li>
|
||||
<li><strong>trials</strong> 大于 1 时会跑多个随机种子,NUPACK 源码里会并行提交多个 trial。</li>
|
||||
<li><strong>hard constraints</strong> 太多或互相矛盾,会反复搜索甚至失败。</li>
|
||||
</ul>
|
||||
<p>当前服务已显式设置 NUPACK 的 <code>config.threads</code>,不再只依赖 <code>OMP_NUM_THREADS</code>。本地 NUPACK 源码里有 <code>NUPACK_CUDA</code> 编译选项,但当前 wheel/镜像不是 CUDA 构建;直接“打开 GPU”不能生效,除非重新编译 NUPACK 的 CUDA 版本并替换镜像里的 wheel。</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
4
service/favicon.svg
Normal file
4
service/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="16" fill="#0f5d3c"/>
|
||||
<path d="M16 47V17h8l16 20V17h8v30h-8L24 27v20z" fill="#f7f1e6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 192 B |
5625
service/index.html
Normal file
5625
service/index.html
Normal file
File diff suppressed because it is too large
Load diff
2092
service/server.py
Normal file
2092
service/server.py
Normal file
File diff suppressed because it is too large
Load diff
345
service/split_strand_svg.py
Normal file
345
service/split_strand_svg.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass, field
|
||||
from html import escape
|
||||
|
||||
|
||||
NODE_RADIUS = 10.0
|
||||
PRIMARY_SPACE = 20.0
|
||||
PAIR_SPACE = 20.0
|
||||
PADDING = 52.0
|
||||
|
||||
|
||||
def _fmt(value):
|
||||
return f"{float(value):.2f}"
|
||||
|
||||
|
||||
def _fmt3(value):
|
||||
return f"{float(value):.3f}"
|
||||
|
||||
|
||||
def _pairmap_from_structure(structure):
|
||||
pair_stack = []
|
||||
dangling_stack = []
|
||||
pairs = [-1] * len(structure)
|
||||
|
||||
for index, char in enumerate(structure):
|
||||
if char == "(":
|
||||
pair_stack.append(index)
|
||||
elif char == ")":
|
||||
if pair_stack:
|
||||
partner = pair_stack.pop()
|
||||
pairs[index] = partner
|
||||
pairs[partner] = index
|
||||
else:
|
||||
dangling_stack.append(index)
|
||||
|
||||
if pair_stack:
|
||||
if len(pair_stack) != len(dangling_stack):
|
||||
raise ValueError("Unbalanced structure for split-strand layout.")
|
||||
for left, right in zip(pair_stack, reversed(dangling_stack)):
|
||||
pairs[left] = right
|
||||
pairs[right] = left
|
||||
|
||||
return pairs
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TreeNode:
|
||||
children: list["_TreeNode"] = field(default_factory=list)
|
||||
is_pair: bool = False
|
||||
index_a: int = -1
|
||||
index_b: int = -1
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
go_x: float = 0.0
|
||||
go_y: float = 0.0
|
||||
|
||||
|
||||
def _add_nodes_recursive(pairmap, root, start, end):
|
||||
if start > end:
|
||||
raise ValueError(f"Invalid recursive span for split-strand layout: {start}>{end}")
|
||||
|
||||
if pairmap[start] == end:
|
||||
child = _TreeNode(is_pair=True, index_a=start, index_b=end)
|
||||
_add_nodes_recursive(pairmap, child, start + 1, end - 1)
|
||||
root.children.append(child)
|
||||
return
|
||||
|
||||
child = _TreeNode()
|
||||
cursor = start
|
||||
while cursor <= end:
|
||||
partner = pairmap[cursor]
|
||||
if partner > cursor:
|
||||
_add_nodes_recursive(pairmap, child, cursor, partner)
|
||||
cursor = partner + 1
|
||||
continue
|
||||
child.children.append(_TreeNode(index_a=cursor))
|
||||
cursor += 1
|
||||
root.children.append(child)
|
||||
|
||||
|
||||
def _setup_coords_recursive(node, parent, start_x, start_y, go_x, go_y, flipped=False):
|
||||
cross_x = -go_y
|
||||
cross_y = go_x
|
||||
node.go_x = go_x
|
||||
node.go_y = go_y
|
||||
|
||||
if len(node.children) == 1:
|
||||
node.x = start_x
|
||||
node.y = start_y
|
||||
child = node.children[0]
|
||||
next_x = start_x + go_x * PRIMARY_SPACE
|
||||
next_y = start_y + (-1 if flipped else 1) * go_y * PRIMARY_SPACE
|
||||
if child.is_pair or (not child.is_pair and child.index_a >= 0):
|
||||
_setup_coords_recursive(child, node, next_x, next_y, go_x, go_y, flipped=flipped)
|
||||
else:
|
||||
_setup_coords_recursive(child, node, start_x, start_y, go_x, go_y, flipped=flipped)
|
||||
return
|
||||
|
||||
if not node.children:
|
||||
node.x = start_x
|
||||
node.y = start_y
|
||||
return
|
||||
|
||||
pair_count = sum(1 for child in node.children if child.is_pair)
|
||||
circle_length = (len(node.children) + 1) * PRIMARY_SPACE + (pair_count + 1) * PAIR_SPACE
|
||||
circle_radius = circle_length / (2 * math.pi)
|
||||
length_walker = PAIR_SPACE / 2.0
|
||||
|
||||
if parent is None:
|
||||
node.x = go_x * circle_radius
|
||||
node.y = go_y * circle_radius
|
||||
else:
|
||||
node.x = parent.x + go_x * circle_radius
|
||||
node.y = parent.y + (-1 if flipped else 1) * go_y * circle_radius
|
||||
|
||||
for child in node.children:
|
||||
length_walker += PRIMARY_SPACE
|
||||
if child.is_pair:
|
||||
length_walker += PAIR_SPACE / 2.0
|
||||
|
||||
rad_angle = length_walker / circle_length * 2 * math.pi - math.pi / 2.0
|
||||
if parent is None:
|
||||
rad_angle -= math.pi / 2.0
|
||||
|
||||
child_x = node.x + math.cos(rad_angle) * cross_x * circle_radius + math.sin(rad_angle) * go_x * circle_radius
|
||||
child_y = node.y + (-1 if flipped else 1) * math.cos(rad_angle) * cross_y * circle_radius + (-1 if flipped else 1) * math.sin(rad_angle) * go_y * circle_radius
|
||||
|
||||
child_go_x = child_x - node.x
|
||||
child_go_y = child_y - node.y
|
||||
child_go_len = math.hypot(child_go_x, child_go_y) or 1.0
|
||||
|
||||
_setup_coords_recursive(
|
||||
child,
|
||||
node,
|
||||
child_x,
|
||||
child_y,
|
||||
child_go_x / child_go_len,
|
||||
(-1 if flipped else 1) * child_go_y / child_go_len,
|
||||
flipped=flipped,
|
||||
)
|
||||
|
||||
if child.is_pair:
|
||||
length_walker += PAIR_SPACE / 2.0
|
||||
|
||||
|
||||
def _collect_coords_recursive(node, xs, ys, flipped=False):
|
||||
if node.is_pair:
|
||||
cross_x = -node.go_y
|
||||
cross_y = node.go_x
|
||||
xs[node.index_a] = node.x + cross_x * PAIR_SPACE / 2.0
|
||||
xs[node.index_b] = node.x - cross_x * PAIR_SPACE / 2.0
|
||||
ys[node.index_a] = node.y + (-1 if flipped else 1) * cross_y * PAIR_SPACE / 2.0
|
||||
ys[node.index_b] = node.y + (1 if flipped else -1) * cross_y * PAIR_SPACE / 2.0
|
||||
elif node.index_a >= 0:
|
||||
xs[node.index_a] = node.x
|
||||
ys[node.index_a] = node.y
|
||||
|
||||
for child in node.children:
|
||||
_collect_coords_recursive(child, xs, ys, flipped=flipped)
|
||||
|
||||
|
||||
def _layout_positions(display_structure):
|
||||
pairmap = _pairmap_from_structure(display_structure)
|
||||
root = _TreeNode()
|
||||
cursor = 0
|
||||
while cursor < len(pairmap):
|
||||
partner = pairmap[cursor]
|
||||
if partner > cursor:
|
||||
_add_nodes_recursive(pairmap, root, cursor, partner)
|
||||
cursor = partner + 1
|
||||
continue
|
||||
root.children.append(_TreeNode(index_a=cursor))
|
||||
cursor += 1
|
||||
|
||||
xs = [0.0] * len(display_structure)
|
||||
ys = [0.0] * len(display_structure)
|
||||
_setup_coords_recursive(root, None, 0.0, 0.0, 0.0, 1.0, flipped=False)
|
||||
_collect_coords_recursive(root, xs, ys, flipped=False)
|
||||
|
||||
min_x = min(x - NODE_RADIUS for x in xs)
|
||||
min_y = min(y - NODE_RADIUS for y in ys)
|
||||
xs = [x - min_x for x in xs]
|
||||
ys = [y - min_y for y in ys]
|
||||
return pairmap, xs, ys
|
||||
|
||||
|
||||
def _strand_spans(display_sequence):
|
||||
spans = []
|
||||
start = None
|
||||
for index, char in enumerate(display_sequence):
|
||||
if char == " ":
|
||||
if start is not None:
|
||||
spans.append((start, index - 1))
|
||||
start = None
|
||||
continue
|
||||
if start is None:
|
||||
start = index
|
||||
if start is not None:
|
||||
spans.append((start, len(display_sequence) - 1))
|
||||
return spans
|
||||
|
||||
|
||||
def render_split_strands_svg(strand_sequences, structure, title=None):
|
||||
if not strand_sequences:
|
||||
raise ValueError("Split-strand layout requires at least one strand.")
|
||||
|
||||
display_sequence = " ".join(str(sequence) for sequence in strand_sequences)
|
||||
display_structure = str(structure).replace("+", " ")
|
||||
|
||||
if len(display_sequence) != len(display_structure):
|
||||
raise ValueError("Sequence and structure length mismatch for split-strand layout.")
|
||||
|
||||
pairmap, xs, ys = _layout_positions(display_structure)
|
||||
visible_indices = [index for index, char in enumerate(display_sequence) if char != " "]
|
||||
if not visible_indices:
|
||||
raise ValueError("Split-strand layout produced no visible residues.")
|
||||
|
||||
min_x = min(xs[index] for index in visible_indices) - NODE_RADIUS - PADDING
|
||||
min_y = min(ys[index] for index in visible_indices) - NODE_RADIUS - PADDING
|
||||
max_x = max(xs[index] for index in visible_indices) + NODE_RADIUS + PADDING
|
||||
max_y = max(ys[index] for index in visible_indices) + NODE_RADIUS + PADDING
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
def tx(index):
|
||||
return xs[index] - min_x
|
||||
|
||||
def ty(index):
|
||||
return ys[index] - min_y
|
||||
|
||||
compact_index = {}
|
||||
compact_counter = 1
|
||||
for index in visible_indices:
|
||||
compact_index[index] = compact_counter
|
||||
compact_counter += 1
|
||||
|
||||
svg_lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>',
|
||||
(
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{_fmt(width)}" height="{_fmt(height)}" '
|
||||
f'viewBox="0 0 {_fmt(width)} {_fmt(height)}" preserveAspectRatio="xMidYMid meet" '
|
||||
'data-plot-engine="split-strands">'
|
||||
),
|
||||
]
|
||||
if title:
|
||||
svg_lines.append(f"<title>{escape(title)}</title>")
|
||||
svg_lines.extend(
|
||||
[
|
||||
' <script type="text/ecmascript">',
|
||||
" <![CDATA[",
|
||||
" var shown = 1;",
|
||||
" function click() {",
|
||||
' var seq = document.getElementById("seq");',
|
||||
" if (shown==1) {",
|
||||
' seq.setAttribute("style", "visibility: hidden");',
|
||||
" shown = 0;",
|
||||
" } else {",
|
||||
' seq.setAttribute("style", "visibility: visible");',
|
||||
" shown = 1;",
|
||||
" }",
|
||||
" }",
|
||||
" ]]>",
|
||||
" </script>",
|
||||
' <style type="text/css">',
|
||||
" <![CDATA[",
|
||||
" .nucleotide {",
|
||||
" font-family: SansSerif;",
|
||||
" }",
|
||||
" .backbone {",
|
||||
" stroke: grey;",
|
||||
" fill: none;",
|
||||
" stroke-width: 1.5;",
|
||||
" }",
|
||||
" .basepairs {",
|
||||
" stroke: red;",
|
||||
" fill: none;",
|
||||
" stroke-width: 2.5;",
|
||||
" }",
|
||||
" ]]>",
|
||||
" </style>",
|
||||
"",
|
||||
f' <rect style="stroke: white; fill: white" height="{_fmt(height)}" x="0" y="0" width="{_fmt(width)}" onclick="click(evt)" />',
|
||||
' <g transform="scale(1,1) translate(0,0)">',
|
||||
]
|
||||
)
|
||||
|
||||
for strand_index, (start, end) in enumerate(_strand_spans(display_sequence), start=1):
|
||||
points = " ".join(
|
||||
f"{_fmt3(tx(index))},{_fmt3(ty(index))}"
|
||||
for index in range(start, end + 1)
|
||||
)
|
||||
svg_lines.append(f' <polyline class="backbone" id="outline-{strand_index}" points=" {points} " />')
|
||||
|
||||
svg_lines.append(' <g id="pairs">')
|
||||
for left, right in enumerate(pairmap):
|
||||
if right <= left or display_sequence[left] == " " or display_sequence[right] == " ":
|
||||
continue
|
||||
svg_lines.append(
|
||||
f' <line class="basepairs" id="{compact_index[left]},{compact_index[right]}" '
|
||||
f'x1="{_fmt(tx(left))}" y1="{_fmt(ty(left))}" '
|
||||
f'x2="{_fmt(tx(right))}" y2="{_fmt(ty(right))}" />'
|
||||
)
|
||||
svg_lines.append(" </g>")
|
||||
|
||||
svg_lines.append(' <g transform="translate(-4.6, 4)" id="seq">')
|
||||
for index in visible_indices:
|
||||
base = display_sequence[index]
|
||||
svg_lines.append(
|
||||
f' <text class="nucleotide" x="{_fmt3(tx(index))}" y="{_fmt3(ty(index))}">{escape(base)}</text>'
|
||||
)
|
||||
svg_lines.append(" </g>")
|
||||
|
||||
compact_sequence = display_sequence.replace(" ", "")
|
||||
basepair_rows = [
|
||||
f' {{ i: {compact_index[left]}, j: {compact_index[right]}, type: "cWW" }}'
|
||||
for left, right in enumerate(pairmap)
|
||||
if right > left and display_sequence[left] != " " and display_sequence[right] != " "
|
||||
]
|
||||
coord_rows = [
|
||||
f' {{ x: {_fmt3(tx(index))}, y: {_fmt3(ty(index))} }}'
|
||||
for index in visible_indices
|
||||
]
|
||||
svg_lines.extend(
|
||||
[
|
||||
" </g>",
|
||||
'<script type="text/ecmascript">',
|
||||
"<![CDATA[",
|
||||
f" let sequence = {json.dumps(compact_sequence)};",
|
||||
f" let structure = {json.dumps(str(structure))};",
|
||||
" const basepairs = [",
|
||||
",\n".join(basepair_rows) + ("" if not basepair_rows else ""),
|
||||
" ];",
|
||||
" const coords = [",
|
||||
",\n".join(coord_rows) + ("" if not coord_rows else ""),
|
||||
" ];",
|
||||
"]]>",
|
||||
"</script>",
|
||||
"</svg>",
|
||||
]
|
||||
)
|
||||
return "\n".join(svg_lines)
|
||||
Loading…
Add table
Add a link
Reference in a new issue