dimine-python-sdk 0.2.6__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. dimine_python_sdk/__init__.py +1 -1
  2. dimine_python_sdk/lib/blasting/__init__.py +98 -0
  3. dimine_python_sdk/lib/blasting/analysis.py +239 -0
  4. dimine_python_sdk/lib/blasting/api.py +136 -0
  5. dimine_python_sdk/lib/blasting/charge_design.py +186 -0
  6. dimine_python_sdk/lib/blasting/errors.py +37 -0
  7. dimine_python_sdk/lib/blasting/explosive_db.py +366 -0
  8. dimine_python_sdk/lib/blasting/hole_layout.py +206 -0
  9. dimine_python_sdk/lib/blasting/models.py +361 -0
  10. dimine_python_sdk/lib/blasting/network.py +116 -0
  11. dimine_python_sdk/lib/blasting/report.py +95 -0
  12. dimine_python_sdk/lib/io/dmg_file.py +6 -1
  13. dimine_python_sdk/lib/prospecting/compositing.py +6 -1
  14. dimine_python_sdk/lib/prospecting/high_grade.py +18 -1
  15. dimine_python_sdk/site/objects.inv +0 -0
  16. dimine_python_sdk/site/reference/lib/io/dmg_file.html +38 -30
  17. dimine_python_sdk/site/reference/lib/prospecting/api.html +80 -32
  18. dimine_python_sdk/site/reference/lib/prospecting/compositing.html +34 -22
  19. dimine_python_sdk/site/reference/lib/prospecting/high_grade.html +67 -31
  20. dimine_python_sdk/site/search/search_index.json +1 -1
  21. dimine_python_sdk/site/sitemap.xml +84 -84
  22. dimine_python_sdk/site/sitemap.xml.gz +0 -0
  23. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.7.dist-info}/METADATA +1 -1
  24. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.7.dist-info}/RECORD +27 -17
  25. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.7.dist-info}/WHEEL +0 -0
  26. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.7.dist-info}/entry_points.txt +0 -0
  27. {dimine_python_sdk-0.2.6.dist-info → dimine_python_sdk-0.2.7.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Any
5
5
 
6
6
  DOCS_PATH = Path(__file__).parent / "docs"
7
- __VERSION__ = "0.2.6" # 由 build.bat 中的 generate_version.py 自动更新
7
+ __VERSION__ = "0.2.7" # 由 build.bat 中的 generate_version.py 自动更新
8
8
 
9
9
 
10
10
  _LAZY_IMPORTS: dict[str, Any] = {}
@@ -0,0 +1,98 @@
1
+ """露天爆破设计模块:布孔 → 装药 → 起爆网络 → 分析 → 数据输出(DMO)
2
+
3
+ 纯 Python 业务域,不依赖 DmPyBindInterface(DMO 输出复用 :mod:`lib.io.dmo_file`)。
4
+ Excel 报告(爆破设计说明书)由 skill 技能生成,不在此模块内。
5
+ """
6
+
7
+ from dimine_python_sdk.lib.blasting.api import BlastError, run_full_design
8
+ from dimine_python_sdk.lib.blasting.analysis import (
9
+ animate_blast,
10
+ blast_bounds,
11
+ flyrock_range,
12
+ plot_isochrones,
13
+ plot_time_histogram,
14
+ throw_direction,
15
+ )
16
+ from dimine_python_sdk.lib.blasting.charge_design import design_charge
17
+ from dimine_python_sdk.lib.blasting.errors import (
18
+ BlastAnalysisError,
19
+ BlastChargeError,
20
+ BlastLayoutError,
21
+ BlastNetworkError,
22
+ BlastReportError,
23
+ )
24
+ from dimine_python_sdk.lib.blasting.explosive_db import (
25
+ ExplosiveDB,
26
+ ExplosiveDBError,
27
+ ExplosiveDBFormatError,
28
+ ExplosiveDBPathError,
29
+ explosive_db_conn,
30
+ explosivedb_path,
31
+ )
32
+ from dimine_python_sdk.lib.blasting.hole_layout import layout_holes
33
+ from dimine_python_sdk.lib.blasting.models import (
34
+ AdjustRule,
35
+ BlastRegion,
36
+ BlastType,
37
+ ChargeMode,
38
+ ChargeParams,
39
+ ChargeResult,
40
+ ChargeSegment,
41
+ ConnectionType,
42
+ DetonatorType,
43
+ ElevationMode,
44
+ ElevationSpec,
45
+ Explosive,
46
+ FlyrockParams,
47
+ Hole,
48
+ HoleLayoutParams,
49
+ LayoutPattern,
50
+ NetworkParams,
51
+ NetworkResult,
52
+ )
53
+ from dimine_python_sdk.lib.blasting.network import build_network
54
+ from dimine_python_sdk.lib.blasting.report import generate_dmo
55
+
56
+ __all__ = [
57
+ "BlastError",
58
+ "BlastLayoutError",
59
+ "BlastChargeError",
60
+ "BlastNetworkError",
61
+ "BlastAnalysisError",
62
+ "BlastReportError",
63
+ "ExplosiveDBError",
64
+ "ExplosiveDBPathError",
65
+ "ExplosiveDBFormatError",
66
+ "explosivedb_path",
67
+ "ExplosiveDB",
68
+ "explosive_db_conn",
69
+ "LayoutPattern",
70
+ "ChargeMode",
71
+ "AdjustRule",
72
+ "DetonatorType",
73
+ "ConnectionType",
74
+ "BlastType",
75
+ "ElevationMode",
76
+ "ElevationSpec",
77
+ "BlastRegion",
78
+ "HoleLayoutParams",
79
+ "Hole",
80
+ "Explosive",
81
+ "ChargeSegment",
82
+ "ChargeParams",
83
+ "ChargeResult",
84
+ "NetworkParams",
85
+ "NetworkResult",
86
+ "FlyrockParams",
87
+ "layout_holes",
88
+ "design_charge",
89
+ "build_network",
90
+ "throw_direction",
91
+ "blast_bounds",
92
+ "flyrock_range",
93
+ "plot_isochrones",
94
+ "plot_time_histogram",
95
+ "animate_blast",
96
+ "generate_dmo",
97
+ "run_full_design",
98
+ ]
@@ -0,0 +1,239 @@
1
+ """露天爆破设计 —— 爆破设计分析
2
+
3
+ 六项分析:爆破模拟动画、等时线、抛掷方向、起爆时间-孔数柱状图、
4
+ 飞石范围、爆破范围。
5
+
6
+ 对外主入口:
7
+ - ``plot_isochrones`` / ``plot_time_histogram`` / ``animate_blast``
8
+ - ``throw_direction`` / ``flyrock_range`` / ``blast_bounds``
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import math
14
+ from pathlib import Path
15
+ from typing import Dict, List
16
+
17
+ import numpy as np
18
+
19
+ from dimine_python_sdk.lib.blasting.errors import BlastAnalysisError
20
+ from dimine_python_sdk.lib.blasting.models import (
21
+ BlastRegion,
22
+ BlastType,
23
+ FlyrockParams,
24
+ Hole,
25
+ )
26
+
27
+ __all__ = [
28
+ "plot_isochrones",
29
+ "plot_time_histogram",
30
+ "animate_blast",
31
+ "throw_direction",
32
+ "flyrock_range",
33
+ "blast_bounds",
34
+ ]
35
+
36
+
37
+ def _setup_cjk():
38
+ """配置 matplotlib:无界面后端 + 中文字体(避免中文显示为方块)。"""
39
+ import matplotlib
40
+
41
+ matplotlib.use("Agg")
42
+ import matplotlib.pyplot as plt
43
+
44
+ plt.rcParams["font.sans-serif"] = [
45
+ "Microsoft YaHei", "SimHei", "SimSun", "Arial Unicode MS", "DejaVu Sans",
46
+ ]
47
+ plt.rcParams["axes.unicode_minus"] = False
48
+ return plt
49
+
50
+
51
+ def _front_unit(region: BlastRegion) -> np.ndarray:
52
+ az = math.radians(region.front_azimuth)
53
+ return np.array([math.sin(az), math.cos(az)])
54
+
55
+
56
+ def throw_direction(region: BlastRegion) -> Dict[str, object]:
57
+ """抛掷方向 = 前排方向(自由面朝向)单位矢量。"""
58
+ f = _front_unit(region)
59
+ return {
60
+ "azimuth": region.front_azimuth,
61
+ "vector": [float(f[0]), float(f[1])],
62
+ "note": "岩石抛掷方向 = 前排方向(自由面朝向)",
63
+ }
64
+
65
+
66
+ def blast_bounds(holes: List[Hole]) -> Dict[str, float]:
67
+ """爆破范围 = 所有炮孔的最小外接盒(含孔口/孔底)。"""
68
+ if not holes:
69
+ return {}
70
+ xs = [h.mouth_x for h in holes] + [h.bottom_x for h in holes]
71
+ ys = [h.mouth_y for h in holes] + [h.bottom_y for h in holes]
72
+ zs = [h.mouth_z for h in holes] + [h.bottom_z for h in holes]
73
+ return {
74
+ "X最小": min(xs), "X最大": max(xs),
75
+ "Y最小": min(ys), "Y最大": max(ys),
76
+ "Z最小": min(zs), "Z最大": max(zs),
77
+ "炮孔数": len(holes),
78
+ }
79
+
80
+
81
+ def flyrock_range(params: FlyrockParams) -> Dict[str, object]:
82
+ """飞石安全距离(GB 6722-2014)。
83
+
84
+ 理论值 R = Kf·n·W;规范下限:深孔 ≥200m,浅孔/二次破碎 ≥300m;
85
+ 下坡方向增大 50%。最终影响范围 = max(理论值, 规范下限)(下坡再 ×1.5)。
86
+ """
87
+ theory = params.Kf * params.n * params.W
88
+ floor = 300.0 if params.blast_type == BlastType.SHALLOW_HOLE else 200.0
89
+ base = max(theory, floor)
90
+ result = base * 1.5 if params.downhill else base
91
+ return {
92
+ "影响范围": round(result, 2),
93
+ "理论值": round(theory, 2),
94
+ "规范下限值": floor,
95
+ "爆破类型": params.blast_type.value,
96
+ "下坡方向": params.downhill,
97
+ }
98
+
99
+
100
+ def plot_isochrones(
101
+ holes: List[Hole],
102
+ delays: Dict[str, int],
103
+ interval: float,
104
+ output_path: str,
105
+ title: str = "起爆等时线",
106
+ contour: bool = True,
107
+ ) -> str:
108
+ """等时线:按起爆时间着色炮孔,并叠加等高线式的等时线曲线。
109
+
110
+ 等高线通过 Delaunay 三角剖分对起爆时间场插值后绘制(``tricontour``),
111
+ 把同一时刻起爆的炮孔连成等时线曲线;曲线过密时自动加倍间隔,最多 20 条。
112
+ """
113
+ if not holes:
114
+ raise BlastAnalysisError("无炮孔数据,无法绘制等时线")
115
+ plt = _setup_cjk()
116
+
117
+ xs = np.array([h.mouth_x for h in holes], dtype=float)
118
+ ys = np.array([h.mouth_y for h in holes], dtype=float)
119
+ ts = np.array([delays.get(h.number, 0) for h in holes], dtype=float)
120
+ tmin, tmax = ts.min(), ts.max()
121
+
122
+ fig, ax = plt.subplots(figsize=(9, 7))
123
+ sc = ax.scatter(xs, ys, c=ts, cmap="viridis", edgecolors="k", s=55, zorder=3)
124
+ for h in holes:
125
+ d = delays.get(h.number, 0)
126
+ ax.annotate(f"{h.number}\n{d}ms", (h.mouth_x, h.mouth_y), fontsize=5,
127
+ ha="center", textcoords="offset points", xytext=(0, 6), zorder=4)
128
+
129
+ # 等时线曲线(等高线式)
130
+ if contour and len(holes) >= 3:
131
+ try:
132
+ step = float(interval)
133
+ while True:
134
+ levels = np.arange(
135
+ math.floor(tmin / step) * step, tmax + step, step
136
+ )
137
+ levels = levels[(levels > tmin) & (levels < tmax)]
138
+ if len(levels) <= 20 or step >= (tmax - tmin):
139
+ break
140
+ step *= 2.0
141
+ if len(levels) == 0:
142
+ levels = np.array([tmin])
143
+ cs = ax.tricontour(xs, ys, ts, levels=levels, colors="black",
144
+ linewidths=0.7, alpha=0.6, zorder=2)
145
+ ax.clabel(cs, fmt="%.0f ms", fontsize=6, inline=True)
146
+ except Exception:
147
+ # 三角剖分失败(点数不足 / 共线 / 值无变化)时静默跳过等时线
148
+ pass
149
+
150
+ fig.colorbar(sc, ax=ax, label="起爆时间 (ms)")
151
+ ax.set_aspect("equal", adjustable="datalim")
152
+ ax.set_title(title)
153
+ ax.set_xlabel("X"); ax.set_ylabel("Y")
154
+ fig.tight_layout()
155
+ fig.savefig(output_path, dpi=150)
156
+ plt.close(fig)
157
+ return output_path
158
+
159
+
160
+ def plot_time_histogram(
161
+ delays: Dict[str, int],
162
+ output_path: str,
163
+ bin_width: float = 10.0,
164
+ title: str = "起爆时间-爆破孔数量",
165
+ ) -> str:
166
+ """起爆时间-孔数柱状图:预警同时起爆孔过多。"""
167
+ if not delays:
168
+ raise BlastAnalysisError("无延时数据可绘制柱状图")
169
+ plt = _setup_cjk()
170
+
171
+ ts = list(delays.values())
172
+ tmin, tmax = min(ts), max(ts)
173
+ bins = np.arange(math.floor(tmin / bin_width) * bin_width,
174
+ math.ceil(tmax / bin_width) * bin_width + bin_width, bin_width)
175
+
176
+ fig, ax = plt.subplots(figsize=(8, 4))
177
+ ax.hist(ts, bins=bins, edgecolor="black", color="steelblue")
178
+ ax.set_xlabel("起爆时间 (ms)")
179
+ ax.set_ylabel("同时起爆炮孔数")
180
+ ax.set_title(title)
181
+ fig.tight_layout()
182
+ fig.savefig(output_path, dpi=150)
183
+ plt.close(fig)
184
+ return output_path
185
+
186
+
187
+ def animate_blast(
188
+ holes: List[Hole],
189
+ delays: Dict[str, int],
190
+ output_path: str,
191
+ frame_interval: float = 10.0,
192
+ ) -> str:
193
+ """爆破模拟动画:整个网络的延时波前传播(GIF)。
194
+
195
+ 依次点亮「已起爆」的炮孔,展示波前由起爆孔向全网传播。
196
+ """
197
+ if not holes:
198
+ raise BlastAnalysisError("无炮孔数据,无法生成爆破模拟动画")
199
+ plt = _setup_cjk()
200
+ from matplotlib.animation import FuncAnimation
201
+
202
+ xs = [h.mouth_x for h in holes]
203
+ ys = [h.mouth_y for h in holes]
204
+ ts = np.array([delays.get(h.number, 0) for h in holes])
205
+ tmax = int(ts.max()) if len(ts) else 0
206
+ frames = np.arange(0, tmax + frame_interval, frame_interval)
207
+
208
+ fig, ax = plt.subplots(figsize=(7, 6))
209
+ ax.set_aspect("equal", adjustable="datalim")
210
+ fired = ax.scatter([], [], c="red", s=70, label="已起爆")
211
+ unfired = ax.scatter(xs, ys, c="lightgray", s=40, label="未起爆")
212
+ # 标注每孔累积延时(编号 + 延时 ms)
213
+ for h in holes:
214
+ d = delays.get(h.number, 0)
215
+ ax.annotate(f"{h.number}\n{d}ms", (h.mouth_x, h.mouth_y), fontsize=5,
216
+ ha="center", textcoords="offset points", xytext=(0, 6))
217
+ ax.legend(loc="upper right", fontsize=7)
218
+ title = ax.set_title("")
219
+
220
+ def init():
221
+ fired.set_offsets(np.empty((0, 2)))
222
+ return (fired,)
223
+
224
+ def update(t):
225
+ mask = ts <= t
226
+ pts = np.column_stack([np.array(xs)[mask], np.array(ys)[mask]])
227
+ fired.set_offsets(pts)
228
+ title.set_text(f"爆破波前传播 t={t:.0f} ms")
229
+ return (fired, title)
230
+
231
+ anim = FuncAnimation(fig, update, frames=frames, init_func=init, blit=True)
232
+ try:
233
+ anim.save(output_path, writer="pillow", fps=5, dpi=100)
234
+ except Exception:
235
+ # pillow 缺失时退化为帧序列
236
+ output_path = str(Path(output_path).with_suffix(".gif"))
237
+ anim.save(output_path, writer="pillow", fps=5, dpi=100)
238
+ plt.close(fig)
239
+ return output_path
@@ -0,0 +1,136 @@
1
+ """露天爆破设计 —— 端到端编排(业务层)
2
+
3
+ 一键跑通 ①→⑥:布孔 → 装药 → 起爆网络连线 → 起爆孔 → 分析 → 数据输出(DMO)。
4
+
5
+ 对外主入口:``run_full_design(...)``
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional
14
+
15
+ from dimine_python_sdk.lib.blasting.analysis import (
16
+ animate_blast,
17
+ blast_bounds,
18
+ flyrock_range,
19
+ plot_isochrones,
20
+ plot_time_histogram,
21
+ throw_direction,
22
+ )
23
+ from dimine_python_sdk.lib.blasting.charge_design import design_charge
24
+ from dimine_python_sdk.lib.blasting.errors import BlastError
25
+ from dimine_python_sdk.lib.blasting.hole_layout import layout_holes
26
+ from dimine_python_sdk.lib.blasting.models import (
27
+ BlastRegion,
28
+ ChargeParams,
29
+ Explosive,
30
+ FlyrockParams,
31
+ HoleLayoutParams,
32
+ NetworkParams,
33
+ )
34
+ from dimine_python_sdk.lib.blasting.network import build_network
35
+ from dimine_python_sdk.lib.blasting.report import generate_dmo
36
+
37
+ __all__ = ["BlastError", "run_full_design"]
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ def run_full_design(
43
+ region: BlastRegion,
44
+ layout_params: Optional[HoleLayoutParams] = None,
45
+ charge_params: Optional[ChargeParams] = None,
46
+ explosives: Optional[List[Explosive]] = None,
47
+ network_params: Optional[NetworkParams] = None,
48
+ flyrock_params: Optional[FlyrockParams] = None,
49
+ burden: float = 4.0,
50
+ output_dir: str = "blasting_output",
51
+ ) -> Dict[str, object]:
52
+ """执行完整露天爆破设计流程,返回各产物路径与摘要。
53
+
54
+ Args:
55
+ region: 爆区(多边形 + 前排方向 + 标高策略)
56
+ layout_params: 布孔参数,缺省用默认值
57
+ charge_params: 装药参数,缺省用默认值
58
+ explosives: 炸药信息库,缺省用乳化炸药默认库
59
+ network_params: 起爆网络参数,缺省用默认值
60
+ flyrock_params: 飞石参数(None 时不计算飞石范围)
61
+ burden: 最小抵抗线 W(m)
62
+ output_dir: 产物输出目录
63
+
64
+ Returns:
65
+ 摘要字典(含各产物路径、爆破范围、抛掷方向、飞石范围、单段最大药量)
66
+
67
+ Raises:
68
+ BlastError: 任一步骤失败(细分 BlastLayoutError / BlastChargeError 等)
69
+ """
70
+ layout_params = layout_params or HoleLayoutParams()
71
+ charge_params = charge_params or ChargeParams()
72
+ network_params = network_params or NetworkParams()
73
+ explosives = explosives or [
74
+ Explosive(name="乳化炸药", line_density=5.0, unit_price=8.0),
75
+ ]
76
+
77
+ out = Path(output_dir)
78
+ analysis_dir = out / "analysis"
79
+ analysis_dir.mkdir(parents=True, exist_ok=True)
80
+
81
+ # ① 布孔
82
+ holes = layout_holes(region, layout_params)
83
+ if not holes:
84
+ raise BlastError("布孔结果为空,检查爆区多边形与安全距离参数")
85
+ logger.info("布孔完成:%d 个炮孔", len(holes))
86
+
87
+ # ③ 装药
88
+ charges = design_charge(holes, charge_params, explosives, burden)
89
+ logger.info("装药完成:%d 个孔", len(charges))
90
+
91
+ # ④ 起爆网络连线
92
+ network = build_network(holes, network_params,
93
+ init_delay=layout_params.init_delay, charge_results=charges)
94
+ logger.info("起爆网络完成:%d 条连线,起爆孔 %s", len(network.edges), network.initiating_hole)
95
+
96
+ # ⑤ 设置起爆孔(标记)
97
+ for h in holes:
98
+ h.is_initiating = (h.number == network.initiating_hole)
99
+
100
+ # ⑥ 分析
101
+ iso_path = str(analysis_dir / "等时线.png")
102
+ hist_path = str(analysis_dir / "起爆时间-孔数柱状图.png")
103
+ anim_path = str(analysis_dir / "爆破模拟动画.gif")
104
+ plot_isochrones(holes, network.delays, layout_params.isochrone_interval, iso_path)
105
+ plot_time_histogram(network.delays, hist_path)
106
+ try:
107
+ animate_blast(holes, network.delays, anim_path)
108
+ except Exception as exc: # pillow 缺失等
109
+ logger.warning("动画生成失败,跳过: %s", exc)
110
+ anim_path = ""
111
+
112
+ throw = throw_direction(region)
113
+ bounds = blast_bounds(holes)
114
+ flyrock = flyrock_range(flyrock_params) if flyrock_params else None
115
+
116
+ # ⑦ 数据输出:DMO 炮孔数据库(Excel 报告由 skill 技能生成,lib 不负责)
117
+ dmo_out = generate_dmo(holes, charges, network, region, str(out / "炮孔数据库.dmo"))
118
+
119
+ summary = {
120
+ "爆区": region.name,
121
+ "炮孔数": len(holes),
122
+ "连线数": len(network.edges),
123
+ "起爆孔": network.initiating_hole,
124
+ "爆破范围": bounds,
125
+ "抛掷方向": throw,
126
+ "飞石范围": flyrock,
127
+ "单段最大药量": max(network.segment_charges.values(), default=0.0),
128
+ "输出": {
129
+ "dmo": dmo_out,
130
+ "等时线": iso_path,
131
+ "柱状图": hist_path,
132
+ "动画": anim_path,
133
+ },
134
+ }
135
+ (out / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
136
+ return summary
@@ -0,0 +1,186 @@
1
+ """露天爆破设计 —— 装药设计
2
+
3
+ 对每个炮孔按公式计算装药量、装药长度、填塞长度、分段结构,并做
4
+ 填塞长度 / 孔深守恒 / 分段装药校验。
5
+
6
+ 对外主入口:``design_charge(holes, params, explosives, burden) -> list[ChargeResult]``
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import List, Optional
12
+
13
+ from dimine_python_sdk.lib.blasting.errors import BlastChargeError
14
+ from dimine_python_sdk.lib.blasting.models import (
15
+ AdjustRule,
16
+ ChargeMode,
17
+ ChargeParams,
18
+ ChargeResult,
19
+ ChargeSegment,
20
+ Explosive,
21
+ Hole,
22
+ )
23
+
24
+ __all__ = ["design_charge"]
25
+
26
+ _FILL_EXPLOSIVE = "炸药"
27
+ _FILL_STEMMING = "填塞物"
28
+ _FILL_SPACER = "间隔器"
29
+ _FILL_PRIMER = "起爆药包"
30
+
31
+
32
+ def _min_stemming(params: ChargeParams, burden: float, diameter_m: float) -> float:
33
+ """最小填塞长度 = max(最小堵塞长度, 0.7W, 20d, 4.5m)。"""
34
+ return max(params.min_stemming, 0.7 * burden, 20.0 * diameter_m, 4.5)
35
+
36
+
37
+ def _segment_count(hole: Hole, params: ChargeParams) -> int:
38
+ """分段数:显式指定优先;否则间隔装药按孔深自动 2~3 段,连续装药 1 段。"""
39
+ if params.segments is not None:
40
+ return max(1, int(params.segments))
41
+ if params.template == "连续装药":
42
+ return 1
43
+ # 间隔装药:孔深 > 8m 分段,>15m 分 3 段
44
+ if hole.depth > 15.0:
45
+ return 3
46
+ if hole.depth > 8.0:
47
+ return 2
48
+ return 1
49
+
50
+
51
+ def design_charge(
52
+ holes: List[Hole],
53
+ params: ChargeParams,
54
+ explosives: List[Explosive],
55
+ burden: float,
56
+ ) -> List[ChargeResult]:
57
+ """对每个炮孔做装药设计。
58
+
59
+ Args:
60
+ holes: 布孔结果
61
+ params: 装药参数
62
+ explosives: 炸药信息库(非空,第一个为计算主炸药)
63
+ burden: 最小抵抗线 W(m)
64
+
65
+ Returns:
66
+ 每孔的装药结果(装药结构/装药量/填塞长度/间隔长度/单耗面积/警告)
67
+ """
68
+ if not explosives:
69
+ raise BlastChargeError("炸药信息库为空,请先提供炸药名称/线装药密度/单价")
70
+ primary = explosives[0]
71
+
72
+ results: List[ChargeResult] = []
73
+ for hole in holes:
74
+ a = hole.hole_spacing
75
+ b = hole.row_spacing
76
+ H = hole.bench_height # 台阶高度 = 孔深 − 超深
77
+ is_front = hole.row == 0
78
+ diameter_m = hole.diameter / 1000.0 # mm -> m
79
+
80
+ # 单耗面积与装药量
81
+ if is_front:
82
+ area = a * burden # 前排负担面积 a·W
83
+ charge_weight = params.specific_charge * a * burden * H
84
+ else:
85
+ area = a * b # 后排负担面积 a·b
86
+ charge_weight = params.k * params.specific_charge * a * b * H
87
+
88
+ charge_length = charge_weight / primary.line_density
89
+ n_seg = _segment_count(hole, params)
90
+ spacer_total = params.spacer_length * (n_seg - 1) if n_seg > 1 else 0.0
91
+
92
+ warnings: List[str] = []
93
+
94
+ # 最小填塞(规范下限)
95
+ min_stem = _min_stemming(params, burden, diameter_m)
96
+ if params.charge_mode == ChargeMode.STEMMING_SPACER:
97
+ # 优先填塞和间隔器:进一步提高填塞下限
98
+ min_stem = max(min_stem, params.min_stemming + params.spacer_length)
99
+
100
+ # 可装药长度 = 孔深 − 最小填塞 − 间隔总长度;装药超限则截断
101
+ max_charge = hole.depth - min_stem - spacer_total
102
+ if max_charge < 0.0:
103
+ warnings.append(
104
+ f"孔深 {hole.depth:.2f}m 不足以容纳最小填塞 {min_stem:.2f}m + 间隔 {spacer_total:.2f}m"
105
+ )
106
+ max_charge = 0.0
107
+ if charge_length > max_charge + 0.2:
108
+ warnings.append(
109
+ f"装药长度 {charge_length:.2f}m 超过可装药长度 {max_charge:.2f}m,已截断"
110
+ )
111
+ charge_length = max(0.0, max_charge)
112
+ charge_weight = charge_length * primary.line_density
113
+
114
+ # 填塞长度 = 孔深 − 装药长度 − 间隔总长度(填满整个炮孔)
115
+ stemming = hole.depth - charge_length - spacer_total
116
+
117
+ # 构建分段结构(自孔口→孔底)
118
+ segments = _build_segments(
119
+ hole, params, charge_length, charge_weight, stemming, n_seg, primary
120
+ )
121
+
122
+ results.append(ChargeResult(
123
+ hole_number=hole.number,
124
+ depth=hole.depth,
125
+ charge_length=sum(s.length for s in segments if s.fill_type == _FILL_EXPLOSIVE),
126
+ charge_weight=sum(s.weight for s in segments if s.fill_type == _FILL_EXPLOSIVE),
127
+ stemming_length=sum(s.length for s in segments if s.fill_type == _FILL_STEMMING),
128
+ spacer_length=sum(s.length for s in segments if s.fill_type == _FILL_SPACER),
129
+ segments=segments,
130
+ specific_charge_area=area,
131
+ warnings=warnings,
132
+ ))
133
+
134
+ return results
135
+
136
+
137
+ def _build_segments(
138
+ hole: Hole,
139
+ params: ChargeParams,
140
+ charge_length: float,
141
+ charge_weight: float,
142
+ stemming: float,
143
+ n_seg: int,
144
+ explosive: Explosive,
145
+ ) -> List[ChargeSegment]:
146
+ """构建自孔口→孔底的分段结构。
147
+
148
+ 调整规则:
149
+ - 优先孔口装药(正向起爆):炸药靠孔口,填塞靠孔底
150
+ - 优先孔底装药(反向起爆):填塞靠孔口,炸药靠孔底
151
+ - 按比例:填塞在孔口,炸药在中间(常规)
152
+
153
+ 默认(按比例)结构:孔口[填塞] → [炸药]*(间隔) → 孔底
154
+ """
155
+ segments: List[ChargeSegment] = []
156
+
157
+ def add_explosive_segments() -> List[ChargeSegment]:
158
+ """生成 n_seg 段炸药(相邻段间插间隔器)。"""
159
+ segs: List[ChargeSegment] = []
160
+ per_len = charge_length / n_seg
161
+ per_w = charge_weight / n_seg
162
+ for i in range(n_seg):
163
+ segs.append(ChargeSegment(
164
+ fill_type=_FILL_EXPLOSIVE, length=per_len, weight=per_w, delay=0
165
+ ))
166
+ if i < n_seg - 1:
167
+ segs.append(ChargeSegment(
168
+ fill_type=_FILL_SPACER, length=params.spacer_length, weight=0.0, delay=0
169
+ ))
170
+ return segs
171
+
172
+ stem_seg = ChargeSegment(
173
+ fill_type=_FILL_STEMMING, length=stemming, weight=0.0, delay=0
174
+ )
175
+ charge_segs = add_explosive_segments()
176
+
177
+ if params.adjust_rule == AdjustRule.BOTTOM_FIRST:
178
+ # 反向起爆:填塞在孔口,炸药靠孔底(常规结构已是填塞在上、炸药在下)
179
+ segments.extend([stem_seg] + charge_segs)
180
+ elif params.adjust_rule == AdjustRule.MOUTH_FIRST:
181
+ # 正向起爆:炸药靠孔口,填塞靠孔底
182
+ segments.extend(charge_segs + [stem_seg])
183
+ else:
184
+ segments.extend([stem_seg] + charge_segs)
185
+
186
+ return segments