hu_from_slide.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Средний коэффициент Hu порового давления по моделям Slide2.
  2. В ПК Prorock гидростатическое воздействие задаётся формулой P = gamma_w h H_u
  3. (раздел «Учёт порового давления»), где h -- вертикальное расстояние от узла
  4. до заданной поверхности грунтовых вод, H_u -- коэффициент вклада поровой
  5. воды. Поровое давление, рассчитанное в Slide2 фильтрационным анализом
  6. (метод Groundwater), заменяется одним коэффициентом H_u, наилучшим образом
  7. воспроизводящим рассчитанное поле. Поверхность грунтовых вод в Prorock при
  8. этом задаётся по рельефу дневной поверхности, поэтому h -- глубина узла
  9. ниже рельефа.
  10. Файл .slim -- zip-архив модели Slide2, содержащий три текстовых файла:
  11. .sli -- модель: удельный вес воды gamma_w (ключ gammaw), координаты вершин
  12. геометрии (секция vertices) и внешний контур (exterior), верхняя
  13. огибающая которого задаёт рельеф дневной поверхности;
  14. .slw -- сетка фильтрационного расчёта: координаты узлов (секция nodes);
  15. .w01 -- результаты расчёта: строки «номер узла, полный напор, пьезометри-
  16. ческий напор» в метрах; поровое давление u = gamma_w h_p, кПа.
  17. Для каждого узла сетки вычисляются глубина h ниже рельефа и поровое
  18. давление u, после чего строятся три оценки коэффициента:
  19. 1. H_u по МНК через начало координат u = H_u gamma_w h (узлы с h >= 1 м):
  20. H_u = sum(u h) / (gamma_w sum(h^2)); минимизирует среднеквадратичную
  21. невязку поля давлений -- основное значение для переноса в Prorock;
  22. 2. среднее и медиана поточечных отношений H_u,i = u / (gamma_w h) по узлам
  23. с h >= 5 м (порог отсекает неустойчивые отношения у поверхности);
  24. 3. среднее H_u,i в полосе изобары 1 МПа (900--1100 кПа) -- аналог прежнего
  25. ручного способа, в котором коэффициент определялся по глубине залегания
  26. изобары 1000 кПа.
  27. Запуск (из корня репозитория):
  28. python assets/code/hu_from_slide.py "E:\\...\\Исходники_Slide" [ещё пути...]
  29. Аргументы командной строки -- файлы .slim или каталоги; для каталога
  30. обрабатываются все файлы .slim в его подкаталогах. Результаты печатаются
  31. в консоль и записываются в assets/tables/hu_from_slide.csv. Имя модели
  32. «35 1.slim» разбирается как контур 2035 года и профильная линия 1.
  33. Используется только стандартная библиотека.
  34. """
  35. import csv
  36. import math
  37. import re
  38. import statistics
  39. import sys
  40. import zipfile
  41. from pathlib import Path
  42. # Пороги глубины, м: для МНК и для поточечных отношений H_u,i.
  43. H_MIN_FIT, H_MIN_POINT = 1.0, 5.0
  44. # Полоса изобары 1 МПа для сопоставления с прежним ручным способом, кПа.
  45. ISOBAR_LO, ISOBAR_HI = 900.0, 1100.0
  46. # Префикс номера контура в имени файла -- год контура.
  47. CONTOUR_YEARS = {"26": 2026, "31": 2031, "33": 2033, "35": 2035}
  48. _NUM = r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?"
  49. def section(text, name):
  50. """Строки секции name. Заголовок -- слово с двоеточием в начале строки,
  51. данные -- всё до следующего заголовка (в .sli с отступом, в .slw нет)."""
  52. lines, inside = [], False
  53. for line in text.splitlines():
  54. if line[:1].isspace() or line[:1].isdigit():
  55. if inside:
  56. lines.append(line)
  57. else:
  58. inside = line.split(":")[0].strip() == name
  59. return lines
  60. def read_vertices(sli):
  61. """Координаты вершин геометрии: {номер: (x, y)}."""
  62. rx = re.compile(rf"(\d+)\s+x:\s*({_NUM})\s+y:\s*({_NUM})")
  63. return {int(m[1]): (float(m[2]), float(m[3]))
  64. for m in map(rx.search, section(sli, "vertices")) if m}
  65. def read_exterior(sli):
  66. """Номера вершин внешнего контура (замкнутого многоугольника)."""
  67. for line in section(sli, "exterior"):
  68. m = re.search(r"\[([0-9,\s]+)\]", line)
  69. if m:
  70. return [int(v) for v in m[1].split(",")]
  71. return []
  72. def surface_function(ids, verts):
  73. """Рельеф дневной поверхности: верхняя огибающая внешнего контура
  74. как функция от x."""
  75. ring = [verts[i] for i in ids if i in verts]
  76. ring += ring[:1] # замыкание контура
  77. segments = [(a, b) for a, b in zip(ring, ring[1:]) if a[0] != b[0]]
  78. def y_at(x):
  79. top = None
  80. for (x1, y1), (x2, y2) in segments:
  81. lo, hi = (x1, x2) if x1 < x2 else (x2, x1)
  82. if lo <= x <= hi:
  83. y = y1 + (y2 - y1)*(x - x1)/(x2 - x1)
  84. top = y if top is None or y > top else top
  85. return top
  86. return y_at
  87. def read_nodes(slw):
  88. """Координаты узлов сетки фильтрационного расчёта: {номер: (x, y)}."""
  89. rx = re.compile(rf"(\d+)\s+x:\s*({_NUM})\s+y:\s*({_NUM})")
  90. return {int(m[1]): (float(m[2]), float(m[3]))
  91. for m in map(rx.search, section(slw, "nodes")) if m}
  92. def read_heads(w01):
  93. """Напоры из результатов: {номер узла: (полный, пьезометрический)}, м."""
  94. heads = {}
  95. rx = re.compile(rf"(\d+)\s+({_NUM})\s+({_NUM})$")
  96. for line in w01.split("# Fluxes")[0].splitlines(): # блок напоров
  97. m = rx.match(line.strip())
  98. if m:
  99. heads[int(m[1])] = (float(m[2]), float(m[3]))
  100. return heads
  101. def model_stats(path):
  102. """Оценки H_u по одной модели .slim."""
  103. with zipfile.ZipFile(path) as z:
  104. def pick(suffix):
  105. names = [n for n in z.namelist() if n.lower().endswith(suffix)]
  106. return z.read(names[0]).decode("cp1251", "replace") if names else ""
  107. sli, slw, w01 = pick(".sli"), pick(".slw"), pick(".w01")
  108. if not (sli and slw and w01):
  109. raise ValueError("нет .sli/.slw/.w01 -- модель без результатов расчёта")
  110. m = re.search(rf"gammaw:\s*({_NUM})", sli)
  111. gamma_w = float(m[1]) if m else 9.81
  112. verts = read_vertices(sli)
  113. y_surface = surface_function(read_exterior(sli), verts)
  114. nodes, heads = read_nodes(slw), read_heads(w01)
  115. # Глубина ниже рельефа и поровое давление каждого узла, м и кПа.
  116. # Контроль сопоставления узлов: полный напор минус высота узла равен
  117. # пьезометрическому напору (допускается невязка до 0,5 м).
  118. pts = []
  119. for n, (x, y) in nodes.items():
  120. if n in heads:
  121. total, ph = heads[n]
  122. if abs(total - y - ph) > 0.5:
  123. raise ValueError("напоры не сходятся с координатами узлов")
  124. y_top = y_surface(x)
  125. if y_top is not None and y_top - y >= 0:
  126. pts.append((y_top - y, gamma_w*ph))
  127. fit = [(h, u) for h, u in pts if h >= H_MIN_FIT]
  128. hu_lsm = sum(u*h for h, u in fit)/(gamma_w*sum(h*h for h, _ in fit))
  129. rms = math.sqrt(sum((u - hu_lsm*gamma_w*h)**2 for h, u in fit)/len(fit))
  130. point = [u/(gamma_w*h) for h, u in pts if h >= H_MIN_POINT and u > 0]
  131. isobar = [u/(gamma_w*h) for h, u in pts
  132. if h >= H_MIN_POINT and ISOBAR_LO <= u <= ISOBAR_HI]
  133. return {
  134. "n": len(fit), "h_max": max(h for h, _ in fit),
  135. "u_max": max(u for _, u in fit), "hu_lsm": hu_lsm, "rms": rms,
  136. "hu_mean": statistics.mean(point) if point else None,
  137. "hu_median": statistics.median(point) if point else None,
  138. "hu_isobar": statistics.mean(isobar) if isobar else None,
  139. }
  140. def model_name(stem):
  141. """«35 1» -> (год контура, номер линии) по имени файла модели."""
  142. parts = stem.split()
  143. year = str(CONTOUR_YEARS.get(parts[0], parts[0]))
  144. return year, parts[1] if len(parts) > 1 else "--"
  145. def fmt(value, digits=3):
  146. """Число с десятичной запятой и типографским знаком минус."""
  147. if value is None:
  148. return "--"
  149. return f"{value:.{digits}f}".replace(".", ",").replace("-", "\u2212")
  150. def main():
  151. if len(sys.argv) < 2:
  152. sys.exit("укажите файлы .slim или каталоги с моделями Slide2")
  153. paths = []
  154. for arg in sys.argv[1:]:
  155. p = Path(arg)
  156. paths.extend(sorted(p.rglob("*.slim")) if p.is_dir() else [p])
  157. root = Path(__file__).resolve().parents[2]
  158. header = ("Год контура", "№ профильной линии", "Hu (МНК)",
  159. "Hu среднее", "Hu медиана", "Hu у изобары 1 МПа",
  160. "Узлов", "h_max, м", "u_max, кПа", "СКО поля, кПа")
  161. rows, values = [header], []
  162. print(f"{'год':>4} {'лин':>4} {'Hu МНК':>8} {'среднее':>8} "
  163. f"{'медиана':>8} {'1 МПа':>8} {'узлов':>6} "
  164. f"{'h_max':>6} {'u_max':>7} {'СКО':>7}")
  165. for path in paths:
  166. try:
  167. st = model_stats(path)
  168. except (ValueError, KeyError, zipfile.BadZipFile) as e:
  169. print(f"{path.stem}: пропущена ({e})")
  170. continue
  171. year, line = model_name(path.stem)
  172. print(f"{year:>4} {line:>4} {fmt(st['hu_lsm']):>8} "
  173. f"{fmt(st['hu_mean']):>8} {fmt(st['hu_median']):>8} "
  174. f"{fmt(st['hu_isobar']):>8} {st['n']:>6} "
  175. f"{fmt(st['h_max'], 0):>6} {fmt(st['u_max'], 0):>7} "
  176. f"{fmt(st['rms'], 0):>7}")
  177. rows.append((year, line, fmt(st["hu_lsm"]), fmt(st["hu_mean"]),
  178. fmt(st["hu_median"]), fmt(st["hu_isobar"]), st["n"],
  179. fmt(st["h_max"], 1), fmt(st["u_max"], 0),
  180. fmt(st["rms"], 0)))
  181. values.append(st["hu_lsm"])
  182. if values:
  183. print(f"среднее Hu (МНК) по всем моделям: {fmt(statistics.mean(values))}")
  184. out = root/"assets/tables/hu_from_slide.csv"
  185. with open(out, "w", encoding="utf-8", newline="") as f:
  186. csv.writer(f, delimiter=";").writerows(rows)
  187. print(f"таблица записана: {out}")
  188. if __name__ == "__main__":
  189. main()