trenchfoot 0.2.2__tar.gz → 0.2.4__tar.gz

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.

Files changed (87) hide show
  1. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/CHANGELOG.md +17 -0
  2. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/PKG-INFO +1 -1
  3. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/trench_scene_generator_v3.py +75 -27
  4. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/pyproject.toml +1 -1
  5. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/tests/test_trenchfoot_generation.py +112 -0
  6. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/uv.lock +1 -1
  7. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/.env +0 -0
  8. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/.github/workflows/ci.yml +0 -0
  9. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/.github_token.env +0 -0
  10. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/CLAUDE.md +0 -0
  11. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/LICENSE +0 -0
  12. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/README.md +0 -0
  13. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/STATUS.md +0 -0
  14. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/docs/scenario_gallery.md +0 -0
  15. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/__init__.py +0 -0
  16. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/Dockerfile +0 -0
  17. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/README.md +0 -0
  18. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/__init__.py +0 -0
  19. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/generate_scenarios.py +0 -0
  20. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/gmsh_sloped_trench_mesher.py +0 -0
  21. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/plot_mesh.py +0 -0
  22. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/render_colors.py +0 -0
  23. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/meshes/trench_scene_culled.obj +0 -0
  24. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/metrics.json +0 -0
  25. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/point_clouds/culled/resolution0p050.pth +0 -0
  26. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/point_clouds/full/resolution0p050.pth +0 -0
  27. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview.png +0 -0
  28. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_oblique.png +0 -0
  29. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_side.png +0 -0
  30. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_top.png +0 -0
  31. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/scene.json +0 -0
  32. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/trench_scene.obj +0 -0
  33. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S01_straight_vwalls/volumetric/trench_volume.msh +0 -0
  34. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/meshes/trench_scene_culled.obj +0 -0
  35. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/metrics.json +0 -0
  36. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/culled/resolution0p050.pth +0 -0
  37. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/full/resolution0p050.pth +0 -0
  38. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview.png +0 -0
  39. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_oblique.png +0 -0
  40. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_side.png +0 -0
  41. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_top.png +0 -0
  42. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/scene.json +0 -0
  43. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/trench_scene.obj +0 -0
  44. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/volumetric/trench_volume.msh +0 -0
  45. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/meshes/trench_scene_culled.obj +0 -0
  46. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/metrics.json +0 -0
  47. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/culled/resolution0p050.pth +0 -0
  48. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/full/resolution0p050.pth +0 -0
  49. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview.png +0 -0
  50. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_oblique.png +0 -0
  51. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_side.png +0 -0
  52. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_top.png +0 -0
  53. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/scene.json +0 -0
  54. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/trench_scene.obj +0 -0
  55. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/volumetric/trench_volume.msh +0 -0
  56. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/meshes/trench_scene_culled.obj +0 -0
  57. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/metrics.json +0 -0
  58. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview.png +0 -0
  59. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_oblique.png +0 -0
  60. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_side.png +0 -0
  61. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_top.png +0 -0
  62. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/scene.json +0 -0
  63. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/trench_scene.obj +0 -0
  64. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/volumetric/trench_volume.msh +0 -0
  65. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/metrics.json +0 -0
  66. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_oblique.png +0 -0
  67. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_side.png +0 -0
  68. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_top.png +0 -0
  69. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/scene.json +0 -0
  70. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/trench_scene.obj +0 -0
  71. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S05_wide_slope_pair/volumetric/trench_volume.msh +0 -0
  72. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/metrics.json +0 -0
  73. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_oblique.png +0 -0
  74. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_side.png +0 -0
  75. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_top.png +0 -0
  76. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/scene.json +0 -0
  77. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/trench_scene.obj +0 -0
  78. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/volumetric/trench_volume.msh +0 -0
  79. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/metrics.json +0 -0
  80. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/preview_oblique.png +0 -0
  81. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/preview_side.png +0 -0
  82. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/preview_top.png +0 -0
  83. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/scene.json +0 -0
  84. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/trench_scene.obj +0 -0
  85. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/S07_circular_well/volumetric/trench_volume.msh +0 -0
  86. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scenarios/SUMMARY.json +0 -0
  87. {trenchfoot-0.2.2 → trenchfoot-0.2.4}/packages/trenchfoot/scene_spec_example.json +0 -0
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.4] - 2025-12-24
4
+
5
+ ### Added
6
+ - **Ground end caps**: For open-path trenches, the ground surface now extends past the trench endpoints with semicircular caps, providing a buffer of ground at each end instead of terminating abruptly.
7
+
8
+ ## [0.2.3] - 2025-12-20
9
+
10
+ ### Fixed
11
+ - **Closed path offset direction bug**: `_offset_closed_polyline` was using CCW rotation for normals, causing positive offsets to go inward instead of outward. Fixed to use CW rotation for correct outward-pointing normals on CCW polygons.
12
+ - **Circular well ground surface**: Removed incorrect center island from closed path ground surfaces. For circular wells, only the outer ground ring exists, leaving the trench opening completely open.
13
+
14
+ ### Added
15
+ - **Open-topped trench tests**: New tests to verify trenches are truly open (no geometry covering the trench opening):
16
+ - `test_trench_opening_is_open_for_straight_trench`
17
+ - `test_circular_well_trench_opening_is_open`
18
+ - `test_ground_surface_annular_structure`
19
+
3
20
  ## [0.2.2] - 2025-12-20
