RWEQ计算——EF和SCF因子

1.EF因子

RWEQ 将 EF 定义为:表层约 25 mm 土壤中,粒径小于 0.84 mm 的部分所占比例。它需要通过标准旋转筛分测定。

例如:

EF=0.60 EF=0.60 EF=0.60

意味着表层土样中约 60% 的质量由小于 0.84 mm 的颗粒或团聚体组成。EF 越大,说明地表可被风起动、跳跃和搬运的细小物质越多,潜在风蚀性通常越强。

实际应用中,最理想的方法不是只测一次,而是在多年中定期采集表层土样、筛分,并关注一年内较高的 EF 状态。原因很简单:风蚀往往发生在地表最脆弱的时期,而不是土壤最稳定的时期。

当没有筛分数据时,RWEQ 用土壤基本性质估算 EF:

EF=29.09+0.31Sa+0.17Si+0.33(Sa/Cl)−2.59OM−0.95CaCO3100 EF= \frac{ 29.09+0.31Sa+0.17Si+0.33(Sa/Cl)-2.59OM-0.95CaCO_3 }{100} EF=10029.09+0.31Sa+0.17Si+0.33(Sa/Cl)−2.59OM−0.95CaCO3

其中:

Sa:砂粒含量(%);

Si:粉粒含量(%);

Cl:黏粒含量(%);

OM:有机质含量(%);

CaCO_3:碳酸钙含量(%)。

从符号方向可以快速理解这个经验式:

  • 砂粒 (Sa) 增加,EF 倾向增加;

  • 粉粒 (Si) 增加,EF 倾向增加;

  • 砂/黏比 (Sa/Cl) 增加,EF 倾向增加;

  • 有机质 (OM) 增加,EF 倾向降低;

  • 碳酸钙 (CaCO_3) 增加,EF 倾向降低。

这背后的土壤学直觉是:较多砂粒、粉粒或较高砂/黏比,往往意味着更多易松散、易移动的颗粒;而有机质有助于团聚体形成和稳定,碳酸钙也可能带来一定胶结或结构稳定作用,因此会降低模型估计的可蚀比例。

但要注意:这不是一个"由质地决定一切"的公式。它是用土壤性质去预测筛分 EF 的经验关系,文档报告其 r2=0.67r^2=0.67r2=0.67。因此,若已经有本地表层土的筛分结果,应优先使用实测 EF;若输入值超出原始建模数据范围,外推结果更应谨慎解释。

2.SCF因子

SCF 的全称是 Soil Crust Factor,即土壤结皮因子。它描述的是:当雨滴打击地表、重新分配颗粒并形成结皮后,土壤团聚体抵抗风沙磨蚀的能力如何变化。在RWEQ中,当自上次耕作以来累积降雨量等于或超过12毫米时,计算土壤地壳因子。当粘土含量低于5%或耕作后无表壳时,SCF设为1。

利用Hagen等人(1992)的磨料系数数据库开发SCF的经验系数。其的估算式为:

SCF=11+0.0066(Cl)2+0.021(OM)2 SCF= \frac{1} {1+0.0066(Cl)^2+0.021(OM)^2} SCF=1+0.0066(Cl)2+0.021(OM)21

可以立刻看出:

0<SCF≤1 0<SCF\leq1 0<SCF≤1

且黏粒 (Cl) 或有机质 (OM) 增加时,分母增大,SCF 减小。

在 RWEQ 的计算中,SCF 是乘到风蚀潜力上的因子。因而:

SCF≈1SCF\approx1SCF≈1:结皮造成的抑制较弱;

SCFSCFSCF 较小:模型认为该结皮表面的抗磨蚀作用较强,对风蚀潜力的削弱更明显。

二、因子计算原理

1. 有机碳转换为有机质

HWSD 输入为有机碳 OC,而公式要求有机质 OM:

text 复制代码
OM = 1.724 × OC

OM、OC 均以百分数表示。转换结果单独保存为 OM_from_OC_HWSD.tif,便于检查和复用。

2. 土壤可蚀因子 EF

text 复制代码
EF = (29.09 + 0.31×Sa + 0.17×Si + 0.33×Sa/Cl
      - 2.59×OM - 0.95×CaCO3) / 100

