储量估算完整管线
从钻孔数据库 .dmd 和矿体约束 .dmf 出发,依次完成格式转换、特高品位处理、样长组合、块段模型创建和距离幂估值,最终输出估值后的 .dmb 块段模型。
前置条件
uv add pandas numpy
- 环境变量
DIMINE_HOME指向包含DmPyBindInterface.pyd的目录 - Python >= 3.12
- 输入文件:钻孔数据库
.dmd+ 矿体约束.dmf
流程概览
钻孔数据库(.dmd) 矿体模型(.dmf)
│ │
▼ │
① DMD→DMG 格式转换 │
│ │
▼ │
② 特高品位处理(迭代收敛) │
│ │
▼ │
③ 样长组合 │
│ │
▼ ▼
④ 创建空块段模型 ←── 矿体包围盒
│
▼
⑤ 搜索椭球体参数 ←── MVEE 算法
│
▼
⑥ 距离幂估值(IDW,迭代扩张)
步骤一:DMD → DMG 格式转换 + 品位字段检测
将钻孔数据库从 .dmd 转换为 .dmg 分段文件格式,并自动检测有效的品位字段。
from dimine_python_sdk.lib.io import DmdFile, DmgFile, convert_file
dmd_path = "01_地质勘探数据库.dmd"
dmg_raw_path = "01_raw.dmg"
# 1.1 格式转换
convert_file(dmd_path, dmg_raw_path)
print(f"已转换: {dmd_path} → {dmg_raw_path}")
# 1.2 加载 DMG 分段数据,自动检测品位字段
dmg = DmgFile(dmg_raw_path)
segments_df = dmg.to_segments_dataframe()
dmg.close()
print(f"DMG 分段数据: {len(segments_df)} 行")
print(f"列名: {list(segments_df.columns)}")
# 1.3 从数值列中排除非品位字段,自动筛选有效品位
numeric_cols = segments_df.select_dtypes(include="number").columns.tolist()
excluded = {
"起始", "结束", "样长", "X", "Y", "Z", "覆盖长度", "覆盖率",
"测斜深度", "方位角", "倾角", "样本编号", "钻孔编号",
"start_x", "start_y", "start_z", "end_x", "end_y", "end_z",
"SAMPLE-ID", "SAMFROM", "SAMTO",
}
candidate_fields = [c for c in numeric_cols if c not in excluded]
# 1.4 过滤无有效数据的字段(全为 ≤0、NaN、Inf)
valid_grade_fields = []
for field in candidate_fields:
col = segments_df[field]
is_invalid = (
col.isna() | col.isin([float("inf"), float("-inf")]) | (col <= 0)
).all()
if not is_invalid:
valid_grade_fields.append(field)
print(f"检测到的有效品位字段: {valid_grade_fields}")
# 1.5 读取钻孔元信息(仅用于显示)
db = DmdFile(dmd_path)
print(f"钻孔数: {len(db.collar)}")
print(f"样品数: {len(db.sample)}")
步骤二:特高品位处理(迭代收敛)
特高品位处理需要迭代执行:一轮处理会降低矿体平均品位,原本不超限的样品在新平均值下可能成为新的特高品位。需要反复迭代直到再无样品超过阈值。
import shutil
from dimine_python_sdk.lib.io import DmgFile
from dimine_python_sdk.lib.prospecting import DrillFunctionWrapper
from dimine_python_sdk.lib.prospecting.models import HighGradeProcessParam
# 2.1 拷贝原始文件(特高品位处理原地修改)
dmg_hg_path = "02_high_grade.dmg"
shutil.copy2(dmg_raw_path, dmg_hg_path)
# 2.2 参数配置
HG_AVERAGE_MULTIPLE = 8 # 平均值倍数(铁矿石=8,金/有色金属=6,煤矿/非金属=7)
HG_PROCESS_MODE = 0 # 0=国内规范(均值倍数),1=国外规范(累积频率)
HG_REPLACE_METHOD = 3 # 0=剔除 1=给定值 2=相邻样品均值 3=矿体平均品位 4=单一工程 5=截止品位
MAX_HG_ITERATIONS = 5 # 安全上限
REPLACE_NAMES = {
0: "剔除法", 1: "给定值法", 2: "相邻样品平均值法",
3: "矿体平均品位法", 4: "单一工程法", 5: "截止品位法",
}
for grade_field in valid_grade_fields:
print(f"\n处理元素: {grade_field}")
print(f" 模式: {'国内规范' if HG_PROCESS_MODE == 0 else '国外规范'}")
print(f" 倍数: {HG_AVERAGE_MULTIPLE}x, 替换方法: {REPLACE_NAMES[HG_REPLACE_METHOD]}")
for iteration in range(1, MAX_HG_ITERATIONS + 1):
# 读取当前 DMG 品位数据
dmg = DmgFile(dmg_hg_path)
segments_df = dmg.to_segments_dataframe()
dmg.close()
if grade_field not in segments_df.columns:
print(f" [SKIP] 字段不存在")
break
# 过滤有效品位(排除 ≤0、NaN、Inf)
grades = segments_df[grade_field].dropna()
grades = grades[~grades.isin([float("inf"), float("-inf")])]
grades = grades[grades > 0]
if len(grades) == 0:
print(f" [SKIP] 无有效品位数据")
break
# 计算阈值
avg_grade = float(grades.mean())
threshold = avg_grade * HG_AVERAGE_MULTIPLE
exceeding_count = int((grades > threshold).sum())
print(f" 迭代 #{iteration}: 样品数={len(grades)}, "
f"均值={avg_grade:.4f}, 阈值={threshold:.4f}, "
f"超限={exceeding_count}, 最大值={float(grades.max()):.4f}")
if exceeding_count == 0:
print(f" [收敛] 已无超限样品")
break
# 调用 C++ 特高品位处理
hg_params = HighGradeProcessParam(
input_file=dmg_hg_path,
grade_field=grade_field,
process_mode=HG_PROCESS_MODE,
average_multiple=HG_AVERAGE_MULTIPLE,
frequency=0.95,
replace_method=HG_REPLACE_METHOD,
assign_value=2.6,
adjoin_average=3.2,
average_method=0,
contain_mode=0,
)
result = DrillFunctionWrapper.extra_high_grade_process(hg_params)
print(f" C++ 处理: {result.get('message', result)}")
else:
print(f" [警告] 达到最大迭代次数 {MAX_HG_ITERATIONS},可能未完全收敛")
replace_method 枚举:
| 值 | 方式 | 适用场景 |
|---|---|---|
| 0 | 剔除法 | 样品充足时 |
| 1 | 给定值法 | 有经验阈值时 |
| 2 | 相邻样品平均值法 | 通用推荐 |
| 3 | 矿体平均品位法 | 矿体品位稳定时 |
| 4 | 单一工程法 | 钻孔独立性强时 |
| 5 | 截止品位法 | 有工业指标时 |
步骤三:样长组合
将不等长样品合并为指定长度的等长组合样,覆盖率低于阈值的残段将被丢弃。
from dimine_python_sdk.lib.prospecting import DrillFunctionWrapper
from dimine_python_sdk.lib.prospecting.models import SampleLengthCombineParam
dmg_combined_path = "03_combined.dmg"
combine_params = SampleLengthCombineParam(
input_file=dmg_hg_path,
combine_length=2.0, # 组合样长度(m)
combine_percent=0.75, # 最小覆盖率
output_file=dmg_combined_path,
)
print(f"组合长度: {combine_params.combine_length}m")
print(f"最小覆盖率: {combine_params.combine_percent}")
result = DrillFunctionWrapper.sample_length_combine(combine_params)
print(f"组合结果: {result}")
步骤四:从矿体包围盒创建空块段模型
从矿体 .dmf 模型中提取所有几何顶点,计算包围盒,加上边距后创建空块段模型。
import math
import numpy as np
from dimine_python_sdk.lib.io import DmfFile, DmbFile
from dimine_python_sdk.lib.prospecting import create_block_model
from dimine_python_sdk.lib.prospecting.models import (
CreateBlockModelParams,
FieldDefinition,
)
from dimine_python_sdk.models.types import Shell, Point as DmfPoint, Line as DmfLine
dmf_constraint_path = "约束_矿体.dmf"
dmb_output_path = "04_block_model.dmb"
# 4.1 从 DMF 计算矿体包围盒
dmf = DmfFile(dmf_constraint_path)
all_points = []
for layer in dmf.layers:
for entity in layer.entities:
if isinstance(entity, Shell):
pts = entity.geometry.points
if pts is not None and len(pts) > 0:
all_points.append(pts)
elif isinstance(entity, DmfLine):
geom = entity.geometry
if geom is not None and len(geom) > 0:
all_points.append(geom)
elif isinstance(entity, DmfPoint):
geom = entity.geometry
if geom is not None and len(geom) > 0:
all_points.append(geom.reshape(1, 3))
pts = np.vstack(all_points)
dmf.close()
min_x, max_x = float(pts[:, 0].min()), float(pts[:, 0].max())
min_y, max_y = float(pts[:, 1].min()), float(pts[:, 1].max())
min_z, max_z = float(pts[:, 2].min()), float(pts[:, 2].max())
print(f"矿体包围盒:")
print(f" X: {min_x:.2f} ~ {max_x:.2f}")
print(f" Y: {min_y:.2f} ~ {max_y:.2f}")
print(f" Z: {min_z:.2f} ~ {max_z:.2f}")
# 4.2 计算块段模型参数(加边距并取整)
BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z = 5.0, 5.0, 5.0
MARGIN_XY, MARGIN_Z = 20.0, 10.0
origin_x = min_x - MARGIN_XY
origin_y = min_y - MARGIN_XY
origin_z = min_z - MARGIN_Z
length_x_raw = max_x - min_x + 2 * MARGIN_XY
length_y_raw = max_y - min_y + 2 * MARGIN_XY
length_z_raw = max_z - min_z + 2 * MARGIN_Z
nx = math.ceil(length_x_raw / BLOCK_SIZE_X)
ny = math.ceil(length_y_raw / BLOCK_SIZE_Y)
nz = math.ceil(length_z_raw / BLOCK_SIZE_Z)
length_x = nx * BLOCK_SIZE_X
length_y = ny * BLOCK_SIZE_Y
length_z = nz * BLOCK_SIZE_Z
print(f"\n块段模型参数:")
print(f" 原点: ({origin_x:.2f}, {origin_y:.2f}, {origin_z:.2f})")
print(f" 单元块: {BLOCK_SIZE_X}m × {BLOCK_SIZE_Y}m × {BLOCK_SIZE_Z}m")
print(f" 范围: {length_x:.1f}m × {length_y:.1f}m × {length_z:.1f}m")
print(f" 块数: {nx} × {ny} × {nz} = {nx * ny * nz:,}")
# 4.3 创建空块段模型
block_params = CreateBlockModelParams(
file_name=dmb_output_path,
origin_x=origin_x,
origin_y=origin_y,
origin_z=origin_z,
size_x=BLOCK_SIZE_X,
size_y=BLOCK_SIZE_Y,
size_z=BLOCK_SIZE_Z,
length_x=length_x,
length_y=length_y,
length_z=length_z,
field_definition=[
FieldDefinition(field_name="岩石类型", field_type="字节型"),
FieldDefinition(field_name="体重", field_type="浮点型"),
],
)
create_result = create_block_model(block_params)
print(f"创建结果: {create_result}")
# 4.4 验证块段模型属性
with DmbFile(dmb_output_path) as block_model:
print(f"\n块段模型属性:")
print(f" 原点: {block_model.origin}")
print(f" 尺寸: X={block_model.xyz_length[0]:.1f}m, "
f"Y={block_model.xyz_length[1]:.1f}m, "
f"Z={block_model.xyz_length[2]:.1f}m")
print(f" 最大精度层级: {block_model.max_level}")
print(f" 最小块段: {block_model.min_size}")
print(f" 块段总数: {block_model.total_block_count:,}")
print(f" 字段定义: {block_model.field_definitions}")
步骤五:距离幂估值(IDW,迭代扩张)
这是最关键的步骤,分四个阶段:
- 5a — 从矿体模型用 MVEE 算法计算搜索椭球体角度和矿体几何尺寸
- 5b — 从钻孔孔口数据计算勘探线间距
- 5c — 基于勘探线间距 + 矿体几何比例推导搜索椭球体三轴半径
- 5d — 迭代执行 IDW 估值,未估值块段 > 0 时主轴半径翻倍重试
5a. MVEE 搜索椭球体参数
from dimine_python_sdk.lib.prospecting import compute_search_ellipsoid_from_dmf
print("[5a] 计算搜索椭球体参数(MVEE 算法)...")
ellipsoid = compute_search_ellipsoid_from_dmf(
dmf_constraint_path, voxel_size=5.0
)
print(f" MVEE 椭球体(矿体几何):")
print(f" 主轴半径(半走向长): {ellipsoid.ellipsoid_main_radius:.1f}m")
print(f" 次轴半径(半延伸长): {ellipsoid.second_radius:.1f}m")
print(f" 短轴半径(半厚度): {ellipsoid.short_radius:.1f}m")
print(f" 方位角: {ellipsoid.ellipsoid_azimuth:.1f}°")
print(f" 倾伏角: {ellipsoid.ellipsoid_plunge:.1f}°")
print(f" 倾角: {ellipsoid.ellipsoid_dip:.1f}°")
print(f" 次轴/主轴: {ellipsoid.ellipsoid_second_main_rate:.4f}")
print(f" 短轴/主轴: {ellipsoid.ellipsoid_short_main_rate:.4f}")
print(f" 椭球中心: {ellipsoid.center}")
print(f" 主轴方向: {ellipsoid.pc1_direction}")
5b. 勘探线间距计算
import math
import numpy as np
def find_xy_columns(collar_df):
"""从孔口表识别 X/Y 坐标列名"""
x_candidates = ["横坐标", "EAST", "X", "Easting", "easting", "x"]
y_candidates = ["纵坐标", "NORTH", "Y", "Northing", "northing", "y"]
x_col = next((c for c in x_candidates if c in collar_df.columns), None)
y_col = next((c for c in y_candidates if c in collar_df.columns), None)
if x_col is None or y_col is None:
numeric_cols = collar_df.select_dtypes(include="number").columns.tolist()
if len(numeric_cols) >= 2:
x_col, y_col = numeric_cols[0], numeric_cols[1]
else:
raise ValueError("无法识别X/Y坐标列")
return x_col, y_col
def compute_exploration_line_spacing(collar_df, strike_direction,
line_field=None, same_line_threshold=5.0):
"""计算勘探线间距
优先按 line_field 分组计算;否则将所有孔口投影到走向方向取相邻间距中位数。
"""
if len(collar_df) < 2:
return 50.0
# 方法1:按勘探线字段分组
if line_field is not None and line_field in collar_df.columns:
lines = collar_df[line_field].dropna().unique()
if len(lines) >= 2:
x_col, y_col = find_xy_columns(collar_df)
centers = []
for line in sorted(lines):
line_df = collar_df[collar_df[line_field] == line]
centers.append((line_df[x_col].mean(), line_df[y_col].mean()))
dx_s, dy_s = strike_direction[0], strike_direction[1]
norm_xy = math.sqrt(dx_s**2 + dy_s**2)
if norm_xy < 1e-6:
return 50.0
dx_s, dy_s = dx_s / norm_xy, dy_s / norm_xy
projections = sorted(cx * dx_s + cy * dy_s for cx, cy in centers)
gaps = [projections[i] - projections[i-1]
for i in range(1, len(projections))
if projections[i] - projections[i-1] > same_line_threshold]
if gaps:
return float(np.median(gaps))
# 方法2:投影法(无勘探线字段时)
x_col, y_col = find_xy_columns(collar_df)
dx_s, dy_s = strike_direction[0], strike_direction[1]
norm_xy = math.sqrt(dx_s**2 + dy_s**2)
if norm_xy < 1e-6:
return 50.0
dx_s, dy_s = dx_s / norm_xy, dy_s / norm_xy
projections = sorted(collar_df[x_col] * dx_s + collar_df[y_col] * dy_s)
gaps = [projections[i] - projections[i-1]
for i in range(1, len(projections))
if projections[i] - projections[i-1] > same_line_threshold]
if not gaps:
return 50.0
return float(np.median(gaps))
print("[5b] 计算勘探线间距...")
# 自动检测勘探线字段
line_field = None
for candidate in ["勘探线", "勘探线号"]:
if candidate in db.collar.columns:
line_field = candidate
break
spacing = compute_exploration_line_spacing(
db.collar, ellipsoid.pc1_direction, line_field=line_field
)
print(f" 勘探线间距: {spacing:.1f}m")
print(f" 钻孔数: {len(db.collar)}")
5c. 搜索椭球体半径计算
基于勘探线间距和矿体几何比例推导三轴搜索半径:
def compute_search_radii(ellipsoid_params, line_spacing, composite_length,
spacing_multiplier=1.0, short_sample_multiplier=3):
"""根据勘探线间距和矿体几何计算搜索椭球体三轴半径
算法:
(1) 长半轴 = 勘探线间距 × spacing_multiplier(建议 1.0~1.2)
(2) 次半轴 = 长半轴 × (延伸长度 / 矿体走向长度)
(3) 短半轴 = max(长半轴 × (厚度 / 矿体走向长度),
组合样长 × short_sample_multiplier)
"""
strike_len = 2.0 * ellipsoid_params.ellipsoid_main_radius
extension_len = 2.0 * ellipsoid_params.second_radius
thickness = 2.0 * ellipsoid_params.short_radius
main_radius = line_spacing * spacing_multiplier
second_rate = (extension_len / strike_len) if strike_len > 0 else 0.8
second_radius = main_radius * second_rate
short_by_ratio = main_radius * (thickness / strike_len) if strike_len > 0 else main_radius * 0.2
short_min = composite_length * short_sample_multiplier
short_radius = max(short_by_ratio, short_min)
short_rate = short_radius / main_radius if main_radius > 0 else 0.2
return {
"main_radius": round(main_radius, 2),
"second_main_rate": round(second_rate, 4),
"short_main_rate": round(short_rate, 4),
}
print("[5c] 计算搜索椭球体半径...")
SPACING_MULTIPLIER = 1.2 # 勘探线间距倍数(1.0~1.2)
SHORT_SAMPLE_MULTIPLIER = 3 # 短轴组合样长倍数(2~4)
radii = compute_search_radii(
ellipsoid, spacing, composite_length=2.0,
spacing_multiplier=SPACING_MULTIPLIER,
short_sample_multiplier=SHORT_SAMPLE_MULTIPLIER,
)
print(f" 长半轴 = 勘探线间距({spacing:.1f}m) × {SPACING_MULTIPLIER} = {radii['main_radius']}m")
print(f" 次半轴/主轴 = {radii['second_main_rate']:.4f}")
print(f" 短半轴/主轴 = {radii['short_main_rate']:.4f}")
5d. 迭代 IDW 估值
from dimine_python_sdk.lib.prospecting import BlockModelEvaluator
from dimine_python_sdk.lib.prospecting.models import (
BlockModelDistancePowerParams,
EntityConstraint,
)
print("[5d] 迭代距离幂估值...")
# 约束条件:矿体实体内部
constraints = [
EntityConstraint(
file=dmf_constraint_path,
range=0, # 0=内部,1=外部
bool_operate="and",
),
]
# IDW 参数
IDW_POWER = 2
MAX_IDW_ITERATIONS = 10
angle_kwargs = {
"angle_main": ellipsoid.ellipsoid_azimuth,
"angle_second": ellipsoid.ellipsoid_plunge,
"angle_short": ellipsoid.ellipsoid_dip,
}
current_main_radius = radii["main_radius"]
evaluator = BlockModelEvaluator()
for iteration in range(1, MAX_IDW_ITERATIONS + 1):
sec_r = current_main_radius * radii["second_main_rate"]
short_r = current_main_radius * radii["short_main_rate"]
print(f"\n --- IDW 迭代 #{iteration}/{MAX_IDW_ITERATIONS} ---")
print(f" 搜索椭球体: 主轴={current_main_radius:.1f}m, "
f"次轴={sec_r:.1f}m, 短轴={short_r:.1f}m")
idw_params = BlockModelDistancePowerParams(
sample_file=dmg_combined_path,
variable=valid_grade_fields,
extra_attribute="",
power=IDW_POWER,
min_value=0.0001,
max_value=9999.0,
single_block_min=4,
single_block_max=8,
sub_block_main=1,
sub_block_second=1,
sub_block_short=1,
main_radius=current_main_radius,
second_main_rate=radii["second_main_rate"],
short_main_rate=radii["short_main_rate"],
**angle_kwargs,
octant_max=0,
project_count_min=0,
single_project_sample_max=0,
)
result = evaluator.distance_power_evaluation(
block_model_file=dmb_output_path,
constraint_params=constraints,
evaluation_params=idw_params,
overwrite_result=True,
)
print(f" 估值结果: 成功={result.success}")
if result.elements:
print(f" {'元素':<8} {'单元块总数':>12} {'已估值':>12} "
f"{'本次估值':>12} {'未估值':>12}")
print(f" {'-'*8} {'-'*12} {'-'*12} {'-'*12} {'-'*12}")
for e in result.elements:
print(f" {e.element:<8} {e.total_blocks:>12,} "
f"{e.evaluated_blocks:>12,} "
f"{e.current_evaluated_blocks:>12,} "
f"{e.unevaluated_blocks:>12,}")
# 检查是否还有未估值块段
has_unevaluated = any(e.unevaluated_blocks > 0 for e in result.elements)
if not has_unevaluated:
print(f"\n [收敛] 第 {iteration} 轮后所有块段已估值完成!")
break
# 仍有未估值块段,主轴半径翻倍
current_main_radius *= 2
print(f" 仍有未估值块段 → 主轴半径扩大至 {current_main_radius:.1f}m")
else:
remaining = []
if result.elements:
remaining = [f"{e.element}={e.unevaluated_blocks}"
for e in result.elements if e.unevaluated_blocks > 0]
print(f"\n [警告] 达到最大迭代次数,剩余未估值: {', '.join(remaining) if remaining else '无'}")
# 验证估值后字段
with DmbFile(dmb_output_path) as block_after:
print(f"\n估值后字段定义: {block_after.field_definitions}")
IDW 估值参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
power |
2 | 距离幂次,常用 1–3 |
single_block_min |
4 | 单块最小样品数 |
single_block_max |
8 | 单块最大样品数 |
octant_max |
0 | 八分圆最大样品数,0=不限制 |
project_count_min |
0 | 最少工程数(保证空间代表性) |
single_project_sample_max |
0 | 单工程最大样品数(避免单孔主导) |
min_value / max_value |
0.0001 / 9999 | 估值范围截断 |
sub_block_main/second/short |
1 | 子块离散数,>1 可提高精度 |
完整管线脚本
以下是把上述所有步骤整合在一起的端到端脚本:
"""
储量估算完整管线
输入: 01_地质勘探数据库.dmd + 约束_矿体.dmf
输出: 估值后的块段模型 .dmb + 储量统计结果
Usage:
python reserve_estimation_pipeline.py
"""
import math
import os
import shutil
import sys
from pathlib import Path
import numpy as np
# ==============================================================================
# 路径与参数配置
# ==============================================================================
# 输入文件
DMD_FILE = Path("C:/Users/76081/Desktop/yanshi/储量估值/01_地质勘探数据库.dmd")
DMF_CONSTRAINT = Path("C:/Users/76081/Desktop/yanshi/储量估值/约束_矿体.dmf")
# 输出目录
OUTPUT_DIR = Path("./reserves_output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 中间/输出文件
DMG_RAW = OUTPUT_DIR / "01_raw.dmg"
DMG_HG = OUTPUT_DIR / "02_high_grade.dmg"
DMG_COMBINED = OUTPUT_DIR / "03_combined.dmg"
DMB_OUTPUT = OUTPUT_DIR / "04_block_model.dmb"
# -- 特高品位处理 --
HG_AVERAGE_MULTIPLE = 8
HG_PROCESS_MODE = 0
HG_REPLACE_METHOD = 3
# -- 样长组合 --
COMPOSITE_LENGTH = 2.0
COMBINE_PERCENT = 0.75
# -- 块段模型 --
BLOCK_SIZE_X, BLOCK_SIZE_Y, BLOCK_SIZE_Z = 5.0, 5.0, 5.0
MARGIN_XY, MARGIN_Z = 20.0, 10.0
# -- IDW 估值 --
IDW_POWER = 2
SPACING_MULTIPLIER = 1.2
SHORT_SAMPLE_MULTIPLIER = 3
MAX_IDW_ITERATIONS = 10
# -- DIMINE_HOME --
if not os.environ.get("DIMINE_HOME"):
os.environ["DIMINE_HOME"] = r"E:\数采软件\x64\x64"
# ==============================================================================
# 导入 SDK
# ==============================================================================
from dimine_python_sdk.lib.io import DmdFile, DmgFile, DmbFile, convert_file, DmfFile
from dimine_python_sdk.lib.prospecting import (
DrillFunctionWrapper,
BlockModelEvaluator,
create_block_model,
compute_search_ellipsoid_from_dmf,
)
from dimine_python_sdk.lib.prospecting.models import (
HighGradeProcessParam,
SampleLengthCombineParam,
CreateBlockModelParams,
FieldDefinition,
BlockModelDistancePowerParams,
EntityConstraint,
)
from dimine_python_sdk.models.types import Shell, Point as DmfPoint, Line as DmfLine
# ==============================================================================
# 工具函数
# ==============================================================================
def find_xy_columns(collar_df):
x_candidates = ["横坐标", "EAST", "X", "Easting", "easting", "x"]
y_candidates = ["纵坐标", "NORTH", "Y", "Northing", "northing", "y"]
x_col = next((c for c in x_candidates if c in collar_df.columns), None)
y_col = next((c for c in y_candidates if c in collar_df.columns), None)
if x_col is None or y_col is None:
numeric_cols = collar_df.select_dtypes(include="number").columns.tolist()
if len(numeric_cols) >= 2:
x_col, y_col = numeric_cols[0], numeric_cols[1]
else:
raise ValueError("无法识别X/Y坐标列")
return x_col, y_col
def compute_exploration_line_spacing(collar_df, strike_direction,
line_field=None, same_line_threshold=5.0):
if len(collar_df) < 2:
return 50.0
if line_field is not None and line_field in collar_df.columns:
lines = collar_df[line_field].dropna().unique()
if len(lines) >= 2:
x_col, y_col = find_xy_columns(collar_df)
centers = [(collar_df[collar_df[line_field] == l][x_col].mean(),
collar_df[collar_df[line_field] == l][y_col].mean())
for l in sorted(lines)]
dx_s, dy_s = strike_direction[0], strike_direction[1]
norm_xy = math.sqrt(dx_s**2 + dy_s**2)
if norm_xy < 1e-6:
return 50.0
dx_s, dy_s = dx_s / norm_xy, dy_s / norm_xy
proj = sorted(cx * dx_s + cy * dy_s for cx, cy in centers)
gaps = [proj[i] - proj[i-1] for i in range(1, len(proj))
if proj[i] - proj[i-1] > same_line_threshold]
if gaps:
return float(np.median(gaps))
x_col, y_col = find_xy_columns(collar_df)
dx_s, dy_s = strike_direction[0], strike_direction[1]
norm_xy = math.sqrt(dx_s**2 + dy_s**2)
if norm_xy < 1e-6:
return 50.0
dx_s, dy_s = dx_s / norm_xy, dy_s / norm_xy
proj = sorted(collar_df[x_col] * dx_s + collar_df[y_col] * dy_s)
gaps = [proj[i] - proj[i-1] for i in range(1, len(proj))
if proj[i] - proj[i-1] > same_line_threshold]
return float(np.median(gaps)) if gaps else 50.0
def compute_search_radii(ellipsoid_params, line_spacing, composite_length,
spacing_multiplier=1.0, short_sample_multiplier=3):
strike_len = 2.0 * ellipsoid_params.ellipsoid_main_radius
extension_len = 2.0 * ellipsoid_params.second_radius
thickness = 2.0 * ellipsoid_params.short_radius
main_radius = line_spacing * spacing_multiplier
second_rate = (extension_len / strike_len) if strike_len > 0 else 0.8
short_by_ratio = main_radius * (thickness / strike_len) if strike_len > 0 else main_radius * 0.2
short_min = composite_length * short_sample_multiplier
short_radius = max(short_by_ratio, short_min)
short_rate = short_radius / main_radius if main_radius > 0 else 0.2
return {
"main_radius": round(main_radius, 2),
"second_main_rate": round(second_rate, 4),
"short_main_rate": round(short_rate, 4),
}
def compute_dmf_bounding_box(dmf_path):
dmf = DmfFile(dmf_path)
all_points = []
for layer in dmf.layers:
for entity in layer.entities:
if isinstance(entity, Shell):
pts = entity.geometry.points
if pts is not None and len(pts) > 0:
all_points.append(pts)
elif isinstance(entity, DmfLine):
geom = entity.geometry
if geom is not None and len(geom) > 0:
all_points.append(geom)
elif isinstance(entity, DmfPoint):
geom = entity.geometry
if geom is not None and len(geom) > 0:
all_points.append(geom.reshape(1, 3))
pts = np.vstack(all_points)
dmf.close()
return {
"min_x": float(pts[:, 0].min()), "max_x": float(pts[:, 0].max()),
"min_y": float(pts[:, 1].min()), "max_y": float(pts[:, 1].max()),
"min_z": float(pts[:, 2].min()), "max_z": float(pts[:, 2].max()),
}
# ==============================================================================
# 主流程
# ==============================================================================
def main():
print("=" * 60)
print(" 储量估算完整管线")
print("=" * 60)
# --- [1] DMD → DMG 格式转换 ---
print("\n[1/7] DMD → DMG 格式转换...")
convert_file(str(DMD_FILE), str(DMG_RAW))
dmg = DmgFile(str(DMG_RAW))
segments_df = dmg.to_segments_dataframe()
dmg.close()
# 自动检测品位字段
numeric_cols = segments_df.select_dtypes(include="number").columns.tolist()
excluded = {"起始", "结束", "样长", "X", "Y", "Z", "覆盖长度", "覆盖率",
"测斜深度", "方位角", "倾角", "样本编号", "钻孔编号",
"start_x", "start_y", "start_z", "end_x", "end_y", "end_z",
"SAMPLE-ID", "SAMFROM", "SAMTO"}
candidate_fields = [c for c in numeric_cols if c not in excluded]
valid_grade_fields = []
for field in candidate_fields:
col = segments_df[field]
if not (col.isna() | col.isin([float("inf"), float("-inf")]) | (col <= 0)).all():
valid_grade_fields.append(field)
print(f" 有效品位字段: {valid_grade_fields}")
db = DmdFile(str(DMD_FILE))
print(f" 钻孔数: {len(db.collar)}, 样品数: {len(db.sample)}")
# --- [2] 特高品位处理(迭代收敛) ---
print(f"\n[2/7] 特高品位处理({HG_AVERAGE_MULTIPLE}x 均值倍数)...")
shutil.copy2(str(DMG_RAW), str(DMG_HG))
for grade_field in valid_grade_fields:
print(f" 处理元素: {grade_field}")
for iteration in range(1, 6):
dmg = DmgFile(str(DMG_HG))
seg = dmg.to_segments_dataframe()
dmg.close()
grades = seg[grade_field].dropna()
grades = grades[~grades.isin([float("inf"), float("-inf")])]
grades = grades[grades > 0]
avg = float(grades.mean())
threshold = avg * HG_AVERAGE_MULTIPLE
exceeding = int((grades > threshold).sum())
print(f" 迭代#{iteration}: 均值={avg:.4f}, 阈值={threshold:.4f}, 超限={exceeding}")
if exceeding == 0:
print(f" [收敛]")
break
hg_params = HighGradeProcessParam(
input_file=str(DMG_HG), grade_field=grade_field,
process_mode=HG_PROCESS_MODE, average_multiple=HG_AVERAGE_MULTIPLE,
frequency=0.95, replace_method=HG_REPLACE_METHOD,
)
DrillFunctionWrapper.extra_high_grade_process(hg_params)
# --- [3] 样长组合 ---
print(f"\n[3/7] 样长组合({COMPOSITE_LENGTH}m)...")
combine_params = SampleLengthCombineParam(
input_file=str(DMG_HG), combine_length=COMPOSITE_LENGTH,
combine_percent=COMBINE_PERCENT, output_file=str(DMG_COMBINED),
)
DrillFunctionWrapper.sample_length_combine(combine_params)
# --- [4] 创建空块段模型 ---
print("\n[4/7] 创建空块段模型...")
bbox = compute_dmf_bounding_box(str(DMF_CONSTRAINT))
origin_x = bbox["min_x"] - MARGIN_XY
origin_y = bbox["min_y"] - MARGIN_XY
origin_z = bbox["min_z"] - MARGIN_Z
nx = math.ceil((bbox["max_x"] - bbox["min_x"] + 2 * MARGIN_XY) / BLOCK_SIZE_X)
ny = math.ceil((bbox["max_y"] - bbox["min_y"] + 2 * MARGIN_XY) / BLOCK_SIZE_Y)
nz = math.ceil((bbox["max_z"] - bbox["min_z"] + 2 * MARGIN_Z) / BLOCK_SIZE_Z)
block_params = CreateBlockModelParams(
file_name=str(DMB_OUTPUT),
origin_x=origin_x, origin_y=origin_y, origin_z=origin_z,
size_x=BLOCK_SIZE_X, size_y=BLOCK_SIZE_Y, size_z=BLOCK_SIZE_Z,
length_x=nx * BLOCK_SIZE_X, length_y=ny * BLOCK_SIZE_Y, length_z=nz * BLOCK_SIZE_Z,
field_definition=[
FieldDefinition(field_name="岩石类型", field_type="字节型"),
FieldDefinition(field_name="体重", field_type="浮点型"),
],
)
create_block_model(block_params)
print(f" 块数: {nx} × {ny} × {nz} = {nx * ny * nz:,}")
# --- [5] MVEE 椭球体 + 勘探线间距 ---
print("\n[5/7] 计算搜索椭球体...")
ellipsoid = compute_search_ellipsoid_from_dmf(str(DMF_CONSTRAINT), voxel_size=5.0)
print(f" 方位角={ellipsoid.ellipsoid_azimuth:.1f}°, "
f"倾伏角={ellipsoid.ellipsoid_plunge:.1f}°, "
f"倾角={ellipsoid.ellipsoid_dip:.1f}°")
line_field = next((c for c in ["勘探线", "勘探线号"] if c in db.collar.columns), None)
spacing = compute_exploration_line_spacing(
db.collar, ellipsoid.pc1_direction, line_field=line_field
)
print(f" 勘探线间距: {spacing:.1f}m")
radii = compute_search_radii(ellipsoid, spacing, COMPOSITE_LENGTH,
spacing_multiplier=SPACING_MULTIPLIER,
short_sample_multiplier=SHORT_SAMPLE_MULTIPLIER)
print(f" 搜索半径: 主轴={radii['main_radius']}m")
# --- [6] 迭代 IDW 估值 ---
print(f"\n[6/6] 距离幂估值(IDW, 幂次={IDW_POWER})...")
constraints = [EntityConstraint(file=str(DMF_CONSTRAINT), range=0, bool_operate="and")]
angle_kwargs = {
"angle_main": ellipsoid.ellipsoid_azimuth,
"angle_second": ellipsoid.ellipsoid_plunge,
"angle_short": ellipsoid.ellipsoid_dip,
}
current_main_radius = radii["main_radius"]
evaluator = BlockModelEvaluator()
for iteration in range(1, MAX_IDW_ITERATIONS + 1):
idw_params = BlockModelDistancePowerParams(
sample_file=str(DMG_COMBINED), variable=valid_grade_fields,
extra_attribute="", power=IDW_POWER,
min_value=0.0001, max_value=9999.0,
single_block_min=4, single_block_max=8,
sub_block_main=1, sub_block_second=1, sub_block_short=1,
main_radius=current_main_radius,
second_main_rate=radii["second_main_rate"],
short_main_rate=radii["short_main_rate"],
octant_max=0, project_count_min=0, single_project_sample_max=0,
**angle_kwargs,
)
result = evaluator.distance_power_evaluation(
block_model_file=str(DMB_OUTPUT),
constraint_params=constraints,
evaluation_params=idw_params,
overwrite_result=True,
)
print(f" 迭代#{iteration}: 主轴={current_main_radius:.1f}m, 成功={result.success}")
if result.elements:
for e in result.elements:
print(f" {e.element}: 总块={e.total_blocks}, 已估值={e.evaluated_blocks}, "
f"本次={e.current_evaluated_blocks}, 未估值={e.unevaluated_blocks}")
if not any(e.unevaluated_blocks > 0 for e in result.elements):
print(f" [收敛] 所有块段已估值完成!")
break
current_main_radius *= 2
print(f" 主轴半径扩大至 {current_main_radius:.1f}m")
else:
print(f" [警告] 达到最大迭代次数")
# 清理
db.close()
print(f"\n{'=' * 60}")
print(f" 流程完成!输出目录: {OUTPUT_DIR.resolve()}")
print(f"{'=' * 60}")
for label, path in [
("原始 DMG", DMG_RAW), ("特高品位处理后 DMG", DMG_HG),
("样长组合后 DMG", DMG_COMBINED), ("块段模型(估值后)", DMB_OUTPUT),
]:
size_kb = path.stat().st_size / 1024 if path.exists() else 0
print(f" {label}: {path.name} ({size_kb:.1f} KB)")
if __name__ == "__main__":
main()
输出说明
输出文件清单
| 序号 | 文件 | 说明 |
|---|---|---|
| ① | 01_raw.dmg |
原始 DMG 分段文件(DMD→DMG 转换结果) |
| ② | 02_high_grade.dmg |
特高品位处理后的 DMG |
| ③ | 03_combined.dmg |
样长组合后的 DMG |
| ④ | 04_block_model.dmb |
估值后的块段模型(含品位字段) |
参数速查
特高品位处理
| 参数 | 建议值 | 说明 |
|---|---|---|
average_multiple |
8(铁)/ 6(有色、金)/ 7(煤、非金属) | 均值倍数阈值 |
process_mode |
0 | 0=国内规范,1=国外规范(累积频率) |
replace_method |
3 | 3=矿体平均品位法(推荐) |
块段模型
| 参数 | 建议值 | 说明 |
|---|---|---|
size_x/y/z |
5m / 5m / 5m | 单元块尺寸,根据矿体规模和开采方式调整 |
margin_xy |
20m | XY 方向边距 |
margin_z |
10m | Z 方向边距 |
搜索椭球体
| 参数 | 建议值 | 说明 |
|---|---|---|
spacing_multiplier |
1.0–1.2 | 长半轴 = 勘探线间距 × 倍数 |
short_sample_multiplier |
2–4 | 短半轴 ≥ 组合样长 × 倍数 |
异常处理
from dimine_python_sdk.lib.prospecting import (
BlockDataError,
BlockModelCreateError,
BlockModelEvaluationError,
)
try:
evaluator.distance_power_evaluation(...)
except BlockModelEvaluationError as e:
print(f"估值失败: {e}")
if e.response:
print(f"响应详情: {e.response}")
except BlockModelCreateError as e:
print(f"块段模型创建失败: {e}")
except BlockDataError as e:
print(f"块段模型数据异常: {e}")