lsdo-function-spaces 1.0.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.
- lsdo_function_spaces/__init__.py +64 -0
- lsdo_function_spaces/core/__init__.py +0 -0
- lsdo_function_spaces/core/function.py +1322 -0
- lsdo_function_spaces/core/function_set.py +1081 -0
- lsdo_function_spaces/core/function_set_space.py +379 -0
- lsdo_function_spaces/core/function_space.py +482 -0
- lsdo_function_spaces/core/operations/__init__.py +0 -0
- lsdo_function_spaces/core/operations/basic_ops.py +85 -0
- lsdo_function_spaces/core/operations/operations.py +5 -0
- lsdo_function_spaces/core/optimization.py +183 -0
- lsdo_function_spaces/core/spaces/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/b_spline_space.py +418 -0
- lsdo_function_spaces/core/spaces/conditional_space.py +65 -0
- lsdo_function_spaces/core/spaces/constant_space.py +57 -0
- lsdo_function_spaces/core/spaces/idw_space.py +271 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/__init__.py +0 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_csdl_custom_ops.py +420 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection.py +1022 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_non_differentiable.py +186 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_patch_projection_optimized.py +594 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/b_spline_space_new.py +6 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax.py +172 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_factory.py +382 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_jax_stencil.py +451 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy.py +249 -0
- lsdo_function_spaces/core/spaces/non_cython_bsplines/compute_basis_matrix_numpy_factory.py +391 -0
- lsdo_function_spaces/core/spaces/operation_space.py +64 -0
- lsdo_function_spaces/core/spaces/polynomial_space.py +79 -0
- lsdo_function_spaces/core/spaces/rbf_space.py +136 -0
- lsdo_function_spaces/core/spaces/tri_space.py +256 -0
- lsdo_function_spaces/utils/__init__.py +0 -0
- lsdo_function_spaces/utils/file_io.py +484 -0
- lsdo_function_spaces/utils/internal_utilities.py +11 -0
- lsdo_function_spaces/utils/plotting_functions.py +357 -0
- lsdo_function_spaces/utils/utility_functions.py +148 -0
- lsdo_function_spaces-1.0.0.dist-info/METADATA +189 -0
- lsdo_function_spaces-1.0.0.dist-info/RECORD +40 -0
- lsdo_function_spaces-1.0.0.dist-info/WHEEL +5 -0
- lsdo_function_spaces-1.0.0.dist-info/licenses/LICENSE.txt +165 -0
- lsdo_function_spaces-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
import numpy as np
|
|
4
|
+
from functools import partial
|
|
5
|
+
from jax import custom_jvp, custom_vjp
|
|
6
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_jax_stencil import evaluate_b_spline_jax
|
|
7
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_numpy import compute_basis_matrix_numpy
|
|
8
|
+
import csdl_alpha as csdl
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
jax.config.update("jax_enable_x64", True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@partial(jax.jit, static_argnames=("degrees", "knot_vectors"))
|
|
16
|
+
def compute_projection_residual(
|
|
17
|
+
point_in_space,
|
|
18
|
+
para_coords,
|
|
19
|
+
degrees,
|
|
20
|
+
coefficients,
|
|
21
|
+
knot_vectors,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Compute the residual of the projection of a point in space onto a B-spline surface.
|
|
25
|
+
|
|
26
|
+
Parameters:
|
|
27
|
+
-----------
|
|
28
|
+
point_in_space : jnp.ndarray, shape (d,)
|
|
29
|
+
The point in space to project.
|
|
30
|
+
para_coords : jnp.ndarray, shape (M, d)
|
|
31
|
+
Parameter coordinates for the B-spline surface.
|
|
32
|
+
degrees : tuple of int
|
|
33
|
+
Degrees of the B-spline in each dimension.
|
|
34
|
+
coefficients : jnp.ndarray, shape (N, num_phys_dims)
|
|
35
|
+
Coefficients of the B-spline basis functions.
|
|
36
|
+
knot_vectors : tuple of jnp.ndarray
|
|
37
|
+
Knot vectors for each dimension.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
--------
|
|
41
|
+
residual : jnp.ndarray, shape (M,)
|
|
42
|
+
The residuals of the projection.
|
|
43
|
+
"""
|
|
44
|
+
n_dims = len(degrees)
|
|
45
|
+
n_phys_dims = coefficients.shape[-1]
|
|
46
|
+
point_in_space = point_in_space.reshape(-1, n_phys_dims)
|
|
47
|
+
|
|
48
|
+
# Convert static, hashable knot_vectors (tuples of floats) to JAX arrays
|
|
49
|
+
knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
|
|
50
|
+
|
|
51
|
+
bsp_eval = evaluate_b_spline_jax(
|
|
52
|
+
us=para_coords,
|
|
53
|
+
degrees=degrees,
|
|
54
|
+
knot_vectors=knot_vectors,
|
|
55
|
+
coeffs=coefficients,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
surface_jacobian = [
|
|
59
|
+
evaluate_b_spline_jax(
|
|
60
|
+
us=para_coords,
|
|
61
|
+
degrees=degrees,
|
|
62
|
+
knot_vectors=knot_vectors,
|
|
63
|
+
coeffs=coefficients,
|
|
64
|
+
der_orders=tuple(
|
|
65
|
+
int(i == j) for j in range(n_dims)
|
|
66
|
+
)
|
|
67
|
+
) for i in range(n_dims)
|
|
68
|
+
]
|
|
69
|
+
surface_jacobian_array = jnp.array(surface_jacobian).squeeze().T
|
|
70
|
+
|
|
71
|
+
diff = bsp_eval - point_in_space
|
|
72
|
+
|
|
73
|
+
residuals = [jnp.sum(diff * d, axis=1) for d in surface_jacobian] # shape: (num_pts,) for each
|
|
74
|
+
res = jnp.array(residuals).reshape(n_dims, )
|
|
75
|
+
|
|
76
|
+
return res, bsp_eval, surface_jacobian, surface_jacobian_array
|
|
77
|
+
|
|
78
|
+
@partial(jax.jit, static_argnames=("degrees", "knot_vectors"))
|
|
79
|
+
def compute_projection_jacobian(
|
|
80
|
+
point_in_space,
|
|
81
|
+
para_coords,
|
|
82
|
+
degrees,
|
|
83
|
+
coefficients,
|
|
84
|
+
knot_vectors,
|
|
85
|
+
bsp_eval,
|
|
86
|
+
surface_jacobian,
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Compute the Jacobian of the projection of a point in space onto a B-spline surface.
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
-----------
|
|
93
|
+
point_in_space : jnp.ndarray, shape (d,)
|
|
94
|
+
The point in space to project.
|
|
95
|
+
para_coords : jnp.ndarray, shape (M, d)
|
|
96
|
+
Parameter coordinates for the B-spline surface.
|
|
97
|
+
degrees : tuple of int
|
|
98
|
+
Degrees of the B-spline in each dimension.
|
|
99
|
+
coefficients : jnp.ndarray, shape (N, num_phys_dims)
|
|
100
|
+
Coefficients of the B-spline basis functions.
|
|
101
|
+
knot_vectors : tuple of jnp.ndarray
|
|
102
|
+
Knot vectors for each dimension.
|
|
103
|
+
bsp_eval : jnp.ndarray, shape (M, num_phys_dims)
|
|
104
|
+
B-spline evaluation at the parameter coordinates.
|
|
105
|
+
surface_jacobian : list of jnp.ndarray, shape (M, num_phys_dims)
|
|
106
|
+
Jacobian of the B-spline surface at the parameter coordinates.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
--------
|
|
110
|
+
jacobian : jnp.ndarray, shape (M, d)
|
|
111
|
+
The Jacobian of the projection.
|
|
112
|
+
"""
|
|
113
|
+
n_dims = len(degrees)
|
|
114
|
+
|
|
115
|
+
# Convert static, hashable knot_vectors (tuples of floats) to JAX arrays
|
|
116
|
+
knot_vectors = tuple(jnp.array(U) for U in knot_vectors)
|
|
117
|
+
|
|
118
|
+
diff = (bsp_eval - point_in_space).flatten()
|
|
119
|
+
|
|
120
|
+
surface_hessian_tensor = [
|
|
121
|
+
[evaluate_b_spline_jax(
|
|
122
|
+
us=para_coords,
|
|
123
|
+
degrees=degrees,
|
|
124
|
+
knot_vectors=knot_vectors,
|
|
125
|
+
coeffs=coefficients,
|
|
126
|
+
der_orders=tuple((i == k) + (j == k) for k in range(n_dims))
|
|
127
|
+
) for j in range(n_dims)
|
|
128
|
+
] for i in range(n_dims)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
projection_jacobian = jnp.zeros((n_dims, n_dims), dtype=bsp_eval.dtype)
|
|
132
|
+
|
|
133
|
+
for i in range(n_dims):
|
|
134
|
+
for j in range(n_dims):
|
|
135
|
+
inner = jnp.sum(surface_jacobian[i] * surface_jacobian[j], axis=1) + jnp.sum(diff * surface_hessian_tensor[i][j], axis=1)
|
|
136
|
+
projection_jacobian = projection_jacobian.at[i, j].set(inner[0])
|
|
137
|
+
|
|
138
|
+
return projection_jacobian
|
|
139
|
+
|
|
140
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
141
|
+
def compute_point_to_bspline_projection(
|
|
142
|
+
point,
|
|
143
|
+
degrees,
|
|
144
|
+
coefficients,
|
|
145
|
+
para_coords,
|
|
146
|
+
knots,
|
|
147
|
+
max_iter=100,
|
|
148
|
+
tol=1e-12,
|
|
149
|
+
):
|
|
150
|
+
"""Project a point onto an n-dimensional B-spline using Newton iteration."""
|
|
151
|
+
n_dims = len(degrees) # parametric dimensions
|
|
152
|
+
|
|
153
|
+
para_coords = para_coords.reshape(n_dims, )
|
|
154
|
+
|
|
155
|
+
# Convert static, hashable knots (tuples of floats) to JAX arrays
|
|
156
|
+
knots = tuple(jnp.array(k) for k in knots)
|
|
157
|
+
|
|
158
|
+
# knots = tuple([knots[i] for i in range(n_dims)])
|
|
159
|
+
|
|
160
|
+
def body(state):
|
|
161
|
+
i, para_coords, _, _, _, _, _ = state
|
|
162
|
+
|
|
163
|
+
res, bsp_eval, surface_jacobian, surface_jacobian_array = compute_projection_residual(
|
|
164
|
+
point, para_coords, degrees, coefficients, knots
|
|
165
|
+
)
|
|
166
|
+
J = compute_projection_jacobian(
|
|
167
|
+
point, para_coords, degrees, coefficients, knots, bsp_eval, surface_jacobian,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
grad = res
|
|
171
|
+
# Applying active-set like approach
|
|
172
|
+
# Inactive directions are those where the gradient is zero or the parameter is at the boundary
|
|
173
|
+
inactive_mask = jnp.logical_or(
|
|
174
|
+
jnp.logical_and(para_coords <= 0.0, grad > 0.0),
|
|
175
|
+
jnp.logical_and(para_coords >= 1.0, grad < 0.0),
|
|
176
|
+
)
|
|
177
|
+
inactive_mask = jnp.logical_or(inactive_mask, grad == 0.0)
|
|
178
|
+
active_mask = ~inactive_mask
|
|
179
|
+
|
|
180
|
+
# Mask Jacobian and residual using active_mask
|
|
181
|
+
J_masked = J * (active_mask[:, None] & active_mask[None, :])
|
|
182
|
+
res_masked = res * active_mask
|
|
183
|
+
|
|
184
|
+
res_norm = jnp.linalg.norm(res_masked)
|
|
185
|
+
|
|
186
|
+
# NOTE: we are solving an augmented system (always 2x2)
|
|
187
|
+
# Ideally we would remove the inactive directions from the system
|
|
188
|
+
# However, for JAX, the dimensions must be fixed for jit compilation
|
|
189
|
+
step = -jnp.linalg.solve(J_masked + jnp.eye(n_dims) * (~active_mask), res_masked)
|
|
190
|
+
step = step * active_mask # zero out inactive directions
|
|
191
|
+
|
|
192
|
+
para_coords_new = jnp.clip(para_coords + step, 0.0, 1.0)
|
|
193
|
+
converged_new = res_norm < tol
|
|
194
|
+
|
|
195
|
+
return (i + 1, para_coords_new, res_masked, converged_new, J, active_mask, surface_jacobian_array)
|
|
196
|
+
|
|
197
|
+
def cond(state):
|
|
198
|
+
i, _, _, converged, _, _, _ = state
|
|
199
|
+
return (i < max_iter) & (~converged)
|
|
200
|
+
|
|
201
|
+
dim = len(degrees)
|
|
202
|
+
bool_init = jnp.zeros_like(para_coords, dtype=jnp.bool_)
|
|
203
|
+
dS_dxi_init = jnp.zeros((3, dim))
|
|
204
|
+
init_state = (0, para_coords, jnp.zeros_like(para_coords), False, jnp.zeros((dim, dim)), bool_init, dS_dxi_init)
|
|
205
|
+
final_i, final_coords, final_res, final_converged, J, mask, surface_jacobian = jax.lax.while_loop(cond, body, init_state)
|
|
206
|
+
|
|
207
|
+
return final_coords, final_res, final_converged, final_i, J, mask, surface_jacobian
|
|
208
|
+
|
|
209
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
210
|
+
def res_fun_single(para_coords, point, coefficients, degrees=None, knots=None):
|
|
211
|
+
"""Compute the residual for a single point."""
|
|
212
|
+
return jax.jit(compute_projection_residual, static_argnums=(2, ))(
|
|
213
|
+
point_in_space=point,
|
|
214
|
+
para_coords=para_coords,
|
|
215
|
+
degrees=degrees,
|
|
216
|
+
coefficients=coefficients,
|
|
217
|
+
knot_vectors=knots,
|
|
218
|
+
)[0]
|
|
219
|
+
|
|
220
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
221
|
+
def newton_solve_single(point, coefficients, para_coords_0, degrees=None, knots=None):
|
|
222
|
+
"""Newton's method to solve the projection for a single point."""
|
|
223
|
+
return compute_point_to_bspline_projection(
|
|
224
|
+
point=point,
|
|
225
|
+
degrees=degrees,
|
|
226
|
+
coefficients=coefficients,
|
|
227
|
+
para_coords=para_coords_0,
|
|
228
|
+
knots=knots,
|
|
229
|
+
)[0]
|
|
230
|
+
|
|
231
|
+
@jax.custom_jvp
|
|
232
|
+
@jax.custom_vjp
|
|
233
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
234
|
+
def implicit_solve_single(point, coefficients, para_coords_0, degrees=None, knots=None):
|
|
235
|
+
return newton_solve_single(
|
|
236
|
+
point=point,
|
|
237
|
+
coefficients=coefficients,
|
|
238
|
+
para_coords_0=para_coords_0,
|
|
239
|
+
degrees=degrees,
|
|
240
|
+
knots=knots,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
244
|
+
def fwd(point, coefficients, para_coords_0, degrees=None, knots=None):
|
|
245
|
+
para_coord_star = implicit_solve_single(
|
|
246
|
+
point=point,
|
|
247
|
+
coefficients=coefficients,
|
|
248
|
+
para_coords_0=para_coords_0,
|
|
249
|
+
degrees=degrees,
|
|
250
|
+
knots=knots,
|
|
251
|
+
)
|
|
252
|
+
return para_coord_star, (para_coord_star, point, coefficients)
|
|
253
|
+
|
|
254
|
+
@partial(jax.jit, static_argnames=("degrees", "knots"))
|
|
255
|
+
def bwd(primals, x_bar, degrees=None, knots=None):
|
|
256
|
+
n_dims = len(degrees)
|
|
257
|
+
|
|
258
|
+
para_coord_star, point, coefficients = primals
|
|
259
|
+
# Compute the Jacobian of the projection
|
|
260
|
+
res, bsp_eval, surface_jacobian, _ = compute_projection_residual(
|
|
261
|
+
point_in_space=point,
|
|
262
|
+
para_coords=para_coord_star,
|
|
263
|
+
degrees=degrees,
|
|
264
|
+
coefficients=coefficients,
|
|
265
|
+
knot_vectors=knots,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
grad = res
|
|
269
|
+
# Applying active-set like approach
|
|
270
|
+
# Inactive directions are those where the gradient is zero or the parameter is at the boundary
|
|
271
|
+
inactive_mask = jnp.logical_or(
|
|
272
|
+
jnp.logical_and(para_coord_star.flatten() <= 0.0, grad > 0.0),
|
|
273
|
+
jnp.logical_and(para_coord_star.flatten() >= 1.0, grad < 0.0),
|
|
274
|
+
)
|
|
275
|
+
# inactive_mask = jnp.logical_or(inactive_mask, grad == 0.0)
|
|
276
|
+
active_mask = ~inactive_mask
|
|
277
|
+
jax.lax.stop_gradient(active_mask) # ensure mask is not differentiated
|
|
278
|
+
|
|
279
|
+
J = compute_projection_jacobian(
|
|
280
|
+
point_in_space=point,
|
|
281
|
+
para_coords=para_coord_star,
|
|
282
|
+
degrees=degrees,
|
|
283
|
+
coefficients=coefficients,
|
|
284
|
+
knot_vectors=knots,
|
|
285
|
+
bsp_eval=bsp_eval,
|
|
286
|
+
surface_jacobian=surface_jacobian,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Mask Jacobian and residual using active_mask
|
|
290
|
+
J_masked = J * (active_mask[:, None] & active_mask[None, :])
|
|
291
|
+
x_bar_masked = x_bar * active_mask
|
|
292
|
+
J_masked = J_masked + jnp.eye(n_dims) * (~active_mask) # add identity to avoid singularity
|
|
293
|
+
|
|
294
|
+
# lambda_ = jnp.linalg.solve(J.T, x_bar)
|
|
295
|
+
lambda_ = jnp.linalg.solve(J_masked.T, x_bar_masked)
|
|
296
|
+
|
|
297
|
+
def res_fun_wrapped(point, coefficients):
|
|
298
|
+
res = res_fun_single(
|
|
299
|
+
para_coords=para_coord_star,
|
|
300
|
+
point=point,
|
|
301
|
+
coefficients=coefficients,
|
|
302
|
+
degrees=degrees,
|
|
303
|
+
knots=knots,
|
|
304
|
+
)
|
|
305
|
+
return res.squeeze()
|
|
306
|
+
|
|
307
|
+
_, vjp_res = jax.vjp(res_fun_wrapped, point, coefficients)
|
|
308
|
+
dpoint, dcoeffs = vjp_res(-lambda_)
|
|
309
|
+
|
|
310
|
+
return (dpoint, dcoeffs, None)
|
|
311
|
+
implicit_solve_single.defvjp(fwd, bwd)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@implicit_solve_single.defjvp
|
|
315
|
+
def iv_jvp(primals, tangents):
|
|
316
|
+
"""
|
|
317
|
+
primals: tuple of positional arguments passed to implicit_solve_single
|
|
318
|
+
tangents: tuple of corresponding tangents (same length as primals)
|
|
319
|
+
"""
|
|
320
|
+
# --- Defensive unpacking of primals (support 3..5 entries) ---
|
|
321
|
+
if len(primals) < 3:
|
|
322
|
+
raise ValueError("iv_jvp expected at least (point, coefficients, para_coords_0) primals")
|
|
323
|
+
|
|
324
|
+
point = primals[0]
|
|
325
|
+
coefficients = primals[1]
|
|
326
|
+
para_coords_0 = primals[2]
|
|
327
|
+
|
|
328
|
+
degrees = None
|
|
329
|
+
knots = None
|
|
330
|
+
if len(primals) >= 4:
|
|
331
|
+
degrees = primals[3]
|
|
332
|
+
if len(primals) >= 5:
|
|
333
|
+
knots = primals[4]
|
|
334
|
+
|
|
335
|
+
# --- Defensive unpacking of tangents (fill missing with zeros) ---
|
|
336
|
+
# tangents may be shorter if some primals are static/non-differentiable
|
|
337
|
+
def safe_get(tup, idx, fill):
|
|
338
|
+
try:
|
|
339
|
+
return tup[idx]
|
|
340
|
+
except Exception:
|
|
341
|
+
return fill
|
|
342
|
+
|
|
343
|
+
point_dot = safe_get(tangents, 0, jnp.zeros_like(point))
|
|
344
|
+
coeffs_dot = safe_get(tangents, 1, jnp.zeros_like(coefficients))
|
|
345
|
+
|
|
346
|
+
# --- Compute the primal solution (call with the same kwargs your forward used) ---
|
|
347
|
+
# Use keyword args to be robust to ordering; implicit_solve_single must accept them.
|
|
348
|
+
if (degrees is None) and (knots is None):
|
|
349
|
+
para_coord_star = implicit_solve_single(point, coefficients, para_coords_0)
|
|
350
|
+
else:
|
|
351
|
+
para_coord_star = implicit_solve_single(
|
|
352
|
+
point=point,
|
|
353
|
+
coefficients=coefficients,
|
|
354
|
+
para_coords_0=para_coords_0,
|
|
355
|
+
degrees=degrees,
|
|
356
|
+
knots=knots,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# --- Build residual wrapper evaluated at the converged parametric location ---
|
|
360
|
+
# res_fun_single(para_coords, point, coefficients, degrees, knots) -> residual (n_dim,)
|
|
361
|
+
def res_wrapped(p_in, coeffs_in):
|
|
362
|
+
# call the same residual function you use elsewhere; make sure shapes consistent
|
|
363
|
+
return res_fun_single(
|
|
364
|
+
para_coords=para_coord_star,
|
|
365
|
+
point=p_in,
|
|
366
|
+
coefficients=coeffs_in,
|
|
367
|
+
degrees=degrees,
|
|
368
|
+
knots=knots,
|
|
369
|
+
).squeeze()
|
|
370
|
+
|
|
371
|
+
# Directional derivative of residual w.r.t. (point, coefficients) in directions (point_dot, coeffs_dot)
|
|
372
|
+
_, res_dir = jax.jvp(res_wrapped, (point, coefficients), (point_dot, coeffs_dot))
|
|
373
|
+
|
|
374
|
+
# Evaluate Jacobian J = dr/dpara (size = n_param_dims x n_param_dims)
|
|
375
|
+
res_val, bsp_eval, surface_jacobian, _ = compute_projection_residual(
|
|
376
|
+
point_in_space=point,
|
|
377
|
+
para_coords=para_coord_star,
|
|
378
|
+
degrees=degrees,
|
|
379
|
+
coefficients=coefficients,
|
|
380
|
+
knot_vectors=knots if (knots is not None) else None,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Build active mask as in forward
|
|
384
|
+
grad = res_val
|
|
385
|
+
inactive_mask = jnp.logical_or(
|
|
386
|
+
jnp.logical_and(para_coord_star.flatten() <= 0.0, grad > 0.0),
|
|
387
|
+
jnp.logical_and(para_coord_star.flatten() >= 1.0, grad < 0.0),
|
|
388
|
+
)
|
|
389
|
+
# inactive_mask = jnp.logical_or(inactive_mask, grad == 0.0)
|
|
390
|
+
active_mask = ~inactive_mask
|
|
391
|
+
jax.lax.stop_gradient(active_mask)
|
|
392
|
+
|
|
393
|
+
J = compute_projection_jacobian(
|
|
394
|
+
point_in_space=point,
|
|
395
|
+
para_coords=para_coord_star,
|
|
396
|
+
degrees=degrees,
|
|
397
|
+
coefficients=coefficients,
|
|
398
|
+
knot_vectors=knots if (knots is not None) else None,
|
|
399
|
+
bsp_eval=bsp_eval,
|
|
400
|
+
surface_jacobian=surface_jacobian,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
n_dims = J.shape[0]
|
|
404
|
+
J_masked = J * (active_mask[:, None] & active_mask[None, :])
|
|
405
|
+
J_aug = J_masked + jnp.eye(n_dims, dtype=J.dtype) * (~active_mask)
|
|
406
|
+
res_dir_masked = res_dir * active_mask
|
|
407
|
+
|
|
408
|
+
# Solve for parametric tangent: J * para_dot = - res_dir
|
|
409
|
+
# para_dot = - jnp.linalg.solve(J, res_dir)
|
|
410
|
+
para_dot = - jnp.linalg.solve(J_aug, res_dir_masked)
|
|
411
|
+
|
|
412
|
+
# return primal and tangent (exact pair required by defjvp)
|
|
413
|
+
return para_coord_star, para_dot
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _nearest_idx_for_point(bsp_pts: jnp.ndarray, query_pt: jnp.ndarray) -> jnp.ndarray:
|
|
417
|
+
"""
|
|
418
|
+
Find the index of the nearest neighbor in bsp_pts to a single query_pt,
|
|
419
|
+
using a fori_loop to keep memory footprint O(1) per point.
|
|
420
|
+
"""
|
|
421
|
+
def body(i, carry):
|
|
422
|
+
best_i, best_d2 = carry
|
|
423
|
+
# compute squared distance to bsp_pts[i]
|
|
424
|
+
d2 = jnp.sum((bsp_pts[i] - query_pt) ** 2)
|
|
425
|
+
# update if smaller
|
|
426
|
+
better = d2 < best_d2
|
|
427
|
+
new_best_i = jax.lax.select(better, i, best_i)
|
|
428
|
+
new_best_d2 = jax.lax.select(better, d2, best_d2)
|
|
429
|
+
return (new_best_i, new_best_d2)
|
|
430
|
+
|
|
431
|
+
# initialize with idx=0, distance=∞
|
|
432
|
+
init = (jnp.array(0, dtype=jnp.int64), jnp.array(jnp.inf, dtype=bsp_pts.dtype))
|
|
433
|
+
best_idx, _ = jax.lax.fori_loop(0, bsp_pts.shape[0], body, init)
|
|
434
|
+
return best_idx
|
|
435
|
+
|
|
436
|
+
def brute_force_nn(bsp_pts: jnp.ndarray,
|
|
437
|
+
queries: jnp.ndarray) -> jnp.ndarray:
|
|
438
|
+
"""
|
|
439
|
+
For each row in queries (shape [Q, D]), returns the index of the closest
|
|
440
|
+
point in bsp_pts (shape [P, D]). Result is an array of shape [Q].
|
|
441
|
+
"""
|
|
442
|
+
# vmapped over the first axis of queries
|
|
443
|
+
return jax.vmap(lambda q: _nearest_idx_for_point(bsp_pts, q))(queries)
|
|
444
|
+
|
|
445
|
+
class ProjectionOperationVJPVJP(csdl.experimental.CustomExplicitOperationBeta):
|
|
446
|
+
def __init__(
|
|
447
|
+
self,
|
|
448
|
+
num_parametric_dimensions,
|
|
449
|
+
degrees,
|
|
450
|
+
knots,
|
|
451
|
+
num_grid_search_points=100,
|
|
452
|
+
):
|
|
453
|
+
super().__init__()
|
|
454
|
+
self.num_parametric_dimensions = num_parametric_dimensions
|
|
455
|
+
self.degrees = degrees
|
|
456
|
+
self.knots = knots
|
|
457
|
+
self.num_grid_search_points = num_grid_search_points
|
|
458
|
+
|
|
459
|
+
def sensitivity_analysis_single(
|
|
460
|
+
point,
|
|
461
|
+
point_dot,
|
|
462
|
+
coefficients,
|
|
463
|
+
coefficients_dot,
|
|
464
|
+
x_bar,
|
|
465
|
+
para_coords_closest,
|
|
466
|
+
degrees,
|
|
467
|
+
knots,
|
|
468
|
+
):
|
|
469
|
+
# Build vjp-wrapper for a single point. It returns (dpoint, dcoeffs) for the current x_bar.
|
|
470
|
+
def vjp_wrapper(point_in, coeffs_in, xbar_in):
|
|
471
|
+
# 1) compute primal para_coord_star for this point (use the same initial guess logic as the forward op)
|
|
472
|
+
# We reuse para_coords_closest[i] as the initial guess for Newton.
|
|
473
|
+
para0 = jnp.array(para_coords_closest)
|
|
474
|
+
para_star = implicit_solve_single(
|
|
475
|
+
point=point_in,
|
|
476
|
+
coefficients=coeffs_in,
|
|
477
|
+
para_coords_0=para0,
|
|
478
|
+
degrees=degrees,
|
|
479
|
+
knots=knots,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# 2) build primals tuple in the same shape ordering used by bwd:
|
|
483
|
+
primals = (para_star, point_in, coeffs_in)
|
|
484
|
+
|
|
485
|
+
# 3) call bwd to get the first-order VJP outputs (dpoint, dcoeffs)
|
|
486
|
+
# bwd returns (dpoint, dcoeffs, None)
|
|
487
|
+
dpt, dcoeffs, _ = bwd(primals=primals, x_bar=xbar_in, degrees=degrees, knots=knots)
|
|
488
|
+
# Return both outputs so jvp can linearize them both
|
|
489
|
+
return dpt, dcoeffs
|
|
490
|
+
|
|
491
|
+
# Now linearize vjp_wrapper via jax.jvp.
|
|
492
|
+
# primals for vjp_wrapper: (point, coefficients, x_bar)
|
|
493
|
+
primals_vw = (point, coefficients, x_bar)
|
|
494
|
+
# tangents: how the inputs change — we use d_d_points[i] and d_d_control_points; assume x_bar perturbation = 0
|
|
495
|
+
tangents_vw = (point_dot, coefficients_dot, jnp.zeros_like(x_bar))
|
|
496
|
+
|
|
497
|
+
# perform jvp; it returns (primal_outs, tangent_outs)
|
|
498
|
+
(primal_outs), (tangent_outs) = jax.jvp(vjp_wrapper, primals_vw, tangents_vw)
|
|
499
|
+
|
|
500
|
+
# tangent_outs is a tuple (dpoint_dot, dcoeffs_dot)
|
|
501
|
+
dpoint_dot, dcoeffs_dot = tangent_outs
|
|
502
|
+
|
|
503
|
+
para0 = jnp.array(para_coords_closest)
|
|
504
|
+
# primals and tangents for the implicit solve
|
|
505
|
+
primals_solve = (point, coefficients, para0, degrees, knots) # match ordering your defjvp expects
|
|
506
|
+
tangents_solve = (point_dot, coefficients_dot, jnp.zeros_like(para0))
|
|
507
|
+
|
|
508
|
+
# call iv_jvp directly: it returns (primal_para_star, para_dot)
|
|
509
|
+
para_star_primal, para_dot = iv_jvp(primals_solve, tangents_solve)
|
|
510
|
+
|
|
511
|
+
return dpoint_dot, dcoeffs_dot, para_dot
|
|
512
|
+
|
|
513
|
+
self.sensitivity_analysis_single = sensitivity_analysis_single
|
|
514
|
+
|
|
515
|
+
self._cache = {
|
|
516
|
+
"points" : None,
|
|
517
|
+
"control_points" : None,
|
|
518
|
+
"para_coords" : None,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Perform grid search
|
|
522
|
+
self.n = len(self.degrees)
|
|
523
|
+
|
|
524
|
+
def generate_parametric_grid(N):
|
|
525
|
+
samples_1d = []
|
|
526
|
+
for U in self.knots:
|
|
527
|
+
# take only the unique knots in sorted order
|
|
528
|
+
knots = np.unique(U)
|
|
529
|
+
# for each interval [knots[j], knots[j+1]), sample N points
|
|
530
|
+
pts = []
|
|
531
|
+
for j in range(len(knots) - 1):
|
|
532
|
+
a, b = knots[j], knots[j+1]
|
|
533
|
+
pts.append(np.linspace(a, b, N, endpoint=False))
|
|
534
|
+
# finally include the very last knot
|
|
535
|
+
pts.append(np.array([knots[-1]]))
|
|
536
|
+
samples_1d.append(np.concatenate(pts))
|
|
537
|
+
|
|
538
|
+
# build the d‐dimensional tensor grid
|
|
539
|
+
mesh = np.meshgrid(*samples_1d, indexing="ij")
|
|
540
|
+
# flatten each coordinate array and stack into shape (M, d)
|
|
541
|
+
coord_arrays = [m.flatten() for m in mesh]
|
|
542
|
+
grid = np.stack(coord_arrays, axis=-1)
|
|
543
|
+
return grid
|
|
544
|
+
|
|
545
|
+
self.para_grid = generate_parametric_grid(num_grid_search_points)
|
|
546
|
+
|
|
547
|
+
def evaluate(self, inputs, d_outputs):
|
|
548
|
+
points = inputs["points"].reshape(-1, 3)
|
|
549
|
+
control_points = inputs["control_points"]
|
|
550
|
+
d_para_coords = inputs["d_para_coords"]
|
|
551
|
+
|
|
552
|
+
d_points = d_outputs["d_points"]
|
|
553
|
+
d_control_points = d_outputs["d_control_points"]
|
|
554
|
+
|
|
555
|
+
# Inputs from 1st VJP
|
|
556
|
+
self.declare_input("points", points)
|
|
557
|
+
self.declare_input("control_points", control_points)
|
|
558
|
+
self.declare_input("d_para_coords", d_para_coords)
|
|
559
|
+
|
|
560
|
+
# 2nd order "V"s
|
|
561
|
+
self.declare_input("d_d_points", d_points)
|
|
562
|
+
self.declare_input("d_d_control_points", d_control_points)
|
|
563
|
+
|
|
564
|
+
d_d_points = self.create_output("d_vjp_points", points.shape)
|
|
565
|
+
d_d_control_points = self.create_output("d_vjp_control_points", control_points.shape)
|
|
566
|
+
d_d_para_cords = self.create_output("d_d_para_cords", d_para_coords.shape)
|
|
567
|
+
|
|
568
|
+
d_inputs = {
|
|
569
|
+
"points": d_d_points,
|
|
570
|
+
"control_points": d_d_control_points,
|
|
571
|
+
"d_para_coords": d_d_para_cords,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return d_inputs
|
|
575
|
+
|
|
576
|
+
def compute(self, inputs, outputs):
|
|
577
|
+
points = jnp.array(inputs["points"]).reshape((-1, 3))
|
|
578
|
+
coefficients = jnp.array(inputs["control_points"])
|
|
579
|
+
d_para_coords = jnp.array(inputs["d_para_coords"])
|
|
580
|
+
|
|
581
|
+
d_d_points = jnp.array(inputs["d_d_points"])
|
|
582
|
+
d_d_control_points = jnp.array(inputs["d_d_control_points"])
|
|
583
|
+
|
|
584
|
+
save_name = f"points_shape_{points.shape}_coefficients_shape_{coefficients.shape}"
|
|
585
|
+
para_coords = self.para_grid
|
|
586
|
+
|
|
587
|
+
if f"{save_name}_basis_mat" in self._cache:
|
|
588
|
+
basis_mat = self._cache[f"{save_name}_basis_mat"]
|
|
589
|
+
# print("Using cached basis matrix.")
|
|
590
|
+
else:
|
|
591
|
+
# convert stored knots (hashable tuples) back to numpy arrays for
|
|
592
|
+
# the NumPy implementation
|
|
593
|
+
knot_vectors_np = tuple(np.array(U) for U in self.knots)
|
|
594
|
+
basis_mat = compute_basis_matrix_numpy(
|
|
595
|
+
us=para_coords,
|
|
596
|
+
degrees=self.degrees,
|
|
597
|
+
knot_vectors=knot_vectors_np,
|
|
598
|
+
)
|
|
599
|
+
self._cache[f"{save_name}_basis_mat"] = basis_mat
|
|
600
|
+
bsp_eval = basis_mat @ coefficients.reshape(-1, coefficients.shape[-1])
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
if f"{save_name}_nearest_indices" in self._cache:
|
|
604
|
+
nearest_indices_fun = self._cache[f"{save_name}_nearest_indices"]
|
|
605
|
+
# print("Using cached nearest indices REVERSE (2nd order VJP).")
|
|
606
|
+
else:
|
|
607
|
+
nearest_indices_fun = jax.jit(brute_force_nn)
|
|
608
|
+
self._cache[f"{save_name}_nearest_indices"] = nearest_indices_fun
|
|
609
|
+
|
|
610
|
+
nearest_indices = nearest_indices_fun(jnp.array(bsp_eval), points)
|
|
611
|
+
para_coords_closest = para_coords[nearest_indices]
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
if f"{save_name}_sensitivity_analysis" in self._cache:
|
|
615
|
+
sensitivity_analysis_jitted = self._cache[f"{save_name}_sensitivity_analysis"]
|
|
616
|
+
# print("Using cached sensitivity analysis single.")
|
|
617
|
+
else:
|
|
618
|
+
sensitivity_analysis_single = self.sensitivity_analysis_single
|
|
619
|
+
sensitivity_analysis_batched = jax.vmap(
|
|
620
|
+
sensitivity_analysis_single,
|
|
621
|
+
in_axes=(0, 0, None, None, 0, 0, None, None),
|
|
622
|
+
)
|
|
623
|
+
# sensitivity_analysis_jitted = jax.jit(sensitivity_analysis_batched, static_argnums=(6, ))
|
|
624
|
+
sensitivity_analysis_jitted = sensitivity_analysis_batched
|
|
625
|
+
|
|
626
|
+
self._cache[f"{save_name}_sensitivity_analysis"] = sensitivity_analysis_jitted
|
|
627
|
+
|
|
628
|
+
t1 = time.time()
|
|
629
|
+
dpoint_dot, dcoeffs_dot, para_dot = sensitivity_analysis_jitted(
|
|
630
|
+
points,
|
|
631
|
+
d_d_points,
|
|
632
|
+
coefficients,
|
|
633
|
+
d_d_control_points,
|
|
634
|
+
d_para_coords,
|
|
635
|
+
para_coords_closest,
|
|
636
|
+
self.degrees,
|
|
637
|
+
self.knots,
|
|
638
|
+
)
|
|
639
|
+
t2 = time.time()
|
|
640
|
+
print(f"Second-order sensitivity analysis took {t2 - t1:.4f} seconds.")
|
|
641
|
+
|
|
642
|
+
outputs["d_vjp_points"] = jax.device_get(dpoint_dot)
|
|
643
|
+
outputs["d_vjp_control_points"] = jax.device_get(jnp.sum(dcoeffs_dot, axis=0)) # accumulate control points
|
|
644
|
+
outputs["d_d_para_cords"] = jax.device_get(para_dot)
|
|
645
|
+
|
|
646
|
+
class ProjectionOperationVJP(csdl.experimental.CustomExplicitOperationBeta):
|
|
647
|
+
def __init__(
|
|
648
|
+
self,
|
|
649
|
+
num_parametric_dimensions,
|
|
650
|
+
degrees,
|
|
651
|
+
knots,
|
|
652
|
+
num_grid_search_points=100,
|
|
653
|
+
):
|
|
654
|
+
super().__init__()
|
|
655
|
+
self.num_parametric_dimensions = num_parametric_dimensions
|
|
656
|
+
self.degrees = degrees
|
|
657
|
+
self.knots = knots
|
|
658
|
+
self.num_grid_search_points = num_grid_search_points
|
|
659
|
+
|
|
660
|
+
def sensitivity_analysis_single(
|
|
661
|
+
point,
|
|
662
|
+
coefficients,
|
|
663
|
+
para_coords_closest,
|
|
664
|
+
d_para_coords,
|
|
665
|
+
):
|
|
666
|
+
para_coords_star = implicit_solve_single(
|
|
667
|
+
point, coefficients, jnp.array(para_coords_closest),
|
|
668
|
+
degrees=self.degrees, knots=self.knots
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
para_coords_star = para_coords_star.reshape(-1, len(self.degrees))
|
|
672
|
+
|
|
673
|
+
primals = (para_coords_star, point, coefficients)
|
|
674
|
+
|
|
675
|
+
dpoint, dcoeffs, _ = bwd(
|
|
676
|
+
primals=primals,
|
|
677
|
+
x_bar=d_para_coords,
|
|
678
|
+
degrees=self.degrees,
|
|
679
|
+
knots=self.knots,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return dpoint, dcoeffs
|
|
683
|
+
self.sensitivity_analysis_single = sensitivity_analysis_single
|
|
684
|
+
|
|
685
|
+
self._cache = {
|
|
686
|
+
"points" : None,
|
|
687
|
+
"control_points" : None,
|
|
688
|
+
"para_coords" : None,
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
# Perform grid search
|
|
692
|
+
self.n = len(self.degrees)
|
|
693
|
+
|
|
694
|
+
def generate_parametric_grid(N):
|
|
695
|
+
samples_1d = []
|
|
696
|
+
for U in self.knots:
|
|
697
|
+
# take only the unique knots in sorted order
|
|
698
|
+
knots = np.unique(U)
|
|
699
|
+
# for each interval [knots[j], knots[j+1]), sample N points
|
|
700
|
+
pts = []
|
|
701
|
+
for j in range(len(knots) - 1):
|
|
702
|
+
a, b = knots[j], knots[j+1]
|
|
703
|
+
pts.append(np.linspace(a, b, N, endpoint=False))
|
|
704
|
+
# finally include the very last knot
|
|
705
|
+
pts.append(np.array([knots[-1]]))
|
|
706
|
+
samples_1d.append(np.concatenate(pts))
|
|
707
|
+
|
|
708
|
+
# build the d‐dimensional tensor grid
|
|
709
|
+
mesh = np.meshgrid(*samples_1d, indexing="ij")
|
|
710
|
+
# flatten each coordinate array and stack into shape (M, d)
|
|
711
|
+
coord_arrays = [m.flatten() for m in mesh]
|
|
712
|
+
grid = np.stack(coord_arrays, axis=-1)
|
|
713
|
+
return grid
|
|
714
|
+
|
|
715
|
+
self.para_grid = generate_parametric_grid(num_grid_search_points)
|
|
716
|
+
|
|
717
|
+
def evaluate(self, inputs, d_outputs):
|
|
718
|
+
# print("evaluate first")
|
|
719
|
+
points = inputs["points"].reshape(-1, 3)
|
|
720
|
+
control_points = inputs["control_points"]
|
|
721
|
+
d_para_coords = d_outputs["para_coords"]
|
|
722
|
+
|
|
723
|
+
self.declare_input("points", points)
|
|
724
|
+
self.declare_input("control_points", control_points)
|
|
725
|
+
self.declare_input("d_para_coords", d_para_coords)
|
|
726
|
+
|
|
727
|
+
d_points = self.create_output("d_points", points.shape)
|
|
728
|
+
d_control_points = self.create_output("d_control_points", control_points.shape)
|
|
729
|
+
|
|
730
|
+
self.declare_vjp_function(
|
|
731
|
+
ProjectionOperationVJPVJP,
|
|
732
|
+
num_parametric_dimensions=self.num_parametric_dimensions,
|
|
733
|
+
knots=self.knots,
|
|
734
|
+
degrees=self.degrees,
|
|
735
|
+
num_grid_search_points=self.num_grid_search_points,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
d_inputs = {
|
|
739
|
+
"points": d_points,
|
|
740
|
+
"control_points": d_control_points,
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return d_inputs
|
|
744
|
+
|
|
745
|
+
def compute(self, inputs, outputs):
|
|
746
|
+
points = jnp.array(inputs["points"]).reshape((-1, 3))
|
|
747
|
+
coefficients = jnp.array(inputs["control_points"])
|
|
748
|
+
d_para_coords = jnp.array(inputs["d_para_coords"])
|
|
749
|
+
|
|
750
|
+
save_name = f"points_shape_{points.shape}_coefficients_shape_{coefficients.shape}"
|
|
751
|
+
para_coords = self.para_grid
|
|
752
|
+
|
|
753
|
+
if f"{save_name}_basis_mat" in self._cache:
|
|
754
|
+
basis_mat = self._cache[f"{save_name}_basis_mat"]
|
|
755
|
+
# print("Using cached basis matrix.")
|
|
756
|
+
else:
|
|
757
|
+
knot_vectors_np = tuple(np.array(U) for U in self.knots)
|
|
758
|
+
basis_mat = compute_basis_matrix_numpy(
|
|
759
|
+
us=para_coords,
|
|
760
|
+
degrees=self.degrees,
|
|
761
|
+
knot_vectors=knot_vectors_np,
|
|
762
|
+
)
|
|
763
|
+
self._cache[f"{save_name}_basis_mat"] = basis_mat
|
|
764
|
+
bsp_eval = basis_mat @ coefficients.reshape(-1, coefficients.shape[-1])
|
|
765
|
+
|
|
766
|
+
if f"{save_name}_nearest_indices" in self._cache:
|
|
767
|
+
nearest_indices_fun = self._cache[f"{save_name}_nearest_indices"]
|
|
768
|
+
# print("Using cached nearest indices REVERSE.")
|
|
769
|
+
else:
|
|
770
|
+
nearest_indices_fun = jax.jit(brute_force_nn)
|
|
771
|
+
self._cache[f"{save_name}_nearest_indices"] = nearest_indices_fun
|
|
772
|
+
|
|
773
|
+
nearest_indices = nearest_indices_fun(jnp.array(bsp_eval), points)
|
|
774
|
+
para_coords_closest = para_coords[nearest_indices]
|
|
775
|
+
|
|
776
|
+
if f"{save_name}_sensitivity_analysis" in self._cache:
|
|
777
|
+
sensitivity_analysis_jitted = self._cache[f"{save_name}_sensitivity_analysis"]
|
|
778
|
+
# print("Using cached sensitivity analysis single.")
|
|
779
|
+
else:
|
|
780
|
+
sensitivity_analysis_single = self.sensitivity_analysis_single
|
|
781
|
+
sensitivity_analysis_batched = jax.vmap(
|
|
782
|
+
sensitivity_analysis_single,
|
|
783
|
+
in_axes=(0, None, 0, 0),
|
|
784
|
+
)
|
|
785
|
+
# sensitivity_analysis_jitted = jax.jit(sensitivity_analysis_batched, static_argnums=(2, ))
|
|
786
|
+
sensitivity_analysis_jitted = sensitivity_analysis_batched
|
|
787
|
+
|
|
788
|
+
self._cache[f"{save_name}_sensitivity_analysis"] = sensitivity_analysis_jitted
|
|
789
|
+
|
|
790
|
+
dpoint, dcoeffs = sensitivity_analysis_jitted(
|
|
791
|
+
points,
|
|
792
|
+
coefficients,
|
|
793
|
+
para_coords_closest,
|
|
794
|
+
d_para_coords,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
outputs["d_points"] = jax.device_get(dpoint)
|
|
799
|
+
outputs["d_control_points"] = jax.device_get(jnp.sum(dcoeffs, axis=0)) # accumulate control points
|
|
800
|
+
|
|
801
|
+
class ProjectionOperation(csdl.experimental.CustomExplicitOperationBeta):
|
|
802
|
+
def __init__(
|
|
803
|
+
self,
|
|
804
|
+
num_parametric_dimensions,
|
|
805
|
+
degrees,
|
|
806
|
+
knots,
|
|
807
|
+
num_grid_search_points=100,
|
|
808
|
+
):
|
|
809
|
+
super().__init__()
|
|
810
|
+
self.num_parametric_dimensions = num_parametric_dimensions
|
|
811
|
+
self.degrees = degrees
|
|
812
|
+
self.knots = knots
|
|
813
|
+
self.num_grid_search_points = num_grid_search_points
|
|
814
|
+
self.res_jac = None
|
|
815
|
+
self.mask = None
|
|
816
|
+
self.final_coords = None
|
|
817
|
+
|
|
818
|
+
def point_to_b_spline_projection_single(point, para_coords, coefficients):
|
|
819
|
+
return compute_point_to_bspline_projection(
|
|
820
|
+
point=point,
|
|
821
|
+
degrees=degrees,
|
|
822
|
+
coefficients=coefficients,
|
|
823
|
+
para_coords=para_coords,
|
|
824
|
+
knots=knots,
|
|
825
|
+
)
|
|
826
|
+
self.point_to_b_spline_projection_batched = jax.vmap(point_to_b_spline_projection_single, in_axes=(0, 0, None), out_axes=0)
|
|
827
|
+
|
|
828
|
+
self._cache = {
|
|
829
|
+
"points" : None,
|
|
830
|
+
"control_points" : None,
|
|
831
|
+
"para_coords" : None,
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
# Perform grid search
|
|
835
|
+
self.n = len(self.degrees)
|
|
836
|
+
|
|
837
|
+
def generate_parametric_grid(N):
|
|
838
|
+
samples_1d = []
|
|
839
|
+
for U in self.knots:
|
|
840
|
+
# take only the unique knots in sorted order
|
|
841
|
+
knots = np.unique(U)
|
|
842
|
+
# for each interval [knots[j], knots[j+1]), sample N points
|
|
843
|
+
pts = []
|
|
844
|
+
for j in range(len(knots) - 1):
|
|
845
|
+
a, b = knots[j], knots[j+1]
|
|
846
|
+
pts.append(np.linspace(a, b, N, endpoint=False))
|
|
847
|
+
# finally include the very last knot
|
|
848
|
+
pts.append(np.array([knots[-1]]))
|
|
849
|
+
samples_1d.append(np.concatenate(pts))
|
|
850
|
+
|
|
851
|
+
# build the d‐dimensional tensor grid
|
|
852
|
+
mesh = np.meshgrid(*samples_1d, indexing="ij")
|
|
853
|
+
# flatten each coordinate array and stack into shape (M, d)
|
|
854
|
+
coord_arrays = [m.flatten() for m in mesh]
|
|
855
|
+
grid = np.stack(coord_arrays, axis=-1)
|
|
856
|
+
return grid
|
|
857
|
+
|
|
858
|
+
self.para_grid = generate_parametric_grid(num_grid_search_points)
|
|
859
|
+
|
|
860
|
+
def evaluate(self, points, control_points):
|
|
861
|
+
points = points.reshape(-1, 3)
|
|
862
|
+
num_points = points.shape[0]
|
|
863
|
+
self.declare_input("points", points)
|
|
864
|
+
self.declare_input("control_points", control_points)
|
|
865
|
+
|
|
866
|
+
para_coords = self.create_output("para_coords", (num_points, self.num_parametric_dimensions))
|
|
867
|
+
|
|
868
|
+
self.declare_vjp_function(
|
|
869
|
+
ProjectionOperationVJP,
|
|
870
|
+
num_parametric_dimensions=self.num_parametric_dimensions,
|
|
871
|
+
knots=self.knots,
|
|
872
|
+
degrees=self.degrees,
|
|
873
|
+
num_grid_search_points=self.num_grid_search_points,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
return para_coords
|
|
877
|
+
|
|
878
|
+
def compute(self, inputs, outputs):
|
|
879
|
+
points = jnp.array(inputs["points"])
|
|
880
|
+
control_points = jnp.array(inputs["control_points"])
|
|
881
|
+
|
|
882
|
+
save_name = f"points_shape_{points.shape}_control_points_shape_{control_points.shape}"
|
|
883
|
+
para_coords = self.para_grid
|
|
884
|
+
|
|
885
|
+
if f"{save_name}_basis_mat" in self._cache:
|
|
886
|
+
basis_mat = self._cache[f"{save_name}_basis_mat"]
|
|
887
|
+
# print("Using cached basis matrix.")
|
|
888
|
+
else:
|
|
889
|
+
knot_vectors_np = tuple(np.array(U) for U in self.knots)
|
|
890
|
+
basis_mat = compute_basis_matrix_numpy(
|
|
891
|
+
us=para_coords,
|
|
892
|
+
degrees=self.degrees,
|
|
893
|
+
knot_vectors=knot_vectors_np,
|
|
894
|
+
)
|
|
895
|
+
self._cache[f"{save_name}_basis_mat"] = basis_mat
|
|
896
|
+
bsp_eval = basis_mat @ control_points.reshape(-1, control_points.shape[-1])
|
|
897
|
+
|
|
898
|
+
if f"{save_name}_nearest_indices" in self._cache:
|
|
899
|
+
nearest_indices_fun = self._cache[f"{save_name}_nearest_indices"]
|
|
900
|
+
print("Using cached nearest indices FORWARD.")
|
|
901
|
+
else:
|
|
902
|
+
nearest_indices_fun = jax.jit(brute_force_nn)
|
|
903
|
+
self._cache[f"{save_name}_nearest_indices"] = nearest_indices_fun
|
|
904
|
+
|
|
905
|
+
nearest_indices = nearest_indices_fun(jnp.array(bsp_eval), points)
|
|
906
|
+
para_coords_closest = para_coords[nearest_indices]
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
if f"{save_name}_projection_fun" in self._cache:
|
|
910
|
+
projection_fun = self._cache[f"{save_name}_projection_fun"]
|
|
911
|
+
# print("Using cached projection function.")
|
|
912
|
+
else:
|
|
913
|
+
projection_fun = jax.jit(
|
|
914
|
+
self.point_to_b_spline_projection_batched,
|
|
915
|
+
)
|
|
916
|
+
self._cache[f"{save_name}_projection_fun"] = projection_fun
|
|
917
|
+
|
|
918
|
+
final_coords, final_res, converged, final_i, J, mask, _ = projection_fun(
|
|
919
|
+
jnp.array(points),
|
|
920
|
+
jnp.array(para_coords_closest),
|
|
921
|
+
jnp.array(control_points),
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
self.mask = mask
|
|
925
|
+
|
|
926
|
+
if not converged.all():
|
|
927
|
+
print(f"Warning: {np.sum(~converged)} out of {len(converged)} projection points did not converge.")
|
|
928
|
+
print("Initial guess for these points was:", para_coords_closest[~converged])
|
|
929
|
+
print("Final parameter coordinates for these points were:", final_coords[~converged])
|
|
930
|
+
print("Final residuals for these points were:", final_res[~converged])
|
|
931
|
+
print("Final iteration counts for these points were:", final_i[~converged])
|
|
932
|
+
print("Jacobian for these points was:", J[~converged])
|
|
933
|
+
|
|
934
|
+
outputs["para_coords"] = jax.device_get(final_coords)
|
|
935
|
+
|
|
936
|
+
if __name__ == "__main__":
|
|
937
|
+
import numpy as np
|
|
938
|
+
np.random.seed(42) # For reproducibility
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
rec = csdl.Recorder(inline=True)
|
|
942
|
+
rec.start()
|
|
943
|
+
|
|
944
|
+
# Define the B-spline space parameters
|
|
945
|
+
num_cp_x = 10
|
|
946
|
+
num_cp_y = 8
|
|
947
|
+
nx = num_cp_x - 1 # Number of control points - 1
|
|
948
|
+
ny = num_cp_y - 1 # Number of control points - 1
|
|
949
|
+
px = 3 # Degree of the B-spline
|
|
950
|
+
py = 2 # Degree of the B-spline
|
|
951
|
+
p = (px, py)
|
|
952
|
+
coefficients_shape = (num_cp_x, num_cp_y)
|
|
953
|
+
derivative_orders = (1, 0) # # derivatives for the evaluation
|
|
954
|
+
|
|
955
|
+
knots_x = np.concatenate(
|
|
956
|
+
[np.zeros(px),
|
|
957
|
+
np.linspace(0, 1, num_cp_x - px + 1),
|
|
958
|
+
np.ones(px)]
|
|
959
|
+
)
|
|
960
|
+
knots_y = np.concatenate(
|
|
961
|
+
[np.zeros(py),
|
|
962
|
+
np.linspace(0, 1, num_cp_y - py + 1),
|
|
963
|
+
np.ones(py)]
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
# Make knot vectors hashable Python tuples so they can be used as static
|
|
967
|
+
# arguments to jax.jit. Functions in this module convert them back to
|
|
968
|
+
# JAX arrays when needed.
|
|
969
|
+
knots = (
|
|
970
|
+
tuple(knots_x.tolist()),
|
|
971
|
+
tuple(knots_y.tolist()),
|
|
972
|
+
)
|
|
973
|
+
knots_jnp = (jnp.array(knots_x), jnp.array(knots_y)) # JAX-compatible knot vectors
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
test_dv = csdl.Variable(
|
|
977
|
+
name="test_design_variable",
|
|
978
|
+
value=np.array([0.25, 0., 0.5]),
|
|
979
|
+
)
|
|
980
|
+
# test_dv.set_as_design_variable()
|
|
981
|
+
|
|
982
|
+
coeffs_x, coeffs_y = np.meshgrid(np.linspace(0, 5, num_cp_x), np.linspace(0, 2, num_cp_y), indexing='ij')
|
|
983
|
+
coeffs = np.array(np.stack((coeffs_x, coeffs_y, 0.2 * np.random.rand(num_cp_x, num_cp_y)), axis=-1))
|
|
984
|
+
coeffs_jnp = jnp.array(coeffs)
|
|
985
|
+
coeffs_csdl = csdl.Variable(name="coefficients", value=coeffs)
|
|
986
|
+
coeffs_csdl = coeffs_csdl
|
|
987
|
+
# test_dv = csdl.expand(test_dv, out_shape=coeffs_csdl.shape, action="k->ijk")
|
|
988
|
+
# coeffs_csdl = coeffs_csdl + test_dv
|
|
989
|
+
coeffs_csdl.set_as_design_variable()
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
random_points_in_space = np.random.rand(100, 3) # [1, :].reshape(-1, 3) # Random points in space
|
|
993
|
+
random_points_in_space[:, 0] *= 5
|
|
994
|
+
random_points_in_space[:, 1] *= 2
|
|
995
|
+
random_points_in_space[:, 2] = 5
|
|
996
|
+
|
|
997
|
+
random_points_in_space_csdl = csdl.Variable(name="points_to_project", value=random_points_in_space)
|
|
998
|
+
random_points_in_space_csdl.set_as_design_variable()
|
|
999
|
+
|
|
1000
|
+
projection_op = ProjectionOperation(
|
|
1001
|
+
num_parametric_dimensions=2,
|
|
1002
|
+
degrees=p,
|
|
1003
|
+
knots=knots,
|
|
1004
|
+
num_grid_search_points=100,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
uvs = projection_op.evaluate(
|
|
1008
|
+
points=random_points_in_space_csdl,
|
|
1009
|
+
control_points=coeffs_csdl,
|
|
1010
|
+
)
|
|
1011
|
+
uvs_sum = csdl.sum(uvs)
|
|
1012
|
+
d_uvs_sum_d_coeffs = csdl.derivative(uvs_sum, coeffs_csdl)
|
|
1013
|
+
d_uvs_sum_d_coeffs_sum = csdl.sum(d_uvs_sum_d_coeffs)
|
|
1014
|
+
d_uvs_sum_d_coeffs_sum.name = "projection_derivative (objective)"
|
|
1015
|
+
d_uvs_sum_d_coeffs_sum.set_as_objective()
|
|
1016
|
+
|
|
1017
|
+
sim = csdl.experimental.JaxSimulator(recorder=rec, gpu=False)
|
|
1018
|
+
# sim = csdl.experimental.PySimulator(recorder=rec)
|
|
1019
|
+
sim.check_optimization_derivatives(step_size=1e-8, raise_on_error=False)
|
|
1020
|
+
|
|
1021
|
+
print("Parametric coordinates:", uvs.value)
|
|
1022
|
+
|