.gen 文件定义

.gen 是 GenePad 的完整项目文件,文件本质是标准 SQLite 3 数据库 (文件头 16 字节为 SQLite format 3\0,MIME 登记为 application/vnd.genepad.database)。它比 .gjson 更完整, 除序列与注释外还保存引物结合位点、测序图谱(chromatogram)、比对条目、撤销/重做栈、 编辑历史和附件。任何语言的标准 SQLite 驱动都能直接读写——不需要安装任何专用解析库。

SQLite format 3 12 张表 1-based inclusive BLOCK_SIZE = 10000 WAL + checkpoint SCHEMA_VERSION = 5

Overview

读取原则

  • 无需自定义二进制 parser——用任意 SQLite 驱动打开 .gen,再按下方表结构查询即可。
  • sequence_blocks 必须按 block_index 升序拼接,不能依赖数据库默认返回顺序。
  • segmentsqualifierscomponentsalignmentreferences 等字段是 JSON 字符串(chromatogram 的 trace 是「UTF-8 JSON 装进 BLOB」),需二次解析。
  • .gen 不持久化 restrictionSites(酶切位点),由读取程序根据主序列重新检测:GenePad 在序列长度 ≤ 50,000 bp 时同步检测,超长序列走后台流式检测。
  • 保存出的 .gen 永远是自包含单文件——GenePad 落盘前执行 PRAGMA wal_checkpoint(TRUNCATE),用户目录里不会出现 -wal/-shm 伴生文件。
  • 布尔值全部以字符串 'true'/'false' 保存;键可能缺失,读取端要给默认值(见 project_meta 键参考)。

Getting Started

快速上手:读一个 .gen(Python)

Python 内置的 sqlite3 模块就够,零第三方依赖。下面 20 行代码读出元信息、完整序列和全部 feature:

import json
import sqlite3

con = sqlite3.connect("plasmid.gen")
con.row_factory = sqlite3.Row

# —— 元信息:key-value 表,布尔以字符串保存
meta = {row["key"]: row["value"] for row in con.execute(
    "SELECT key, value FROM project_meta")}
is_circular = meta.get("isCircular") == "true"

# —— 主序列:必须按 block_index 升序拼接
sequence = "".join(row["bases"] for row in con.execute(
    "SELECT bases FROM sequence_blocks ORDER BY block_index"))
print(meta.get("name", "Untitled"), len(sequence), "bp,",
      "环状" if is_circular else "线状")

# —— features:segments / qualifiers 列是 JSON 字符串
for f in con.execute("SELECT * FROM features ORDER BY start_pos"):
    length = f["end_pos"] - f["start_pos"] + 1
    if f["start_pos"] > f["end_pos"]:                  # 跨原点的环形 feature
        length = len(sequence) - f["start_pos"] + f["end_pos"] + 1
    qualifiers = json.loads(f["qualifiers"]) if f["qualifiers"] else {}
    print(f["name"], f["type"], f["strand"], length, "bp", qualifiers)

提示:坐标是 1-based inclusivestart=1, end=10 表示第 1~10 号碱基共 10 个;环形序列跨原点时 start > end(详见坐标与环形序列)。

可以下载真实示例文件试跑:41-pPUR-P2A-BFPnls-sgRNA1-RNF2_4AtoG_MS2-MA.gen(112 KB)——7,952 bp 的环形慢病毒 sgRNA 载体,含 25 个 feature、2 条引物及结合位点。它也是一个很好的教学样本:碱基全为小写(验证「不强制大写」)、isCircular/isDamMethylated 等布尔以字符串保存、feature Misc_Feature_1 带 4 段 segments。对它运行上面代码的输出:

41-pPUR-P2A-BFPnls-sgRNA1-RNF2_4AtoG_MS2-MA 7952 bp, 环状
hPGK promoter misc_feature forward 507 bp {}
Puromycin misc_feature forward 594 bp {}
P2A misc_feature forward 57 bp {}
TagBFP2 misc_feature forward 711 bp {}
...(共 25 个 feature)

Schema

核心表结构(全部 12 张)

project_meta项目元信息 key-value 表
字段类型说明
keyTEXT PRIMARY KEY元信息键
valueTEXT元信息值,布尔值也以字符串保存

