witwin 0.3.0__py3-none-win_amd64.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.
@@ -0,0 +1,53 @@
1
+ """Witwin Core - Shared geometry, material, and structure contracts."""
2
+
3
+ __version__ = "0.3.0"
4
+
5
+ from .material import (
6
+ FrequencyMaterialSample,
7
+ Material,
8
+ MaterialCapabilities,
9
+ MaterialSpec,
10
+ StaticMaterialSample,
11
+ Structure,
12
+ )
13
+ from .geometry import (
14
+ Box,
15
+ Cone,
16
+ Cylinder,
17
+ Ellipsoid,
18
+ Geometry,
19
+ GeometryBase,
20
+ GeometrySpec,
21
+ HollowBox,
22
+ Mesh,
23
+ Prism,
24
+ Pyramid,
25
+ Sphere,
26
+ Torus,
27
+ )
28
+ from .math import (
29
+ quat_from_euler,
30
+ quat_identity,
31
+ quat_multiply,
32
+ quat_to_rotation_matrix,
33
+ quat_to_rotation_matrix_np,
34
+ )
35
+ __all__ = [
36
+ "GeometryBase",
37
+ "GeometrySpec",
38
+ "Box", "Sphere", "Cylinder", "Cone", "Ellipsoid",
39
+ "Pyramid", "Prism", "Torus", "HollowBox",
40
+ "Material",
41
+ "MaterialCapabilities",
42
+ "MaterialSpec",
43
+ "Mesh",
44
+ "FrequencyMaterialSample",
45
+ "Geometry",
46
+ "StaticMaterialSample",
47
+ "Structure",
48
+ "quat_from_euler",
49
+ "quat_identity",
50
+ "quat_multiply",
51
+ "quat_to_rotation_matrix",
52
+ "quat_to_rotation_matrix_np",
53
+ ]
@@ -0,0 +1,23 @@
1
+ """Shared differentiable geometry package."""
2
+
3
+ from .base import GeometryBase, GeometrySpec
4
+ from .mesh import Mesh
5
+ from .primitives import Box, Cone, Cylinder, Ellipsoid, HollowBox, Prism, Pyramid, Sphere, Torus
6
+
7
+ Geometry = Box | Sphere | Cylinder | Cone | Ellipsoid | Pyramid | Prism | Torus | HollowBox | Mesh
8
+
9
+ __all__ = [
10
+ "GeometryBase",
11
+ "GeometrySpec",
12
+ "Box",
13
+ "Sphere",
14
+ "Cylinder",
15
+ "Cone",
16
+ "Ellipsoid",
17
+ "Pyramid",
18
+ "Prism",
19
+ "Torus",
20
+ "HollowBox",
21
+ "Mesh",
22
+ "Geometry",
23
+ ]
@@ -0,0 +1,211 @@
1
+ """Shared geometry base classes and tensor helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+ import numpy as np
8
+ import torch
9
+
10
+ from ..math import (
11
+ quat_from_euler,
12
+ quat_identity,
13
+ quat_to_rotation_matrix,
14
+ quat_to_rotation_matrix_np,
15
+ )
16
+
17
+
18
+ def _as_position(value, *, device=None) -> torch.Tensor:
19
+ if isinstance(value, torch.Tensor):
20
+ return value.to(device=device, dtype=torch.float32)
21
+ return torch.tensor([float(v) for v in value], dtype=torch.float32, device=device)
22
+
23
+
24
+ def _as_rotation(value, *, device=None) -> torch.Tensor:
25
+ """Accept quaternion (4,), Euler tuple (3,), or None -> identity."""
26
+ if value is None:
27
+ return quat_identity(device=device)
28
+ if isinstance(value, torch.Tensor):
29
+ if value.shape == (4,):
30
+ return value.to(device=device, dtype=torch.float32)
31
+ if value.shape == (3,):
32
+ return quat_from_euler(value[0], value[1], value[2], device=device)
33
+ raise ValueError(f"rotation tensor must be shape (4,) quaternion or (3,) Euler, got {value.shape}")
34
+ seq = list(value)
35
+ if len(seq) == 4:
36
+ return torch.tensor([float(v) for v in seq], dtype=torch.float32, device=device)
37
+ if len(seq) == 3:
38
+ return quat_from_euler(float(seq[0]), float(seq[1]), float(seq[2]), device=device)
39
+ raise ValueError(f"rotation must have 3 (Euler) or 4 (quaternion) elements, got {len(seq)}")
40
+
41
+
42
+ def _as_scalar(value, *, device=None) -> torch.Tensor:
43
+ if isinstance(value, torch.Tensor):
44
+ return value.to(device=device, dtype=torch.float32).reshape(())
45
+ return torch.tensor(float(value), dtype=torch.float32, device=device)
46
+
47
+
48
+ def _as_vec3(value, *, device=None) -> torch.Tensor:
49
+ if isinstance(value, torch.Tensor):
50
+ return value.to(device=device, dtype=torch.float32)
51
+ if isinstance(value, (int, float)):
52
+ scalar = float(value)
53
+ return torch.tensor([scalar, scalar, scalar], dtype=torch.float32, device=device)
54
+ return torch.tensor([float(v) for v in value], dtype=torch.float32, device=device)
55
+
56
+
57
+ def _rotate_coords(xx, yy, zz, position: torch.Tensor, rotation: torch.Tensor):
58
+ """Translate + inverse-rotate 3D grid coordinates into local frame."""
59
+ dx = xx - position[0]
60
+ dy = yy - position[1]
61
+ dz = zz - position[2]
62
+ rotation_matrix = quat_to_rotation_matrix(rotation)
63
+ rotation_inverse = rotation_matrix.T
64
+ dx_rot = rotation_inverse[0, 0] * dx + rotation_inverse[0, 1] * dy + rotation_inverse[0, 2] * dz
65
+ dy_rot = rotation_inverse[1, 0] * dx + rotation_inverse[1, 1] * dy + rotation_inverse[1, 2] * dz
66
+ dz_rot = rotation_inverse[2, 0] * dx + rotation_inverse[2, 1] * dy + rotation_inverse[2, 2] * dz
67
+ return dx_rot, dy_rot, dz_rot
68
+
69
+
70
+ def _axial_split(dx, dy, dz, axis: str):
71
+ """Return (axial, radial_a, radial_b) for a named axis."""
72
+ if axis == "z":
73
+ return dz, dx, dy
74
+ if axis == "y":
75
+ return dy, dx, dz
76
+ return dx, dy, dz
77
+
78
+
79
+ def _apply_rotation_np(vertices: np.ndarray, rotation_matrix: np.ndarray) -> np.ndarray:
80
+ """Apply a (3,3) rotation matrix to (N,3) vertices."""
81
+ return (vertices @ rotation_matrix.T).astype(np.float32)
82
+
83
+
84
+ def _constant_tensor(data, *, device, dtype=torch.float32) -> torch.Tensor:
85
+ return torch.tensor(data, dtype=dtype, device=device)
86
+
87
+
88
+ def _remap_axis_torch(vertices: torch.Tensor, axis: str) -> torch.Tensor:
89
+ """Remap local z-up coordinates to the requested axis."""
90
+ if axis == "y":
91
+ return vertices[:, [0, 2, 1]]
92
+ if axis == "x":
93
+ return vertices[:, [2, 1, 0]]
94
+ return vertices
95
+
96
+
97
+ def _coordinate_spacing(coord: torch.Tensor, dim: int) -> torch.Tensor | None:
98
+ if not isinstance(coord, torch.Tensor) or coord.ndim <= dim or coord.shape[dim] <= 1:
99
+ return None
100
+ delta = torch.diff(coord, dim=dim).abs()
101
+ positive = delta[delta > 0]
102
+ if positive.numel() == 0:
103
+ return None
104
+ return positive.min()
105
+
106
+
107
+ def _default_beta_from_coords(xx, yy, zz, *, reference: torch.Tensor) -> torch.Tensor:
108
+ spacings = [
109
+ spacing
110
+ for spacing in (
111
+ _coordinate_spacing(xx, 0),
112
+ _coordinate_spacing(yy, 1),
113
+ _coordinate_spacing(zz, 2),
114
+ )
115
+ if spacing is not None
116
+ ]
117
+ if not spacings:
118
+ return reference.new_tensor(1.0e-3)
119
+
120
+ spacing = spacings[0].to(device=reference.device, dtype=reference.dtype)
121
+ for candidate in spacings[1:]:
122
+ spacing = torch.minimum(spacing, candidate.to(device=reference.device, dtype=reference.dtype))
123
+ return spacing * 0.05
124
+
125
+
126
+ def _resolve_beta(beta, xx, yy, zz, *, reference: torch.Tensor) -> torch.Tensor:
127
+ if beta is None:
128
+ resolved = _default_beta_from_coords(xx, yy, zz, reference=reference)
129
+ else:
130
+ resolved = torch.as_tensor(beta, device=reference.device, dtype=reference.dtype)
131
+ return torch.clamp(resolved, min=torch.finfo(reference.dtype).eps)
132
+
133
+
134
+ def occupancy_from_signed_distance(signed_distance: torch.Tensor, *, xx, yy, zz, offset=0.0, beta=None) -> torch.Tensor:
135
+ offset_tensor = torch.as_tensor(offset, device=signed_distance.device, dtype=signed_distance.dtype)
136
+ beta_tensor = _resolve_beta(beta, xx, yy, zz, reference=signed_distance)
137
+ occupancy = 0.5 * (1.0 - torch.tanh((signed_distance - offset_tensor) / beta_tensor))
138
+ return occupancy.clamp(0.0, 1.0)
139
+
140
+
141
+ @runtime_checkable
142
+ class GeometrySpec(Protocol):
143
+ """Minimal geometry contract consumed by solver-specific scenes."""
144
+
145
+ kind: str
146
+
147
+ def to_mesh(self, segments: int = 16) -> tuple:
148
+ ...
149
+
150
+
151
+ class GeometryBase:
152
+ """Base class for all geometry primitives."""
153
+
154
+ kind: str = "base"
155
+
156
+ def __init__(self, position=(0, 0, 0), rotation=None, *, device=None):
157
+ self.position: torch.Tensor = _as_position(position, device=device)
158
+ self.rotation: torch.Tensor = _as_rotation(rotation, device=device)
159
+
160
+ @property
161
+ def device(self):
162
+ return self.position.device
163
+
164
+ def _local_coords(self, xx, yy, zz):
165
+ return _rotate_coords(xx, yy, zz, self.position, self.rotation)
166
+
167
+ def _rotation_matrix_np(self) -> np.ndarray:
168
+ return quat_to_rotation_matrix_np(self.rotation)
169
+
170
+ @staticmethod
171
+ def _validate_axis(axis: str) -> str:
172
+ axis_name = str(axis).lower()
173
+ if axis_name not in {"x", "y", "z"}:
174
+ raise ValueError("axis must be 'x', 'y', or 'z'.")
175
+ return axis_name
176
+
177
+ def _apply_axis_transform(self, vertices: torch.Tensor, axis: str) -> torch.Tensor:
178
+ return _remap_axis_torch(vertices, axis)
179
+
180
+ def _transform_mesh_verts(self, vertices: torch.Tensor) -> torch.Tensor:
181
+ if not isinstance(vertices, torch.Tensor):
182
+ vertices = torch.as_tensor(vertices, dtype=torch.float32, device=self.device)
183
+ rotation_matrix = quat_to_rotation_matrix(self.rotation.to(device=vertices.device, dtype=vertices.dtype))
184
+ position = self.position.to(device=vertices.device, dtype=vertices.dtype)
185
+ return vertices @ rotation_matrix.T + position
186
+
187
+ def with_material(self, material, **kwargs):
188
+ from ..material import Structure
189
+
190
+ return Structure(geometry=self, material=material, **kwargs)
191
+
192
+ def signed_distance(self, xx, yy, zz):
193
+ raise NotImplementedError
194
+
195
+ def to_mask(self, xx, yy, zz, offset=0.0, beta=None):
196
+ signed_distance = self.signed_distance(xx, yy, zz)
197
+ return occupancy_from_signed_distance(
198
+ signed_distance,
199
+ xx=xx,
200
+ yy=yy,
201
+ zz=zz,
202
+ offset=offset,
203
+ beta=beta,
204
+ )
205
+
206
+ def to_mesh(self, segments=16):
207
+ raise NotImplementedError
208
+
209
+ def __repr__(self):
210
+ fields = ", ".join(f"{key}={value}" for key, value in self.__dict__.items() if not key.startswith("_"))
211
+ return f"{type(self).__name__}({fields})"
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from .backend import get_compiled_extension, get_native_mesh_sdf_module, is_available
4
+
5
+ __all__ = [
6
+ "get_compiled_extension",
7
+ "get_native_mesh_sdf_module",
8
+ "is_available",
9
+ ]
@@ -0,0 +1,177 @@
1
+ """Loader for the native CUDA mesh-SDF extension.
2
+
3
+ Exposes a module object that is call-compatible with the former JIT-kernel
4
+ module: ``module.<kernelName>(**kwargs).launchRaw(blockSize=..., gridSize=...)``.
5
+ The kernels compute their own launch grid, so ``launchRaw`` ignores the block /
6
+ grid hints and simply dispatches the compiled function.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from typing import Any
13
+
14
+ import torch
15
+
16
+
17
+ _COMPILED_EXTENSION: Any | None = None
18
+
19
+
20
+ def is_available() -> bool:
21
+ return bool(torch.cuda.is_available())
22
+
23
+
24
+ def get_compiled_extension(*, verbose: bool = False) -> Any:
25
+ global _COMPILED_EXTENSION
26
+ if _COMPILED_EXTENSION is None:
27
+ from .build import build_extension
28
+
29
+ _COMPILED_EXTENSION = build_extension(verbose=verbose)
30
+ return _COMPILED_EXTENSION
31
+
32
+
33
+ def _query_mesh_unsigned_distance(*, triangles, points, unsignedDistance, closestTriangleIndex):
34
+ get_compiled_extension().query_mesh_unsigned_distance(
35
+ triangles, points, unsignedDistance, closestTriangleIndex
36
+ )
37
+
38
+
39
+ def _query_mesh_distance_and_winding(*, triangles, points, unsignedDistance, windingAngle, closestTriangleIndex):
40
+ get_compiled_extension().query_mesh_distance_and_winding(
41
+ triangles, points, unsignedDistance, windingAngle, closestTriangleIndex
42
+ )
43
+
44
+
45
+ def _query_mesh_unsigned_distance_bvh(
46
+ *,
47
+ triangles,
48
+ points,
49
+ nodeBBoxMin,
50
+ nodeBBoxMax,
51
+ nodeLeft,
52
+ nodeRight,
53
+ nodeStart,
54
+ nodeCount,
55
+ triangleIndices,
56
+ unsignedDistance,
57
+ closestTriangleIndex,
58
+ ):
59
+ get_compiled_extension().query_mesh_unsigned_distance_bvh(
60
+ triangles,
61
+ points,
62
+ nodeBBoxMin,
63
+ nodeBBoxMax,
64
+ nodeLeft,
65
+ nodeRight,
66
+ nodeStart,
67
+ nodeCount,
68
+ triangleIndices,
69
+ unsignedDistance,
70
+ closestTriangleIndex,
71
+ )
72
+
73
+
74
+ def _query_mesh_parity_sign_bvh(
75
+ *,
76
+ triangles,
77
+ points,
78
+ nodeBBoxMin,
79
+ nodeBBoxMax,
80
+ nodeLeft,
81
+ nodeRight,
82
+ nodeStart,
83
+ nodeCount,
84
+ triangleIndices,
85
+ jitterScale,
86
+ inside,
87
+ ):
88
+ get_compiled_extension().query_mesh_parity_sign_bvh(
89
+ triangles,
90
+ points,
91
+ nodeBBoxMin,
92
+ nodeBBoxMax,
93
+ nodeLeft,
94
+ nodeRight,
95
+ nodeStart,
96
+ nodeCount,
97
+ triangleIndices,
98
+ float(jitterScale),
99
+ inside,
100
+ )
101
+
102
+
103
+ def _backward_mesh_unsigned_distance(
104
+ *, triangles, points, closestTriangleIndex, gradUnsignedDistance, gradTriangles, gradPoints
105
+ ):
106
+ get_compiled_extension().backward_mesh_unsigned_distance(
107
+ triangles, points, closestTriangleIndex, gradUnsignedDistance, gradTriangles, gradPoints
108
+ )
109
+
110
+
111
+ def _backward_mesh_distance_and_winding(
112
+ *,
113
+ triangles,
114
+ points,
115
+ closestTriangleIndex,
116
+ gradUnsignedDistance,
117
+ gradWindingAngle,
118
+ gradTriangles,
119
+ gradPoints,
120
+ ):
121
+ get_compiled_extension().backward_mesh_distance_and_winding(
122
+ triangles,
123
+ points,
124
+ closestTriangleIndex,
125
+ gradUnsignedDistance,
126
+ gradWindingAngle,
127
+ gradTriangles,
128
+ gradPoints,
129
+ )
130
+
131
+
132
+ _KERNELS: dict[str, Callable[..., None]] = {
133
+ "queryMeshUnsignedDistance": _query_mesh_unsigned_distance,
134
+ "queryMeshDistanceAndWinding": _query_mesh_distance_and_winding,
135
+ "queryMeshUnsignedDistanceBVH": _query_mesh_unsigned_distance_bvh,
136
+ "queryMeshParitySignBVH": _query_mesh_parity_sign_bvh,
137
+ "backwardMeshUnsignedDistance": _backward_mesh_unsigned_distance,
138
+ "backwardMeshDistanceAndWinding": _backward_mesh_distance_and_winding,
139
+ }
140
+
141
+
142
+ class _Launch:
143
+ __slots__ = ("_kernel", "_kwargs")
144
+
145
+ def __init__(self, kernel: Callable[..., None], kwargs: dict):
146
+ self._kernel = kernel
147
+ self._kwargs = kwargs
148
+
149
+ def launchRaw(self, *, blockSize=None, gridSize=None): # noqa: N802 - kernel launch entry point
150
+ del blockSize, gridSize
151
+ self._kernel(**self._kwargs)
152
+ return None
153
+
154
+
155
+ class NativeMeshSDFModule:
156
+ def __getattr__(self, name: str):
157
+ kernel = _KERNELS.get(name)
158
+ if kernel is None:
159
+ raise AttributeError(f"Native CUDA mesh SDF backend does not implement kernel {name!r}.")
160
+
161
+ def bind(**kwargs):
162
+ return _Launch(kernel, kwargs)
163
+
164
+ bind.__name__ = name
165
+ object.__setattr__(self, name, bind)
166
+ return bind
167
+
168
+
169
+ _NATIVE_MODULE = NativeMeshSDFModule()
170
+
171
+
172
+ def get_native_mesh_sdf_module(*, verbose: bool = False) -> NativeMeshSDFModule:
173
+ """Compile the extension (once) and return the launch-compatible module."""
174
+ if not is_available():
175
+ raise RuntimeError("Native CUDA mesh SDF backend requires torch.cuda.is_available() to be True.")
176
+ get_compiled_extension(verbose=verbose)
177
+ return _NATIVE_MODULE