Snakemake规则在输入变更后未重新生成输出的问题
问题场景
我定义了如下Snakemake规则compose,用于处理预存在工作流中的XML文件、资源文件和转换脚本,输出一个标记运行成功的flag文件:
rule compose: input: manuscripts=[ str(a) for a in Path(".").glob(f"./{config['series']}_*_*_*/tei/*.xml") if not (config["publication"] in str(a)) ], assets=[str(a) for a in Path(".").glob(f"{config['series']}_*/assets/*.*")], script=expand("{dir}/other/compose.xsl", dir=config["transform"]), output: touch("composed.flag"), params: publication=config["publication"], saxon=config["saxon"], run: # 创建目录 (Path(".") / "tei/media/").mkdir(parents=True, exist_ok=True) (Path(".") / "assets/").mkdir(parents=True, exist_ok=True) # 生成dummy docx shell("touch {params.publication}.docx") # 复制资源文件 for f in input.assets: shell("cp {f} assets/") shell("cp -rf assets/* tei/media/") # 复制转换脚本 shell("cp -a {input.script} tei/compose.xsl") # 生成手稿列表XML shell("echo '<collection/>' > tei/manuscripts.xml") for man in input.manuscripts: m = man.split("/")[-1].split(".")[0] shell( "xmlstarlet ed -L -s '/collection' -t elem -n 'doc' -v '' -i '/collection/doc[last()]' -t attr -n 'href' -v '../{m}/tei/{m}.xml' tei/manuscripts.xml" ) # 执行XSLT转换 shell( "java -jar {params.saxon} -o:tei/{params.publication}.xml tei/manuscripts.xml tei/compose.xsl projectfolder={params.publication}" ) # 清理临时文件 shell("rm -f tei/manuscripts.xml tei/compose.xsl")
首次调用命令snakemake -p -c1 compose --config workdir="/home/lector/manuscripts/ends-of-humanities/eoh_01_mein_eoh"可正常生成输出,但修改其中一个输入XML文件后,即使该文件时间戳晚于composed.flag,Snakemake仍提示"无任务需执行(所有请求文件已存在且为最新)",甚至添加-f参数强制重跑也无效。
原因分析
1. 静态输入列表导致依赖跟踪失效
规则input中的Path.glob()是在Snakemake启动时的规则解析阶段执行的,只会生成一次输入文件列表。后续修改文件时,Snakemake不会重新扫描路径更新列表:
- 如果修改的文件不在初始扫描的列表中(比如第一次运行时未创建),Snakemake根本不会把它纳入依赖跟踪;
- 即使文件在初始列表中,若Snakemake的元数据缓存(
.snakemake/目录)记录的文件状态未更新,也会误判为输入无变化。
2. -f参数未生效的逻辑
-f参数强制重跑目标规则,但如果当前运行时解析的输入列表为空,或者Snakemake判定规则输出(composed.flag)无需更新,就不会执行规则。
解决方案
1. 改用动态输入函数延迟计算
将输入的glob操作改为自定义函数,确保每次运行Snakemake时都会重新扫描文件路径,生成最新的输入列表:
def get_manuscripts(wildcards, config): return [ str(a) for a in Path(".").glob(f"./{config['series']}_*_*_*/tei/*.xml") if not (config["publication"] in str(a)) ] def get_assets(wildcards, config): return [str(a) for a in Path(".").glob(f"{config['series']}_*/assets/*.*")] rule compose: input: manuscripts=get_manuscripts, assets=get_assets, script=expand("{dir}/other/compose.xsl", dir=config["transform"]), output: touch("composed.flag"), params: publication=config["publication"], saxon=config["saxon"], run: # 原run代码保持不变 ...
2. 清理Snakemake缓存
如果元数据缓存导致异常,删除.snakemake/目录后重新运行,强制Snakemake重新计算所有依赖:
rm -rf .snakemake/ snakemake -p -c1 compose --config workdir="/home/lector/manuscripts/ends-of-humanities/eoh_01_mein_eoh"
3. 声明所有产物为输出(可选)
run代码中生成的tei/{params.publication}.xml、{params.publication}.docx等文件未被声明为output,Snakemake无法跟踪这些产物的变化。若需要维护工作流的完整性,建议将这些文件加入output:
output: touch("composed.flag"), "tei/{params.publication}.xml", "{params.publication}.docx",
内容的提问来源于stack exchange,提问作者Cutú Chiqueño