参考相关文献,本次计算未考虑碳酸钙,令 CaCO3=0。因此输出文件名明确标记为 EF_HWSD_CaCO3_ignored.tif,避免以后误认为已经包含碳酸钙项。

公式产生的负 EF 没有物理意义,程序按文献报告的值域将其截为 0;同时设置上限为 1。

3.其他的考虑

有研究表明,美国制的土壤粒度和中国制的有所差别,因而要进行调整。研究基于HWSD数据发现土壤有机质和其范围的差别比较大,因而对其限制在4.7%范围内(研究也分析了soilgrid数据也发现有诸如此类的问题)。

三、代码实现

1. 最终口径

本版本根据土壤变量含义和RWEQ经验公式适用范围作如下处理:

  • Sa、Si、Cl:直接使用HWSD v1.2表层0--30 cm质量百分数;
  • OC:保留HWSD原始值;
  • 原始OM:OM_raw = 1.724 × OC,用于数据审计;
  • 模型OM:OM_model = min(OM_raw, 4.79)
  • CaCO3:直接使用HWSD原始质量百分数0--29.5%,不截断;
  • 基准EF:使用OM_model,不包含CaCO3项;
  • 敏感性EF:使用OM_model和完整原始CaCO3;
  • 推荐SCF:使用OM_model。

2. 推荐结果

  • rasters/EF_baseline_OMcap4p79.tif:推荐基准EF;
  • rasters/EF_sensitivity_CaCO3raw.tif:加入未截断CaCO3的敏感性EF;
  • rasters/SCF_OMcap4p79.tif:推荐SCF;
  • rasters/OM_0_4p79pct.tif:推荐RWEQ模型OM输入;
  • rasters/CaCO3_raw_pct.tif:未截断CaCO3输入。

EF_baseline_rawOM.tifSCF_rawOM.tifOM_raw_pct.tif用于复现与审计,不建议作为最终RWEQ输入。

3. 修订依据

HWSD的T_OCT_CACO3均为表层0--30 cm质量百分数。高OM主要来自Histosols等有机土,并非单位错误,但超过了RWEQ经验方程常用的OM标定范围。本版本按用户确认将OM最大值限制为4.79%。

HWSD CaCO3的0--29.5%范围与钙质土、盐土等土壤类型相符,且接近文献中RWEQ方程使用过的0--25.2%范围。本版本依据用户判断保留HWSD完整CaCO3范围,不做截断;29.5%附近属于轻度超出常见标定范围的外推,已在限制部分注明。

4. 公式

text 复制代码
OM_raw   = 1.724 × OC
OM_model = min(OM_raw, 4.79)

EF_baseline = (29.09 + 0.31×Sa + 0.17×Si + 0.33×Sa/Cl
               - 2.59×OM_model) / 100

EF_sensitivity = (29.09 + 0.31×Sa + 0.17×Si + 0.33×Sa/Cl
                  - 2.59×OM_model - 0.95×CaCO3_raw) / 100

SCF = 1 / (1 + 0.0066×Cl² + 0.021×OM_model²)
python 复制代码
# -*- coding: utf-8 -*-
"""Create auditable HWSD inputs and RWEQ EF/SCF products for China."""

from __future__ import annotations

import csv
import json
import logging
from pathlib import Path

import arcpy
from arcpy.sa import Con, Raster


ROOT = Path(r"D:\RWEQ_china\data\RWEQ_EF_SCF_OMcap4p79_CaCO3raw_20260905")
RASTERS = ROOT / "rasters"
TABLES = ROOT / "tables"
LOGS = ROOT / "logs"
SOURCE = Path(r"F:\geodata\15土壤数据\HWSD")

INPUTS = {
    "Sa_pct": SOURCE / "HWSD_china_sand.tif",
    "Si_pct": SOURCE / "HWSD_china_slit.tif",
    "Cl_pct": SOURCE / "HWSD_china_clay.tif",
    "OC_pct": SOURCE / "HWSD_china_oc.tif",
    "CaCO3_raw_pct": SOURCE / "HWSD_china_CaCO3.tif",
}
OC_TO_OM = 1.724
OM_CAP_PCT = 4.79


