Module curlew.utils.gwn
Generalized winding numbers and fault-geometry proximity helpers.
2-D: open polylines, sum of signed subtended angles (radians). 3-D: oriented open triangle soups, sum of signed solid angles (steradians; Jacobson et al. 2013).
Functions
def blended_tangency_polyline(x: torch.Tensor, verts: torch.Tensor, *, blend_power: float = 1.0)-
Expand source code
def blended_tangency_polyline( x: torch.Tensor, verts: torch.Tensor, *, blend_power: float = 1.0, ): """ Strike frame from a blend of segment tangents weighted by the subtended- angle magnitudes — the same angular visibility GWN already uses. ``blend_power`` sharpens the blend: ``1`` is the default soft corner rotation; larger values (e.g. ``4``) favour the dominant visible segment; very large values approximate closest-segment tangents. Returns closest point, blended unit tangent, blended distance. """ if blend_power <= 0: raise ValueError(f"blend_power must be > 0, got {blend_power}") angles, seg_tang, pa, pb = _polyline_segment_angles(x, verts) v, p = verts.detach(), x.detach() weights = angles.abs().pow(blend_power) w_sum = weights.sum(-1) tang = (weights.unsqueeze(-1) * seg_tang.unsqueeze(0)).sum(1) small = w_sum < 1e-10 if small.any(): i = weights.argmax(dim=1) tang[small] = seg_tang[i[small]] tang = F.normalize(tang, dim=-1, eps=1e-12) a, ab = v[:-1], v[1:] - v[:-1] t = ((p[:, None] - a) * ab).sum(-1) / (ab * ab).sum(-1).clamp_min(1e-12) cp_seg = a.unsqueeze(0) + t.clamp(0.0, 1.0).unsqueeze(-1) * ab.unsqueeze(0) dist_seg = (p[:, None, :] - cp_seg).norm(dim=-1) dist = (weights * dist_seg).sum(-1) / w_sum.clamp_min(1e-12) if small.any(): rows = torch.arange(len(p), device=p.device) dist[small] = dist_seg[rows[small], i[small]] cp = cp_seg[rows, 0].clone() cp[small] = cp_seg[rows[small], i[small]] else: cp = (weights.unsqueeze(-1) * cp_seg).sum(1) / w_sum.unsqueeze(-1).clamp_min(1e-12) return cp, tang, distStrike frame from a blend of segment tangents weighted by the subtended- angle magnitudes — the same angular visibility GWN already uses.
blend_powersharpens the blend:1is the default soft corner rotation; larger values (e.g.4) favour the dominant visible segment; very large values approximate closest-segment tangents.Returns closest point, blended unit tangent, blended distance.
def closest_point_mesh(x: torch.Tensor, verts: torch.Tensor, faces: torch.Tensor)-
Expand source code
def closest_point_mesh( x: torch.Tensor, verts: torch.Tensor, faces: torch.Tensor, ): """ Closest point, distance and unit face normal on an open triangle mesh. Returns ``(cp, dist, normal)`` with shapes ``(N, 3)``, ``(N,)``, ``(N, 3)``. """ v, p, f = verts.detach(), x.detach(), faces best_cp = None best_d = None best_n = None for fi in f: a, b, c = v[fi[0]], v[fi[1]], v[fi[2]] cp, d = _closest_point_triangle(p, a, b, c) fn = F.normalize(torch.linalg.cross(b - a, c - a), dim=-1, eps=1e-12) n = fn.unsqueeze(0).expand(len(p), -1) if best_d is None: best_cp, best_d, best_n = cp, d, n else: pick = d < best_d best_cp = torch.where(pick.unsqueeze(-1), cp, best_cp) best_d = torch.where(pick, d, best_d) best_n = torch.where(pick.unsqueeze(-1), n, best_n) return best_cp, best_d, best_nClosest point, distance and unit face normal on an open triangle mesh.
Returns
(cp, dist, normal)with shapes(N, 3),(N,),(N, 3). def closest_point_polyline(x: torch.Tensor, verts: torch.Tensor)-
Expand source code
def closest_point_polyline(x: torch.Tensor, verts: torch.Tensor): """Closest point, unit tangent and distance to an open polyline.""" v, p = verts.detach(), x.detach() a, ab = v[:-1], v[1:] - v[:-1] t = ((p[:, None] - a) * ab).sum(-1) / (ab * ab).sum(-1).clamp_min(1e-12) cp = a + t.clamp(0.0, 1.0)[..., None] * ab d = (p[:, None] - cp).norm(dim=-1) i = d.argmin(dim=1) rows = torch.arange(len(p), device=p.device) tang = F.normalize(ab[i], dim=-1) return cp[rows, i], tang, d[rows, i]Closest point, unit tangent and distance to an open polyline.
def gwn_mesh(x: torch.Tensor, verts: torch.Tensor, faces: torch.Tensor) ‑> torch.Tensor-
Expand source code
def gwn_mesh( x: torch.Tensor, verts: torch.Tensor, faces: torch.Tensor, ) -> torch.Tensor: """ Generalized winding number of an oriented open triangle mesh. Sum of signed solid angles (steradians) subtended by each face at query points ``x`` (N, 3). ``verts`` is (V, 3); ``faces`` is (F, 3) vertex indices. Mesh geometry is detached. """ if x.shape[-1] != 3 or verts.shape[-1] != 3: raise ValueError("gwn_mesh expects 3-D coordinates") if faces.ndim != 2 or faces.shape[1] != 3: raise ValueError("faces must be (F, 3)") v, p, f = verts.detach(), x.detach(), faces v0 = v[f[:, 0]] v1 = v[f[:, 1]] v2 = v[f[:, 2]] omega = _solid_angle_triangle( p[:, None, :], v0[None, :, :], v1[None, :, :], v2[None, :, :], ) return omega.sum(dim=-1)Generalized winding number of an oriented open triangle mesh.
Sum of signed solid angles (steradians) subtended by each face at query points
x(N, 3).vertsis (V, 3);facesis (F, 3) vertex indices. Mesh geometry is detached. def gwn_polyline(x: torch.Tensor, verts: torch.Tensor) ‑> torch.Tensor-
Expand source code
def gwn_polyline(x: torch.Tensor, verts: torch.Tensor) -> torch.Tensor: """ Generalized winding number (sum of signed subtended angles, radians) of an open polyline at query points ``x`` (N, 2). Trace geometry is detached. """ if verts.shape[-1] != 2: raise ValueError( "gwn_polyline expects 2-D traces; for 3-D faults use gwn_mesh on a " "triangle surface." ) angles, _, _, _ = _polyline_segment_angles(x, verts) return angles.sum(-1)Generalized winding number (sum of signed subtended angles, radians) of an open polyline at query points
x(N, 2). Trace geometry is detached. def gwn_ribbon_mesh(polyline: torch.Tensor, normals: torch.Tensor, *, half_width: float = 1.0) ‑> Tuple[torch.Tensor, torch.Tensor]-
Expand source code
def gwn_ribbon_mesh( polyline: torch.Tensor, normals: torch.Tensor, *, half_width: float = 1.0, ) -> Tuple[torch.Tensor, torch.Tensor]: """ One-sided ribbon mesh along an open 3-D polyline. ``normals`` (P, 3) defines the fault-plane orientation at each vertex. Returns ``(verts, faces)`` suitable for :func:`gwn_mesh`. ``half_width`` sets the extrusion; GWN magnitude scales with it, so treat as a shape parameter rather than a physical thickness. """ if polyline.ndim != 2 or polyline.shape[1] != 3: raise ValueError("polyline must be (P, 3)") if normals.shape != polyline.shape: raise ValueError("normals must match polyline shape (P, 3)") if half_width <= 0: raise ValueError("half_width must be > 0") v = polyline.detach() n = F.normalize(normals.detach(), dim=-1, eps=1e-12) offset = n * float(half_width) top = v + offset verts = torch.cat([v, top], dim=0) p = len(v) faces = [] for i in range(p - 1): faces.append([i, i + 1, p + i + 1]) faces.append([i, p + i + 1, p + i]) return verts, torch.tensor(faces, dtype=torch.long, device=v.device)One-sided ribbon mesh along an open 3-D polyline.
normals(P, 3) defines the fault-plane orientation at each vertex. Returns(verts, faces)suitable for :func:gwn_mesh().half_widthsets the extrusion; GWN magnitude scales with it, so treat as a shape parameter rather than a physical thickness. def mesh_boundary_vertices(faces: torch.Tensor) ‑> torch.Tensor-
Expand source code
def mesh_boundary_vertices(faces: torch.Tensor) -> torch.Tensor: """Vertex indices on the open boundary of a triangle mesh.""" counts = {} for tri in faces.tolist(): for i, j in ((0, 1), (1, 2), (2, 0)): key = (min(tri[i], tri[j]), max(tri[i], tri[j])) counts[key] = counts.get(key, 0) + 1 verts = set() for (i, j), c in counts.items(): if c == 1: verts.add(i) verts.add(j) if not verts: return faces.new_zeros(0) return torch.tensor(sorted(verts), dtype=faces.dtype, device=faces.device)Vertex indices on the open boundary of a triangle mesh.
def smoothstep(t: torch.Tensor) ‑> torch.Tensor-
Expand source code
def smoothstep(t: torch.Tensor) -> torch.Tensor: t = t.clamp(0.0, 1.0) return t * t * (3.0 - 2.0 * t) def smoothstep_scalar(s: float) ‑> float-
Expand source code
def smoothstep_scalar(s: float) -> float: s = min(max(s, 0.0), 1.0) return s * s * (3.0 - 2.0 * s)