保存时按 key upsertINSERT ... ON CONFLICT(key) DO UPDATE),从不整表清空——因此旧文件里多余的自定义 key 会原样保留。下表是 GenePad 会写入的全部键:

key写入值读取行为 / 默认说明
namestring缺失 → 'Untitled'项目名(GenBank LOCUS 名)
descriptionstring缺失 → ''描述(GenBank DEFINITION)
isCircular'true'/'false'缺失 → false拓扑:环状 / 线状,无独立 topology 列
isDoubleStranded'true'/'false'缺失或任何非 'false' 值 → true双链 / 单链
isDamMethylated'true'/'false'/''键缺失 → 未知(undefined);''falseDam 甲基化状态
isDcmMethylated'true'/'false'/''同上Dcm 甲基化状态
accessionstring缺失 → undefinedGenBank ACCESSION
organismstring缺失 → undefinedGenBank ORGANISM
datestring缺失 → undefinedGenBank LOCUS 行日期
moleculeTypestring缺失 → undefinedLOCUS 分子类型(DNA 等)
divisionstring缺失 → undefinedGenBank division 代码
versionstring缺失 → undefinedGenBank VERSION 行
keywordsstring缺失 → undefinedGenBank KEYWORDS
sourcestring缺失 → undefinedGenBank SOURCE
commentsstring缺失 → undefinedGenBank COMMENT
referencesJSON 字符串 GenBankRef[]非法 JSON → undefined文献条目,形状见下方
typeOfDisplay'file_name'/'custom'缺失 → 'file_name'标签页标题来源
customNamestring缺失 → ''自定义显示名(typeOfDisplay 为 custom 时生效)
fileNamestring缺失 → undefined原始导入文件名;桌面端打开时会被实际文件名覆盖
isDirty'0'加载时不读取保存流程的簿记字段,每次落盘后写 '0'

GenBankRef 的 JSON 形状(references 键的值):

// JSON: GenBankRef[]
{
  "number": 1,                  // 必有
  "authors":  "Smith J., Lee K.",
  "title":    "Cloning of pBR322",
  "journal":  "Nucleic Acids Res 2026",
  "pubmed":   "12345678",
  "remark":   "..."             // 其余字段均可省略
}
sequence_blocks主序列分块
字段类型说明
block_indexINTEGER PRIMARY KEY0-based block index
basesTEXT NOT NULL该块碱基字符串,除最后一块外恰为 10,000 bases
SELECT bases FROM sequence_blocks ORDER BY block_index;

分块规则:每块 BLOCK_SIZE = 10000 bp,block_index = floor(offset / 10000)(offset 为 0-based 偏移)——块 0 存偏移 0–9999(即 1-based 位置 1–10000),块 1 存 10000–19999,最后一块可以不足 10,000。没有 start_pos 列:块起点 = block_index × 10000。序列总长用 SELECT COALESCE(SUM(LENGTH(bases)), 0) FROM sequence_blocks 计算。写入时全删全写(非增量更新),block_index 必须从 0 起连续无空洞——GenePad 按范围取块拼接,中间缺块会静默错位。碱基大小写保留原样(不强制大写),字符集为 IUPAC。