def setup_logging() -> None:
    """Configure UTF-8 file and console logging."""
    LOGS.mkdir(parents=True, exist_ok=True)
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(message)s",
        handlers=[
            logging.FileHandler(LOGS / "run.log", encoding="utf-8", mode="w"),
            logging.StreamHandler(),
        ],
    )


def validate_grids(paths: list[Path]) -> None:
    """Assert that all source rasters share one grid and spatial reference."""
    for path in paths:
        if not arcpy.Exists(str(path)):
            raise FileNotFoundError(f"Missing source raster: {path}")
    ref = arcpy.Describe(str(paths[0]))
    for path in paths[1:]:
        desc = arcpy.Describe(str(path))
        checks = (
            desc.width == ref.width,
            desc.height == ref.height,
            abs(desc.meanCellWidth - ref.meanCellWidth) < 1e-12,
            abs(desc.meanCellHeight - ref.meanCellHeight) < 1e-12,
            desc.spatialReference.factoryCode == ref.spatialReference.factoryCode,
            abs(desc.extent.XMin - ref.extent.XMin) < 1e-10,
            abs(desc.extent.YMin - ref.extent.YMin) < 1e-10,
        )
        if not all(checks):
            raise ValueError(f"Grid mismatch: {path}")


def save_raster(raster: Raster, path: Path) -> None:
    """Save a compressed GeoTIFF and calculate exact statistics."""
    raster.save(str(path))
    arcpy.management.CalculateStatistics(str(path), 1, 1, "", "OVERWRITE")
    logging.info("Saved %s", path)


def copy_input(source: Path, target: Path) -> None:
    """Copy a source raster into the self-contained calculation folder."""
    arcpy.management.CopyRaster(
        str(source), str(target), nodata_value="", pixel_type="", format="TIFF"
    )
    arcpy.management.CalculateStatistics(str(target), 1, 1, "", "OVERWRITE")
    logging.info("Copied %s", target)


def property_value(path: Path, prop: str) -> float:
    """Read a numeric ArcGIS raster property."""
    return float(arcpy.management.GetRasterProperties(str(path), prop).getOutput(0))


def raster_stats(path: Path) -> dict[str, object]:
    """Return exact descriptive statistics and grid metadata."""
    desc = arcpy.Describe(str(path))
    return {
        "raster": path.name,
        "path": str(path),
        "minimum": property_value(path, "MINIMUM"),
        "maximum": property_value(path, "MAXIMUM"),
        "mean": property_value(path, "MEAN"),
        "std": property_value(path, "STD"),
        "rows": desc.height,
        "columns": desc.width,
        "cell_width_deg": desc.meanCellWidth,
        "cell_height_deg": desc.meanCellHeight,
        "crs": desc.spatialReference.name,
    }


