draw_wedge_cases.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. """RocSlope3 wedge-formation examples: which 2-plane sets form a block, which don't.
  2. Frame: x = east, y = north, z = up. Azimuths from north, clockwise.
  3. Slope: face dips 60 deg toward 180 (due south = -y). H = 8 m, toe at y=0, crest y=4.619.
  4. """
  5. import numpy as np
  6. import matplotlib
  7. matplotlib.use("Agg")
  8. import matplotlib.pyplot as plt
  9. from matplotlib.patches import Ellipse
  10. from mpl_toolkits.mplot3d.art3d import Poly3DCollection
  11. rd = np.radians
  12. OK, BAD = "\u2713", "\u2717" # check / cross
  13. # ---------------------------------------------------------------- geometry ---
  14. def normal(dip, dd):
  15. d, a = rd(dip), rd(dd)
  16. return np.array([np.sin(a) * np.sin(d), np.cos(a) * np.sin(d), np.cos(d)])
  17. def clip_poly(poly, n, h):
  18. out = []
  19. for i in range(len(poly)):
  20. A, B = poly[i], poly[(i + 1) % len(poly)]
  21. da, db = float(n @ A) - h, float(n @ B) - h
  22. if da <= 0:
  23. out.append(A)
  24. if (da < 0) != (db < 0):
  25. t = da / (da - db)
  26. out.append(A + t * (B - A))
  27. return out
  28. def plane_polygon(n, c, box, L=40.0):
  29. a = np.array([0.13, 0.52, 0.84])
  30. if abs(n @ a) > 0.9:
  31. a = np.array([0.9, 0.2, 0.35])
  32. u = np.cross(n, a); u /= np.linalg.norm(u)
  33. v = np.cross(n, u)
  34. pts = [c + L * (u + v), c + L * (-u + v), c + L * (-u - v), c + L * (u - v)]
  35. for axis, (lo, hi) in enumerate(box):
  36. e = np.zeros(3); e[axis] = 1.0
  37. pts = clip_poly(pts, e, hi)
  38. e = np.zeros(3); e[axis] = -1.0
  39. pts = clip_poly(pts, e, -lo)
  40. return np.array(pts)
  41. def line_in_box(p0, d, box):
  42. t0, t1 = -1e9, 1e9
  43. for axis, (lo, hi) in enumerate(box):
  44. if abs(d[axis]) < 1e-12:
  45. continue
  46. ta, tb = (lo - p0[axis]) / d[axis], (hi - p0[axis]) / d[axis]
  47. ta, tb = min(ta, tb), max(ta, tb)
  48. t0, t1 = max(t0, ta), min(t1, tb)
  49. return p0 + t0 * d, p0 + t1 * d
  50. def inter_point(n1, c1, n2, c2):
  51. A = np.vstack([n1, n2])
  52. p, *_ = np.linalg.lstsq(A, np.array([c1, c2]), rcond=None)
  53. return p
  54. def isect_dir(n1, n2):
  55. L = np.cross(n1, n2)
  56. return L / np.linalg.norm(L)
  57. def down_plunge(L): # ensure z<0, return (plunge, trend)
  58. if L[2] > 0:
  59. L = -L
  60. plunge = np.degrees(np.arcsin(-L[2]))
  61. trend = np.degrees(np.arctan2(L[0], L[1])) % 360.0
  62. return L, plunge, trend
  63. # slope constants
  64. FDIP, FDD, H = 60.0, 180.0, 8.0
  65. YC = H / np.tan(rd(FDIP)) # crest y = 4.6188
  66. pbox = ((-5.7, 5.7), (-1.2, 9.3), (0.15, 8.45)) # clip box for joint sheets
  67. # ------------------------------------------------------------------ cases ---
  68. # case 1: WORKS J1 55/145, J2 55/215 through a toe point on the face
  69. F1 = np.array([0.0, 1.8, np.tan(rd(FDIP)) * 1.8])
  70. n1a, n1b = normal(55, 145), normal(55, 215)
  71. c1a, c1b = n1a @ F1, n1b @ F1
  72. La, pl1, tr1 = down_plunge(isect_dir(n1a, n1b))
  73. T1 = F1 - ((8.0 - F1[2]) / -La[2]) * La # walk up-plunge from F to the top surface
  74. x1a = (c1a - n1a[1] * YC - n1a[2] * 8.0) / n1a[0]
  75. C1a, C1b = np.array([x1a, YC, 8.0]), np.array([-x1a, YC, 8.0])
  76. print("case1 isect plunge %.1f trend %.0f | F y=%.2f | T y=%.2f | crest x=+/-%.2f"
  77. % (pl1, tr1, F1[1], T1[1], abs(x1a)))
  78. # case 2: FAIL steep J 80/145 & 80/215 through crest points x=+/-1.9
  79. n2a, n2b = normal(80, 145), normal(80, 215)
  80. P2a, P2b = np.array([-1.9, YC, 8.0]), np.array([1.9, YC, 8.0])
  81. c2a, c2b = n2a @ P2a, n2b @ P2b
  82. L2, pl2, tr2 = down_plunge(isect_dir(n2a, n2b))
  83. p2 = inter_point(n2a, c2a, n2b, c2b)
  84. s2a, s2b = line_in_box(p2, L2, ((-6, 6), (-2.2, 11), (0.0, 8.0)))
  85. print("case2 isect plunge %.1f trend %.0f | line %s -> %s" %
  86. (pl2, tr2, np.round(s2a, 2), np.round(s2b, 2)))
  87. # case 3: FAIL dips into slope J 55/035 & 55/325 through crest points x=+/-1.9
  88. n3a, n3b = normal(55, 35), normal(55, 325)
  89. P3a, P3b = np.array([-1.9, YC, 8.0]), np.array([1.9, YC, 8.0])
  90. c3a, c3b = n3a @ P3a, n3b @ P3b
  91. L3, pl3, tr3 = down_plunge(isect_dir(n3a, n3b))
  92. p3 = inter_point(n3a, c3a, n3b, c3b)
  93. s3a, s3b = line_in_box(p3, L3, ((-6, 6), (-2.2, 11), (0.0, 8.0)))
  94. print("case3 isect plunge %.1f trend %.0f | line %s -> %s" %
  95. (pl3, tr3, np.round(s3a, 2), np.round(s3b, 2)))
  96. # case 4: FAIL no physical intersection - small offset patches on case-1 planes
  97. def patch(n, c, center, h=1.15):
  98. a = np.array([0.13, 0.52, 0.84])
  99. if abs(n @ a) > 0.9:
  100. a = np.array([0.9, 0.2, 0.35])
  101. u = np.cross(n, a); u /= np.linalg.norm(u)
  102. v = np.cross(n, u)
  103. return np.array([center + h * (u + v), center + h * (-u + v),
  104. center + h * (-u - v), center + h * (u - v)])
  105. PA = np.array([-1.8, 2.2, (c1a - n1a[0] * -1.8 - n1a[1] * 2.2) / n1a[2]])
  106. PB = np.array([1.8, 2.2, (c1b - n1b[0] * 1.8 - n1b[1] * 2.2) / n1b[2]])
  107. pa4, pb4 = patch(n1a, c1a, PA), patch(n1b, c1b, PB)
  108. print("case4 patch centers", np.round(PA, 2), np.round(PB, 2))
  109. # ---------------------------------------------------------------- drawing ---
  110. BLUE, ORNG, RED, GRN = "#1f77b4", "#ff7f0e", "#c0392b", "#1e8449"
  111. fig = plt.figure(figsize=(17.5, 9.8))
  112. titles = [
  113. (OK + " WORKS \u2014 wedge daylights in the face", GRN,
  114. "J1 55/145 + J2 55/215 \u2192 line of intersection 49/180,\nshallower than the 60/180 face \u2192 toe daylights mid-face"),
  115. (BAD + " FAILS \u2014 intersection too steep", RED,
  116. "J1 80/145 + J2 80/215 \u2192 intersection 78/180 is steeper\nthan the face (60/180) \u2192 toe never reaches the face"),
  117. (BAD + " FAILS \u2014 planes dip into the slope", RED,
  118. "J1 55/035 + J2 55/325 \u2192 intersection plunges backward\n(49/360, up-hill) \u2192 tapered wedge, no release"),
  119. (BAD + " FAILS \u2014 planes never meet", RED,
  120. "orientations like case 1, but finite planes too small / offset\n\u2192 no intersection line inside the slope \u2192 no closed block"),
  121. ]
  122. for i in range(4):
  123. xc = (i + 0.5) / 4
  124. fig.text(xc, 0.955, titles[i][0], ha="center", va="top", fontsize=14,
  125. fontweight="bold", color=titles[i][1])
  126. fig.text(xc, 0.916, titles[i][2], ha="center", va="top", fontsize=10.2,
  127. color="#34495e")
  128. fig.text(0.5, 0.999, "RocSlope3: when do two planes form a wedge block? "
  129. "(slope face dips 60\u00b0 toward azimuth 180\u00b0 \u2014 dip / dip-direction given as \u03b4/\u03b1)",
  130. ha="center", va="top", fontsize=15, fontweight="bold")
  131. # ------------------------------------------------------------- map views ----
  132. for i in range(4):
  133. ax = fig.add_subplot(2, 4, i + 1)
  134. ax.set_xlim(-6.7, 7.0); ax.set_ylim(-3.0, 8.6)
  135. ax.set_aspect("equal"); ax.set_xticks([]); ax.set_yticks([])
  136. for s in ax.spines.values():
  137. s.set_edgecolor("#bbbbbb")
  138. ax.add_patch(plt.Rectangle((-6.2, YC), 12.9, 8.6 - YC, fc="#e4e8ec", ec="none", zorder=0))
  139. ax.add_patch(plt.Rectangle((-6.2, 0), 12.9, YC, fc="#ddd0b6", ec="none", zorder=0))
  140. ax.axhline(YC, color="#3c3c3c", lw=2.2, zorder=2)
  141. ax.axhline(0, color="#6b6b6b", lw=1.3, zorder=2)
  142. ax.text(6.3, YC + 0.15, "crest", fontsize=8.5, color="#3c3c3c")
  143. ax.text(-6.55, 0.15, "toe", fontsize=8.5, color="#6b6b6b", ha="left")
  144. ax.text(-6.45, 6.9, "rock", fontsize=9, color="#7a8288", ha="left")
  145. ax.text(-6.45, -2.0, "air", fontsize=9, color="#9aa1a6", ha="left")
  146. def face_arrow(x=0.0):
  147. ax.annotate("", xy=(x, -1.15), xytext=(x, 3.7),
  148. arrowprops=dict(arrowstyle="-|>", lw=2.6, color="#222222"))
  149. ax.text(x + 0.35, -1.85, "face 60/180", fontsize=9.5,
  150. fontweight="bold", color="#222222", ha="left")
  151. def strike_trace(x0, dd, color):
  152. s = np.array([np.cos(rd(dd)), -np.sin(rd(dd))])
  153. p0 = np.array([x0, YC])
  154. a, b = p0 - 4.6 * s, p0 + 4.6 * s
  155. ax.plot([a[0], b[0]], [a[1], b[1]], ls=":", lw=1.1, color=color,
  156. alpha=0.75, zorder=2)
  157. def joint_arrow(x0, dip, dd, color, label, lab_off):
  158. v = np.array([np.sin(rd(dd)), np.cos(rd(dd))])
  159. p0 = np.array([x0, YC]); tip = p0 + 2.6 * v
  160. ax.annotate("", xy=tuple(tip), xytext=tuple(p0),
  161. arrowprops=dict(arrowstyle="-|>", lw=2.5, color=color))
  162. ax.text(*(tip + np.array(lab_off)), label, fontsize=9.3, color=color,
  163. fontweight="bold", ha="center")
  164. def isect_arrow(trend, good, label):
  165. v = np.array([np.sin(rd(trend)), np.cos(rd(trend))])
  166. p0 = np.array([0.0, YC]); tip = p0 + 3.9 * v
  167. col = GRN if good else RED
  168. ax.annotate("", xy=tuple(tip), xytext=tuple(p0),
  169. arrowprops=dict(arrowstyle="-|>", lw=3.2, color=col,
  170. linestyle=(0, (5, 3))))
  171. lx = 1.15 if abs(v[0]) < 0.3 else (0.9 * np.sign(v[0]))
  172. ly = tip[1] + (-0.55 if v[1] < 0 else 0.45)
  173. ax.text(tip[0] + lx, ly, label, fontsize=9.3, color=col,
  174. fontweight="bold", ha="left" if lx > 0 else "right")
  175. if i == 0:
  176. strike_trace(-3.2, 145, BLUE); strike_trace(3.2, 215, ORNG)
  177. joint_arrow(-3.2, 55, 145, BLUE, "J1 55/145", (-0.35, -0.75))
  178. joint_arrow(3.2, 55, 215, ORNG, "J2 55/215", (0.35, -0.75))
  179. isect_arrow(180, True, "intersection 49/180 " + OK)
  180. face_arrow()
  181. ax.annotate("", xy=(5.55, 8.15), xytext=(5.55, 7.15),
  182. arrowprops=dict(arrowstyle="-|>", lw=1.8, color="#222222"))
  183. ax.text(5.55, 8.32, "N", fontsize=10, ha="center", fontweight="bold")
  184. elif i == 1:
  185. strike_trace(-3.2, 145, BLUE); strike_trace(3.2, 215, ORNG)
  186. joint_arrow(-3.2, 80, 145, BLUE, "J1 80/145", (-0.35, -0.75))
  187. joint_arrow(3.2, 80, 215, ORNG, "J2 80/215", (0.35, -0.75))
  188. isect_arrow(180, False, "78/180 " + BAD + " > face 60\u00b0")
  189. face_arrow()
  190. elif i == 2:
  191. strike_trace(-3.2, 35, BLUE); strike_trace(3.2, 325, ORNG)
  192. joint_arrow(-3.2, 55, 35, BLUE, "J1 55/035", (-0.45, 0.55))
  193. joint_arrow(3.2, 55, 325, ORNG, "J2 55/325", (0.45, 0.55))
  194. isect_arrow(360, False, "49/360 " + BAD + " backward")
  195. face_arrow()
  196. else:
  197. sB = np.array([np.cos(rd(145)), -np.sin(rd(145))])
  198. pB = np.array([-1.15, 2.0]); a, b = pB - 1.55 * sB, pB + 1.55 * sB
  199. ax.plot([a[0], b[0]], [a[1], b[1]], lw=4.5, color=BLUE, solid_capstyle="round")
  200. ax.text(-3.6, 1.1, "J1", fontsize=9.5, color=BLUE, fontweight="bold")
  201. sO = np.array([np.cos(rd(215)), -np.sin(rd(215))])
  202. pO = np.array([1.15, 2.0]); a, b = pO - 1.55 * sO, pO + 1.55 * sO
  203. ax.plot([a[0], b[0]], [a[1], b[1]], lw=4.5, color=ORNG, solid_capstyle="round")
  204. ax.text(3.6, 1.1, "J2", fontsize=9.5, color=ORNG, fontweight="bold")
  205. ax.add_patch(Ellipse((0, 2.0), 2.1, 2.7, fill=False, lw=2.0,
  206. ec=RED, ls="--", zorder=3))
  207. ax.text(0, 3.75, "gap " + BAD + " planes never meet", fontsize=9.6,
  208. color=RED, fontweight="bold", ha="center")
  209. face_arrow(x=-4.9)
  210. fig.text(0.5, 0.487, "map view (looking down; arrows = dip directions)", ha="center",
  211. fontsize=11, style="italic", color="#5d6d7e")
  212. # ------------------------------------------------------------- 3d views -----
  213. rock_faces = [
  214. ([(-6, 0, 0), (6, 0, 0), (6, YC, 8), (-6, YC, 8)], "#d8c9ad"), # face
  215. ([(-6, YC, 8), (6, YC, 8), (6, 11, 8), (-6, 11, 8)], "#eef1f3"), # top
  216. ([(-6, 0, 0), (6, 0, 0), (6, 11, 0), (-6, 11, 0)], "#cfd4d8"), # bottom
  217. ([(-6, 11, 0), (6, 11, 0), (6, 11, 8), (-6, 11, 8)], "#dfe3e6"), # back
  218. ([(-6, 0, 0), (-6, YC, 8), (-6, 11, 8), (-6, 11, 0)], "#dfe3e6"),
  219. ([(6, 0, 0), (6, YC, 8), (6, 11, 8), (6, 11, 0)], "#d7dbdf"),
  220. ]
  221. rock_alpha = 0.16
  222. wire = [ # crisp prism outline drawn on top
  223. [(-6, 0, 0), (6, 0, 0)], [(6, 0, 0), (6, YC, 8)], [(6, YC, 8), (-6, YC, 8)],
  224. [(-6, YC, 8), (-6, 0, 0)], [(-6, YC, 8), (-6, 11, 8)], [(6, YC, 8), (6, 11, 8)],
  225. [(-6, 11, 8), (6, 11, 8)], [(-6, 0, 0), (-6, 11, 0)], [(6, 0, 0), (6, 11, 0)],
  226. [(-6, 11, 0), (6, 11, 0)], [(-6, 11, 0), (-6, 11, 8)], [(6, 11, 0), (6, 11, 8)],
  227. ]
  228. def add_3d(i):
  229. ax = fig.add_subplot(2, 4, i + 5, projection="3d")
  230. ax.set_xlim(-6, 6); ax.set_ylim(-2.2, 11); ax.set_zlim(0, 8.6)
  231. ax.set_box_aspect((12, 13.2, 8.6))
  232. ax.view_init(elev=16, azim=-64)
  233. ax.set_axis_off()
  234. return ax
  235. def draw_polys(ax, polys): # one collection so polygons are depth-sorted together
  236. coll = Poly3DCollection([p for p, _, _, _ in polys],
  237. facecolors=[c for _, c, _, _ in polys],
  238. edgecolors=[e for _, _, e, _ in polys],
  239. linewidths=[w for _, _, _, w in polys])
  240. ax.add_collection3d(coll)
  241. def rock_polys():
  242. return [(np.array(pts), matplotlib.colors.to_rgba(col, rock_alpha),
  243. matplotlib.colors.to_rgba("#8a9095", 0.5), 0.5)
  244. for pts, col in rock_faces]
  245. import matplotlib.colors
  246. def line3(ax, a, b, color, style="--", lw=2.6):
  247. ax.plot([a[0], b[0]], [a[1], b[1]], [a[2], b[2]], ls=style, lw=lw, color=color)
  248. def wire3(ax):
  249. for a, b in wire:
  250. ax.plot([a[0], b[0]], [a[1], b[1]], [a[2], b[2]], color="#64707a", lw=1.2)
  251. def cap(ax, s, col):
  252. ax.text2D(0.04, 0.05, s, transform=ax.transAxes, fontsize=9.3, color=col,
  253. fontweight="bold", va="bottom", ha="left",
  254. bbox=dict(fc="white", ec="none", alpha=0.85, pad=1.5))
  255. # case 1
  256. ax = add_3d(0)
  257. polys = rock_polys()
  258. polys.append((plane_polygon(n1a, c1a, pbox), matplotlib.colors.to_rgba(BLUE, 0.40),
  259. matplotlib.colors.to_rgba(BLUE, 0.9), 1.4))
  260. polys.append((plane_polygon(n1b, c1b, pbox), matplotlib.colors.to_rgba(ORNG, 0.40),
  261. matplotlib.colors.to_rgba(ORNG, 0.9), 1.4))
  262. tet = [[F1, T1, C1a], [F1, T1, C1b], [F1, C1a, C1b], [T1, C1a, C1b]]
  263. for t in tet:
  264. polys.append((np.array(t), matplotlib.colors.to_rgba("#e74c3c", 0.62),
  265. matplotlib.colors.to_rgba("#7b241c", 0.95), 1.6))
  266. draw_polys(ax, polys)
  267. wire3(ax)
  268. ax.scatter(*F1, s=150, marker="*", color=GRN, depthshade=False, zorder=10)
  269. cap(ax, "J1 55/145 (blue) + J2 55/215 (orange)\nred = wedge block \u00b7 star = daylighting toe " + OK, GRN)
  270. # case 2
  271. ax = add_3d(1)
  272. polys = rock_polys()
  273. polys.append((plane_polygon(n2a, c2a, pbox), matplotlib.colors.to_rgba(BLUE, 0.40),
  274. matplotlib.colors.to_rgba(BLUE, 0.9), 1.4))
  275. polys.append((plane_polygon(n2b, c2b, pbox), matplotlib.colors.to_rgba(ORNG, 0.40),
  276. matplotlib.colors.to_rgba(ORNG, 0.9), 1.4))
  277. draw_polys(ax, polys)
  278. wire3(ax)
  279. line3(ax, s2a, s2b, RED)
  280. ax.scatter(*s2b, s=150, marker="X", color=RED, depthshade=False, zorder=10)
  281. cap(ax, "steep joints 80/145 + 80/215\ndashed = intersection line: buried, hits the bottom " + BAD, RED)
  282. # case 3
  283. ax = add_3d(2)
  284. polys = rock_polys()
  285. polys.append((plane_polygon(n3a, c3a, pbox), matplotlib.colors.to_rgba(BLUE, 0.40),
  286. matplotlib.colors.to_rgba(BLUE, 0.9), 1.4))
  287. polys.append((plane_polygon(n3b, c3b, pbox), matplotlib.colors.to_rgba(ORNG, 0.40),
  288. matplotlib.colors.to_rgba(ORNG, 0.9), 1.4))
  289. draw_polys(ax, polys)
  290. wire3(ax)
  291. line3(ax, s3a, s3b, RED)
  292. ax.scatter(*s3b, s=150, marker="X", color=RED, depthshade=False, zorder=10)
  293. cap(ax, "joints dip INTO the slope (55/035 + 55/325)\ndashed = intersection plunges backward, up-hill " + BAD, RED)
  294. # case 4
  295. ax = add_3d(3)
  296. polys = rock_polys()
  297. polys.append((pa4, matplotlib.colors.to_rgba(BLUE, 0.55),
  298. matplotlib.colors.to_rgba(BLUE, 0.95), 2.2))
  299. polys.append((pb4, matplotlib.colors.to_rgba(ORNG, 0.55),
  300. matplotlib.colors.to_rgba(ORNG, 0.95), 2.2))
  301. draw_polys(ax, polys)
  302. wire3(ax)
  303. w1, w2 = line_in_box(inter_point(n1a, c1a, n1b, c1b), La,
  304. ((-6, 6), (-1.2, 9.3), (0.2, 8.4)))
  305. line3(ax, w1, w2, RED, style=(0, (2, 3)), lw=1.8)
  306. ax.scatter(0, 2.2, 4.6, s=150, marker="X", color=RED, depthshade=False, zorder=10)
  307. cap(ax, "small / offset planes pass by each other\nthin dashed = where the intersection line would be " + BAD, RED)
  308. fig.text(0.5, 0.028, "A valid block needs BOTH: (1) the two planes physically intersect inside the slope volume, and "
  309. "(2) the wedge daylights into the free face \u2014\n"
  310. "i.e. the line of intersection plunges out of the slope (trend \u2248 face dip direction) at an angle shallower than the face dip. "
  311. "In RocSlope3, enlarge joint persistence and reposition if planes miss.",
  312. ha="center", fontsize=10.8, color="#34495e")
  313. fig.subplots_adjust(left=0.02, right=0.98, top=0.878, bottom=0.075,
  314. wspace=0.06, hspace=0.10)
  315. import pathlib
  316. out = pathlib.Path(__file__).with_name("rocslope3_wedge_cases.png")
  317. fig.savefig(out, dpi=165, facecolor="white")
  318. print("saved", out)