4
21
 
5
22
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trenchfoot
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Synthetic trench scenario generator bundle (surfaces + volumetrics).
5
5
  Author: Liam Moore
6
6
  License-File: LICENSE
@@ -128,6 +128,7 @@ def _offset_closed_polyline(path: List[Tuple[float,float]], offset: float) -> Li
128
128
  raise ValueError("Closed polyline needs at least 3 points")
129
129
 
130
130
  # Compute tangents treating path as closed loop
131
+ # For CCW-oriented polygons, use CW rotation to get outward-pointing normals
131
132
  tangents = []
132
133
  normals = []
133
134
  for i in range(n):
@@ -135,7 +136,7 @@ def _offset_closed_polyline(path: List[Tuple[float,float]], offset: float) -> Li
135
136
  if np.linalg.norm(t) < 1e-12:
136
137
  t = np.array([1.0, 0.0])
137
138
  tangents.append(t)
138
- normals.append(_rotate_ccw(t))
139
+ normals.append(_rotate_cw(t)) # CW rotation gives outward normal for CCW polygon
139
140
 
140
141
  # Compute offset points with proper miter at each vertex
141
142
  offset_pts = []
@@ -546,6 +547,64 @@ def make_sphere(center: np.ndarray, radius: float, n_theta: int=48, n_phi: int=2
546
547
  def _ring_from_LR(L: List[np.ndarray], R: List[np.ndarray]) -> np.ndarray:
547
548
  return np.array(L + list(R[::-1]), float)
548
549
 
550
+
551
+ def _ring_from_LR_with_end_caps(
552
+ L: List[np.ndarray], R: List[np.ndarray],
553
+ path_xy: List[Tuple[float, float]], end_extension: float, n_cap_points: int = 5
554
+ ) -> np.ndarray:
555
+ """Create a ring from L/R offsets with semicircular end caps.
556
+
557
+ Extends the outer boundary past the path endpoints to create ground buffer
558
+ at the ends of open trenches.
559
+ """
560
+ P = np.array(path_xy, float)
561
+
562
+ # Get tangent directions at endpoints
563
+ t_start = _normalize(P[1] - P[0])
564
+ t_end = _normalize(P[-1] - P[-2])
565
+
566
+ # Start cap: semicircle from R[0] around to L[0], extending in -t_start direction
567
+ # Center of cap is at path start, offset backward by end_extension
568
+ cap_center_start = P[0] - end_extension * t_start
569
+ # Radius is distance from center to L[0] or R[0]
570
+ radius_start = np.linalg.norm(np.array(L[0]) - cap_center_start)
571
+
572
+ # Generate semicircle points from R[0] to L[0] going around the back
573
+ # R[0] is at angle theta_r, L[0] is at angle theta_l
574
+ # We go from theta_r counterclockwise to theta_l (the long way around the back)
575
+ vec_r = np.array(R[0]) - cap_center_start
576
+ vec_l = np.array(L[0]) - cap_center_start
577
+ theta_r = np.arctan2(vec_r[1], vec_r[0])
578
+ theta_l = np.arctan2(vec_l[1], vec_l[0])
579
+
580
+ # Ensure we go the "back" way (counterclockwise from R to L)
581
+ if theta_l > theta_r:
582
+ theta_l -= 2 * np.pi
583
+
584
+ angles_start = np.linspace(theta_r, theta_l, n_cap_points + 2)[1:-1] # Exclude endpoints (already have R[0], L[0])
585
+ cap_start = [cap_center_start + radius_start * np.array([np.cos(a), np.sin(a)]) for a in angles_start]
586
+
587
+ # End cap: semicircle from L[-1] around to R[-1], extending in +t_end direction
588
+ cap_center_end = P[-1] + end_extension * t_end
589
+ radius_end = np.linalg.norm(np.array(L[-1]) - cap_center_end)
590
+
591
+ vec_l_end = np.array(L[-1]) - cap_center_end
592
+ vec_r_end = np.array(R[-1]) - cap_center_end
593
+ theta_l_end = np.arctan2(vec_l_end[1], vec_l_end[0])
594
+ theta_r_end = np.arctan2(vec_r_end[1], vec_r_end[0])
595
+
596
+ # Go counterclockwise from L[-1] to R[-1] (the back way)
597
+ if theta_r_end > theta_l_end:
598
+ theta_r_end -= 2 * np.pi
599
+
600
+ angles_end = np.linspace(theta_l_end, theta_r_end, n_cap_points + 2)[1:-1]
601
+ cap_end = [cap_center_end + radius_end * np.array([np.cos(a), np.sin(a)]) for a in angles_end]
602
+
603
+ # Build the full ring: L + end_cap + R_reversed + start_cap
604
+ ring_points = list(L) + cap_end + list(R[::-1]) + cap_start
605
+
606
+ return np.array(ring_points, float)
607
+
549
608
  def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: float, depth: float, wall_slope: float, ground) -> Tuple[Dict,str,str,dict]:
