Optimize nupack service startup and compute
This commit is contained in:
commit
e1b3156754
25 changed files with 9435 additions and 0 deletions
19
.dockerignore
Normal file
19
.dockerignore
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.git
|
||||
.gitignore
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
.DS_Store
|
||||
other
|
||||
nupack-history-*.json
|
||||
NUPACK.html
|
||||
NUPACK_files
|
||||
1.png
|
||||
nupack/vendor/nupack-4.0.2.0/source
|
||||
nupack/vendor/nupack-4.0.2.0/package/*.whl
|
||||
!nupack/vendor/nupack-4.0.2.0/package/nupack-4.0.2.0-cp312-cp312-linux_x86_64.whl
|
||||
rna/*
|
||||
!rna/ViennaRNA-2.7.2.tar.gz
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.DS_Store
|
||||
|
||||
# Local exports and captured reference site assets are not needed to rebuild the service.
|
||||
nupack-history-*.json
|
||||
NUPACK.html
|
||||
NUPACK_files/
|
||||
1.png
|
||||
|
||||
# The Docker build uses the vendored cp312 Linux x86_64 wheel plus the ViennaRNA tarball.
|
||||
rna/ViennaRNA-2.7.2/
|
||||
rna/nupack-4.0.2.0.zip
|
||||
nupack/vendor/nupack-4.0.2.0/source/
|
||||
nupack/vendor/nupack-4.0.2.0/package/*.whl
|
||||
!nupack/vendor/nupack-4.0.2.0/package/nupack-4.0.2.0-cp312-cp312-linux_x86_64.whl
|
||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV NP_HOST=0.0.0.0
|
||||
ENV NP_PORT=18765
|
||||
ENV ENABLE_RNAPLOT=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
automake \
|
||||
build-essential \
|
||||
libgsl-dev \
|
||||
libtool \
|
||||
pkg-config \
|
||||
perl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY nupack/vendor/nupack-4.0.2.0/package /tmp/nupack-package
|
||||
COPY rna/ViennaRNA-2.7.2.tar.gz /tmp/ViennaRNA-2.7.2.tar.gz
|
||||
|
||||
RUN cd /tmp \
|
||||
&& tar -xzf ViennaRNA-2.7.2.tar.gz \
|
||||
&& cd ViennaRNA-2.7.2 \
|
||||
&& ./configure --without-python --without-perl \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& ldconfig \
|
||||
&& rm -rf /tmp/ViennaRNA-2.7.2 /tmp/ViennaRNA-2.7.2.tar.gz
|
||||
|
||||
RUN python -m pip install --no-cache-dir \
|
||||
numpy \
|
||||
scipy \
|
||||
pandas \
|
||||
pyyaml \
|
||||
jinja2 \
|
||||
/tmp/nupack-package/nupack-4.0.2.0-cp312-cp312-linux_x86_64.whl
|
||||
|
||||
RUN python -m pip install --no-cache-dir redis
|
||||
|
||||
WORKDIR /app/service
|
||||
COPY service /app/service
|
||||
|
||||
EXPOSE 18765
|
||||
|
||||
CMD ["python", "server.py"]
|
||||
56
README.md
Normal file
56
README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# NP Replica
|
||||
|
||||
This project provides a local Chinese web replica of the core NUPACK `analysis/input` workflow.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:18765`.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
The service listens on port `18765`. You can later reverse proxy `np.lihato.icu` to this port from your public server.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /`
|
||||
- `GET /health`
|
||||
- `GET /api/example`
|
||||
- `POST /api/analyze`
|
||||
- `POST /api/jobs`
|
||||
- `GET /api/jobs/<job_id>`
|
||||
|
||||
## Notes
|
||||
|
||||
- `tube` mode supports strand concentrations and `SetSpec(max_size=...)`.
|
||||
- `complex` mode supports explicit complex definitions such as `A+B`.
|
||||
- Pair matrices are returned as a preview block to keep responses manageable.
|
||||
- The Docker image compiles ViennaRNA `RNAplot` from `rna/ViennaRNA-2.7.2.tar.gz` and embeds SVG structure plots into MFE results.
|
||||
- Multistrand MFE plots now use a split-strand layout by default when a structure contains `+`, with `RNAplot` retained as the fallback path.
|
||||
- The web UI now supports Chinese and English switching, browser-side history, result export, and async polling for submitted jobs.
|
||||
- Docker now starts three services: `np-replica`, `np-worker`, and `redis`.
|
||||
- Async jobs use Redis-backed storage and queueing when `NP_REDIS_URL` is configured; otherwise the app falls back to the older in-memory mode.
|
||||
- The browser-facing API and page behavior stay the same after enabling Redis-backed jobs.
|
||||
- Worker concurrency is controlled by `NP_WORKER_CONCURRENCY` and defaults to `2`.
|
||||
- Per-job native thread usage is capped by `NP_PER_JOB_THREAD_LIMIT` and defaults to `1` to prevent a single analysis from monopolizing the machine.
|
||||
- `NP_STRUCTURE_PLOT_MODE` supports `auto` (default), `split`, and `rnaplot`.
|
||||
- Static pages are served with ETag-based browser caching; JSON API responses remain uncached.
|
||||
- The force-directed structure viewer lazy-loads D3 only when that view is opened, so the initial page load is not blocked by the external CDN.
|
||||
- `/health` uses Redis queue length plus a running-job set instead of scanning every historical job on each poll.
|
||||
|
||||
|
||||
|
||||
现在 `docker-compose.yml` 面向 64 核 / 64G WSL 服务器的并发策略是:
|
||||
|
||||
- 最多同时跑 16 个任务
|
||||
- 每个任务最多用 4 个计算线程
|
||||
- 后续任务进入队列等待
|
||||
- 理论上最多占用约 64 个 native 计算线程,避免 16 × 8 这类过度超卖
|
||||
|
||||
如果后面你发现机器还会被压满,最直接的调法就是在 docker-compose.yml 里继续压:
|
||||
|
||||
- 把 NP_WORKER_CONCURRENCY 改小
|
||||
- 或保持并发不变,把 NP_PER_JOB_THREAD_LIMIT 改成 2 或 1
|
||||
30
design(2).txt
Normal file
30
design(2).txt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from nupack import *
|
||||
|
||||
my_model = Model(material='dna', celsius=37,sodium=0.1, magnesium=0.02)
|
||||
|
||||
c = Domain('N8', name='c')
|
||||
cc = Domain('N3', name='cc')
|
||||
cc2 = Domain('N3', name='cc2')
|
||||
d = Domain('N8', name='d')
|
||||
e = Domain('N7R1', name='e')
|
||||
f = Domain('N10', name='f')
|
||||
g = Domain('N8', name='g')
|
||||
k = Domain('N8', name='k')
|
||||
h = Domain('N15R1', name='h')
|
||||
hh = Domain('N6', name='hh')
|
||||
E = Domain('GGCTAGCTACAACGA', name='E')
|
||||
|
||||
E1 = TargetStrand([h, E, ~e, ~c], name='Strand E1')
|
||||
F1 = TargetStrand([~f, ~d, ~g, ~k, f, c, e, hh], name='Strand F1')
|
||||
|
||||
E1F1 = TargetComplex([E1, F1], '.10(6.15(16+(10.24)32', name='E1F1')
|
||||
|
||||
t1 = TargetTube(on_targets={E1F1: 1e-8}, name='t1',
|
||||
off_targets=SetSpec(max_size=4))
|
||||
|
||||
pattern = Pattern(['A4', 'C4', 'G4', 'U4', 'T4'])
|
||||
my_tubes = [t1,t2]
|
||||
my_design = tube_design(tubes=my_tubes,
|
||||
hard_constraints=[pattern],model=my_model)
|
||||
my_result = my_design.run(trials=1)
|
||||
print(my_result)
|
||||
56
docker-compose.yml
Normal file
56
docker-compose.yml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
services:
|
||||
redis:
|
||||
image: redis:7.4.8-bookworm
|
||||
container_name: np-redis
|
||||
restart: always
|
||||
mem_limit: 2g
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
|
||||
np-replica:
|
||||
image: np-app:v4.2
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: np-replica
|
||||
restart: always
|
||||
mem_limit: 4g
|
||||
depends_on:
|
||||
- redis
|
||||
ports:
|
||||
- "18765:18765"
|
||||
environment:
|
||||
NP_HOST: 0.0.0.0
|
||||
NP_PORT: 18765
|
||||
NP_RUN_MODE: server
|
||||
NP_REDIS_URL: redis://redis:6379/0
|
||||
NP_JOB_TTL_SECONDS: 3600
|
||||
NP_WORKER_CONCURRENCY: 16
|
||||
NP_PER_JOB_THREAD_LIMIT: 4
|
||||
NP_NUPACK_CACHE_GB: 8.0
|
||||
OMP_NUM_THREADS: 4
|
||||
OPENBLAS_NUM_THREADS: 4
|
||||
MKL_NUM_THREADS: 4
|
||||
NUMEXPR_NUM_THREADS: 4
|
||||
VECLIB_MAXIMUM_THREADS: 4
|
||||
GOTO_NUM_THREADS: 4
|
||||
|
||||
np-worker:
|
||||
image: np-app:v4.2
|
||||
container_name: np-worker
|
||||
restart: always
|
||||
mem_limit: 56g
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
NP_RUN_MODE: worker
|
||||
NP_REDIS_URL: redis://redis:6379/0
|
||||
NP_JOB_TTL_SECONDS: 3600
|
||||
NP_WORKER_CONCURRENCY: 16
|
||||
NP_PER_JOB_THREAD_LIMIT: 4
|
||||
NP_NUPACK_CACHE_GB: 8.0
|
||||
OMP_NUM_THREADS: 4
|
||||
OPENBLAS_NUM_THREADS: 4
|
||||
MKL_NUM_THREADS: 4
|
||||
NUMEXPR_NUM_THREADS: 4
|
||||
VECLIB_MAXIMUM_THREADS: 4
|
||||
GOTO_NUM_THREADS: 4
|
||||
39
need.md
Normal file
39
need.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
请修改当前 RNA 二级结构可视化中的“配对概率”视图,重点不是简单美化,而是修正可视化语义和可读性。
|
||||
如图:[[1.png]]
|
||||
当前问题:
|
||||
1. 现在用同一种绿色的不同透明度表示概率,用户几乎无法区分不同概率值。
|
||||
2. 当前视图容易让人误解“配对概率”到底表示节点概率还是边概率。
|
||||
3. 小尺寸视图下,透明度编码效果很差,截图后更不清楚。
|
||||
4. 右侧图例是 0~1,但图中节点颜色变化不够清晰。
|
||||
|
||||
修改目标:
|
||||
1. 不要再使用“同色 + alpha透明度”作为主编码。
|
||||
2. 改成“固定不透明度 + 明确的颜色梯度”来表示数值大小。
|
||||
3. 优先使用单调、感知一致的 colormap(例如 viridis / plasma / magma),不要用当前这种浅绿色透明度方案。
|
||||
4. 节点颜色必须在 0~1 范围内有清楚区分,低值和高值要一眼能看出。
|
||||
5. 右侧 colorbar 要和图中实际颜色完全一致。
|
||||
6. 保持当前布局基本不变,不要先大改 UI。
|
||||
|
||||
语义要求:
|
||||
1. 如果当前数据实际上是“每个碱基的边际配对倾向 / per-base probability”,那么按钮或图例文字不要再直接写 Pair probabilities,而要改成更准确的名称,例如:
|
||||
- Per-base pairing probability
|
||||
- Base-wise pairing score
|
||||
- Marginal pairing probability
|
||||
2. 如果当前数据确实是配对矩阵 P(i,j),那就不要只给节点上色,后续需要支持把概率映射到配对边上。
|
||||
|
||||
这一步先做的事情:
|
||||
1. 先保留当前节点着色模式。
|
||||
2. 去掉 alpha 映射,改成纯颜色映射。
|
||||
3. 调整图例标题,使其准确表达“节点值”而不是“边值”。
|
||||
4. 检查节点、文字、主链、配对线在高低概率下是否仍然清晰可读。
|
||||
|
||||
验收标准:
|
||||
1. 同一颜色不同透明度的问题被彻底移除。
|
||||
2. 在小图(类似当前截图尺寸)下,0.2、0.5、0.8 三档能明显区分。
|
||||
3. 用户不会再把这个图误解为“边概率图”。
|
||||
4. 代码尽量少改,先保证现有功能可用。
|
||||
|
||||
请直接修改代码,并说明:
|
||||
1. 改了哪些绘图参数
|
||||
2. 改了哪些 label / title / legend 文案
|
||||
3. 如果当前数据语义和 Pair probabilities 不一致,请明确指出并给出更合适命名
|
||||
64
nupack/app.py
Normal file
64
nupack/app.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from typing import List
|
||||
from nupack import Model, Strand, Tube, SetSpec, tube_analysis
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class ModelInput(BaseModel):
|
||||
material: str = "rna"
|
||||
ensemble: str = "stacking"
|
||||
celsius: float = 37
|
||||
sodium: float = 1.0
|
||||
magnesium: float = 0.0
|
||||
|
||||
class StrandInput(BaseModel):
|
||||
name: str
|
||||
sequence: str
|
||||
concentration: float
|
||||
unit: str = "uM"
|
||||
|
||||
class TubeInput(BaseModel):
|
||||
name: str = "tube1"
|
||||
max_size: int = 2
|
||||
|
||||
class AnalysisPayload(BaseModel):
|
||||
model: ModelInput
|
||||
strands: List[StrandInput]
|
||||
tube: TubeInput
|
||||
|
||||
def unit_to_molar(value: float, unit: str) -> float:
|
||||
mapping = {"M": 1, "mM": 1e-3, "uM": 1e-6, "nM": 1e-9, "pM": 1e-12}
|
||||
return value * mapping[unit]
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/run")
|
||||
def run(payload: AnalysisPayload):
|
||||
model = Model(
|
||||
material=payload.model.material,
|
||||
ensemble=payload.model.ensemble,
|
||||
celsius=payload.model.celsius,
|
||||
sodium=payload.model.sodium,
|
||||
magnesium=payload.model.magnesium,
|
||||
)
|
||||
|
||||
strands = {}
|
||||
for s in payload.strands:
|
||||
obj = Strand(s.sequence, name=s.name)
|
||||
strands[obj] = unit_to_molar(s.concentration, s.unit)
|
||||
|
||||
tube = Tube(
|
||||
strands=strands,
|
||||
complexes=SetSpec(max_size=payload.tube.max_size),
|
||||
name=payload.tube.name,
|
||||
)
|
||||
|
||||
result = tube_analysis(tubes=[tube], model=model)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"result": str(result)
|
||||
}
|
||||
8
nupack/requirements.txt
Normal file
8
nupack/requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fastapi
|
||||
uvicorn[standard]
|
||||
numpy
|
||||
scipy
|
||||
pip
|
||||
matplotlib
|
||||
pandas
|
||||
jupyterlab
|
||||
19
nupack/vendor/nupack-4.0.2.0/LICENSE.txt
vendored
Normal file
19
nupack/vendor/nupack-4.0.2.0/LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
NUPACK Software License Agreement for Non-Commercial Academic Use
|
||||
|
||||
Copyright (c) 2003–2022. California Institute of Technology. All rights reserved.
|
||||
|
||||
Use of the NUPACK Python module and/or source code (“Software”) in source form and/or binary form, with or without modification, is permitted for non-commercial academic purposes only, subject to the conditions and disclaimer stated below.
|
||||
|
||||
Conditions
|
||||
|
||||
1. Redistribution of the Software in source form and/or binary form is not permitted.
|
||||
2. Web applications that use the Software in source form and/or binary form are not permitted.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote derivative works without specific prior written permission.
|
||||
|
||||
Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Contact
|
||||
|
||||
For any questions about this Software License Agreement please contact info@nupack.org
|
||||
9
nupack/vendor/nupack-4.0.2.0/README.txt
vendored
Normal file
9
nupack/vendor/nupack-4.0.2.0/README.txt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
README file for NUPACK 4.0
|
||||
Copyright (c) 2003-2022. California Institute of Technology. All Rights Reserved.
|
||||
|
||||
See LICENSE.txt file
|
||||
|
||||
See NUPACK User Guide for installation instructions:
|
||||
https://docs.nupack.org
|
||||
|
||||
Technical support: support@nupack.org
|
||||
19
nupack/vendor/nupack-4.0.2.0/package/LICENSE.txt
vendored
Normal file
19
nupack/vendor/nupack-4.0.2.0/package/LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
NUPACK Software License Agreement for Non-Commercial Academic Use
|
||||
|
||||
Copyright (c) 2003–2022. California Institute of Technology. All rights reserved.
|
||||
|
||||
Use of the NUPACK Python module and/or source code (“Software”) in source form and/or binary form, with or without modification, is permitted for non-commercial academic purposes only, subject to the conditions and disclaimer stated below.
|
||||
|
||||
Conditions
|
||||
|
||||
1. Redistribution of the Software in source form and/or binary form is not permitted.
|
||||
2. Web applications that use the Software in source form and/or binary form are not permitted.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote derivative works without specific prior written permission.
|
||||
|
||||
Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Contact
|
||||
|
||||
For any questions about this Software License Agreement please contact info@nupack.org
|
||||
BIN
nupack/vendor/nupack-4.0.2.0/package/nupack-4.0.2.0-cp312-cp312-linux_x86_64.whl
vendored
Normal file
BIN
nupack/vendor/nupack-4.0.2.0/package/nupack-4.0.2.0-cp312-cp312-linux_x86_64.whl
vendored
Normal file
Binary file not shown.
460
rna/TrnaStructureBeautifier.py
Normal file
460
rna/TrnaStructureBeautifier.py
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
# _*_ coding:utf-8 _*_
|
||||
#
|
||||
# @Version : 1.1
|
||||
# @Project : https://github.com/shueho/BioDataTools
|
||||
# @Time : 2025/7/23 20:00
|
||||
# @Update : 2025/8/13 14:00
|
||||
# @Author : Hao Xue
|
||||
# @E-mail : studid@163.com
|
||||
# @File : TrnaStructureBeautifier.py
|
||||
#
|
||||
# Enhancement of tRNA secondary structure diagrams generated using the ViennaRNA package.
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
|
||||
|
||||
# old = sys.argv[1]
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description="RNA结构图美化参数配置")
|
||||
|
||||
# === 输入控制 ===
|
||||
parser.add_argument(
|
||||
"-i", "--input",
|
||||
type=str,
|
||||
required=True,
|
||||
help="SVG文件或者SVG文件存放的文件夹路径"
|
||||
)
|
||||
|
||||
# === 基础布局 ===
|
||||
parser.add_argument(
|
||||
"-s", "--size-weight",
|
||||
type=float,
|
||||
default=1.4,
|
||||
help="图形缩放比例(默认 1.4)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--per-row",
|
||||
type=int,
|
||||
default=4,
|
||||
help="每行图片数量(默认 4)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-hg", "--horizontal-gap",
|
||||
type=int,
|
||||
default=8,
|
||||
help="图片水平间隔(默认 8)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-vg", "--vertical-gap",
|
||||
type=int,
|
||||
default=5,
|
||||
help="图片垂直间隔(默认 5)"
|
||||
)
|
||||
|
||||
# === 碱基连线颜色 ===
|
||||
parser.add_argument(
|
||||
"-ac", "--adjacent-color",
|
||||
type=str,
|
||||
default="blue",
|
||||
help='相邻碱基连线颜色(支持名称或 HEX,如 "blue" 或 "#00FF00",默认 "blue")'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-pc", "--pair-color",
|
||||
type=str,
|
||||
default="red",
|
||||
help='配对碱基连线颜色(支持名称或 HEX,如 "red" 或 "#FF0000",默认 "red")'
|
||||
)
|
||||
|
||||
# === 碱基圆圈样式 ===
|
||||
parser.add_argument(
|
||||
"-bf", "--base-fill",
|
||||
type=str,
|
||||
default="white",
|
||||
help='碱基圆圈填充色(默认 "white")'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-bs", "--base-stroke",
|
||||
type=str,
|
||||
default="black",
|
||||
help='碱基圆圈轮廓色(默认 "black")'
|
||||
)
|
||||
|
||||
# === 碱基美化图 ===
|
||||
parser.add_argument(
|
||||
"-A", "--base-a",
|
||||
type=str,
|
||||
default="red",
|
||||
help='美化A碱基圆圈填充色(默认 "red")'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-U", "--base-u",
|
||||
type=str,
|
||||
default="blue",
|
||||
help='美化U/T碱基圆圈填充色(默认 "blue")'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-G", "--base-g",
|
||||
type=str,
|
||||
default="green",
|
||||
help='美化G碱基圆圈填充色(默认 "green")'
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C", "--base-c",
|
||||
type=str,
|
||||
default="yellow",
|
||||
help='美化C碱基圆圈填充色(默认 "yellow")'
|
||||
)
|
||||
|
||||
# === 反密码子控制 ===
|
||||
# parser.add_argument(
|
||||
# "-af", "--anticodon-file",
|
||||
# type=str,
|
||||
# default=None,
|
||||
# help="反密码子位点文件路径(文本文件,每行一个位置)"
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# "-aF", "--anti-fill",
|
||||
# type=str,
|
||||
# default="red",
|
||||
# help='反密码子圆圈填充色(默认 "red")'
|
||||
# )
|
||||
# parser.add_argument(
|
||||
# "-aS", "--anti-stroke",
|
||||
# type=str,
|
||||
# default="black",
|
||||
# help='反密码子圆圈轮廓色(默认 "black")'
|
||||
# )
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
|
||||
args = parse_arguments()
|
||||
base_color = {"A": args.base_a, "U": args.base_u, "G": args.base_g, "C": args.base_c, }
|
||||
# 设置目标文件夹名称
|
||||
modified_folder = "modified"
|
||||
# 创建文件夹(如果已存在则忽略)
|
||||
os.makedirs(modified_folder, exist_ok=True)
|
||||
print("已创建文件夹: modified")
|
||||
|
||||
|
||||
def read_svg(spath):
|
||||
with open(spath) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def set_new_text(x_y_char, fill_=args.base_fill, stroke_=args.base_stroke):
|
||||
x = x_y_char[0]
|
||||
y = x_y_char[1]
|
||||
t = x_y_char[2].upper().replace("T", "U")
|
||||
# 核心修改:保持圆圈半径r=5不变,调整dy=0使字母在垂直方向也居中(结合text-anchor=middle实现正中心)
|
||||
return '<circle cx="{}" cy="{}" r="5" fill="{}" stroke="{}" />\n <text x="{}" y="{}" text-anchor="middle" dy="2"\n font-size="8" fill="black">{}</text>\n'.format(
|
||||
x, y, fill_, stroke_, x, y, t)
|
||||
# r=圆圈半径,dy=字体垂直位移,font-size=字体大小
|
||||
|
||||
def set_new_lines(x_y_char_1, x_y_char_2, stroke_):
|
||||
x1, y1, _ = x_y_char_1
|
||||
x2, y2, _ = x_y_char_2
|
||||
xy = (x1, y1, x2, y2)
|
||||
return '<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="1"/>\n'.format(*xy, stroke_)
|
||||
|
||||
|
||||
# 保留原有 get_add_line 函数,不修改(保持代码秩序)
|
||||
def get_add_line(svg_text):
|
||||
# pattern = r'<line.*?x1="([^"]+)"[^>]*?y1="([^"]+)"[^>]*?x2="([^"]+)"[^>]*?y2="([^"]+)"'
|
||||
pattern = r'<line\b[^>]*\s*x1="([^"]+)"[^>]*\s*y1="([^"]+)"[^>]*\s*x2="([^"]+)"[^>]*\s*y2="([^"]+)"'
|
||||
matches = re.findall(pattern, svg_text)
|
||||
result = []
|
||||
for x1_str, y1_str, x2_str, y2_str in matches:
|
||||
try:
|
||||
x1 = float(x1_str)
|
||||
y1 = float(y1_str)
|
||||
x2 = float(x2_str)
|
||||
y2 = float(y2_str)
|
||||
result.append(set_new_lines((x1, y1, ""), (x2, y2, ""), args.pair_color))
|
||||
except ValueError:
|
||||
# 如果坐标无法转换为 float,跳过该元素
|
||||
continue
|
||||
return "".join(result)
|
||||
|
||||
|
||||
# 核心修改1:extract_text_info 补充返回坐标边界(用于居中计算),生成内容逻辑完全不变
|
||||
def extract_text_info(svg_text):
|
||||
# 正则表达式匹配 <text> 标签中的 x、y 和字符内容
|
||||
# pattern = r'<text\s+x="([^"]+)"\s+y="([^"]+)"[^>]*>([^<]+)</text>'
|
||||
pattern = r'<text\b[^>]*\s*x="([^"]+)"[^>]*\s*y="([^"]+)"[^>]*>(.*?)<\/text>'
|
||||
matches = re.findall(pattern, svg_text)
|
||||
result = []
|
||||
for x_str, y_str, char in matches:
|
||||
try:
|
||||
x = float(x_str)
|
||||
y = float(y_str)
|
||||
result.append((x, y, char))
|
||||
except ValueError:
|
||||
# 如果 x 或 y 无法转换为 float,跳过该元素
|
||||
continue
|
||||
|
||||
# 计算坐标边界(新增:用于后续居中)
|
||||
if not result:
|
||||
min_x = max_x = min_y = max_y = 0
|
||||
else:
|
||||
xs = [p[0] for p in result]
|
||||
ys = [p[1] for p in result]
|
||||
min_x, max_x = min(xs), max(xs)
|
||||
min_y, max_y = min(ys), max(ys)
|
||||
|
||||
# 原有生成内容逻辑完全不变(保证字母在圆圈内)
|
||||
content = '<g style="font-family: Times New Roman" transform="translate(-4.6, 4)" id="seq">\n'
|
||||
content += get_add_line(svg_text)
|
||||
for i in range(1, len(result)):
|
||||
# content += set_new_text(result[i])
|
||||
content += set_new_lines(result[i - 1], result[i], args.adjacent_color)
|
||||
content += set_new_text(result[i - 1])
|
||||
|
||||
content += set_new_text(result[len(result) - 1]) + '</g>\n'
|
||||
|
||||
# 新增返回坐标边界,用于居中计算
|
||||
return content, min_x, max_x, min_y, max_y
|
||||
|
||||
|
||||
def save_svg(content, outpath):
|
||||
with open(outpath, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
# 核心修改2:modi_svg 改为超大画布 + 自动居中,保留原有逻辑框架
|
||||
def modi_svg(path):
|
||||
t = read_svg(path)
|
||||
name_ = os.path.basename(path)
|
||||
if "-" in name_:
|
||||
name_ = name_.split("-")[0]
|
||||
else:
|
||||
name_ = name_.split(".")[0]
|
||||
|
||||
# 提取内容 + 坐标边界(新增)
|
||||
mid_, min_x, max_x, min_y, max_y = extract_text_info(t)
|
||||
scale = args.size_weight
|
||||
|
||||
# 1. 超大画布(2000×2000,可自行调大)
|
||||
canvas_w = 2000
|
||||
canvas_h = 2000
|
||||
|
||||
# 2. 计算结构中心(用于居中)
|
||||
if min_x == max_x and min_y == max_y:
|
||||
cx, cy = 0, 0
|
||||
else:
|
||||
cx = (min_x + max_x) / 2 # 结构水平中心
|
||||
cy = (min_y + max_y) / 2 # 结构垂直中心
|
||||
|
||||
# 3. 计算居中偏移量(保证结构在画布正中间)
|
||||
translate_x = (canvas_w / 2) / scale - cx # 水平居中
|
||||
translate_y = (canvas_h / 2) / scale - cy # 垂直居中
|
||||
|
||||
# 保留原有 head_ 结构,仅替换画布尺寸和偏移量,同时修改名称位置避免重叠
|
||||
head_ = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="{}" width="{}" viewBox="0 0 {} {}">
|
||||
<rect style="stroke: none; fill: none" height="{}" x="0" y="0" width="{}" onclick="click(evt)" />
|
||||
<g transform="scale({},{}) translate({:.2f},{:.2f})">
|
||||
<g style="font-family: Times New Roman" id="name">
|
||||
<text font-size="20" x="{:.2f}" y="{:.2f}" text-anchor="middle">{}</text>
|
||||
</g>
|
||||
'''.format(canvas_h, canvas_w, canvas_w, canvas_h, canvas_h, canvas_w, scale, scale, translate_x, translate_y, cx, min_y - 20, name_)
|
||||
|
||||
tail_ = ' </g>\n</svg>'
|
||||
save_svg(head_ + mid_ + tail_, "modified/modified_" + name_ + ".svg")
|
||||
return name_
|
||||
|
||||
|
||||
# save_svg(modi_svg(old), "new.svg")
|
||||
def remove_xml_declaration(content):
|
||||
"""Remove XML declaration from SVG content."""
|
||||
return re.sub(r'<\?xml[^>]*\?>\s*', '', content)
|
||||
|
||||
|
||||
def extract_dimensions(svg_content):
|
||||
"""Extract width and height from SVG content."""
|
||||
width_match = re.search(r'width="([^"]+)"', svg_content)
|
||||
height_match = re.search(r'height="([^"]+)"', svg_content)
|
||||
if not width_match or not height_match:
|
||||
raise ValueError("Could not extract width or height")
|
||||
return width_match.group(1), height_match.group(1)
|
||||
|
||||
|
||||
def merge_horizontal_group(svg_files, output_file):
|
||||
"""Merge SVGs in a group horizontally."""
|
||||
svgs = []
|
||||
total_width = 0
|
||||
max_height = 0
|
||||
gap = args.horizontal_gap
|
||||
|
||||
for filename in svg_files:
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read()
|
||||
content = remove_xml_declaration(content)
|
||||
width, height = extract_dimensions(content)
|
||||
try:
|
||||
width_px = float(width.strip('px'))
|
||||
height_px = float(height.strip('px'))
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid unit in file {filename}")
|
||||
|
||||
svgs.append({
|
||||
'filename': filename,
|
||||
'content': content,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'width_px': width_px,
|
||||
'height_px': height_px
|
||||
})
|
||||
|
||||
total_width += sum(svg_info['width_px'] for svg_info in svgs) + gap * (len(svgs) - 1)
|
||||
max_height = max(max_height, height_px)
|
||||
|
||||
new_svg = [
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">'.format(total_width, max_height)
|
||||
]
|
||||
|
||||
current_x = 0.0
|
||||
for svg_info in svgs:
|
||||
inner_content = re.sub(r'<svg[^>]*>', '', svg_info['content'])
|
||||
inner_content = re.sub(r'</svg>', '', inner_content)
|
||||
new_svg.append('<g transform="translate({:.2f}, 0)">'.format(current_x))
|
||||
new_svg.append(inner_content)
|
||||
new_svg.append('</g>')
|
||||
current_x += svg_info['width_px'] + gap
|
||||
|
||||
new_svg.append('</svg>')
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
f.write('\n'.join(new_svg))
|
||||
|
||||
|
||||
def merge_vertical_groups(group_files, output_file):
|
||||
"""Merge all horizontal groups vertically."""
|
||||
svgs = []
|
||||
max_width = 0
|
||||
total_height = 0
|
||||
gap = args.vertical_gap
|
||||
|
||||
for filename in group_files:
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read()
|
||||
content = remove_xml_declaration(content)
|
||||
width, height = extract_dimensions(content)
|
||||
try:
|
||||
width_px = float(width.strip('px'))
|
||||
height_px = float(height.strip('px'))
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid unit in file {filename}")
|
||||
|
||||
svgs.append({
|
||||
'filename': filename,
|
||||
'content': content,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'width_px': width_px,
|
||||
'height_px': height_px
|
||||
})
|
||||
|
||||
max_width = max(max_width, width_px)
|
||||
total_height += sum(svg_info['height_px'] for svg_info in svgs) + gap * (len(svgs) - 1)
|
||||
|
||||
new_svg = [
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">'.format(max_width, total_height)
|
||||
]
|
||||
|
||||
current_y = 0.0
|
||||
for svg_info in svgs:
|
||||
inner_content = re.sub(r'<svg[^>]*>', '', svg_info['content'])
|
||||
inner_content = re.sub(r'</svg>', '', inner_content)
|
||||
new_svg.append('<g transform="translate(0, {:.2f})">'.format(current_y))
|
||||
new_svg.append(inner_content)
|
||||
new_svg.append('</g>')
|
||||
current_y += svg_info['height_px'] + gap
|
||||
|
||||
new_svg.append('</svg>')
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
f.write('\n'.join(new_svg))
|
||||
|
||||
|
||||
def update_circle_colors(svg_content, base_color_map):
|
||||
new_ = ""
|
||||
ls = svg_content.split("\n")
|
||||
for i in range(len(ls)):
|
||||
if "<circle cx" in ls[i]:
|
||||
flag = re.findall(r'(?<=>)[A-Z](?=<\/text>)', ls[i + 2])[0]
|
||||
if flag not in base_color_map:
|
||||
new_ += ls[i] + "\n"
|
||||
continue
|
||||
new_ += ls[i].replace(args.base_fill, base_color_map[flag]) + "\n"
|
||||
else:
|
||||
new_ += ls[i] + "\n"
|
||||
return new_
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例调用
|
||||
print("参数解析结果:")
|
||||
# print(f"主数据文件: {args.input}")
|
||||
print(f"大小权重: {args.size_weight}")
|
||||
print(f"每行图片数量: {args.per_row}")
|
||||
print(f"图片水平间隔: {args.horizontal_gap}")
|
||||
print(f"图片垂直间隔: {args.vertical_gap}")
|
||||
print(f"相邻碱基连线颜色: {args.adjacent_color}")
|
||||
print(f"配对碱基连线颜色: {args.pair_color}")
|
||||
print(f"碱基填充色: {args.base_fill}")
|
||||
print(f"碱基轮廓色: {args.base_stroke}")
|
||||
# print(f"反密码子文件: {args.anticodon_file}")
|
||||
# print(f"反密码子填充色: {args.anti_fill}")
|
||||
# print(f"反密码子轮廓色: {args.anti_stroke}")
|
||||
# 如果是文件,直接添加到列表
|
||||
file_list = []
|
||||
group_size = args.per_row
|
||||
if os.path.isfile(args.input):
|
||||
file_list.append(args.input)
|
||||
print(f"已添加文件: {args.input}")
|
||||
|
||||
# 如果是文件夹,添加文件夹内所有文件(不递归子文件夹)
|
||||
elif os.path.isdir(args.input):
|
||||
for filename in os.listdir(args.input):
|
||||
file_path = os.path.join(args.input, filename)
|
||||
if os.path.isfile(file_path) and "modified_" not in file_path and ".svg" == file_path[-4:]: # 只处理文件
|
||||
file_list.append(file_path)
|
||||
file_list.sort()
|
||||
print(f"已添加文件夹内所有文件: {args.input}")
|
||||
modi_files = []
|
||||
for i in file_list:
|
||||
n = modi_svg(i)
|
||||
modi_files.append("modified/modified_" + n + ".svg")
|
||||
|
||||
# Step 2: Merge into horizontal groups
|
||||
group_index = 0
|
||||
group_files = []
|
||||
for i in range(0, len(modi_files), group_size):
|
||||
group = modi_files[i:i + group_size]
|
||||
if not group:
|
||||
break
|
||||
group_output = f"modified/group_{group_index}.svg"
|
||||
merge_horizontal_group(group, str(group_output))
|
||||
group_files.append(group_output)
|
||||
group_index += 1
|
||||
|
||||
# Step 3: Merge all groups vertically
|
||||
final_output = "modified/final.svg"
|
||||
merge_vertical_groups(group_files, str(final_output))
|
||||
print(f"未上色SVG保存到: {final_output}")
|
||||
print(f"美化A碱基填充色: {args.base_a}")
|
||||
print(f"美化U碱基填充色: {args.base_u}")
|
||||
print(f"美化G碱基填充色: {args.base_g}")
|
||||
print(f"美化C碱基填充色: {args.base_c}")
|
||||
fisvg = read_svg(str(final_output))
|
||||
cosvg = update_circle_colors(fisvg, base_color)
|
||||
save_svg(cosvg, "modified/final_color.svg")
|
||||
# 核心修改3:修复打印路径错误(原写成 final.svg,改为 final_color.svg)
|
||||
print(f"上色SVG保存到: modified/final_color.svg")
|
||||
# print(modi_files)
|
||||
BIN
rna/ViennaRNA-2.7.2.tar.gz
Normal file
BIN
rna/ViennaRNA-2.7.2.tar.gz
Normal file
Binary file not shown.
10
rna/nupack代码示例.txt
Normal file
10
rna/nupack代码示例.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
model1 = Model(material='dna', celsius=37,sodium=0.1, magnesium=0.02)
|
||||
Y1 = Strand('CGTTAACGCAGTGAGGACGGTAGTTTGTCGTTCCATCGCACC', name='Y1')
|
||||
Y2 = Strand('CGTTAACGGGTGCGATGGAACGACTTTCGACAGGCCTGGTGTAATTTCACCCATGTTAGTCGA', name='Y2')
|
||||
Y3 = Strand('ATACGGAAAATGGAGATAGGAAGAGTACAATGTCAGCGATAAATTCCGTATACGACACCAGGCCTGTCGATTACTACCGTCCTCACTG', name='Y3')
|
||||
|
||||
t1 = Tube({Y1: 1e-8, Y2: 1e-8, Y3:1e-8 },complexes=SetSpec(max_size=4), name='Tube 1')
|
||||
|
||||
my_result = tube_analysis(tubes=[t1], model=model1,
|
||||
compute=['pfunc', 'pairs', 'mfe', 'sample', 'subopt'])
|
||||
print(my_result)
|
||||
139
rna/工具调用.txt
Normal file
139
rna/工具调用.txt
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from nupack import *
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# ---------- 独立的辅助函数 ----------
|
||||
def get_sequence(Complex, result, skip_errors=True):
|
||||
"""
|
||||
获取复合物的序列。
|
||||
- 若 Complex 为 None,返回字典 {复合物名: 序列},跳过解析失败的复合物。
|
||||
- 若 Complex 为字符串或 Complex 对象,返回该复合物的序列字符串;失败时抛出异常(除非 skip_errors=False 时由调用者处理)。
|
||||
"""
|
||||
# 统一转换为字符串名称
|
||||
if Complex is not None and not isinstance(Complex, str):
|
||||
Complex = Complex.name if hasattr(Complex, 'name') else str(Complex)
|
||||
|
||||
def _get_seq_from_strands(strand_names, comp_name=None):
|
||||
"""从链名列表获取拼接后的序列,如果失败则抛出异常"""
|
||||
seq_parts = []
|
||||
for s in strand_names:
|
||||
if not s.isidentifier():
|
||||
raise NameError(
|
||||
f"复合物 '{comp_name}' 中的链名 '{s}' 不是有效的 Python 标识符,"
|
||||
"请检查链的 name 属性是否包含空格或特殊字符。"
|
||||
)
|
||||
try:
|
||||
strand_obj = eval(s)
|
||||
if not isinstance(strand_obj, Strand):
|
||||
raise TypeError(f"'{s}' 不是 Strand 对象")
|
||||
seq_parts.append(str(strand_obj))
|
||||
except NameError:
|
||||
raise NameError(f"链名 '{s}' 未定义,请确保该变量已创建为 Strand 对象。")
|
||||
return ''.join(seq_parts)
|
||||
|
||||
if Complex is None:
|
||||
# 批量模式
|
||||
sequences = {}
|
||||
for comp in result:
|
||||
name = comp.name if hasattr(comp, 'name') else str(comp)
|
||||
try:
|
||||
# 去除括号,按 '+' 分割
|
||||
inp = ''.join([ch for ch in name if ch not in '()'])
|
||||
strands = inp.split('+')
|
||||
sequences[name] = _get_seq_from_strands(strands, comp_name=name)
|
||||
except Exception as e:
|
||||
if skip_errors:
|
||||
print(f"⚠️ 跳过复合物 '{name}',原因:{e}")
|
||||
else:
|
||||
raise # 如果不跳过,则抛出异常
|
||||
return sequences
|
||||
else:
|
||||
# 单复合物模式
|
||||
inp = ''.join([ch for ch in Complex if ch not in '()'])
|
||||
strands = inp.split('+')
|
||||
return _get_seq_from_strands(strands, comp_name=Complex)
|
||||
|
||||
def get_structure(Complex, result, skip_errors=True):
|
||||
"""
|
||||
获取复合物的 MFE 结构(已去除 '+')。
|
||||
- 若 Complex 为 None,返回字典 {复合物名: 结构},跳过 result 中不存在的复合物。
|
||||
- 若 Complex 为字符串或 Complex 对象,返回该复合物的结构字符串;失败时抛出异常。
|
||||
"""
|
||||
if Complex is not None and not isinstance(Complex, str):
|
||||
Complex = Complex.name if hasattr(Complex, 'name') else str(Complex)
|
||||
|
||||
if Complex is None:
|
||||
structures = {}
|
||||
for comp in result:
|
||||
name = comp.name if hasattr(comp, 'name') else str(comp)
|
||||
try:
|
||||
structure = result[comp].mfe[0].structure
|
||||
structures[name] = str(structure).replace('+', '')
|
||||
except Exception as e:
|
||||
if skip_errors:
|
||||
print(f"⚠️ 跳过复合物 '{name}',获取结构失败:{e}")
|
||||
else:
|
||||
raise
|
||||
return structures
|
||||
else:
|
||||
structure = result[Complex].mfe[0].structure
|
||||
return str(structure).replace('+', '')
|
||||
|
||||
# ---------- 绘图函数(带错误跳过) ----------
|
||||
def danlian(Complex, result, output_folder="/home/zsy"):
|
||||
"""
|
||||
绘制单个复合物的二级结构图。如果处理过程中出现错误,打印错误信息并返回,不中断程序。
|
||||
Complex 可为复合物名称字符串或 Complex 对象。
|
||||
"""
|
||||
try:
|
||||
sequence = get_sequence(Complex, result, skip_errors=False) # 这里不跳过,让异常抛出以便捕获
|
||||
best_structure = get_structure(Complex, result, skip_errors=False)
|
||||
except Exception as e:
|
||||
name = Complex if isinstance(Complex, str) else (Complex.name if hasattr(Complex, 'name') else str(Complex))
|
||||
print(f"❌ 处理复合物 '{name}' 时出错:{e},已跳过绘图")
|
||||
return
|
||||
|
||||
# 获取用于文件名的字符串名称
|
||||
if isinstance(Complex, str):
|
||||
seq_name = Complex
|
||||
else:
|
||||
seq_name = Complex.name if hasattr(Complex, 'name') else str(Complex)
|
||||
|
||||
temp_file_path = os.path.join(output_folder, f"{seq_name}.seq")
|
||||
try:
|
||||
with open(temp_file_path, "w", newline='\n') as f:
|
||||
f.write(f">{seq_name}\n")
|
||||
f.write(f"{sequence}\n")
|
||||
f.write(f"{best_structure}\n")
|
||||
except Exception as e:
|
||||
print(f"❌ 写入临时文件失败:{e}")
|
||||
return
|
||||
|
||||
try:
|
||||
input_filename = f"{seq_name}.seq"
|
||||
process = subprocess.run(["RNAplot", "--o", "svg", input_filename], cwd=output_folder, capture_output=True, text=True)
|
||||
if process.returncode == 0:
|
||||
print(f"🎉 绘图成功!请前往 {output_folder} 文件夹查看 {seq_name}_ss.svg")
|
||||
os.remove(temp_file_path)
|
||||
else:
|
||||
print(f"❌ RNAplot 运行失败,报错信息:\n{process.stderr}")
|
||||
except FileNotFoundError:
|
||||
print("❌ 找不到 RNAplot 命令,请确认环境变量。")
|
||||
except Exception as e:
|
||||
print(f"❌ 绘图过程中出现未知错误:{e}")
|
||||
|
||||
|
||||
使用方法
|
||||
算全部结果
|
||||
for comp in my_result:
|
||||
danlian(comp, my_result, output_folder="/home/zsy")
|
||||
|
||||
算单个结果
|
||||
# 假设您的计算结果保存在 my_result 中
|
||||
target = '(Y2+Y3)' # 替换为您的目标复合物名称
|
||||
# 输出结构
|
||||
print(get_structure(target, my_result))
|
||||
# 输出序列
|
||||
print(get_sequence(target, my_result))
|
||||
# 绘制二级结构图
|
||||
danlian(target, my_result, output_folder="/home/zsy")
|
||||
44
rna/操作流程.txt
Normal file
44
rna/操作流程.txt
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
准备工作:
|
||||
在 Windows 界面中搜索 控制面板 打开 程序-程序和功能-启动或关闭 Windows 功能-勾选‘适用于 Linux 的 Windows 子系统’与‘虚拟机平台’后点击确定后重启电脑
|
||||
在 Microsoft Store 中下载 Ubunut
|
||||
按 Win+X 键 点击 ‘命令提示符(管理员)’ 输入 wsl --install 后回车。下载完毕后关闭即可。 如果遇到链接失败就是网络问题,链接手机热点后重新输入+回车即可
|
||||
|
||||
安装:打开 Ubunut
|
||||
将群内的所有压缩包直接保存在D盘
|
||||
nupack 安装
|
||||
mkdir nupack-latest
|
||||
cd nupack-latest
|
||||
cp /mnt/d/nupack-4.0.2.0.zip ./
|
||||
sudo apt install unzip
|
||||
unzip nupack-4.0.2.0.zip
|
||||
cd ..
|
||||
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
||||
/bin/bash Miniconda3-latest-Linux-x86_64.sh -b
|
||||
miniconda3/bin/conda update -n base -c defaults conda
|
||||
rm Miniconda3-latest-Linux-x86_64.sh
|
||||
export PATH=$HOME/miniconda3/bin:$PATH
|
||||
echo 'export PATH=$HOME/miniconda3/bin:$PATH' >> ~/.bashrc
|
||||
conda install numpy scipy pip matplotlib pandas jupyterlab
|
||||
pip install -U nupack -f ./nupack-latest/nupack-4.0.2.0/package
|
||||
jupyter lab
|
||||
|
||||
RNAplot 安装
|
||||
mkdir rna
|
||||
cd rna
|
||||
cp /mnt/d/ViennaRNA-2.7.2.tar.gz ./
|
||||
tar -zxvf ViennaRNA-2.7.2.tar.gz
|
||||
cd ViennaRNA-2.7.2
|
||||
(可能出现报错的解决方法
|
||||
sudo apt update
|
||||
sudo apt install build-essentia
|
||||
sudo apt install pkg-config)
|
||||
./configure
|
||||
make
|
||||
sudo make install
|
||||
|
||||
图片美化方法
|
||||
|
||||
将生成后的svg图片保存在一个文件夹内,并将该文件夹与群内的 py 文件保存在一个文件夹。后 Shift+鼠标右键 点击在此处打开 Powershell 窗口后
|
||||
输入命令
|
||||
python TrnaStructureBeautifier.py -i 文件夹名称 用于指定一个文件夹
|
||||
python TrnaStructureBeautifier.py -i 文件名称 用于指定一个文件
|
||||
46
rna/调用RNAplot的代码.txt
Normal file
46
rna/调用RNAplot的代码.txt
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from nupack import *
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
def huatu(Complex,result):#Complex是你想查看的复合物,eg: '(StrandA+StrandB)' 引号和括号都要有;
|
||||
#result是nupack计算(tube_analysis)的结果,eg: nupack指南里用过my_result、my_results和tube_results等, 或者你自己有其他命名
|
||||
def jiegou(Complex,result):
|
||||
m=str(result[Complex].mfe[0].structure)
|
||||
mm=""
|
||||
for i in m:
|
||||
if i != "+":
|
||||
mm=mm+i
|
||||
if i=="":
|
||||
continue
|
||||
return mm
|
||||
def xulie(Complex):
|
||||
inp=''
|
||||
for i in Complex:
|
||||
if i != '(' and i != ')':
|
||||
inp=inp+i
|
||||
else:
|
||||
continue
|
||||
l=inp.split('+',-1)
|
||||
xulie=''
|
||||
for i in l:
|
||||
xulie=xulie+str(eval(i))
|
||||
return xulie
|
||||
sequence=xulie(Complex)
|
||||
best_structure=jiegou(Complex,result)
|
||||
seq_name =Complex
|
||||
output_folder="/home/liusiye/桌面/nupack-4.0.2.0"#指定图片保存在哪个文件夹,需自己更改
|
||||
temp_file_path = os.path.join(output_folder, f"{seq_name}.seq")
|
||||
with open(temp_file_path, "w", newline='\n') as f:
|
||||
f.write(f">{seq_name}\n")
|
||||
f.write(f"{sequence}\n")
|
||||
f.write(f"{best_structure}\n")
|
||||
try:
|
||||
input_filename = f"{seq_name}.seq"
|
||||
process = subprocess.run(["RNAplot", "--o", "svg", input_filename], cwd=output_folder, capture_output=True, text=True)
|
||||
if process.returncode == 0:
|
||||
print(f"🎉 绘图成功!请前往 {output_folder} 文件夹查看 {seq_name}_ss.svg")
|
||||
os.remove(temp_file_path)
|
||||
else:
|
||||
print("❌ RNAplot 运行失败,报错信息:\n", process.stderr)
|
||||
except FileNotFoundError:
|
||||
print("❌ 找不到 RNAplot 命令,请确认环境变量。")
|
||||
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)
|
||||
94
绘图自动分割.txt
Normal file
94
绘图自动分割.txt
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
在ununtu上使用该命令安装draw_rna
|
||||
pip install matplotlib numpy draw_rna --upgrade -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
|
||||
-------------------------------打开原本步骤中lab中的记事本--------------------------
|
||||
# 依赖导入
|
||||
from nupack import *
|
||||
from draw_rna.ipynb_draw import draw_struct
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# 开启显示(恢复正常绘图展示)
|
||||
plt.ion()
|
||||
|
||||
# ======================================
|
||||
# 仅保留两个核心函数,完全用 draw_rna
|
||||
# ======================================
|
||||
def get_mfe_structure(complex_name: str, nupack_result, strands: list):
|
||||
"""获取复合物MFE结构,支持同源二聚体 Y1+Y1"""
|
||||
strand_names = complex_name.strip('()').split('+')
|
||||
strand_map = {s.name: s for s in strands}
|
||||
strand_objs = [strand_map[name] for name in strand_names]
|
||||
complex_obj = Complex(strand_objs)
|
||||
return str(nupack_result[complex_obj].mfe[0].structure)
|
||||
|
||||
def draw_complex(complex_name: str, nupack_result, strands: list, output_folder="./"):
|
||||
"""draw_rna绘图:无页面显示,直接保存图片到目标路径"""
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
|
||||
# 获取结构和序列(完全不变)
|
||||
struct = get_mfe_structure(complex_name, nupack_result, strands)
|
||||
strand_names = complex_name.strip('()').split('+')
|
||||
strand_map = {s.name: s for s in strands}
|
||||
seq = " ".join([str(strand_map[name]) for name in strand_names]) # 序列用空格分隔
|
||||
draw_structure = struct.replace('+', ' ') # 结构用空格分隔
|
||||
# 设置全局字体大小,可以根据需要调整数字 (例如 14, 18, 20)
|
||||
plt.rcParams['font.size'] = 12
|
||||
# ---------- 修正点:显式创建画布并传递给 draw_struct ----------
|
||||
fig, ax = plt.subplots(figsize=(15, 15)) # 可调尺寸,保证图形清晰
|
||||
draw_struct(seq, draw_structure, ax=ax) # 在指定 ax 上绘图
|
||||
|
||||
# 保存并关闭
|
||||
save_name = complex_name.strip('()').replace('+', '_') + '.png'
|
||||
save_path = os.path.join(output_folder, save_name)
|
||||
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
||||
|
||||
print(f"\n✅ 图片已保存至:{save_path}")
|
||||
print(f"MFE 结构:{struct}")
|
||||
def draw_all_complexes(nupack_result, strands: list, output_folder="./"):
|
||||
print("\n========== 开始批量绘制所有复合物 ==========")
|
||||
|
||||
# 过滤出 Complex 对象
|
||||
complex_objs = [obj for obj in nupack_result.keys() if not isinstance(obj, Tube)]
|
||||
total = len(complex_objs)
|
||||
success_count = 0
|
||||
|
||||
for idx, complex_obj in enumerate(complex_objs, 1):
|
||||
complex_name = complex_obj.name # ✅ 修正点
|
||||
try:
|
||||
draw_complex(complex_name, nupack_result, strands, output_folder)
|
||||
success_count += 1
|
||||
print(f"进度:{idx}/{total} 完成")
|
||||
except Exception as e:
|
||||
print(f"❌ 绘制复合物 {complex_name} 失败: {e}")
|
||||
|
||||
print("\n========== 所有复合物绘制完成 ==========")
|
||||
print(f"✅ 批量绘制完成!成功:{success_count} / 总计:{total}")
|
||||
print(f"📁 图片保存在:{os.path.abspath(output_folder)}")
|
||||
-------------------------------- nupack计算--------------------------------------
|
||||
|
||||
model1 = Model(material='dna', celsius=37,sodium=0.1, magnesium=0.02)
|
||||
Y1 = Strand('CGTTAACGCAGTGAGGACGGTAGTTTGTCGTTCCATCGCACC', name='Y1')
|
||||
Y2 = Strand('CGTTAACGGGTGCGATGGAACGACTTTCGACAGGCCTGGTGTAATTTCACCCATGTTAGTCGA', name='Y2')
|
||||
Y3 = Strand('ATACGGAAAATGGAGATAGGAAGAGTACAATGTCAGCGATAAATTCCGTATACGACACCAGGCCTGTCGATTACTACCGTCCTCACTG', name='Y3')
|
||||
|
||||
t1 = Tube({Y1: 1e-8, Y2: 1e-8, Y3:1e-8 },complexes=SetSpec(max_size=4), name='Tube 1')
|
||||
|
||||
my_result = tube_analysis(tubes=[t1], model=model1,
|
||||
compute=['pfunc', 'pairs', 'mfe', 'sample', 'subopt'],
|
||||
options={'num_sample': 1, 'energy_gap': 0.5})
|
||||
print(my_result)
|
||||
|
||||
--------------------------- --------------- 单复合物绘图------------------------------
|
||||
target = '(Y1+Y1+Y2+Y3)' # 替换为你的目标复合物名称
|
||||
all_strands = [Y1, Y2, Y3] # 所有链的列表
|
||||
|
||||
# 1. 获取并打印MFE结构
|
||||
mfe_struct = get_mfe_structure(target, my_result, all_strands)
|
||||
#print("MFE结构:", mfe_struct)
|
||||
|
||||
# 2. 绘制二级结构
|
||||
draw_complex(target, my_result, all_strands, output_folder="/home/zsy")
|
||||
--------------------------------------------批量绘图-----------------------------------
|
||||
strands_list = [Y1, Y2, Y3]
|
||||
draw_all_complexes(my_result, strands_list, output_folder="/home/zsy/jihe")
|
||||
Loading…
Add table
Add a link
Reference in a new issue