features序列注释
字段类型说明
idTEXT PRIMARY KEYfeature id,GenePad 生成格式 {prefix}-{timestamp}-{counter}
nameTEXT NOT NULLfeature 名称
typeTEXT NOT NULL如 gene, CDS, promoter, rep_origin, misc_feature
start_posINTEGER NOT NULL1-based inclusive start
end_posINTEGER NOT NULL1-based inclusive end
strandTEXT NOT NULLforward / reverse / none
colorTEXT显示颜色,CSS 颜色字符串(如 #e0502a),NULL 走默认色
labelTEXT显示标签(缺省用 name)
noteTEXT备注
frameINTEGER阅读框(0/1/2),可 NULL
visibleINTEGER DEFAULT 11 可见,0 隐藏;缺省按 1 处理
segmentsTEXTJSON:FeatureSegment[],仅多片段(>1 段)feature 写入,否则 NULL
qualifiersTEXTJSON:Record<string, string[]>,GenBank 限定词(/translation、/codon_start、/gene…)

索引:idx_features_range ON features(start_pos, end_pos);GenePad 读取时按 ORDER BY start_pos 返回,范围查询用闭区间重叠判定 start_pos <= rangeEnd AND end_pos >= rangeStart

两个 JSON 列的形状:

// segments 列 → JSON: FeatureSegment[](段数 > 1 时才写入)
[
  { "start": 1,    "end": 120,  "strand": "forward" },
  { "start": 500,  "end": 874,  "strand": "forward",
    "name": " linker ",  "color": "#3aaea0" }   // name / color / strand 可选
]

// qualifiers 列 → JSON: Record<string, string[]>
{
  "gene":        ["AmpR"],
  "codon_start": ["1"],
  "translation": ["MSIQ..."]
}
primers / primer_binding_sites引物与结合位点(按 primer_id 关联)
字段类型说明
primers.idTEXT PRIMARY KEY引物 id
primers.nameTEXT NOT NULL引物名称
primers.sequenceTEXT NOT NULL引物序列(5'→3')
primers.descriptionTEXT描述
primers.visibleINTEGER DEFAULT 11 显示,0 隐藏
primer_binding_sites.idTEXT PRIMARY KEY固定格式 {primer_id}-site-{start}
primer_binding_sites.primer_idTEXT NOT NULL所属引物
primer_binding_sites.start_pos / end_posINTEGER NOT NULL1-based inclusive 结合范围
primer_binding_sites.bound_strandTEXT NOT NULLforward / reverse
primer_binding_sites.annealed_basesTEXT退火碱基,读取时 NULL → ''
primer_binding_sites.melting_temperatureREAL解链温度,读取时 NULL → 0
primer_binding_sites.componentsTEXTJSON:PrimerComponent[],恒写入(可为 []
primer_binding_sites.alignmentTEXTJSON:AlignmentSegment[],可 NULL

索引:idx_binding_range ON primer_binding_sites(start_pos, end_pos)idx_binding_primer ON primer_binding_sites(primer_id)。两个 JSON 列的形状:

// components 列 → JSON: PrimerComponent[]
[ { "hybridizedRange": "25-48", "bases": "ATGGC..." } ]  // hybridizedRange 可选

// alignment 列 → JSON: AlignmentSegment[]
[
  { "type": "annealed",   "primerBases": "ATG",
    "templateBases": "TAC", "templateStart": 101, "templateEnd": 103 },
  { "type": "unannealed", "primerBases": "AAA",
    "templateBases": "CGT", "templateStart": 104, "templateEnd": 106 },
  { "type": "del",        "primerBases": "TT",
    "templateBases": "",   "templateStart": 107, "templateEnd": 107 }
]
base_color_ranges碱基颜色范围(手工着色)
字段类型说明
idTEXT PRIMARY KEY颜色段 id
start_posINTEGER NOT NULL1-based inclusive start
end_posINTEGER NOT NULL1-based inclusive end
strandTEXT NOT NULLforward / reverse(读取时其他值一律归为 forward)
colorTEXT NOT NULL颜色值

索引:idx_base_color_ranges ON base_color_ranges(start_pos, end_pos, strand)。整表替换式写入(DELETE 后逐行 INSERT)。

alignment_entries比对条目(Sanger 测序比对到主序列)
字段类型说明
idTEXT PRIMARY KEY比对条目 id
nameTEXT NOT NULL条目名(通常为样品/引物名)
sequenceTEXT NOT NULL被比对的读段序列
visibleINTEGER DEFAULT 11 显示,0 隐藏
features_jsonTEXT预留列,当前版本不写入
chromatogram_jsonTEXTJSON:ChromatogramData(见下),可 NULL

chromatogram_json 的形状(与 chromatogram_data 表同构,但这里存 JSON 字符串、非 BLOB):

// chromatogram_json → JSON: ChromatogramData
{
  "traceA":        [12, 45, 178, ...],   // 四通道荧光强度,number[]
  "traceC":        [8,  31, 96,  ...],
  "traceG":        [0,  12, 240, ...],
  "traceT":        [3,  60, 15,  ...],
  "peakLocations": [14, 25, 36, ...]     // 峰位索引,number[]
}
chromatogram_data主序列测序图谱(id 固定 'primary')
字段类型说明
idTEXT PRIMARY KEY主序列图谱固定为 'primary',整表仅此一行
trace_a / trace_c / trace_g / trace_tBLOBA/C/G/T 四通道荧光曲线
peak_locationsBLOB碱基峰位索引数组

BLOB 不是二进制浮点——内容是 JSON.stringify(number[]) 的 UTF-8 字节(TextEncoder 编码)。解码:JSON.parse(bytes.decode('utf-8')) / new TextDecoder().decode(blob)。写入流程为 DELETE WHERE id='primary' 再 INSERT,无该行表示主序列无图谱。

undo_entries撤销/重做栈(持久化,跨会话可用)
字段类型说明
sequence_numberINTEGER PRIMARY KEY AUTOINCREMENT单调递增序号,栈顶 = 最大值
stack_typeTEXT NOT NULL DEFAULT 'undo''undo''redo'(同一张表存两条栈)
operation_typeTEXT NOT NULL操作类型,取值见下
target_idTEXT目标实体 id,序列级操作为 NULL
before_diff / after_diffTEXTJSON:操作前/后差异负载,形状见下

栈机制:undo 栈上限 200 条,超出删最旧(按 sequence_number);每次压入新操作先清空整个 redo 栈;撤销 = 把最新 undo 行的 stack_type 翻转为 'redo';重做 = 读取后直接删除该行。operation_type 取值:addFeature, removeFeature, updateFeature, applyBaseColor, clearBaseColor, addPrimer, removePrimer, updatePrimer, insertBases, deleteBases, replaceBases, replaceSequence, addAlignmentEntry, removeAlignmentEntry。diff payload 形状:

// 实体操作:before/after 为完整对象或 null
add:    { before: null,            after: Feature | Primer }
update: { before: { id, changes }, after: { id, changes } }   // changes = Partial<Feature|Primer>
applyBaseColor / clearBaseColor:
        { baseColorRanges: BaseColorRange[] }

// 序列编辑:附带编辑时刻的注释快照 + 编辑参数
InsertBasesDiff  = Snapshot & { start: number, bases: string }
DeleteBasesDiff  = Snapshot & { start: number, bases?: string, length?: number }
ReplaceBasesDiff = Snapshot & { start: number, oldBases?: string, newBases?: string }
// Snapshot = { features, baseColorRanges, primers, restrictionSites }
edit_history编辑审计日志(只增不删)
字段类型说明
idINTEGER PRIMARY KEY AUTOINCREMENT行号
timestampTEXT NOT NULLnew Date().toISOString() 格式(如 2026-08-22T03:14:07.000Z
operation_typeTEXT NOT NULL与 undo_entries 同一套操作类型
target_idTEXT目标实体 id,可 NULL
descriptionTEXT人类可读描述,如 Added feature 'AmpR' at 12..874
before_snapshot / after_snapshotTEXTJSON:受影响实体(增删为完整对象)或小编辑对象(如 { position, bases }{ start, oldBases }

只追加、无上限、保存时不清空——可以用它做外部审计或改动统计。

attachments / schema_version预留表
结构状态
attachmentsid TEXT PK, name TEXT NOT NULL, mime_type TEXT NOT NULL, data BLOB NOT NULL预留:schema 每次都会建表,但当前版本无写入入口(id 约定为 att-{timestamp},data 存原始字节)
schema_versionkey TEXT PK, value TEXT预留:当前版本从不写入行。常量 SCHEMA_VERSION = 5 仅存在于代码中

兼容机制因此不是版本号门控:GenePad 打开文件时执行幂等 DDL(全部 CREATE TABLE IF NOT EXISTS,外加两条吞掉「列已存在」错误的 ALTER TABLE features ADD COLUMN segments/qualifiers),旧文件缺列/缺表会在打开时自动补齐。外部读取程序应对缺列容错(老文件 features 没有 segments/qualifiers 列)。

连接参数:GenePad 打开数据库即执行 PRAGMA journal_mode=WALPRAGMA synchronous=NORMAL;保存前执行 PRAGMA wal_checkpoint(TRUNCATE) 再整体复制字节,所以成品 .gen 永远自包含。外部工具用默认 journal(delete 模式)写入同样合法——GenePad 重新打开时会自动切回 WAL。

Conventions

坐标与环形序列

feature、primer binding site、base_color_range 的 start_pos / end_pos 均为 1-based inclusive:第一个碱基的位置是 1,start=1, end=10 表示第 1 到第 10 个碱基,长度 = end - start + 1

环形序列(isCircular='true')的 feature 跨越原点时直接存 start > end,长度公式变为 seqLength - start + end + 1——从 start 走到序列末尾,再从 1 绕回到 end。例:1000 bp 环形质粒上 start=995, end=5 覆盖 995–1000 加 1–5 共 11 个碱基。除 isCircular 标志外没有任何额外的环形专用字段。

多段 feature(如移码拼接、内含子跳过的 CDS)不用多行表示,而是把各段写进 segments JSON 数组,每段独立 { start, end, strand?, name?, color? },同样是 1-based inclusive、支持 start > end 跨原点。

注意两套体系:sequence_blocks.block_index 与序列偏移是 0-based,而所有注释坐标是 1-based。换算:1-based 位置 p 位于块 floor((p-1) / 10000)

Behavior

保存与兼容行为

行为规则
保存策略内容表(sequence_blocksfeaturesbase_color_rangesprimersprimer_binding_siteschromatogram_data,及已加载比对的 alignment_entries全删全写project_meta 按 key upsert;undo_entriesedit_historyattachments 从不清空。没有「精简导出」选项——历史随文件走
批量写入sequence_blocks 每 100 行一条多值 INSERT,features 每 40 行一条(13 列 × 40 = 520 参数);无显式事务,逐条语句自动提交
序列实时写编辑过程中序列整表重建(从位置 0 重写全部分块),不做块级增量更新
工作副本打开 .gen 时先整体复制到临时目录(genepad-<random>.gen)再编辑,保存时 checkpoint 后把临时库字节整体写回目标路径
损坏容错所有 JSON 列解析失败一律降级为 undefined/空,不报错;Android 导入路径会先校验 16 字节魔头 SQLite format 3\0
大小写碱基大小写按原样存取(不强制大写);字符集为 IUPAC 扩展字母
加密无——标准未加密 SQLite 文件,任何工具可读

Recipe

导出为 .gjson

把整个项目导出成 .gjson——GenePad 的文本 JSON 交换格式。下面这个脚本复刻了 GenePad 写出器的全部行为(键名、null 语义、空数组省略规则都一致),可直接交给 GenePad 或任何工具读回:

import json, sqlite3, time

def gen_to_gjson(path):
    con = sqlite3.connect(path)
    con.row_factory = sqlite3.Row
    j = lambda s: json.loads(s) if s else None
    meta = {r["key"]: r["value"] for r in con.execute("SELECT * FROM project_meta")}
    n = lambda k: meta.get(k) or None          # '' -> None:空串等同未设置
    sequence = "".join(r["bases"] for r in con.execute(
        "SELECT bases FROM sequence_blocks ORDER BY block_index"))

    def segs_of(raw):                          # 多段(>1)feature 才带 segments 键
        if not raw: return {}
        segs = json.loads(raw)
        return {"segments": [{"start": s["start"], "end": s["end"],
                              **({"strand": s["strand"]} if s.get("strand") else {})}
                             for s in segs]} if len(segs) > 1 else {}

    # .gen 里这两个 JSON 列是 camelCase,.gjson 里是 snake_case
    comp  = lambda c: [{"hybridized_range": x.get("hybridizedRange"),
                        "bases": x["bases"]} for x in c]
    align = lambda a: [{"type": x["type"], "primer_bases": x["primerBases"],
                        "template_bases": x["templateBases"],
                        "template_start": x["templateStart"],
                        "template_end": x["templateEnd"]} for x in a]

    features = [{
        "id": f["id"], "name": f["name"], "type": f["type"],
        "start": f["start_pos"], "end": f["end_pos"], "strand": f["strand"],
        "color": f["color"], "label": f["label"], "note": f["note"],
        "frame": f["frame"], "visible": bool(f["visible"]),
        **segs_of(f["segments"]), "qualifiers": j(f["qualifiers"]),
    } for f in con.execute("SELECT * FROM features ORDER BY start_pos")]

    primers = [{
        "id": p["id"], "name": p["name"], "sequence": p["sequence"],
        "description": p["description"],
        "bindingSites": [{
            "start": b["start_pos"], "end": b["end_pos"], "boundStrand": b["bound_strand"],
            "annealedBases": b["annealed_bases"] or "",
            "meltingTemperature": b["melting_temperature"] or 0.0,
            "components": comp(json.loads(b["components"]) if b["components"] else []),
            **({"alignment": align(json.loads(b["alignment"]))} if b["alignment"] else {}),
        } for b in con.execute("SELECT * FROM primer_binding_sites WHERE primer_id = ?",
                               (p["id"],))],
        "visible": None,                       # 写出器固定写 null
    } for p in con.execute("SELECT * FROM primers")]

    cnt = lambda t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
    doc = {
        "version": "1.9",
        "id": f"gjson-{int(time.time() * 1000)}",      # 毫秒时间戳
        "type_of_display": "file_name",
        "custom_name": meta.get("name", "Untitled"),
        "description": meta.get("description", ""),
        "sequence": sequence,
        "length": len(sequence),
        "isCircular": meta.get("isCircular") == "true",
        "isDoubleStranded": meta.get("isDoubleStranded") != "false",
        "accession": n("accession"), "organism": n("organism"), "date": n("date"),
        "moleculeType": n("moleculeType"), "division": n("division"),
        "gbVersion": n("version"), "keywords": n("keywords"),
        "source": n("source"), "comments": n("comments"),
        "references": j(meta.get("references")),
        "features": features,
        **({"baseColorRanges": [{"id": r["id"], "color": r["color"],
                                 "start": r["start_pos"], "end": r["end_pos"],
                                 "strand": r["strand"]}
                                for r in con.execute("SELECT * FROM base_color_ranges")]}
           if cnt("base_color_ranges") else {}),
        **({"primers": primers} if primers else {}),
        **({"alignmentEntries": [{"id": a["id"], "name": a["name"],
                                  "sequence": a["sequence"], "visible": bool(a["visible"])}
                                 for a in con.execute("SELECT * FROM alignment_entries")]}
           if cnt("alignment_entries") else {}),
    }
    con.close()
    return doc

# GenePad 写出 LF 换行;Windows 上 Python 需显式 newline="\n" 才能完全一致
with open("plasmid.gjson", "w", encoding="utf-8", newline="\n") as f:
    json.dump(gen_to_gjson("plasmid.gen"), f, ensure_ascii=False, indent=2)

此脚本已与 GenePad 自身导出的同项目 .gjson 逐一核对:除两处原理性差异外完全一致——id(写出时刻的毫秒时间戳)与 feature 顺序(GenePad 按导出时的内存顺序,重开 .gen 后只能得到 start_pos 顺序)。可以用本页提供的两个样本文件自行复核:.gen 源文件GenePad 导出的 .gjson

转换要点:organism 等 GenBank 元数据在 .gen 里是空串 ''、在 .gjson 里写 null(空串等同未设置);结合位点 JSON 的键名从 camelCase 转为 snake_case;primers[].visible 恒为 nullbaseColorRanges/primers/alignmentEntries 为空时整个键省略;测序图谱(chromatogram)不进 .gjson。完整规则见 .gjson 文件定义

Recipe

写回:外部修改 .gen

GenePad 对外部改动完全宽容——只要仍是合法 SQLite 且符合上述约定,重新打开即生效:

import sqlite3

con = sqlite3.connect("plasmid.gen")

# 改 feature 颜色 / 项目名 / 拓扑
con.execute("UPDATE features SET color = ? WHERE name = ?", ("#7c5cff", "AmpR"))
con.execute("UPDATE project_meta SET value = 'true' WHERE key = 'isCircular'")
con.execute("UPDATE project_meta SET value = ?   WHERE key = 'name'", ("pBR322-mod",))

# 增删碱基必须整表重建 sequence_blocks:block_index 从 0 连续,块长 10000
new_seq = "ATG" + "GCT" * 100          # ...你的新序列
con.execute("DELETE FROM sequence_blocks")
con.executemany("INSERT INTO sequence_blocks VALUES (?, ?)",
                [(i // 10000, new_seq[i:i + 10000])
                 for i in range(0, len(new_seq), 10000)])
con.commit()

注意:改动碱基长度后要同步修正受影响 feature 的坐标(GenePad 不会自动重算);isDirty 只是保存簿记,加载时不读取,外部改动无需维护它。用 Python 默认 journal 写出的文件即为自包含单文件。

Recipe

从零创建 .gen

最小可用文件只需要三张表——GenePad 打开时会自动补齐其余表和索引。控制解读方式的 project_meta 键如下:

key推荐写入缺省时读取结果
isCircular'true'(环形)或 'false'(线形)线形
isDoubleStranded'true'(双链)或 'false'(单链)双链
isDamMethylated'true' / 'false',不写则不设置未知(undefined)
isDcmMethylated'true' / 'false',不写则不设置未知(undefined)
import json, sqlite3

name, seq = "pDemo", "ATGGCTAGC" * 111           # 999 bp 示例序列

con = sqlite3.connect("demo.gen")
con.executescript("""
CREATE TABLE project_meta   (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE sequence_blocks(block_index INTEGER PRIMARY KEY, bases TEXT NOT NULL);
CREATE TABLE features (
    id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL,
    start_pos INTEGER NOT NULL, end_pos INTEGER NOT NULL, strand TEXT NOT NULL,
    color TEXT, label TEXT, note TEXT, frame INTEGER, visible INTEGER DEFAULT 1,
    segments TEXT, qualifiers TEXT);
""")

con.executemany("INSERT INTO project_meta VALUES (?, ?)", {
    "name":       name,
    "isCircular": "true",          # 见上表;isDoubleStranded 缺省即双链
    "isDirty":    "0",
}.items())

BLOCK = 10000                       # 与 GenePad 的 BLOCK_SIZE 一致
con.executemany("INSERT INTO sequence_blocks VALUES (?, ?)",
                [(i // BLOCK, seq[i:i + BLOCK])
                 for i in range(0, len(seq), BLOCK)])

con.execute("INSERT INTO features VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", (
    "feat-1", "GFP", "CDS", 101, 804, "forward",
    None, None, None, None, 1, None,
    json.dumps({"gene": ["gfp"]}),          # qualifiers:Record<string, string[]>
))
con.commit()

要点:feature 的 13 列一个都不能少(后几列写 NULL 即可);块长 10000 且 block_index 连续;布尔键用小写字符串。之后 GenePad(或任何读取程序)就能直接打开这个文件。

Quick Look

用 sqlite3 命令行看一眼

# 打开 .gen(本质就是 SQLite;可换成本页提供的示例文件)
sqlite3 example.gen

# 列出所有表 / 建表语句
.tables
.schema features

# 读取项目名 / 拓扑
SELECT key, value FROM project_meta WHERE key IN ('name','isCircular');

# 拼接完整序列(注意按 block_index 排序)
SELECT group_concat(bases, '') FROM (SELECT bases FROM sequence_blocks ORDER BY block_index);

# 查看前几个 feature(坐标 1-based inclusive)
SELECT name, type, start_pos, end_pos, strand FROM features ORDER BY start_pos LIMIT 5;

# 看绑定位点 / 编辑历史
SELECT primer_id, start_pos, end_pos, bound_strand FROM primer_binding_sites LIMIT 5;
SELECT timestamp, description FROM edit_history ORDER BY id DESC LIMIT 5;

# 解码主序列测序图谱的 A 通道(BLOB 里是 UTF-8 JSON 数组)
SELECT CAST(trace_a AS TEXT) FROM chromatogram_data WHERE id = 'primary';

# 顺手验证魔头:应为 "SQLite format 3" + \0
xxd -l 16 example.gen

只读场景请用 sqlite3 "file:example.gen?immutable=1" 打开——避免在文件旁产生 -wal/-shm 副产品(GenePad 自带的质粒库扫描器就是这么做的,Rust 示例见Rust 读取)。

See Also

延伸

  • .gjson 文件定义——单文件 JSON 交换格式,适合脚本生成与轻量共享。
  • .dna 转换——SnapGene .dna 的读取与转换说明。
  • Rust 读取示例——rusqlite 依赖配置、只读(immutable)打开与 .gjson 读取。