油藏工程师小李盯着跑了 3 小时的 STARS 模拟,突然发现第 75 天时生产井 的井底压力可能要跌破下限。传统做法?停算 → 改 .dat 文件 → 重新提交 → 再等 3 小时。
但如果模拟器能在运行过程中"听"外部 Python 脚本的指挥——实时读取数据、自动判断、即时调整策略——一切将截然不同。
这就是 CMG PyControl 的核心价值。本文将以STARS模拟器自带算例 STPYS001为例,从 SAGD 地质模型解析到 Python 脚本逐行拆解,带你完整掌握这套"油藏模拟遥控器"。
在写任何一行控制代码之前,我们必须先理解"控制对象"本身。STPYS001.DAT 是一个典型的 SAGD(Steam Assisted Gravity Drainage,蒸汽辅助重力泄油)模型,用于模拟稠油热采过程。下面按模块逐一拆解。
SAGD 是开采稠油(Bitumen)的核心技术。其原理可以通俗理解为"地下蒸汽烤箱":通过上部的水平井向油藏注入高温高压蒸汽,形成蒸汽腔(Steam Chamber);蒸汽冷凝释放的热量加热周围的稠油,使其粘度急剧降低;被加热变稀的原油在重力作用下向下流动,被下部的水平生产井采集。
模型将地下油藏划分为规则的网格块(Grid Blocks),每个块都有自己的属性。本模型采用 CART 11×10×10 笛卡尔网格,即在 X 方向 11 列、Y 方向 10 行、Z 方向 10 层,共 1,100 个网格块。
** Grid dataGRID CART 11 10 10* 11列 × 10行 × 10层KDIR UP * Z轴向上为正DI CON 2* X方向网格间距:常数 2mDJ JVAR * Y方向网格间距:逐行变化300. 100. 100. 100. 100. 100. 100. 100. 100. 300.DK KVAR * Z方向网格间距:逐层变化1. 1. 1. 1. 1. 2. 2. 2. 2. 2.VAMOD 2 0.5 1 0.5 0.5* 边界块体积修正(Trimmed side blocks)VAMOD 3 0.5 0.5 0.5 1* 底部网格体积修正VATYPE IVAR * 体积类型逐列变化2 1 1 1 1 1 1 1 1 1 2DEPTH TOP 1 1 10 280.0* 顶面埋深 280mPOR CON 0.34* 孔隙度:常数 0.34(34%)PERMI CON 2200* X方向渗透率:2200 mDPERMJ EQUALSI * Y方向 = X方向PERMK EQUALSI * 0.8* Z方向 = X方向 × 0.8 = 1760 mD本模型采用 *MODEL 3 3 3,即 3 个组分、3 个相(油/气/水)、3 个组分方程。三个组分分别是:水(WATER)、沥青(BITUMEN)和溶解气(CH4)。
*MODEL3 3 3* 3组分 / 3相 / 3方程*COMPNAME'WATER''BITUMEN''CH4'*KV10.00.003.191E+04* 粘度系数*KV40.00.00-3.307E+01*KV50.00.00-2.771E+01*CMM0.00.480.01604* 摩尔质量*PCRIT0.01.36E+34.6E+3* 临界压力 (kPa)*TCRIT0.0624.65-82.55* 临界温度 (°C)模型中最关键的输入之一是粘度-温度查找表(VISCTABLE)。它直接定义了沥青在不同温度下的流动能力:
*VISCTABLE** Temp WATER BITUMEN CH4100.05000000200* 10°C: 沥青粘度 5,000,000 cp500.0802812.6* 50°C: 降至 8,028 cp1000.0212.62.45* 100°C: 仅 212.6 cp2000.09.960.83* 200°C: 接近水的粘度3000.03.00.62* 300°C: 极易流动关键发现:从 10°C 到 200°C,沥青粘度下降了超过 50 万倍!这正是 SAGD 工艺可行性的物理基础。Python 脚本后续读取的压力和温度数据,本质上都是在监控这个"加热-降粘-流动"过程是否按预期进行。
相对渗透率(Relative Permeability)描述了多相流体在多孔介质中共同流动时,每相流体的有效渗透率与绝对渗透率之比。本模型定义了两条核心曲线:
| *SWT | ||
| *SLT |
物理意义解读:*SWT 表中,当含水饱和度 Sw=0.19(束缚水饱和度)时,水相相对渗透率 Krw=0,油相相对渗透率 Krow=1.0——意味着只有油能流动。随着含水饱和度增加,Krw 逐渐增大而 Krow 减小,直到 Sw=0.85 时 Krow=0(残余油饱和度),此时油不再流动。这直接决定了注汽过程中水驱油的效率。
*INITIAL*VERTICAL*DEPTH_AVE* 垂向深度平均初始化*REFPRES1340* 参考压力 1340 kPa @ 280m*REFDEPTH280* 参考深度TEMP CON 10* 初始温度:10°C(油藏原始温度)MFRAC_OIL 'BITUMEN' CON 0.95* 油相中沥青摩尔分数 95%MFRAC_OIL 'CH4' CON 0.05* 油相中甲烷摩尔分数 5%*NUMERICAL*DTMAX15* 最大时间步长 15 天*NORM*PRESS500*TEMP60 SATUR 0.2 Y 0.2 X 0.2 W 0.2* 收敛容差:压力 500 kPa, 温度 60°C, 饱和度 0.2*PIVOT*ON* 开启主元消去(数值稳定性)*SDEGREE2* 二阶空间离散*ITERMAX40* 最大牛顿迭代次数*AIM STAB * Adaptive Implicit Method(稳定模式)
模型定义了 4 口井,构成两组 SAGD 井对:
⚠️ 注意井位对应的层号:injtb 和 injan 都在第 5 层(中层),构成第一对 SAGD 井对;prdtb 和 prdan 都在第 2 层(更深层),构成第二对。Python 脚本在 t=75 天时会关闭 injan 和 prdtb,并重新激活 prdan。
理解数据文件后,我们来看 PyControl 的架构设计。这不是简单的"Python 调用程序",而是一个三方实时耦合系统:STARS 模拟器 ↔ OUTBOARD 接口 ↔ Python 控制脚本。
PyControl 的工作机制可以类比为一个工业 PLC 控制系统:
2.2 核心类与 API 详解
CMG 提供的 Python SDK(cmgpy)中有两个核心类,掌握它们是写控制脚本的基础:
📦 Sim_Data — 数据读取接口
Sim_Data 是 STARS 模拟数据的"读取窗口"。通过它,你可以像查数据库一样查询当前模拟时刻的任何数据。
sim_data.get_current_handshake_time() → float
➡️获取当前的握手时间(模拟天数)。这是判断"现在该做什么"的基准。
sim_data.Grid(range_str).get_property(GRIDPROP.PROP, time) → dict
➡️查询指定网格区域在某个时刻的属性。例如 GRIDPROP.PRESSURE 获取压力场。
sim_data.Well(name).get_property(WELLPROPS.PROP) → float
➡️查询单井的属性。例如 WELLPROPS.WELL_REF_LAYER_BLOCK_PRESSURE 获取井底流压。
sim_data.Layer(well, layer).get_property(PHASEPROPS.PROP, PHASETYPE.PHASE)
➡️查询指定井在某层的分相流量。例如 PHASETYPE.WATER 获取注水速率。
sim_data.Sector(name).get_property(SECTORPROPS.PROP, PHASETYPE.PHASE, CONDITIONS.COND)
➡️查询指定区域(Sector)的体积累计量。例如 SECTORPROPS.PHASE_IN_PLACE 获取地质储量。
sim_data.Well(name).shut_in()
➡️关闭指定井。这是一个写操作,会直接改变模拟进程!
Info_to_Sim 是向 STARS 发送控制指令的"遥控器"。它允许你动态发送任何有效的 STARS 关键字命令。
info_to_sim.update_well_change_first_timestep_size(dt)
➡️修改下一个时间步的步长。在关键操作前缩小步长,可以提高计算精度和稳定性。
info_to_sim.send(command_str)
➡️直接向 STARS 发送原始关键字命令。例如重新定义井的操作制度。
理解"何时 Python 脚本被执行"至关重要。流程如下:
STARS: 计算时间步 [t_n → t_n+1] ↓OUTBOARD: 握手(Handshake)→ 暂停 STARS ↓Python: 调用 compute(sim_data, info_to_sim) - 获取当前时间 cur_time - 根据 cur_time 执行对应逻辑分支 - 发送控制指令(如有) ↓OUTBOARD: 将指令传回 STARS ↓STARS: 继续计算下一个时间步💡 关键认知:Python 脚本不是在每个时间步都执行所有代码,而是通过 if/elif cur_time == X 的条件判断,只在特定模拟时刻触发对应操作。这意味着你可以在模型中预埋多个"控制点",实现分阶段的精细化管理。
现在进入本文最核心的部分——对 STPYC001.PY 的完整逐行解析。脚本按照模拟时间线,在 5 个关键节点执行不同的监控和控制任务。
stpyc001.py — 入口与骨架
from utilities import *from sim_data import Sim_Datafrom constraint import Constraintfrom info_to_sim import Info_to_Simfrom grid_origin_type import Block_UBA, Block_Range, GridOriginTypefrom cmgpy import *import math# 全局标志位,控制某些一次性操作is_geo_read = Falsedefcompute(sim_data: Sim_Data, info_to_sim: Info_to_Sim) -> None:"""核心控制函数,每次握手时被 PyControl 调用"""global is_geo_readcur_time = sim_data.get_current_handshake_time()if cur_time <= 0.0:return# 跳过初始时刻print(f"Current Time: {cur_time:7.2f}")# ========== 根据 cur_time 分发到不同逻辑 ==========if cur_time == 40.0: # → 3.2 网格压力巡检 ...elif cur_time == 60.0: # → 3.3 单井压力监控 ...elif cur_time == 75.0: # → 3.4 关键控制操作 ...elif cur_time == 100.0: # → 3.5 分层流量读取 ...elif cur_time == 110.0: # → 3.6 全场地质储量 ...if __name__ == "__main__": cmgpy = CMGPY(compute) # 注册 compute 函数到 PyControlt = 40.0 — 网格压力"体检"
if cur_time == 40.0:# Grid("1 4 2:5") 表示选择 I=1~4, J=2~5, K=全部 的区域# 即 X 方向前 4 列,Y 方向第 2~5 行的整个垂向柱体 range_prop, range_index = sim_data.Grid("1 4 2:5") \ .get_property(GRIDPROP.PRESSURE, cur_time)for cell, cell_pres in range_prop.items():print(f"\tPressure for grid cell '{cell}': {cell_pres:10.2f}")🔍 技术细节
Grid("1 4 2:5"):使用 STARS 的网格范围语法。"I1 I2 J1:J2" 中,单个数字表示该维度全选,范围表示选取指定索引。这里选取了 4×4=16 个水平位置 × 10 层 = 160 个网格。
GRIDPROP.PRESSURE:枚举值,表示查询压力属性。其他常用选项包括 TEMPERATURE、SATURATION_GAS、SATURATION_OIL 等。
返回结果:range_prop 是一个字典,键为网格块标识(如 "3 5 7"),值为对应压力值。通过遍历可以逐格检查压力分布是否均匀、是否存在异常。
t = 60.0 — 单井压力监控
elif cur_time == 60.0:# 获取井 "prdtb" 的参考层块压力 wbl_pres = sim_data.Well("prdtb") \ .get_property(WELLPROPS.WELL_REF_LAYER_BLOCK_PRESSURE)print(f"\tWell Block Pressure for well 'prdtb': {wbl_pres:10.2f}"f" @ time {cur_time:7.2f}") 技术解读:WELL_REF_LAYER_BLOCK_PRESSURE 返回的是井筒所在参考网格块的压力值。对于生产井来说,这相当于井底流压(Bottom Hole Pressure)。监控该值可以判断井是否在合理的工作制度下运行——如果流压低于设定的 MIN BHP,就需要减产或关井保护。
这是整个脚本最精华的部分。在模拟第 75 天,Python 脚本完成了三个连锁操作:
elif cur_time == 75.0:print(f"\tUpdating recurrent data @ time {cur_time:7.2f}")# === 操作 1:缩小下一个时间步长 === info_to_sim.update_well_change_first_timestep_size(0.1)# 将下一个时间步强制设为 0.1 天(默认最大 15 天)# 目的:在即将发生剧烈变化时提高计算精度# === 操作 2:紧急关井 === sim_data.Well("injan").shut_in() # 关闭生产井 injan sim_data.Well("prdtb").shut_in() # 关闭注入井 prdtb# === 操作 3:重新定义 prdan 的生产制度 === well_name = "prdan" bhp_value = 2300.0# 最低井底压力 2300 kPa(原 2480) stl_value = 500.0# 最高产液量 500 m³/d# 通过 send() 发送原始 STARS 关键字命令 info_to_sim.send(f"*PRODUCER '{well_name}'") info_to_sim.send(f"*OPERATE *MIN *BHP {bhp_value}") info_to_sim.send(f"*OPERATE *MAX *STL {stl_value}")Step 1: 缩小步长至 0.1 天
在 STARS 中,时间步长自适应调整。但在井况突变(关井+开井)时,默认步长可能过大而错过关键动态。update_well_change_first_timestep_size(0.1)强制下一个步长仅为 0.1 天,确保剧烈变化被精确捕捉。
Step 2: 关闭 injan 和 prdtb
shut_in() 是写操作,立即生效。被关闭的井在当前时间步后停止生产/注入。注意这里关闭的是第一对 SAGD 井对(injan 在上层第 5 层,prdtb 在下层第 2 层),可能原因是该井对周围油藏已被充分预热。
Step 3:重新定义 prdan 的生产制度
info_to_sim.send() 允许发送任意 STARS 关键字。这里将 prdan 重新定义为生产者,并设定了两个约束条件:MIN BHP 2300(流压不能低于 2300 kPa,比原 2480 更宽松)和 MAX STL 500(日产液量上限 500 m³)。这意味着该井可以在更低的压力下继续生产。
⚠️ 安全提醒:send() 方法非常强大,但也非常危险。它直接发送原始关键字字符串,不做语法检查。一个拼写错误(如 *OPREATE 而非 *OPERATE)就会导致 STARS 报错甚至崩溃。建议先在单独的 .dat 文件中验证命令,再写入 Python 脚本。
t = 100.0 — 分层流量读取
elif cur_time == 100.0:# 获取 injtb 井在第 5 层的水相标准条件流量 layer_wat = sim_data.Layer("injtb", 5) \ .get_property(PHASEPROPS.STANDARD_CONDITION_RATE, PHASETYPE.WATER)print(f"\tLayer STW rate for well 'injtb' and layer #5: "f"{layer_wat} @ time {cur_time:7.2f}")技术解读:Layer 对象实现了分层数据查询,这是比 Well 更精细的读取粒度。SAGD 过程中,蒸汽主要沿上部层位推进,通过分层监控可以判断蒸汽腔的垂向扩展范围。PHASEPROPS.STANDARD_CONDITION_RATE 返回标准条件(通常指地面条件)下的体积流量,PHASETYPE.WATER 指定查询水相。这里的 "STW" 即 Standard condition Water rate。
elif cur_time == 110.0:# 获取 "Entire Field" 区域在标准条件下的原油地质储量 Oil_InPlace_SC = sim_data.Sector("Entire Field") \ .get_property(SECTORPROPS.PHASE_IN_PLACE, PHASETYPE.OIL, CONDITIONS.STANDARD_CONDITION)print(f"\tVolumetric oil In-Place @ SC for sector 'Entire Field': "f"{Oil_InPlace_SC} @ time {cur_time:7.2f}")技术解读:Sector("Entire Field") 查询名为"Entire Field"的统计区域(在 STARS 中通过 *SECTOR 定义,本例默认为全模型)。SECTORPROPS.PHASE_IN_PLACE 返回指定相(原油)在原位条件下的体积。结合 CONDITIONS.STANDARD_CONDITION,结果换算为标准条件,便于与地面产量数据对比。
这个数据是评估采收率的核心指标,Python 脚本可以在每个时间点自动计算并记录该值,生成采收率变化曲线。
掌握了技术细节后,让我们回到工程视角。PyControl 不仅仅是一个"技术玩具",它能切实解决油藏工程中的多个痛点。
历史拟合(History Matching)是油藏模拟中最耗时的环节。传统方法需要工程师手动调整渗透率、孔隙度、相渗曲线等参数,反复对比模拟结果与实际生产数据。
使用 PyControl,你可以:
info_to_sim.send() 将新参数下发给 STARS,继续模拟。当油田有数十口甚至上百口井时,人工监控每口井的状态是不现实的。PyControl 可以作为"中央控制器",实现:
python --version | ||
import cmgpy | ||
💡 Tip 1:从"只读"开始先只使用 sim_data.get_xxx() 方法读取数据,不要急于使用 shut_in() 或 send()。确保你能正确读取数据后,再逐步添加写操作。
💡 Tip 2:善用 print() 调试PyControl 会将 Python 的 print() 输出重定向到 STARS 的日志文件(.log)。在关键位置打印变量值,是排查问题的最有效手段。
⚠️ 避坑 1:send() 命令不做语法检查info_to_sim.send() 发送的字符串直接传给 STARS 解析器。任何拼写错误、格式错误或引号不匹配都会导致模拟崩溃。建议先在独立的 .dat 文件中手动验证命令。
⚠️ 避坑 2:时间判断用 == 而非近似由于浮点数精度问题,cur_time == 75.0 在 STARS 中通常是安全的(因为握手时间由模拟器精确控制),但如果涉及计算(如 cur_time + 0.1 == 75.1),建议使用 abs(cur_time - 75.0) < 1e-6 的判断方式。
⚠️ 避坑 3:shut_in() 是不可逆操作一旦调用 shut_in(),该井在当前模拟中将被永久关闭(除非后续用 send() 重新激活)。务必在调用前添加确认逻辑,避免误操作。
if/elif 搭建控制框架,在每个时间点打印确认信息。update_well_change_first_timestep_size(),再尝试 send(),最后使用 shut_in()。PyControl 的出现,标志着油藏模拟从"离线计算工具"向"实时决策平台"的演进。结合当前 AI 技术的发展,我们可以预见以下几个方向:

"PyControl 不仅仅是一个接口,它是油藏工程与人工智能之间的桥梁。掌握它,你就掌握了将油藏模拟从'事后分析'升级为'实时决策'的钥匙。"
点击"在看"或转发给正在加班跑模型的同事