550
609
  # Build top and bottom rings by offsetting centerline
551
610
  half_top = width_top/2.0
@@ -763,8 +822,8 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
763
822
  the trench opening as an open hole. This creates a natural shape that
764
823
  hugs L-shaped, U-shaped, and curved trenches.
765
824
 
766
- For closed paths (like circles), also creates a center island inside
767
- the inner edge of the annular trench.
825
+ For closed paths (like circles), the ground surface is a ring around the
826
+ outer edge of the trench, with the trench opening left completely open.
768
827
  """
769
828
  half_top = width_top / 2.0
770
829
  m = float(max(0.5, ground.size_margin))
@@ -772,47 +831,36 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
772
831
  is_closed = _is_path_closed(path_xy)
773
832
 
774
833
  if is_closed:
775
- # For closed paths, we have:
776
- # - Outer ground: annulus from ground_outer to trench_outer
777
- # - Center island: filled polygon inside trench_inner
834
+ # For closed paths (like circular wells), the ground is an annulus
835
+ # from the outer ground boundary to the outer edge of the trench opening.
836
+ # The center (inside the trench) is left completely open.
778
837
 
779
- # Trench boundaries
838
+ # Trench outer boundary (edge of trench opening)
780
839
  trench_outer = np.array(_offset_closed_polyline(path_xy, half_top), float)
781
- trench_inner = np.array(_offset_closed_polyline(path_xy, -half_top), float)
782
840
 
783
- # Ground outer boundary
841
+ # Ground outer boundary (edge of ground surface)
784
842
  ground_outer = np.array(_offset_closed_polyline(path_xy, half_top + m), float)
785
843
 
786
844
  # Ensure proper orientations
787
845
  trench_outer = _ensure_ccw(trench_outer)
788
- trench_inner = _ensure_ccw(trench_inner)
789
846
  ground_outer = _ensure_ccw(ground_outer)
790
847
 
791
- # Outer ground annulus
792
- outer_combined_xy, outer_tris = _triangulate_annulus(ground_outer, trench_outer)
793
- outer_Vg = np.array([[x, y, gfun(x, y)] for (x, y) in outer_combined_xy], float)
794
-
795
- # Center island (simple filled polygon)
796
- center_tris = _ear_clipping_triangulation(trench_inner)
797
- center_Vg = np.array([[x, y, gfun(x, y)] for (x, y) in trench_inner], float)
798
-
799
- # Combine into single ground surface
800
- n_outer_verts = len(outer_Vg)
801
- center_tris_offset = center_tris + n_outer_verts
802
-
803
- Vg = np.vstack([outer_Vg, center_Vg])
804
- tris = np.vstack([outer_tris, center_tris_offset])
848
+ # Ground annulus: from ground_outer to trench_outer
849
+ combined_xy, tris = _triangulate_annulus(ground_outer, trench_outer)
850
+ Vg = np.array([[x, y, gfun(x, y)] for (x, y) in combined_xy], float)
805
851
 
806
852
  return {"ground_surface": (Vg, tris)}
807
853
  else:
808
- # Original logic for open paths
854
+ # Open paths: ground forms annulus with end caps extending past trench endpoints
809
855
  # Inner boundary: the trench opening (same as trench top ring)
810
856
  L_inner, R_inner = _offset_polyline(path_xy, half_top)
811
857
  inner_ring = _ensure_ccw(_ring_from_LR(L_inner, R_inner))
812
858
 
813
- # Outer boundary: offset by additional margin
859
+ # Outer boundary: offset by additional margin, with semicircular end caps
814
860
  L_outer, R_outer = _offset_polyline(path_xy, half_top + m)
815
- outer_ring = _ensure_ccw(_ring_from_LR(L_outer, R_outer))
861
+ outer_ring = _ensure_ccw(_ring_from_LR_with_end_caps(
862
+ L_outer, R_outer, path_xy, end_extension=m
863
+ ))
816
864
 
817
865
  # Triangulate the annular region (leaves hole open)
818
866
  combined_xy, tris = _triangulate_annulus(outer_ring, inner_ring)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "trenchfoot"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Synthetic trench scenario generator bundle (surfaces + volumetrics)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -372,3 +372,115 @@ def test_circular_well_generates_surface(tmp_path):
372
372
 
373
373
  # Verify it has expected pipe count from scenario summary
374
374
  assert summary.object_counts["pipes"] >= 4, "Should have at least 4 pipes"
375
+
376
+
377
+ def test_trench_opening_is_open_for_straight_trench():
378
+ """Verify straight trench has open top with no geometry covering the opening."""
379
+ import numpy as np
380
+
381
+ spec = scene_spec_from_dict(_minimal_spec_dict())
382
+ result = generate_surface_mesh(spec, make_preview=False)
383
+
384
+ # Get ground surface vertices at z=0
385
+ V_ground, F_ground = result.groups["ground_surface"]
386
+
387
+ # Ground surface should be an annulus (ring) with the trench opening as a hole.
388
+ # Check that no face covers the center region (trench opening).
389
+ # The trench in _minimal_spec_dict has width=1.0 centered on y=0,
390
+ # so the opening is roughly y in [-0.5, 0.5].
391
+
392
+ for face in F_ground:
393
+ verts = V_ground[face]
394
+ centroid = np.mean(verts, axis=0)
395
+ # Centroid should NOT be inside the trench opening region (y between -0.5 and 0.5)
396
+ y_coords = verts[:, 1]
397
+ # If all vertices are inside trench y-range, this face is covering the opening
398
+ if np.all(np.abs(y_coords) < 0.45): # small margin
399
+ pytest.fail(
400
+ f"Found face covering trench opening: centroid={centroid}, y_coords={y_coords}"
401
+ )
402
+
403
+
404
+ def test_circular_well_trench_opening_is_open():
405
+ """Verify S07 circular well has open center with no lid geometry."""
406
+ import numpy as np
407
+
408
+ scenarios = default_scenarios()
409
+ s07 = next((s for s in scenarios if s.name == "S07_circular_well"), None)
410
+ if s07 is None:
411
+ pytest.skip("S07_circular_well not available")
412
+
413
+ spec = scene_spec_from_dict(s07.spec)
414
+ result = generate_surface_mesh(spec, make_preview=False)
415
+
416
+ # Ground surface should be an annulus around the outer edge of the circular trench,
417
+ # with the center (trench opening) completely open.
418
+ V_ground, F_ground = result.groups["ground_surface"]
419
+
420
+ # The circular well has inner radius ~0.5m and outer radius ~2.5m from center.
421
+ # Ground vertices should all be at radius > outer_trench_edge (about 2.0-2.5m)
422
+ # or at the outer_trench_edge itself as the inner boundary.
423
+
424
+ # Calculate radius of each ground vertex from center (0,0)
425
+ radii = np.sqrt(V_ground[:, 0] ** 2 + V_ground[:, 1] ** 2)
426
+
427
+ # The minimum radius should be at the trench outer edge (~2.5m), not near center
428
+ min_radius = np.min(radii)
429
+ assert min_radius > 1.0, (
430
+ f"Ground surface vertices extend too close to center (min_radius={min_radius:.3f}m). "
431
+ f"For an open circular well, ground should only surround the outer trench edge."
432
+ )
433
+
434
+ # No face should have centroid inside the trench opening (radius < ~2.0m)
435
+ for face in F_ground:
436
+ face_verts = V_ground[face]
437
+ centroid = np.mean(face_verts, axis=0)
438
+ centroid_radius = np.sqrt(centroid[0] ** 2 + centroid[1] ** 2)
439
+ if centroid_radius < 1.5: # Well inside the trench
440
+ pytest.fail(
441
+ f"Found ground face covering trench opening: centroid radius={centroid_radius:.3f}m"
442
+ )
443
+
444
+
445
+ def test_ground_surface_annular_structure():
446
+ """Verify ground surface properly forms an annulus with the trench as a hole."""
447
+ import numpy as np
448
+
449
+ # Test with L-shaped trench to ensure annular structure works for complex shapes
450
+ spec_dict = {
451
+ "path_xy": [[0.0, 0.0], [3.0, 0.0], [3.0, 2.0]],
452
+ "width": 0.8,
453
+ "depth": 0.6,
454
+ "wall_slope": 0.0,
455
+ "ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 1.0},
456
+ "pipes": [],
457
+ "boxes": [],
458
+ "spheres": [],
459
+ "noise": {"enable": False},
460
+ }
461
+ spec = scene_spec_from_dict(spec_dict)
462
+ result = generate_surface_mesh(spec, make_preview=False)
463
+
464
+ V_ground, F_ground = result.groups["ground_surface"]
465
+
466
+ # All ground vertices should be at z=0 (ground elevation)
467
+ assert np.allclose(V_ground[:, 2], 0.0), "Ground surface should be at z=0"
468
+
469
+ # Ground should have both inner vertices (near trench edge) and outer vertices
470
+ # The trench edge is at offset half_width = 0.4m from path
471
+ # Ground outer edge is at half_width + margin = 0.4 + 1.0 = 1.4m from path
472
+
473
+ # Check that there are vertices at different distances from the origin
474
+ # (indicating annular structure with inner and outer boundaries)
475
+ x_coords = V_ground[:, 0]
476
+ y_coords = V_ground[:, 1]
477
+
478
+ # The L-shaped path goes from (0,0) to (3,0) to (3,2)
479
+ # Inner boundary should be close to path, outer boundary further away
480
+ # For a proper annulus, we should have spread in both x and y
481
+
482
+ x_range = np.max(x_coords) - np.min(x_coords)
483
+ y_range = np.max(y_coords) - np.min(y_coords)
484
+
485
+ assert x_range > 2.0, f"Ground should span significant x range: {x_range:.2f}"
486
+ assert y_range > 2.0, f"Ground should span significant y range: {y_range:.2f}"
@@ -501,7 +501,7 @@ wheels = [
501
501
 
502
502
  [[package]]
503
503
  name = "trenchfoot"
504
- version = "0.2.1"
504
+ version = "0.2.3"
505
505
  source = { editable = "." }
506
506
  dependencies = [
507
507
  { name = "numpy" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes