trenchfoot 0.1.0__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.
Potentially problematic release.
This version of trenchfoot might be problematic. Click here for more details.
- trenchfoot/Dockerfile +5 -0
- trenchfoot/README.md +125 -0
- trenchfoot/__init__.py +67 -0
- trenchfoot/generate_scenarios.py +667 -0
- trenchfoot/gmsh_sloped_trench_mesher.py +508 -0
- trenchfoot/plot_mesh.py +126 -0
- trenchfoot/render_colors.py +59 -0
- trenchfoot/scenarios/S01_straight_vwalls/meshes/trench_scene_culled.obj +46 -0
- trenchfoot/scenarios/S01_straight_vwalls/metrics.json +35 -0
- trenchfoot/scenarios/S01_straight_vwalls/point_clouds/culled/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/point_clouds/full/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/preview.png +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/preview_oblique.png +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/preview_side.png +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/preview_top.png +0 -0
- trenchfoot/scenarios/S01_straight_vwalls/scene.json +30 -0
- trenchfoot/scenarios/S01_straight_vwalls/trench_scene.obj +46 -0
- trenchfoot/scenarios/S01_straight_vwalls/volumetric/trench_volume.msh +1017 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/meshes/trench_scene_culled.obj +60 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/metrics.json +38 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/culled/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/full/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/preview.png +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/preview_oblique.png +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/preview_side.png +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/preview_top.png +0 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/scene.json +39 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/trench_scene.obj +14404 -0
- trenchfoot/scenarios/S02_straight_slope_pipe/volumetric/trench_volume.msh +2389 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/meshes/trench_scene_culled.obj +87 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/metrics.json +42 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/culled/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/full/resolution0p050.pth +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview.png +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_oblique.png +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_side.png +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_top.png +0 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/scene.json +68 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/trench_scene.obj +28803 -0
- trenchfoot/scenarios/S03_L_slope_two_pipes_box/volumetric/trench_volume.msh +4463 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/meshes/trench_scene_culled.obj +150 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/metrics.json +46 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/preview.png +0 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/preview_oblique.png +0 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/preview_side.png +0 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/preview_top.png +0 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/scene.json +79 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/trench_scene.obj +49402 -0
- trenchfoot/scenarios/S04_U_slope_multi_noise/volumetric/trench_volume.msh +7610 -0
- trenchfoot/scenarios/S05_wide_slope_pair/metrics.json +42 -0
- trenchfoot/scenarios/S05_wide_slope_pair/preview_oblique.png +0 -0
- trenchfoot/scenarios/S05_wide_slope_pair/preview_side.png +0 -0
- trenchfoot/scenarios/S05_wide_slope_pair/preview_top.png +0 -0
- trenchfoot/scenarios/S05_wide_slope_pair/scene.json +71 -0
- trenchfoot/scenarios/S05_wide_slope_pair/trench_scene.obj +28803 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/metrics.json +43 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/preview_oblique.png +0 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/preview_side.png +0 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/preview_top.png +0 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/scene.json +82 -0
- trenchfoot/scenarios/S06_bumpy_wide_loop/trench_scene.obj +35084 -0
- trenchfoot/scenarios/SUMMARY.json +159 -0
- trenchfoot/scene_spec_example.json +74 -0
- trenchfoot/trench_scene_generator_v3.py +798 -0
- trenchfoot-0.1.0.dist-info/METADATA +104 -0
- trenchfoot-0.1.0.dist-info/RECORD +69 -0
- trenchfoot-0.1.0.dist-info/WHEEL +4 -0
- trenchfoot-0.1.0.dist-info/entry_points.txt +3 -0
- trenchfoot-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
generate_scenarios.py
|
|
5
|
+
|
|
6
|
+
Produce the bundled trench scenarios from simple → complex using the surface generator,
|
|
7
|
+
and optionally run the volumetric mesher (via Gmsh) when available.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ensure_repo_on_path() -> None:
|
|
25
|
+
if str(_ROOT) not in sys.path:
|
|
26
|
+
sys.path.insert(0, str(_ROOT))
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from .trench_scene_generator_v3 import (
|
|
30
|
+
SceneSpec,
|
|
31
|
+
build_scene,
|
|
32
|
+
scene_spec_from_dict,
|
|
33
|
+
)
|
|
34
|
+
except ImportError: # pragma: no cover - direct script invocation
|
|
35
|
+
_ensure_repo_on_path()
|
|
36
|
+
from trenchfoot.trench_scene_generator_v3 import ( # type: ignore
|
|
37
|
+
SceneSpec,
|
|
38
|
+
build_scene,
|
|
39
|
+
scene_spec_from_dict,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_gmsh_mesher = None
|
|
43
|
+
_gmsh_import_error: Optional[Exception] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_gmsh_mesher() -> None:
|
|
47
|
+
global _gmsh_mesher, _gmsh_import_error
|
|
48
|
+
if _gmsh_mesher is not None or _gmsh_import_error is not None:
|
|
49
|
+
return
|
|
50
|
+
try:
|
|
51
|
+
from . import gmsh_sloped_trench_mesher as _mesher # type: ignore
|
|
52
|
+
except Exception:
|
|
53
|
+
_ensure_repo_on_path()
|
|
54
|
+
try:
|
|
55
|
+
import trenchfoot.gmsh_sloped_trench_mesher as _mesher # type: ignore
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
_gmsh_import_error = exc
|
|
58
|
+
return
|
|
59
|
+
_gmsh_mesher = _mesher
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ScenarioDefinition:
|
|
64
|
+
"""Name + spec dictionary for generating a scenario."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
spec: Dict[str, Any]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ScenarioSummary:
|
|
72
|
+
"""Outputs captured for a single generated scenario."""
|
|
73
|
+
|
|
74
|
+
name: str
|
|
75
|
+
directory: Path
|
|
76
|
+
spec_path: Path
|
|
77
|
+
surface_obj: Path
|
|
78
|
+
metrics_path: Path
|
|
79
|
+
preview_paths: List[Path]
|
|
80
|
+
preview_count: int
|
|
81
|
+
object_counts: Dict[str, int]
|
|
82
|
+
footprint_top: float
|
|
83
|
+
footprint_bottom: float
|
|
84
|
+
trench_from_surface: float
|
|
85
|
+
volumetric_path: Optional[Path]
|
|
86
|
+
volumetric_lc: Optional[float]
|
|
87
|
+
volumetric_error: Optional[str]
|
|
88
|
+
pipe_clearances: List[Dict[str, Any]] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
91
|
+
return {
|
|
92
|
+
"name": self.name,
|
|
93
|
+
"directory": str(self.directory),
|
|
94
|
+
"spec_path": str(self.spec_path),
|
|
95
|
+
"surface_obj": str(self.surface_obj),
|
|
96
|
+
"metrics_path": str(self.metrics_path),
|
|
97
|
+
"preview_paths": [str(p) for p in self.preview_paths],
|
|
98
|
+
"preview_count": self.preview_count,
|
|
99
|
+
"object_counts": dict(self.object_counts),
|
|
100
|
+
"footprint_top": self.footprint_top,
|
|
101
|
+
"footprint_bottom": self.footprint_bottom,
|
|
102
|
+
"trench_from_surface": self.trench_from_surface,
|
|
103
|
+
"volumetric_path": str(self.volumetric_path) if self.volumetric_path else None,
|
|
104
|
+
"volumetric_lc": self.volumetric_lc,
|
|
105
|
+
"volumetric_error": self.volumetric_error,
|
|
106
|
+
"pipe_clearances": self.pipe_clearances,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class RunReport:
|
|
112
|
+
"""Aggregate report describing a generate_scenarios run."""
|
|
113
|
+
|
|
114
|
+
out_root: Path
|
|
115
|
+
preview_enabled: bool
|
|
116
|
+
volumetric_requested: bool
|
|
117
|
+
volumetric_available: bool
|
|
118
|
+
mesh_characteristic_length: float
|
|
119
|
+
scenarios: List[ScenarioSummary]
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
"out_root": str(self.out_root),
|
|
124
|
+
"preview_enabled": self.preview_enabled,
|
|
125
|
+
"volumetric_requested": self.volumetric_requested,
|
|
126
|
+
"volumetric_available": self.volumetric_available,
|
|
127
|
+
"mesh_characteristic_length": self.mesh_characteristic_length,
|
|
128
|
+
"scenarios": [sc.to_dict() for sc in self.scenarios],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def gmsh_available() -> bool:
|
|
133
|
+
"""Return True if the gmsh-based mesher can be imported."""
|
|
134
|
+
_load_gmsh_mesher()
|
|
135
|
+
return _gmsh_mesher is not None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def default_scenarios() -> List[ScenarioDefinition]:
|
|
139
|
+
"""Built-in scenario presets."""
|
|
140
|
+
return [
|
|
141
|
+
ScenarioDefinition(
|
|
142
|
+
"S01_straight_vwalls",
|
|
143
|
+
{
|
|
144
|
+
"path_xy": [[0, 0], [5, 0]],
|
|
145
|
+
"width": 1.0,
|
|
146
|
+
"depth": 1.0,
|
|
147
|
+
"wall_slope": 0.0,
|
|
148
|
+
"ground_margin": 0.5,
|
|
149
|
+
"ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 3.0},
|
|
150
|
+
"pipes": [],
|
|
151
|
+
"boxes": [],
|
|
152
|
+
"spheres": [],
|
|
153
|
+
"noise": {"enable": False},
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
ScenarioDefinition(
|
|
157
|
+
"S02_straight_slope_pipe",
|
|
158
|
+
{
|
|
159
|
+
"path_xy": [[0, 0], [6, 0]],
|
|
160
|
+
"width": 1.2,
|
|
161
|
+
"depth": 1.5,
|
|
162
|
+
"wall_slope": 0.2,
|
|
163
|
+
"ground_margin": 0.5,
|
|
164
|
+
"ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 3.0},
|
|
165
|
+
"pipes": [
|
|
166
|
+
{
|
|
167
|
+
"radius": 0.15,
|
|
168
|
+
"length": 7.0,
|
|
169
|
+
"angle_deg": 0,
|
|
170
|
+
"s_center": 0.5,
|
|
171
|
+
"z": -0.7,
|
|
172
|
+
"offset_u": 0.0,
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"boxes": [],
|
|
176
|
+
"spheres": [],
|
|
177
|
+
"noise": {"enable": False},
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
ScenarioDefinition(
|
|
181
|
+
"S03_L_slope_two_pipes_box",
|
|
182
|
+
{
|
|
183
|
+
"path_xy": [[0, 0], [6, 0], [6, 4]],
|
|
184
|
+
"width": 1.2,
|
|
185
|
+
"depth": 1.8,
|
|
186
|
+
"wall_slope": 0.15,
|
|
187
|
+
"ground_margin": 1.0,
|
|
188
|
+
"ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 4.0},
|
|
189
|
+
"pipes": [
|
|
190
|
+
{
|
|
191
|
+
"radius": 0.15,
|
|
192
|
+
"length": 8.0,
|
|
193
|
+
"angle_deg": 0,
|
|
194
|
+
"s_center": 0.35,
|
|
195
|
+
"z": -1.0,
|
|
196
|
+
"offset_u": 0.0,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"radius": 0.10,
|
|
200
|
+
"length": 5.0,
|
|
201
|
+
"angle_deg": 90,
|
|
202
|
+
"s_center": 0.75,
|
|
203
|
+
"z": -0.9,
|
|
204
|
+
"offset_u": 0.2,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
"boxes": [
|
|
208
|
+
{
|
|
209
|
+
"along": 0.8,
|
|
210
|
+
"across": 0.5,
|
|
211
|
+
"height": 0.4,
|
|
212
|
+
"s": 0.55,
|
|
213
|
+
"offset_u": 0.0,
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
"spheres": [],
|
|
217
|
+
"noise": {
|
|
218
|
+
"enable": True,
|
|
219
|
+
"amplitude": 0.01,
|
|
220
|
+
"corr_length": 0.5,
|
|
221
|
+
"octaves": 2,
|
|
222
|
+
"gain": 0.5,
|
|
223
|
+
"seed": 7,
|
|
224
|
+
"apply_to": ["trench_walls", "trench_bottom"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
),
|
|
228
|
+
ScenarioDefinition(
|
|
229
|
+
"S04_U_slope_multi_noise",
|
|
230
|
+
{
|
|
231
|
+
"path_xy": [[0, 0], [6, 0], [6, 4], [0, 4]],
|
|
232
|
+
"width": 1.4,
|
|
233
|
+
"depth": 2.0,
|
|
234
|
+
"wall_slope": 0.25,
|
|
235
|
+
"ground_margin": 1.2,
|
|
236
|
+
"ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 5.0},
|
|
237
|
+
"pipes": [
|
|
238
|
+
{
|
|
239
|
+
"radius": 0.18,
|
|
240
|
+
"length": 9.0,
|
|
241
|
+
"angle_deg": 0,
|
|
242
|
+
"s_center": 0.25,
|
|
243
|
+
"z": -1.0,
|
|
244
|
+
"offset_u": 0.0,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"radius": 0.10,
|
|
248
|
+
"length": 5.0,
|
|
249
|
+
"angle_deg": 45,
|
|
250
|
+
"s_center": 0.55,
|
|
251
|
+
"z": -1.1,
|
|
252
|
+
"offset_u": -0.2,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"radius": 0.08,
|
|
256
|
+
"length": 3.5,
|
|
257
|
+
"angle_deg": -60,
|
|
258
|
+
"s_center": 0.75,
|
|
259
|
+
"z": -1.3,
|
|
260
|
+
"offset_u": 0.25,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
"boxes": [],
|
|
264
|
+
"spheres": [{"radius": 0.3, "s": 0.85, "offset_u": -0.2}],
|
|
265
|
+
"noise": {
|
|
266
|
+
"enable": True,
|
|
267
|
+
"amplitude": 0.02,
|
|
268
|
+
"corr_length": 0.6,
|
|
269
|
+
"octaves": 2,
|
|
270
|
+
"gain": 0.6,
|
|
271
|
+
"seed": 13,
|
|
272
|
+
"apply_to": ["trench_walls", "trench_bottom", "pipe*_pipe_side"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
),
|
|
276
|
+
ScenarioDefinition(
|
|
277
|
+
"S05_wide_slope_pair",
|
|
278
|
+
{
|
|
279
|
+
"path_xy": [[0, 0], [9, 0], [9, 3]],
|
|
280
|
+
"width": 2.4,
|
|
281
|
+
"depth": 1.2,
|
|
282
|
+
"wall_slope": 0.08,
|
|
283
|
+
"ground_margin": 1.5,
|
|
284
|
+
"ground": {"z0": 0.0, "slope": [0.02, -0.015], "size_margin": 6.0},
|
|
285
|
+
"pipes": [
|
|
286
|
+
{
|
|
287
|
+
"radius": 0.2,
|
|
288
|
+
"length": 5.5,
|
|
289
|
+
"angle_deg": 10,
|
|
290
|
+
"s_center": 0.35,
|
|
291
|
+
"z": -0.7,
|
|
292
|
+
"offset_u": 0.3,
|
|
293
|
+
"clearance_scale": 0.9,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"radius": 0.16,
|
|
297
|
+
"length": 4.2,
|
|
298
|
+
"angle_deg": -15,
|
|
299
|
+
"s_center": 0.7,
|
|
300
|
+
"z": -0.8,
|
|
301
|
+
"offset_u": -0.4,
|
|
302
|
+
"clearance_scale": 1.1,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
"boxes": [
|
|
306
|
+
{
|
|
307
|
+
"along": 1.2,
|
|
308
|
+
"across": 0.9,
|
|
309
|
+
"height": 0.5,
|
|
310
|
+
"s": 0.55,
|
|
311
|
+
"offset_u": -0.25,
|
|
312
|
+
"z": -0.6,
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
"spheres": [],
|
|
316
|
+
"noise": {
|
|
317
|
+
"enable": True,
|
|
318
|
+
"amplitude": 0.015,
|
|
319
|
+
"corr_length": 0.8,
|
|
320
|
+
"octaves": 3,
|
|
321
|
+
"gain": 0.45,
|
|
322
|
+
"seed": 17,
|
|
323
|
+
"apply_to": ["trench_walls", "trench_bottom"],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
),
|
|
327
|
+
ScenarioDefinition(
|
|
328
|
+
"S06_bumpy_wide_loop",
|
|
329
|
+
{
|
|
330
|
+
"path_xy": [[0, 0], [4, -1], [8, 0], [8, 5], [2, 5], [-1, 2]],
|
|
331
|
+
"width": 2.6,
|
|
332
|
+
"depth": 1.4,
|
|
333
|
+
"wall_slope": 0.12,
|
|
334
|
+
"ground_margin": 2.0,
|
|
335
|
+
"ground": {"z0": 0.2, "slope": [0.015, 0.03], "size_margin": 7.0},
|
|
336
|
+
"pipes": [
|
|
337
|
+
{
|
|
338
|
+
"radius": 0.18,
|
|
339
|
+
"length": 6.0,
|
|
340
|
+
"angle_deg": 35,
|
|
341
|
+
"s_center": 0.3,
|
|
342
|
+
"z": -0.9,
|
|
343
|
+
"offset_u": 0.35,
|
|
344
|
+
"clearance_scale": 1.0,
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"radius": 0.14,
|
|
348
|
+
"length": 4.8,
|
|
349
|
+
"angle_deg": -40,
|
|
350
|
+
"s_center": 0.6,
|
|
351
|
+
"z": -0.95,
|
|
352
|
+
"offset_u": -0.45,
|
|
353
|
+
"clearance_scale": 0.85,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
"boxes": [],
|
|
357
|
+
"spheres": [
|
|
358
|
+
{"radius": 0.35, "s": 0.82, "offset_u": 0.3, "z": -0.65}
|
|
359
|
+
],
|
|
360
|
+
"noise": {
|
|
361
|
+
"enable": True,
|
|
362
|
+
"amplitude": 0.035,
|
|
363
|
+
"corr_length": 0.5,
|
|
364
|
+
"octaves": 4,
|
|
365
|
+
"gain": 0.55,
|
|
366
|
+
"seed": 29,
|
|
367
|
+
"apply_to": ["trench_walls", "trench_bottom", "pipe*_pipe_side"],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
),
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _ensure_spec_written(path: Path, spec: Dict[str, Any]) -> None:
|
|
375
|
+
if not path.parent.exists():
|
|
376
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
with path.open("w") as fh:
|
|
378
|
+
json.dump(spec, fh, indent=2)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _build_surface(spec: SceneSpec, out_dir: Path, make_preview: bool) -> Dict[str, Any]:
|
|
382
|
+
return build_scene(spec, str(out_dir), make_preview=make_preview)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _build_volume(spec: Dict[str, Any], out_dir: Path, lc: float) -> tuple[Optional[Path], Optional[str], List[Dict[str, Any]]]:
|
|
386
|
+
_load_gmsh_mesher()
|
|
387
|
+
if _gmsh_mesher is None:
|
|
388
|
+
reason = str(_gmsh_import_error) if _gmsh_import_error else "gmsh_not_available"
|
|
389
|
+
return None, reason, []
|
|
390
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
391
|
+
msh_path = out_dir / "trench_volume.msh"
|
|
392
|
+
clearance_data: List[Dict[str, Any]] = []
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
if hasattr(_gmsh_mesher, "generate_trench_volume"):
|
|
396
|
+
result = _gmsh_mesher.generate_trench_volume(
|
|
397
|
+
spec,
|
|
398
|
+
lc=lc,
|
|
399
|
+
persist_path=str(msh_path),
|
|
400
|
+
)
|
|
401
|
+
clearance_data.extend(
|
|
402
|
+
[dict(entry) for entry in result.pipe_clearances]
|
|
403
|
+
if isinstance(result.pipe_clearances, list)
|
|
404
|
+
else []
|
|
405
|
+
)
|
|
406
|
+
persisted = result.persisted_path if result.persisted_path is not None else Path(str(msh_path))
|
|
407
|
+
else:
|
|
408
|
+
_gmsh_mesher.build_trench_volume_from_spec(
|
|
409
|
+
spec, lc=lc, out_msh=str(msh_path)
|
|
410
|
+
)
|
|
411
|
+
persisted = Path(str(msh_path))
|
|
412
|
+
except Exception as exc: # pragma: no cover - gmsh failure
|
|
413
|
+
return None, str(exc), clearance_data
|
|
414
|
+
|
|
415
|
+
final_path = Path(persisted)
|
|
416
|
+
return final_path, None, clearance_data
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def generate_scenarios(
|
|
420
|
+
out_root: Path | str,
|
|
421
|
+
scenarios: Optional[Sequence[ScenarioDefinition]] = None,
|
|
422
|
+
*,
|
|
423
|
+
make_preview: bool = True,
|
|
424
|
+
make_volumes: bool = True,
|
|
425
|
+
mesh_characteristic_length: float = 0.3,
|
|
426
|
+
write_summary_json: bool = True,
|
|
427
|
+
) -> RunReport:
|
|
428
|
+
"""
|
|
429
|
+
Generate trench scenarios, producing surface meshes (+previews) and optional volumetric meshes.
|
|
430
|
+
Returns a RunReport describing the run.
|
|
431
|
+
"""
|
|
432
|
+
out_root = Path(out_root)
|
|
433
|
+
out_root.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
|
|
435
|
+
scenario_defs = list(scenarios) if scenarios is not None else default_scenarios()
|
|
436
|
+
results: List[ScenarioSummary] = []
|
|
437
|
+
gmsh_ok = gmsh_available() if make_volumes else False
|
|
438
|
+
|
|
439
|
+
for definition in scenario_defs:
|
|
440
|
+
scen_dir = out_root / definition.name
|
|
441
|
+
scen_dir.mkdir(parents=True, exist_ok=True)
|
|
442
|
+
|
|
443
|
+
spec_path = scen_dir / "scene.json"
|
|
444
|
+
_ensure_spec_written(spec_path, definition.spec)
|
|
445
|
+
|
|
446
|
+
scene_spec = scene_spec_from_dict(definition.spec)
|
|
447
|
+
surface_out = _build_surface(scene_spec, scen_dir, make_preview=make_preview)
|
|
448
|
+
|
|
449
|
+
volumetric_path: Optional[Path] = None
|
|
450
|
+
volumetric_error: Optional[str] = None
|
|
451
|
+
pipe_clearances: List[Dict[str, Any]] = []
|
|
452
|
+
if gmsh_ok:
|
|
453
|
+
vol_dir = scen_dir / "volumetric"
|
|
454
|
+
volumetric_path, volumetric_error, pipe_clearances = _build_volume(
|
|
455
|
+
definition.spec, vol_dir, mesh_characteristic_length
|
|
456
|
+
)
|
|
457
|
+
if volumetric_error:
|
|
458
|
+
print(
|
|
459
|
+
f"[volumetric] {definition.name} failed: {volumetric_error}",
|
|
460
|
+
file=sys.stderr,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
summary = ScenarioSummary(
|
|
464
|
+
name=definition.name,
|
|
465
|
+
directory=scen_dir,
|
|
466
|
+
spec_path=spec_path,
|
|
467
|
+
surface_obj=Path(surface_out["obj_path"]),
|
|
468
|
+
metrics_path=scen_dir / "metrics.json",
|
|
469
|
+
preview_paths=[Path(p) for p in surface_out["previews"]],
|
|
470
|
+
preview_count=len(surface_out["previews"]),
|
|
471
|
+
object_counts=dict(surface_out["object_counts"]),
|
|
472
|
+
footprint_top=float(surface_out["metrics"]["footprint_area_top"]),
|
|
473
|
+
footprint_bottom=float(surface_out["metrics"]["footprint_area_bottom"]),
|
|
474
|
+
trench_from_surface=float(surface_out["metrics"]["volumes"]["trench_from_surface"]),
|
|
475
|
+
volumetric_path=volumetric_path,
|
|
476
|
+
volumetric_lc=mesh_characteristic_length if volumetric_path else None,
|
|
477
|
+
volumetric_error=volumetric_error,
|
|
478
|
+
pipe_clearances=pipe_clearances,
|
|
479
|
+
)
|
|
480
|
+
results.append(summary)
|
|
481
|
+
|
|
482
|
+
report = RunReport(
|
|
483
|
+
out_root=out_root,
|
|
484
|
+
preview_enabled=make_preview,
|
|
485
|
+
volumetric_requested=make_volumes,
|
|
486
|
+
volumetric_available=gmsh_ok,
|
|
487
|
+
mesh_characteristic_length=mesh_characteristic_length,
|
|
488
|
+
scenarios=results,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if write_summary_json:
|
|
492
|
+
summary_path = out_root / "SUMMARY.json"
|
|
493
|
+
with summary_path.open("w") as fh:
|
|
494
|
+
json.dump(report.to_dict(), fh, indent=2)
|
|
495
|
+
|
|
496
|
+
return report
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _relative_path(path: Path, base: Path) -> str:
|
|
500
|
+
path_abs = path.resolve()
|
|
501
|
+
base_abs = base.resolve()
|
|
502
|
+
rel = os.path.relpath(path_abs, start=base_abs)
|
|
503
|
+
return Path(rel).as_posix()
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _preview_mapping(paths: List[Path]) -> Dict[str, Optional[Path]]:
|
|
507
|
+
lookup = {"top": None, "side": None, "oblique": None}
|
|
508
|
+
for p in paths:
|
|
509
|
+
name = p.name
|
|
510
|
+
if "preview_top" in name:
|
|
511
|
+
lookup["top"] = p
|
|
512
|
+
elif "preview_side" in name:
|
|
513
|
+
lookup["side"] = p
|
|
514
|
+
elif "preview_oblique" in name:
|
|
515
|
+
lookup["oblique"] = p
|
|
516
|
+
return lookup
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def build_gallery_markdown(report: RunReport, base: Path = _ROOT) -> str:
|
|
520
|
+
lines = ["| Scenario | Top | Side | Oblique |", "| --- | --- | --- | --- |"]
|
|
521
|
+
for scenario in report.scenarios:
|
|
522
|
+
previews = _preview_mapping(scenario.preview_paths)
|
|
523
|
+
|
|
524
|
+
def cell(which: str) -> str:
|
|
525
|
+
path = previews[which]
|
|
526
|
+
if path is None:
|
|
527
|
+
return "_(missing)_"
|
|
528
|
+
rel = _relative_path(path, base)
|
|
529
|
+
return f""
|
|
530
|
+
|
|
531
|
+
lines.append(
|
|
532
|
+
f"| {scenario.name} | {cell('top')} | {cell('side')} | {cell('oblique')} |"
|
|
533
|
+
)
|
|
534
|
+
return "\n".join(lines) + "\n"
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def write_gallery(markdown_path: Path, report: RunReport, base: Optional[Path] = None) -> None:
|
|
538
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
539
|
+
actual_base = base if base is not None else markdown_path.parent
|
|
540
|
+
markdown_path.write_text(build_gallery_markdown(report, base=actual_base))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _format_table(report: RunReport) -> str:
|
|
544
|
+
header = f"{'Scenario':<28} {'Surface OBJ':<40} {'Previews':<9} {'Volumetric':<12}"
|
|
545
|
+
rows = [header, "-" * len(header)]
|
|
546
|
+
for scenario in report.scenarios:
|
|
547
|
+
preview_label = f"{scenario.preview_count}"
|
|
548
|
+
if report.preview_enabled and scenario.preview_count == 0:
|
|
549
|
+
preview_label = "0 (matplotlib?)"
|
|
550
|
+
vol_label = "disabled"
|
|
551
|
+
if report.volumetric_requested:
|
|
552
|
+
if scenario.volumetric_path:
|
|
553
|
+
vol_label = "ok"
|
|
554
|
+
elif scenario.volumetric_error:
|
|
555
|
+
vol_label = "fail"
|
|
556
|
+
elif report.volumetric_available:
|
|
557
|
+
vol_label = "skipped"
|
|
558
|
+
else:
|
|
559
|
+
vol_label = "skipped"
|
|
560
|
+
rows.append(
|
|
561
|
+
f"{scenario.name:<28} "
|
|
562
|
+
f"{scenario.surface_obj.name:<40} "
|
|
563
|
+
f"{preview_label:<9} "
|
|
564
|
+
f"{vol_label:<12}"
|
|
565
|
+
)
|
|
566
|
+
return "\n".join(rows)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
570
|
+
env_out_root = os.environ.get("TRENCHFOOT_SCENARIO_OUT_ROOT")
|
|
571
|
+
default_out_root = (
|
|
572
|
+
Path(env_out_root).expanduser()
|
|
573
|
+
if env_out_root
|
|
574
|
+
else Path(__file__).resolve().parent / "scenarios"
|
|
575
|
+
)
|
|
576
|
+
parser = argparse.ArgumentParser(description="Generate the bundled trench scenarios.")
|
|
577
|
+
parser.add_argument(
|
|
578
|
+
"--out",
|
|
579
|
+
dest="out_root",
|
|
580
|
+
default=default_out_root,
|
|
581
|
+
type=Path,
|
|
582
|
+
help=(
|
|
583
|
+
"Destination directory for scenario outputs "
|
|
584
|
+
"(default: packages/trenchfoot/scenarios or $TRENCHFOOT_SCENARIO_OUT_ROOT)"
|
|
585
|
+
),
|
|
586
|
+
)
|
|
587
|
+
parser.add_argument(
|
|
588
|
+
"--preview",
|
|
589
|
+
dest="make_preview",
|
|
590
|
+
action="store_true",
|
|
591
|
+
help="Enable preview rendering.",
|
|
592
|
+
)
|
|
593
|
+
parser.add_argument(
|
|
594
|
+
"--no-preview",
|
|
595
|
+
dest="make_preview",
|
|
596
|
+
action="store_false",
|
|
597
|
+
help="Disable preview rendering.",
|
|
598
|
+
)
|
|
599
|
+
parser.add_argument(
|
|
600
|
+
"--volumetric",
|
|
601
|
+
dest="make_volumes",
|
|
602
|
+
action="store_true",
|
|
603
|
+
help="Enable volumetric meshing when gmsh is available.",
|
|
604
|
+
)
|
|
605
|
+
parser.add_argument(
|
|
606
|
+
"--skip-volumetric",
|
|
607
|
+
dest="make_volumes",
|
|
608
|
+
action="store_false",
|
|
609
|
+
help="Skip volumetric meshing even if gmsh is installed.",
|
|
610
|
+
)
|
|
611
|
+
parser.add_argument(
|
|
612
|
+
"--lc",
|
|
613
|
+
dest="lc",
|
|
614
|
+
type=float,
|
|
615
|
+
default=0.3,
|
|
616
|
+
help="Characteristic mesh length passed to gmsh (default: 0.3).",
|
|
617
|
+
)
|
|
618
|
+
parser.add_argument(
|
|
619
|
+
"--gallery",
|
|
620
|
+
dest="gallery",
|
|
621
|
+
type=Path,
|
|
622
|
+
help="Write a Markdown gallery of scenario previews to this path.",
|
|
623
|
+
)
|
|
624
|
+
parser.add_argument(
|
|
625
|
+
"--scratch",
|
|
626
|
+
dest="scratch",
|
|
627
|
+
action="store_true",
|
|
628
|
+
help="Generate scenarios in a temporary directory to keep the repository tree clean.",
|
|
629
|
+
)
|
|
630
|
+
parser.add_argument(
|
|
631
|
+
"--include-prebuilt",
|
|
632
|
+
dest="include_prebuilt",
|
|
633
|
+
action="store_true",
|
|
634
|
+
help="Copy the shipped scenario assets into the output directory before regeneration.",
|
|
635
|
+
)
|
|
636
|
+
parser.set_defaults(make_preview=True, make_volumes=True)
|
|
637
|
+
args = parser.parse_args(argv)
|
|
638
|
+
|
|
639
|
+
if args.scratch:
|
|
640
|
+
tmp_dir = Path(tempfile.mkdtemp(prefix="trenchfoot_scenarios_"))
|
|
641
|
+
print(f"[trenchfoot] Using scratch directory: {tmp_dir}", file=sys.stderr)
|
|
642
|
+
args.out_root = tmp_dir
|
|
643
|
+
|
|
644
|
+
if args.include_prebuilt:
|
|
645
|
+
source = (_ROOT / "scenarios")
|
|
646
|
+
if source.exists():
|
|
647
|
+
shutil.copytree(source, args.out_root, dirs_exist_ok=True)
|
|
648
|
+
|
|
649
|
+
report = generate_scenarios(
|
|
650
|
+
out_root=args.out_root,
|
|
651
|
+
make_preview=args.make_preview,
|
|
652
|
+
make_volumes=args.make_volumes,
|
|
653
|
+
mesh_characteristic_length=args.lc,
|
|
654
|
+
write_summary_json=True,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
print(_format_table(report))
|
|
658
|
+
print()
|
|
659
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
660
|
+
|
|
661
|
+
if args.gallery:
|
|
662
|
+
write_gallery(args.gallery, report)
|
|
663
|
+
print(f"[trenchfoot] Wrote gallery to {args.gallery}", file=sys.stderr)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
if __name__ == "__main__":
|
|
667
|
+
main()
|