def main() -> None:
    """Run the complete, idempotent EF/SCF production workflow."""
    for directory in (ROOT, RASTERS, TABLES, LOGS):
        directory.mkdir(parents=True, exist_ok=True)
    setup_logging()
    validate_grids(list(INPUTS.values()))

    arcpy.env.overwriteOutput = True
    arcpy.env.compression = "LZW"
    arcpy.env.parallelProcessingFactor = "100%"
    arcpy.env.snapRaster = str(INPUTS["Sa_pct"])
    arcpy.env.cellSize = str(INPUTS["Sa_pct"])
    arcpy.env.extent = str(INPUTS["Sa_pct"])
    arcpy.env.outputCoordinateSystem = arcpy.Describe(
        str(INPUTS["Sa_pct"])
    ).spatialReference

    arcpy.CheckOutExtension("Spatial")
    try:
        copied: dict[str, Path] = {}
        for name, source in INPUTS.items():
            target = RASTERS / f"{name}.tif"
            copy_input(source, target)
            copied[name] = target

        sa = Raster(str(copied["Sa_pct"]))
        si = Raster(str(copied["Si_pct"]))
        cl = Raster(str(copied["Cl_pct"]))
        oc = Raster(str(copied["OC_pct"]))
        caco3_raw = Raster(str(copied["CaCO3_raw_pct"]))

        products: dict[str, Path] = dict(copied)
        om_raw = oc * OC_TO_OM
        products["OM_raw_pct"] = RASTERS / "OM_raw_pct.tif"
        save_raster(om_raw, products["OM_raw_pct"])

        om_model = Con(om_raw > OM_CAP_PCT, OM_CAP_PCT, om_raw)
        products["OM_0_4p79pct"] = RASTERS / "OM_0_4p79pct.tif"
        save_raster(om_model, products["OM_0_4p79pct"])

        ef_base_raw = (
            29.09 + 0.31 * sa + 0.17 * si + 0.33 * (sa / cl) - 2.59 * om_raw
        ) / 100.0
        ef_base_raw = Con(ef_base_raw < 0, 0, Con(ef_base_raw > 1, 1, ef_base_raw))
        products["EF_baseline_rawOM"] = RASTERS / "EF_baseline_rawOM.tif"
        save_raster(ef_base_raw, products["EF_baseline_rawOM"])

        ef_base = (
            29.09 + 0.31 * sa + 0.17 * si + 0.33 * (sa / cl) - 2.59 * om_model
        ) / 100.0
        ef_base = Con(ef_base < 0, 0, Con(ef_base > 1, 1, ef_base))
        products["EF_baseline_OMcap4p79"] = (
            RASTERS / "EF_baseline_OMcap4p79.tif"
        )
        save_raster(ef_base, products["EF_baseline_OMcap4p79"])

        ef_sensitivity = (
            29.09
            + 0.31 * sa
            + 0.17 * si
            + 0.33 * (sa / cl)
            - 2.59 * om_model
            - 0.95 * caco3_raw
        ) / 100.0
        ef_sensitivity = Con(
            ef_sensitivity < 0, 0, Con(ef_sensitivity > 1, 1, ef_sensitivity)
        )
        products["EF_sensitivity_CaCO3raw"] = (
            RASTERS / "EF_sensitivity_CaCO3raw.tif"
        )
        save_raster(ef_sensitivity, products["EF_sensitivity_CaCO3raw"])

        scf_raw = 1.0 / (1.0 + 0.0066 * (cl**2) + 0.021 * (om_raw**2))
        products["SCF_rawOM"] = RASTERS / "SCF_rawOM.tif"
        save_raster(scf_raw, products["SCF_rawOM"])

        scf = 1.0 / (1.0 + 0.0066 * (cl**2) + 0.021 * (om_model**2))
        products["SCF_OMcap4p79"] = RASTERS / "SCF_OMcap4p79.tif"
        save_raster(scf, products["SCF_OMcap4p79"])

        stats = [raster_stats(path) for path in products.values()]
        csv_path = TABLES / "all_raster_statistics.csv"
        with csv_path.open("w", encoding="utf-8-sig", newline="") as stream:
            writer = csv.DictWriter(stream, fieldnames=list(stats[0]))
            writer.writeheader()
            writer.writerows(stats)
        (TABLES / "all_raster_statistics.json").write_text(
            json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8"
        )
        logging.info("Statistics written to %s", csv_path)
    finally:
        arcpy.CheckInExtension("Spatial")


if __name__ == "__main__":
    main()
相关推荐
木卫四科技1 小时前
Agent Supply Chain Security:MCP、Skills 与 AI Agent 软件供应链安全新边界 | 木卫四科技
大数据·人工智能·安全·ai-bom·robot security·机器人安全·具身安全
SomeOtherTime1 小时前
电场相关问题2(AI回答)
人工智能
牛油果子哥q1 小时前
Prompt工程核心落地:结构化约束、Few-Shot示例、输出稳定性控
大数据·人工智能·prompt
刘天远1 小时前
企业 Agent 需求怎么写:数据结构、流程图与 Python 校验
数据结构·人工智能·python·流程图
一次旅行1 小时前
2026‑09‑05 AI产业深度解读|Claude 11天验证费马大定理、智能体Wiki通信逃逸、GPT-6 Astra安全攻防升级
人工智能·gpt·安全
人工智能AI技术1 小时前
客服数据分析Agent落地实战:告别Demo,打通从零散对话到钉钉自动交付全链路
人工智能
新知图书1 小时前
7.3 LangGraph中TypedDict与BaseModel的共通与嵌套
人工智能·智能体
旖旎夜光1 小时前
【LangChain实战】LangChain 学习笔记(一):从定义大模型到工具调用
人工智能·笔记·python·学习·langchain
月华路1 小时前
《模型不玄学》第16章 概率校准
人工智能·深度学习·机器学习