细胞力学仿真软件里的几何模型创建与编辑,是整个 CellPACK 流水线里最容易被低估的环节。我印象最深的一次经历:从文献补充材料里下载了一个哺乳动物细胞的网格文件,兴冲冲配置好成分清单,跑了一整夜 packing,第二天打开输出一看,所有分子都悬在细胞膜外面。排查了两天才意识到问题根本不在 packing 参数,而在几何——网格不水密、法线朝里,空间内外判定直接反了。这篇是系列的第 5 篇,专门把几何模型创建与编辑这件事拆开讲清楚:几何在 CellPACK 项目里扮演什么角色、从哪来、怎么编辑、怎么保证质量、怎么和 packing 参数联动。适合已经完成前面安装和组件配置、正准备把真实细胞形态放进仿真的读者,也适合被"分子全在外面"这类诡异结果折磨过的人。
1. 先搞清楚 CellPACK 里的"几何"到底指什么
1.1 几何是容器,不是分子本身
CellPACK 的核心工作方式,不是把每个原子的坐标硬编码进去,而是把细胞抽象成一组封闭的三维空间边界,再往这些边界构成的容器里填充分子实例。这里的"几何"指的就是容器本身——细胞膜、核膜、线粒体膜、内质网腔这些空间边界。项目配置里你设定的那些 ingredient(核糖体、RNA 聚合酶、细胞骨架蛋白、代谢酶等等),最终都会被 packing 算法放置到这个或那个几何内部。
用一句话概括:几何定义"哪里不能放",分子填充定义"哪里放了什么"。这两个问题是解耦的,但在实际工程里又互相牵扯。很多新手第一次用 CellPACK 时,会花大量时间调 ingredient 权重、比例、排斥半径,却忘了检查几何本身是否正确。结果就是无论怎么调 packing 参数,输出结果都诡异——因为分子根本放不进一个坏掉的容器里。
这里有一个现成的类比:几何相当于停车场的车位划线,分子相当于车辆。车位线画歪了、画反了,你再怎么安排进出顺序也停不出正常效果。所以每次有人问我"为什么 packing 结果不对",我第一句话永远是:先把你每个 compartment 的几何单独打开看一眼,再谈参数。
1.2 compartment 树状层级:母容器、子容器与共用边界
实际细胞不是单层球壳,而是一套嵌套结构:最外层是质膜,里面有细胞质基质,中间包裹着细胞核,细胞核外面还分布着线粒体、内质网、高尔基体等。CellPACK 用"子区间(compartment)"概念管理这套嵌套结构,组织方式是一棵树:母 compartment 是外边界,子 compartment 是内嵌在其内部的更小区间。
树状结构在每个 compartment 的配置中体现为父节点引用。比如"细胞核"的父节点是"细胞质"或"整个细胞","核仁"的父节点是"细胞核"。packing 的时候,算法先处理外层几何,再向内层填充,同时处理子 compartment 占用的空间——也就是说,子 compartment 内部的区域不会让母 compartment 的成分进入。这种设计让每个 compartment 可以独立配置分子组成,又不会在空间上互相污染。
这里有个特别容易踩的坑:子容器必须完全落在母容器的体积范围内,不能"伸出去"。我见过有人把线粒体几何从细胞膜里穿出去一小截,packing 结果在那一小截里塞满了线粒体蛋白,看起来就像蛋白长在细胞外面。处理这种问题,建议在提交 packing 之前先做一次"包含性测试",或者干脆在编辑阶段用布尔运算把子容器修剪到母容器内部。说实话,包含性测试并不难,难的是养成这个习惯。
另外,相邻 compartment 之间常常共用一层膜。比如质膜的内表面和外表面,在几何上可以用两个同心球壳的差值来表示:外球减去内球,得到一层薄壳。这种"壳层"结构在 CellPACK 里非常常见,后面第 5 章会详细说怎么配合 packing 参数处理。现在你只需要记住:几何不是一堆独立形状的集合,而是一套有层级、有共用边界的三维嵌套系统。
1.3 几何出错的典型症状清单
很多刚接触的人不知道几何问题会以什么形式暴露。这里列一份我实际遇到过的症状清单,方便你对号入座:
| 症状 | 常见的几何根因 |
|---|---|
| packing 跑完后分子坐标全部在细胞膜外 | 法线方向反转,内外判定颠倒 |
| 某个 compartment 统计体积为 0 或负数 | 网格不封闭或法线混乱,体积积分失效 |
| packing 进度在某一步长时间卡住 | 网格自相交或存在退化三角形,空间查询异常 |
| 输出结果里同一位置堆叠了大量互相穿透的分子 | 几何表面过于粗糙,排斥半径在判定时失效 |
| 分子密度明显不均匀,膜附近一坨、内部空荡 | 膜层几何与内部几何之间出现缝隙或重叠 |
这份清单不是让你背下来,而是提醒你:packing 结果的异常,第一嫌疑永远是几何。先花十分钟检查几何,再回头怀疑算法参数,通常能省下一整天。特别是当你换了新网格就出问题、换回内置几何体就正常时,那基本上可以 100% 确定问题出在几何文件本身。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 几何模型的三种来源:内置、导入、程序化生成
说清楚几何的地位之后,自然的问题是:几何模型从哪来?按照我的经验,CellPACK 项目的几何来源基本可以分成三类:内置几何体、外部网格导入、程序化生成。三者各有适用场景,多数项目会混合使用,并不互斥。
2.1 内置几何体:适合跑通流水线
CellPACK 内置了一些基础几何体:球体、椭球体、圆柱体、长方体等。这些几何体适合在项目初期快速验证流水线:你不需要任何外部建模工具,直接在配置里指定形状参数,就能得到一个可用几何。对刚接触工具的人来说,这是成本最低的上手路径。
我一般把内置几何体用在两个场景:一是做新手教学演示,比如拿一个球体代表球形细菌、一个胶囊体代表杆状细菌,跑通全流程;二是做算法调参的基准测试。因为内置几何体的网格是程序生成的,不会有多余的破洞、法线问题,packing 结果如果还有异常,基本可以排除几何因素,专心查 packing 参数。这一点在排查问题上价值很大,相当于"干净的对照组"。
用内置几何体时要注意单位一致性。CellPACK 内部通常采用埃或纳米为长度单位,你在配置里写半径 5000 还是 500,要根据目标尺度换算清楚。球形细菌半径按 0.5 微米算,就是 500 纳米;如果当成 500 埃,体积数量级就差了 10 的 9 次方倍。体积错了,后续所有分子数都会跟着错。我的习惯是每个几何体配一个注释说明单位,避免过一个月后自己都忘了当初写的是纳米还是埃。
内置几何体的局限也很明显:真实细胞的形态远不止球和柱。所以一旦要研究真实形态,就要走第二种路——外部网格导入。
2.2 外部网格导入:贴近真实形态的唯一解
研究真实细胞形态,最直接的方式是导入外部网格文件。常见格式是 OBJ、PLY、STL 三种。OBJ 和 PLY 一般保留顶点、法线和面信息,STL 只有三角面;CellPACK 这类工具需要法线做内外判定,所以 OBJ/PLY 优先,STL 导入后通常要重新计算法线。如果你手上只有 STL,也不用慌,MeshLab 打开后重算法线再另存为 OBJ 就行。
网格文件从哪来?渠道很多:文献的补充材料、公开的分子结构数据库、课题组自己用显微镜数据重建的分割结果,甚至可以从已有原子结构模型的外表面提取。我见过不少人直接下载 PDB 里的蛋白表面转成 OBJ 用,这也是可行的,只要网格质量合格。重要的是不要拿"未处理"的网格直接用,几乎每个外部来源的网格都需要至少一轮清理。
导入前有几件事必须在外部工具里确认:单位,确认网格文件用的单位是不是和目标一致,导出时别搞混微米和纳米;原点,网格的原点最好和期望的细胞中心一致,免得后面添加变换时算得头晕;轴向,某些软件导出的网格 Y 轴朝上,另一些 Z 轴朝上,packing 后看起来"躺着"不要太意外;法线,确认法线朝外且三角形绕序一致;水密性,确认没有孔洞和自相交。
第 4 章会专门讲这些检查怎么做。这里先记住一个原则:网格导入到 CellPACK 之前,先经过一遍 MeshLab 或 Blender 的"网格体检"。我自己的流程是建模工具导出后必进 MeshLab 清理一轮,再跑一遍自动检查,通过之后才会写进项目配置。这个习惯帮我挡住了至少一半的 runtime 报错。
2.3 Python 程序化生成:参数化曲面与自定义形态
有些几何既没有现成文件,也不适合用基础几何体近似,这时候我建议直接用 Python 程序化生成网格。CellPACK 本身基于 Python 工具链,所以用脚本生成几何再导入是完全顺畅的,尤其是做参数扫描的时候,脚本生成的优势会体现得非常明显。
一个我常用的场景是生成"不规则的椭球核膜"——真实细胞核表面不是完美椭球,而是有微小起伏。此时可以用随机噪声扰动椭球面,生成一个表面起伏的封闭网格。核心思路是:先在球面上建立参数网格,再把每个顶点沿径向扰动,最后三角化成 OBJ。
下面这个示例用 numpy 生成一个带噪声扰动的椭球网格(示意代码,按需修改):
python复制import numpy as np
def noisy_ellipsoid(Rx, Ry, Rz, n_u=64, n_v=32, noise=0.05, seed=42):
rng = np.random.default_rng(seed)
u = np.linspace(0, np.pi, n_u) # 极角
v = np.linspace(0, 2 * np.pi, n_v) # 方位角
U, V = np.meshgrid(u, v)
# 基础椭球面
x = Rx * np.sin(U) * np.cos(V)
y = Ry * np.sin(U) * np.sin(V)
z = Rz * np.cos(U)
# 径向扰动:让表面出现生理上可见的不规则
noise_field = 1.0 + noise * rng.standard_normal(U.shape)
x *= noise_field
y *= noise_field
z *= noise_field
pts = np.stack([x.ravel(), y.ravel(), z.ravel()], axis=-1)
# 按参数网格四边形拆三角(简单方案)
faces = []
for i in range(n_u - 1):
for j in range(n_v - 1):
a = i * n_v + j
b = i * n_v + (j + 1) % n_v
c = (i + 1) * n_v + j
d = (i + 1) * n_v + (j + 1) % n_v
faces.append([a, b, d])
faces.append([a, d, c])
return pts, np.array(faces)
pts, faces = noisy_ellipsoid(500, 400, 350, noise=0.03)
# 这里可以继续用 trimesh 构建 mesh,检查水密性后导出 OBJ
这段代码不复杂,但已经能生成一个用于 CellPACK 的封闭椭球网格。生成之后别忘了做第 4 章的检查:水密、法线、面数。程序化生成最大的好处是可控和可复现:每个参数都写在代码里,换一组参数就是一组新形态,方便做批量模拟。缺点是需要自己维护网格质量,比起 MeshLab 的一键操作,对编程基础有一点要求,但收益绝对值得。
3. 几何编辑实操:从外部工具修改到项目配置接入
几何文件有了之后,下一步是编辑。CellPACK 本身不是一个三维建模软件,所以它的几何编辑工作流天然是"外部分段编辑 + 配置接入"两步走。我在大量项目中摸索出的分工是:Blender 负责创建和修改形态,MeshLab 负责网格体检和修复,CellPACK 项目配置只保存几何引用和空间变换。各干各的,职责清晰。
3.1 外部工具粗修:Blender 与 MeshLab 的分工
Blender 适合做"造型类"编辑:调整大小、旋转、平移,做布尔运算得到壳层或切割形态,用细分修改器增加表面平滑度,用形变修改器做拉长、弯曲等形态变化。比如我要构建一个带内质网小管的细胞几何,可以在 Blender 里先建一个椭球细胞膜,再用一堆小圆柱做布尔并集,形成管状网络,最后把整个结构导出为 OBJ。这种复合形态在真实细胞建模里比比皆是,光靠内置几何体完全不够。
MeshLab 则主打"修复类"功能:清除非流形边(non-manifold edges)、去除重复顶点(duplicate vertices)、填补孔洞(close holes)、统一法线方向(re-orient faces)、简化网格(quadric edge collapse decimation)。这些功能在几何导入 CellPACK 前几乎是必做的。注意操作的顺序也有讲究,我习惯先清理重复顶点,再做非流形边修复,最后补洞和统一法线,顺序反了容易把一些本可以修复的网格直接搞坏。
实际姿势是:Blender 导出 OBJ 后,先不要直接进 CellPACK,先丢进 MeshLab 跑一遍 Cleaning and Repairing 菜单里的几个关键算子,再导出一次。别看多这一步,能省掉后面无数 runtime 报错。我自己的习惯是做完后顺手用第 3.3 节的方法再跑一遍检查,双重保险。
3.2 在 CellPACK 项目配置中引用几何
几何文件编辑完成后,需要在 CellPACK 项目配置里把它接进去。CellPACK 的项目配置是 JSON 风格的结构化文件(不同版本字段名略有差异,以你安装版本的 schema 为准),核心是把 compartment 的几何信息和填充信息关联起来。这里的常见误区是以为配置只写成分,实际上几何引用是配置的第一等公民。
一个典型的 compartment 几何配置长这样(示意,字段名按版本调整):
json复制{
"compartments": [
{
"name": "plasma_membrane_shell",
"geometry": {
"source": "file",
"path": "geometry/plasma_membrane.obj",
"transform": {
"scale": 1.0,
"rotation": [0, 0, 0],
"translation": [0, 0, 0]
}
},
"parent": null,
"ingredients": [
{ "name": "lipid", "count": 350000 },
{ "name": "membrane_protein_avg", "count": 22000 }
]
},
{
"name": "cytoplasm",
"geometry": {
"source": "file",
"path": "geometry/cytoplasm.obj",
"transform": { "scale": 1.0, "rotation": [0, 0, 0], "translation": [0, 0, 0] }
},
"parent": "plasma_membrane_shell",
"ingredients": [
{ "name": "ribosome", "fraction": 0.12 },
{ "name": "metabolic_enzyme_set", "fraction": 0.31 }
]
}
]
}
这个结构里最重要的概念是 parent(父容器)。plasma_membrane_shell 是根,cytoplasm 附着在它内部。这里的 ingredients 既可以用绝对 count,也可以用相对体积 fraction,后者在几何体积会调整时更省心(第 5 章展开)。
配置里的 transform 字段用来调整几何在仿真空间中的位置。大多数情况下,我建议几何文件本身在建模工具里就已经摆好位置,配置里的 translation 和 rotation 只做微调。千万别把"建模坐标调整"和"项目内坐标调整"搞混,否则一旦两个 compartment
