第4章 GlacierDirectory:数据中枢
GlacierDirectory 是OGGM架构中核心的数据结构。它不仅仅是一个文件路径管理器——它是每条冰川在整个建模生命周期中的"可复现实例"的数据锚点。从GIS预处理到气候数据插值、物质平衡校准、冰厚度反演,再到最终的动力学演化,所有的输入和输出文件都通过GlacierDirectory的接口进行读写。本章深入剖析 oggm/utils/_workflow.py 中GlacierDirectory类的源码(约第2643-3960行,共约1300行)。
4.1 类的整体定位
GlacierDirectory在OGGM中的角色可以类比为:它是一个冰川实体的"文件系统抽象层"。对于调用者(core模块中的各个entity_task)来说,GlacierDirectory屏蔽了:
- 文件存储的物理路径(
base_dir/RGIID[:8]/RGIID[:11]/RGIID/) - 文件名和扩展名(通过BASENAMES间接寻址)
- 数据格式(pickle vs shapefile vs NetCDF vs JSON)
- 压缩方式(gzip vs tar.gz vs uncompressed)
- 多情景的文件后缀(filesuffix)机制
- 任务状态日志的持久化
想象每条冰川在磁盘上有一个自己的文件夹。GlacierDirectory就是这个文件夹的"管理员"——它知道每个文件应该放在哪里、叫什么名字,能帮你读取和写入,还能记录你在这个文件夹中的所有操作历史。你不需要关心硬盘上的实际路径是怎样的,只需要用逻辑名称(如 'centerlines')说出你想要的文件即可。
4.2 构造方式
GlacierDirectory提供了三种构造途径,满足不同的使用场景:
4.2.1 从RGI GeoSeries创建(全新冰川)
这是最常用的方式,用于从未被处理过的冰川开始:
# 典型用法(gis.py 中 init_glacier_directories 任务相关)
import oggm
# rgi_df 是一个 GeoDataFrame,每行为一条冰川
# entity 是其中一行(GeoSeries)
gdir = oggm.GlacierDirectory(entity, base_dir='/data/oggm/per_glacier/',
reset=True, from_tar=False)
构造函数签名(约第2643行):
class GlacierDirectory(object):
def __init__(self, entity, base_dir=None, reset=False,
from_tar=False, delete_tar=True):
# entity 可以是:
# (1) GeoSeries — 从 RGI GeoDataFrame 中迭代的行
# (2) str — 已有的 RGI ID (用于恢复已有目录)
当 entity 是GeoSeries时,构造函数从中提取RGI ID、几何(geometry)、名称、所属区域等信息,然后构建目录路径。reset=True 会清空已有目录中的所有文件,从头开始。
4.2.2 从已有RGI ID恢复(已处理冰川)
# 恢复已处理过的冰川目录(GlacierDirectory)
gdir = oggm.GlacierDirectory('RGI60-11.00001',
base_dir='/data/oggm/per_glacier/')
当 entity 是字符串时,构造函数假设目录已经存在,直接进入现有目录。构造函数会验证目录和必要的元数据文件是否存在。这是运行后续处理步骤(如气候插值或动力学模拟)时的典型入口。
4.2.3 从tar归档创建(传输/共享)
# 从tar归档解压并创建
gdir = oggm.GlacierDirectory('RGI60-11.00001',
base_dir='/data/oggm/per_glacier/',
from_tar=True)
from_tar=True 指示构造函数查找 RGI60-11.00001.tar 归档文件并解压到目标目录。这在以下场景中很有用:
- 从服务器下载预处理的冰川数据包
- 在不同计算节点之间传输冰川数据
- 归档已完成处理的结果
delete_tar=True(默认)在成功解压后删除tar文件以节省磁盘空间。
4.3 目录结构
GlacierDirectory在磁盘上使用两级子目录分片(sharding)来避免单个目录下存放过多文件(可能导致文件系统性能下降):
base_dir/
└── RGI60-11/ # RGI 区域前缀 (RGI ID 的前8个字符)
└── 00/ # 子分片 (RGI ID 的前11个字符的最后2位)
└── RGI60-11.00001/ # 完整的 RGI ID
├── glacier_grid.json
├── centerlines.pkl
├── ...
└── task_status.json
具体规则:
- 第一级路径:
RGI ID[:8]— 例如RGI60-11(前8个字符,通常包含区域前缀) - 第二级路径:
RGI ID[:11]的最后两位 — 例如00(来自RGI60-11.00的00) - 第三级路径:完整的RGI ID — 例如
RGI60-11.00001
这种分片策略确保即使处理RGI全部约20万条冰川,每个目录下也不会包含超过约200个子目录,从而维持文件系统的高效性。
进阶
RGI ID的格式在RGI 6.0和RGI 7.0之间略有差异。RGI 6.0使用 RGI60-X.XXXXX 格式(5位数字),而RGI 7.0使用 RGI2000-v7.0-G-XXXXXXX 格式。OGGM v1.6同时支持这两种格式,但你的 base_dir 中会包含两种不同结构的路径。确保在使用 initialize() 时指定正确的RGI版本,以便目录结构正确分区。
4.4 关键属性
| 属性 | 类型 | 来源 | 说明 |
|---|---|---|---|
gdir.rgi_id |
str | RGI ID | 冰川的RGI唯一标识符,如 'RGI60-11.00001' |
gdir.rgi_area_km2 |
float | RGI属性 Area |
冰川面积(平方公里),用于多项参数化 |
gdir.cenlon |
float | 几何质心 | 冰川质心经度(度),用于气候数据插值 |
gdir.cenlat |
float | 几何质心 | 冰川质心纬度(度) |
gdir.grid |
salem.Grid | glacier_grid.json |
本地投影网格对象(lazy-loaded) |
gdir.intersects_ids |
list[str] | RGI空间查询 | 与当前冰川缓冲区相交的相邻冰川ID列表 |
gdir.dir |
str/pathlib.Path | 构造函数 | 冰川目录(GlacierDirectory)的完整路径 |
gdir.rgi_region |
str | RGI属性 | RGI区域代码(如 '11' 用于中欧) |
gdir.rgi_version |
str | RGI属性 | RGI版本(如 '60') |
grid 属性值得特别说明。它是一个延迟加载(lazy-loaded)属性——只有在第一次访问时才会从磁盘读取 glacier_grid.json。数据库使用 salem.Grid 对象(salem是OGGM团队维护的另一个开源库),提供投影感知的网格操作:
# grid 属性的内部实现(简化版)
@property
def grid(self):
if self._grid is None:
with open(self.get_filepath('glacier_grid')) as f:
self._grid = salem.Grid.from_json(f.read())
return self._grid
4.5 文件I/O方法
GlacierDirectory提供了一套统一的文件I/O接口,所有接口都遵循"先通过BASENAMES解析文件名,再进行I/O操作"的模式。
4.5.1 get_filepath() — 文件路径解析
def get_filepath(self, filename, filesuffix='',
check_exists=False):
# filename 是 BASENAMES 中的逻辑名
# 例如 'centerlines' → 解析 'centerlines' 后获得物理文件名
# 然后拼接 dir / (物理文件名 + filesuffix)
# 例如 dir / 'centerlines_rcp85.pkl'
filesuffix 参数(默认为 cfg.PARAMS['filesuffix'])会自动追加到文件基名和扩展名之间。例如 get_filepath('centerlines', filesuffix='_rcp85') 返回 .../RGI60-11.00001/centerlines_rcp85.pkl。
4.5.2 read_pickle() / write_pickle() — 压缩序列化
def write_pickle(self, obj, filename):
# 使用 gzip + pickle 序列化对象
# 如果 cfg.PARAMS['use_compression'] 为 True(默认),使用 gzip 压缩
with gzip.open(filepath, 'wb') as f:
pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL)
def read_pickle(self, filename, use_compression=None):
# 从 gzip 压缩的 pickle 文件反序列化
with gzip.open(filepath, 'rb') as f:
return pickle.load(f)
压缩可以显著减少磁盘空间——冰厚度反演后的flowline数据可达数MB每条冰川,压缩后可减小到约20%。
4.5.3 read_shapefile() / write_shapefile() — 地理空间I/O
def write_shapefile(self, shp, filename, tar=False):
# shp: geopandas GeoDataFrame 或 fiona 兼容对象
# tar=False: 写入 .shp + .dbf + .shx + ... 文件套件
# tar=True: 将所有文件打包为 .tar.gz 存档
def read_shapefile(self, filename, tar=False):
# tar=False: 从 .shp + 套件读取
# tar=True: 从 .tar.gz 存档读取
return geopandas.read_file(path)
tar选项特别适合空间索引(如 intersects 输出),因为它们由多个文件组成(.shp, .dbf, .shx, .prj 等)。
4.5.4 read_json() / write_json() — 元数据I/O
def write_json(self, data, filename):
with open(filepath, 'w') as f:
json.dump(data, f, indent=2)
def read_json(self, filename):
with open(filepath, 'r') as f:
return json.load(f)
主要用于存储轻量级元数据:glacier_grid.json(网格定义)、local_mustar.json(温度敏感性参数)、dem_source.json(DEM来源信息)等。
4.5.5 has_file() — 存在性检查
def has_file(self, filename):
# 检查文件是否存在(支持tar shapefile的特殊处理)
# 如果是 shapefile 类型,检查 .tar.gz 或 .shp
# 如果是其他类型,检查 .pkl 或 .nc 等
此方法是 entity_task 自动跳过逻辑的底层支撑。每个entity_task的writes_output列表中的文件都会通过 gdir.has_file() 检查其存在性。
4.5.6 write_monthly_climate_file() — NetCDF气候数据
def write_monthly_climate_file(self, time, prcp, temp,
gradient, ref_pix_lon, ref_pix_lat,
filename, source=''):
# 创建包含时间、降水、温度、温度梯度的NetCDF4文件
# 同时写入参考像素坐标和气候数据来源元数据
# 文件结构符合OGGM的climate_data约定
此方法专门用于创建OGGM标准格式的气候文件(climate_historical.nc 和 gcm_data.nc)。它使用 netCDF4 库创建包含以下变量的文件:
temp:月平均温度 (degC)prcp:月总降水量 (kg m^{-2} month^{-1})gradient:月温度梯度 (K km^{-1})ref_pix_lon,ref_pix_lat:参考像素坐标- 全局属性:
source(气候数据来源)
4.6 任务状态系统
GlacierDirectory维护一个 task_status.json 日志文件,记录所有entity_task的执行历史。这是OGGM自动跳过(auto-skip)和错误恢复(error recovery)机制的基石。
4.6.1 get_task_status()
def get_task_status(self, task_name=None):
# 返回指定任务的状态:
# 'success' — 任务成功完成
# 'skipped' — 任务已跳过(输出文件已存在)
# 'error' — 任务执行出错
# None — 任务尚未执行
# task_name=None 时返回所有任务的状态字典
task_status.json 文件的内部结构:
{
"compute_centerlines": {
"status": "success",
"start_time": "2024-01-15 10:23:45",
"end_time": "2024-01-15 10:23:58",
"duration_seconds": 13.2,
"base_version": "1.6.3"
},
"prepare_climate": {
"status": "error",
"start_time": "2024-01-15 10:24:00",
"end_time": "2024-01-15 10:24:02",
"error_traceback": "..."
}
}
4.6.2 get_task_time()
def get_task_time(self, task_name):
# 返回任务执行的开始/结束时间 (datetime对象)
# 用于判断输出文件的新鲜度
4.6.3 get_error_log()
def get_error_log(self):
# 返回所有状态为 'error' 的任务的错误信息
# 适用于自动化错误报告和调试
进阶
任务状态系统看似简单,但它是OGGM能够可靠处理数万条冰川的关键设计。在没有任何任务状态系统的情况下,并行处理的中断会导致灾难性后果:你需要手动检查每一条冰川的文件是否完整,然后从断点重新开始。有了task_status,execute_entity_task() 在重启时自动跳过所有已完成(status='success')的冰川,仅重新处理失败的(status='error')和未开始的(status=None)。这为故障恢复提供了零成本的重启能力。
4.7 气候参考数据管理
GlacierDirectory包含了专门用于管理物质平衡校准所需的气候参考数据的方法:
4.7.1 set_ref_mb_data()
def set_ref_mb_data(self, ref_mb_df):
# 设置参考物质平衡数据 (GeoDataFrame)
# ref_mb_df 包含知道物质平衡的冰川的测量数据
# 数据存储在 self._ref_mb_df 属性中
参考物质平衡数据来源于全球冰川监测服务(WGMS)的测量数据库,仅覆盖全球约500条冰川。在校准过程中,这些数据用于确定每条冰川(包括未被直接测量的)的温度敏感性参数 mu_star。
4.7.2 get_ref_mb_data()
def get_ref_mb_data(self):
# 返回参考物质平衡数据
# 如果在当前区域找不到参考冰川,可能启动二级搜索
4.7.3 get_ref_mb_profile()
def get_ref_mb_profile(self):
# 返回参考物质平衡随高度变化的梯度曲线
# 这是校准 mu_star 的关键输入
4.7.4 get_climate_info()
def get_climate_info(self):
# 返回气候数据的元信息:
# - 参考像素坐标 (经纬度)
# - 气候数据源和覆盖时间段
# - 是否从GCM数据中进行了时间偏移
这些方法体现了GlacierDirectory不仅管理文件I/O,还管理数据的"上下文"——知道哪里获取参考冰川数据、如何校准参数,以及操作这些数据所需的元信息。
4.8 投影处理:UTM vs Transverse Mercator
GlacierDirectory管理地图投影的配置和转换:
def _reproject_and_write_shapefile(self, in_shp, out_filename,
map_proj=None):
# 将输入shapefile重投影为目标投影并写出
# 支持 cfg.PARAMS['map_proj'] 中的两种投影:
# 'tmerc' (横轴墨卡托) — 距离保持,适合高纬度
# 'utm' (通用横轴墨卡托) — 标准UTM分带
OGGM默认使用横轴墨卡托(Transverse Mercator, tmerc)投影,以冰川质心为中心,因为它在局部区域内能很好地保持距离和方向。对于极地地区的冰川,UTM投影可能失效(UTM在84N以上和80S以下未定义),此时横轴墨卡托是更好的选择。
投影选择对计算精度有直接影响:
- tmerc(默认):为每条冰川单独设置投影中心,确保局部变形最小。适合工作流中的距离测量(如grid_dx计算)。
- aea(Albers等面积):保持面积不变,适合面积相关计算。
- utm:使用标准UTM带,适合区域展示。在极地地区自动回退到tmerc。
4.9 文件生命周期:各prepro级别的产出
以下是OGGM预处理管线各层级(prepro level)在GlacierDirectory中创建的文件清单:
| Prepro Level | 实体任务(entity task) | 创建的输出文件 |
|---|---|---|
| Level 0 | define_glacier_region |
glacier_grid.json, dem_source.json |
| Level 1 | compute_centerlines |
centerlines.pkl, downstream_line.pkl, flowline_catchments.pkl, hypsometry.csv |
| Level 2 | process_climate_data |
climate_historical.nc, gcm_data.nc (if GCM data available) |
| Level 3 | local_t_star |
ref_tstars.csv, local_mustar.json |
| Level 4 | prepare_for_inversion |
inversion_input.pkl |
| Level 5 | volume_inversion |
inversion_output.pkl, inversion_flowlines.pkl |
| Level 6 | init_present_time_glacier |
model_geometry.nc, model_diagnostics.nc (initial state) |
重要约定:较低级别必须在其所有上级文件完整存在的基础上运行。例如,不能在没有完成Level 3(local_mustar.json存在)的情况下运行Level 4(prepare_for_inversion)。entity_task的自动跳过机制通过检查 writes_output 中列出的所有文件来强制执行这层约定。
预定义的prepro levels是OGGM对标准使用场景的封装。如果你需要插入自定义步骤(例如在centerlines和climate之间加入一个遥感冰川速度的步骤),你可以:
- 创建一个新的
@entity_task函数,指定writes_output参数 - 在
BASENAMES中注册你的新输出文件类型 - 将你的任务插入到合适的prepro level之间
关键约束是:你的任务的 writes_output 中必须列出所有你会创建的文件,这样自动跳过逻辑才能正确工作。如果遗漏,后续重新运行时会误判你的任务需要重新执行。
4.10 filesuffix 机制:多情景实验的基石
filesuffix 是OGGM支持在单一工作目录下运行多个气候/参数情景的关键设计:
# 场景1: RCP 8.5 (高排放)
cfg.PARAMS['filesuffix'] = '_rcp85'
workflow.execute_entity_task(tasks.run_random_climate, gdirs, ...)
# 输出: model_geometry_rcp85.nc, model_diagnostics_rcp85.nc
# 场景2: RCP 2.6 (低排放)
cfg.PARAMS['filesuffix'] = '_rcp26'
workflow.execute_entity_task(tasks.run_random_climate, gdirs, ...)
# 输出: model_geometry_rcp26.nc, model_diagnostics_rcp26.nc
实现方式是:GlacierDirectory的所有I/O方法内部调用 get_filepath() 时,自动从 cfg.PARAMS['filesuffix'] 读取后缀并拼接。这意味着你不需要修改任何任务代码,只需在运行不同情景前更改配置即可。
工作原理(简化版源码):
def get_filepath(self, filename, filesuffix=None):
if filesuffix is None:
filesuffix = cfg.PARAMS.get('filesuffix', '')
# 从 BASENAMES 获取物理文件名
base_name = cfg.BASENAMES[filename]
# 插入 filesuffix(在基名和扩展名之间)
stem, ext = os.path.splitext(base_name)
if filesuffix:
actual_name = f"{stem}{filesuffix}{ext}"
else:
actual_name = base_name
return os.path.join(self.dir, actual_name)
(1) 使用描述性的后缀名:'_rcp85'比 '_v2' 更好。
(2) 后缀通常以 '_' 开头以避免与基本文件名混淆。
(3) 你的自定义 @entity_task 应该使用 gdir.get_filepath() 而非手动拼接文件名,以自动享受filesuffix支持。
(4) 在分析脚本中,使用相同的filesuffix从各种文件中一致地读取数据:gdir.read_pickle('model_geometry', filesuffix='_rcp85')。
4.11 GlacierDirectory 类的关键代码量分布
以下是GlacierDirectory类(约1300行)的功能分解:
| 功能区域 | 大致行数 | 说明 |
|---|---|---|
| 构造函数和属性 | ~200 | __init__, grid, rgi_id, 等基础属性 |
| 文件路径和I/O | ~400 | get_filepath, read/write_pickle/shapefile/json/nc |
| 任务状态系统 | ~150 | task_status读写、错误日志 |
| 气候和管理 | ~250 | set/get_ref_mb_data, climate_info, monthly_climate |
| GIS和投影 | ~150 | _reproject_and_write_shapefile, grid属性 |
| 辅助方法 | ~150 | reset_directory, intersects, 等各种工具 |
4.12 总结
GlacierDirectory是OGGM中"关注点分离"原则的典范:
- 对 core 模块透明:core中的科学代码不需要关心文件存储在何处、如何压缩、采用什么命名——只需要调用
gdir.read_pickle('centerlines') - 对用户透明:用户不需要手动管理文件路径——filesuffix机制允许无缝的多情景切换
- 对并行透明:每条冰川的独立目录使其天然适合多进程处理
- 对扩展透明:BASENAMES允许shop模块在无需修改GlacierDirectory代码的情况下注册新的文件类型
在后续章节中,每当看到 gdir.read_xxx() 或 gdir.write_xxx() 调用时,请记住本章的内容——这些简洁的接口背后是OGGM精心设计的文件管理架构。