第4章 GlacierDirectory:数据中枢

GlacierDirectory 是OGGM架构中核心的数据结构。它不仅仅是一个文件路径管理器——它是每条冰川在整个建模生命周期中的"可复现实例"的数据锚点。从GIS预处理到气候数据插值、物质平衡校准、冰厚度反演,再到最终的动力学演化,所有的输入和输出文件都通过GlacierDirectory的接口进行读写。本章深入剖析 oggm/utils/_workflow.py 中GlacierDirectory类的源码(约第2643-3960行,共约1300行)。

4.1 类的整体定位

GlacierDirectory在OGGM中的角色可以类比为:它是一个冰川实体的"文件系统抽象层"。对于调用者(core模块中的各个entity_task)来说,GlacierDirectory屏蔽了:

入门
类比:GlacierDirectory 如同冰川的"专属文件夹"

想象每条冰川在磁盘上有一个自己的文件夹。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全部约20万条冰川,每个目录下也不会包含超过约200个子目录,从而维持文件系统的高效性。

进阶
RGI版本与目录结构的兼容性

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.ncgcm_data.nc)。它使用 netCDF4 库创建包含以下变量的文件:

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以下未定义),此时横轴墨卡托是更好的选择。

投影选择对计算精度有直接影响:

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管线

预定义的prepro levels是OGGM对标准使用场景的封装。如果你需要插入自定义步骤(例如在centerlines和climate之间加入一个遥感冰川速度的步骤),你可以:

  1. 创建一个新的 @entity_task 函数,指定 writes_output 参数
  2. BASENAMES 中注册你的新输出文件类型
  3. 将你的任务插入到合适的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)
filesuffix 最佳实践

(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中"关注点分离"原则的典范:

在后续章节中,每当看到 gdir.read_xxx()gdir.write_xxx() 调用时,请记住本章的内容——这些简洁的接口背后是OGGM精心设计的文件管理架构。