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,183 @@
|
|
|
1
|
+
import csdl_alpha as csdl
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from typing import Union
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Optimization:
|
|
9
|
+
objective:csdl.Variable=None
|
|
10
|
+
design_variables:list[csdl.Variable]=None
|
|
11
|
+
constraints:list[csdl.Variable]=None
|
|
12
|
+
constraint_penalties:list[Union[float,np.ndarray]]=None
|
|
13
|
+
state_residual_pairs:list[tuple[csdl.Variable,csdl.Variable]]=None
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
if self.design_variables is None:
|
|
17
|
+
self.design_variables = []
|
|
18
|
+
if self.constraints is None:
|
|
19
|
+
self.constraints = []
|
|
20
|
+
if self.constraint_penalties is None:
|
|
21
|
+
self.constraint_penalties = []
|
|
22
|
+
if self.state_residual_pairs is None:
|
|
23
|
+
self.state_residual_pairs = []
|
|
24
|
+
|
|
25
|
+
self.design_variable_initial_values = []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_objective(self, objective:csdl.Variable):
|
|
29
|
+
'''
|
|
30
|
+
Add the objective variable to the optimization problem.
|
|
31
|
+
'''
|
|
32
|
+
self.objective = objective
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_design_variable(self, design_variable:csdl.Variable, initial_value:Union[csdl.Variable,float,np.ndarray]=None):
|
|
36
|
+
'''
|
|
37
|
+
Add a design variable to the optimization problem.
|
|
38
|
+
'''
|
|
39
|
+
self.design_variables.append(design_variable)
|
|
40
|
+
self.design_variable_initial_values.append(initial_value)
|
|
41
|
+
if initial_value is not None:
|
|
42
|
+
if isinstance(initial_value, csdl.Variable):
|
|
43
|
+
design_variable.set_value(initial_value.value)
|
|
44
|
+
else:
|
|
45
|
+
design_variable.set_value(initial_value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def add_constraint(self, constraint:csdl.Variable, penalty:Union[float,np.ndarray,csdl.Variable]=None):
|
|
49
|
+
'''
|
|
50
|
+
Add a constraint to the optimization problem.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
constraint : csdl.Variable
|
|
55
|
+
The constraint to be added.
|
|
56
|
+
penalty : Union[float,np.ndarray,csdl.Variable], optional
|
|
57
|
+
The penalty for the constraint, by default None. If None, the penalty is treated as a lagrange multiplier.
|
|
58
|
+
If a penalty is given, it is used as the penalty scaling factor for a quadratic constraint penalty.
|
|
59
|
+
'''
|
|
60
|
+
self.constraints.append(constraint)
|
|
61
|
+
self.constraint_penalties.append(penalty)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def compute_lagrangian(self):
|
|
65
|
+
'''
|
|
66
|
+
Constructs the CSDL variable for the lagrangian of the optimization problem.
|
|
67
|
+
'''
|
|
68
|
+
self.lagrange_multipliers = []
|
|
69
|
+
lagrangian = self.objective
|
|
70
|
+
for constraint, penalty in zip(self.constraints, self.constraint_penalties):
|
|
71
|
+
if penalty is not None:
|
|
72
|
+
lagrangian += penalty*constraint
|
|
73
|
+
self.lagrange_multipliers.append(None)
|
|
74
|
+
else:
|
|
75
|
+
constraint_lagrange_multipliers = csdl.Variable(shape=(constraint.size,), value=0.,
|
|
76
|
+
name=f'{constraint.name}_lagrange_multipliers')
|
|
77
|
+
self.lagrange_multipliers.append(constraint_lagrange_multipliers)
|
|
78
|
+
lagrangian = lagrangian + csdl.vdot(constraint_lagrange_multipliers, constraint)
|
|
79
|
+
self.lagrangian = lagrangian
|
|
80
|
+
return lagrangian
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_objective_gradient(self, objective:csdl.Variable=None, design_variables:list[csdl.Variable]=None):
|
|
84
|
+
'''
|
|
85
|
+
Constructs the CSDL variable for the objective gradient wrt each design variable.
|
|
86
|
+
'''
|
|
87
|
+
if objective is None:
|
|
88
|
+
objective = self.objective
|
|
89
|
+
if design_variables is None:
|
|
90
|
+
design_variables = self.design_variables
|
|
91
|
+
|
|
92
|
+
df_dx = csdl.derivative(self.objective, self.design_variables)
|
|
93
|
+
self.df_dx = df_dx
|
|
94
|
+
return df_dx
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def compute_lagrangian_gradient(self, lagrangian:csdl.Variable=None, design_variables:list[csdl.Variable]=None):
|
|
98
|
+
'''
|
|
99
|
+
Constructs the CSDL variable for the lagrangian gradient wrt each design variable.
|
|
100
|
+
'''
|
|
101
|
+
if lagrangian is None:
|
|
102
|
+
lagrangian = self.lagrangian
|
|
103
|
+
if design_variables is None:
|
|
104
|
+
design_variables = self.design_variables
|
|
105
|
+
|
|
106
|
+
dL_dx = csdl.derivative(self.lagrangian, self.design_variables, loop=False)
|
|
107
|
+
self.dL_dx = dL_dx
|
|
108
|
+
return dL_dx
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compute_constraint_jacobian(self, constraints:list[csdl.Variable]=None, design_variables:list[csdl.Variable]=None):
|
|
112
|
+
'''
|
|
113
|
+
Constructs the CSDL variables for the jacobian of each constraint wrt each design variable
|
|
114
|
+
'''
|
|
115
|
+
if constraints is None:
|
|
116
|
+
constraints = self.constraints
|
|
117
|
+
if design_variables is None:
|
|
118
|
+
design_variables = self.design_variables
|
|
119
|
+
|
|
120
|
+
dc_dx = csdl.derivative(self.constraints, self.design_variables)
|
|
121
|
+
self.dc_dx = dc_dx
|
|
122
|
+
return dc_dx
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def setup(self):
|
|
126
|
+
'''
|
|
127
|
+
Sets up the optimization problem as an implicit model to drive the gradient to 0.
|
|
128
|
+
'''
|
|
129
|
+
if self.objective is not None:
|
|
130
|
+
lagrangian = self.compute_lagrangian()
|
|
131
|
+
dL_dx = self.compute_lagrangian_gradient(lagrangian=lagrangian, design_variables=self.design_variables)
|
|
132
|
+
|
|
133
|
+
for constraint, constraint_lagrange_multipliers in zip(self.constraints, self.lagrange_multipliers):
|
|
134
|
+
if constraint_lagrange_multipliers is not None:
|
|
135
|
+
self.state_residual_pairs.append((constraint_lagrange_multipliers, constraint))
|
|
136
|
+
|
|
137
|
+
for i, design_variable in enumerate(self.design_variables):
|
|
138
|
+
residual = dL_dx[design_variable].reshape((design_variable.size,))
|
|
139
|
+
residual.add_name(f'{design_variable.name}_residual')
|
|
140
|
+
if self.design_variable_initial_values[i] is not None:
|
|
141
|
+
self.state_residual_pairs.append(((design_variable, self.design_variable_initial_values[i]), residual))
|
|
142
|
+
else:
|
|
143
|
+
self.state_residual_pairs.append((design_variable, residual))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NewtonOptimizer:
|
|
147
|
+
'''
|
|
148
|
+
A Newton Optimizer class.
|
|
149
|
+
|
|
150
|
+
NOTE: This is a temporary implementation until integration with the CSDL solvers is done
|
|
151
|
+
(the CSDL solvers need the add_optimization functionality)
|
|
152
|
+
'''
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
# self.solver = csdl.nonlinear_solvers.Newton()
|
|
155
|
+
self.solver = csdl.nonlinear_solvers.Newton(residual_jac_kwargs={"loop": True, "concatenate_ofs": True})
|
|
156
|
+
self.has_been_setup = False
|
|
157
|
+
|
|
158
|
+
def add_optimization(self, optimization:Optimization):
|
|
159
|
+
'''
|
|
160
|
+
Add an optimization problem to the optimizer.
|
|
161
|
+
'''
|
|
162
|
+
self.optimization = optimization
|
|
163
|
+
|
|
164
|
+
def setup(self):
|
|
165
|
+
self.optimization.setup()
|
|
166
|
+
for state, residual in self.optimization.state_residual_pairs:
|
|
167
|
+
if isinstance(state, tuple):
|
|
168
|
+
self.solver.add_state(state[0], residual, initial_value=state[1])
|
|
169
|
+
else:
|
|
170
|
+
if state.shape != residual.shape:
|
|
171
|
+
residual = residual.reshape(state.shape)
|
|
172
|
+
self.solver.add_state(state, residual)
|
|
173
|
+
self.has_been_setup = True
|
|
174
|
+
|
|
175
|
+
def run(self):
|
|
176
|
+
'''
|
|
177
|
+
Runs the Newton Optimization.
|
|
178
|
+
|
|
179
|
+
NOTE: CSDL state/design variables are already updated and therefore are not needed to be returned.
|
|
180
|
+
'''
|
|
181
|
+
if not self.has_been_setup:
|
|
182
|
+
self.setup()
|
|
183
|
+
self.solver.run()
|
|
File without changes
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""B-spline function space representation and operations (pure Python)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Optional, Sequence, Tuple, Union
|
|
7
|
+
|
|
8
|
+
import csdl_alpha as csdl
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
import numpy as np
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
import scipy.sparse as sps
|
|
14
|
+
from scipy.spatial import cKDTree
|
|
15
|
+
|
|
16
|
+
import lsdo_function_spaces as lfs
|
|
17
|
+
from lsdo_function_spaces.core.function_space import LinearFunctionSpace
|
|
18
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.b_spline_csdl_custom_ops import (
|
|
19
|
+
BasisMatrixCustomOp,
|
|
20
|
+
BSplineEvalCustomOp,
|
|
21
|
+
)
|
|
22
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.b_spline_patch_projection import (
|
|
23
|
+
compute_point_to_bspline_projection,
|
|
24
|
+
)
|
|
25
|
+
from lsdo_function_spaces.core.spaces.non_cython_bsplines.compute_basis_matrix_numpy import (
|
|
26
|
+
compute_basis_matrix_numpy,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BSplineSpace(LinearFunctionSpace):
|
|
31
|
+
"""B-spline Function Space for curves, surfaces, and trivariate volumes.
|
|
32
|
+
|
|
33
|
+
Inherits from :class:`LinearFunctionSpace`. Pure Python implementation
|
|
34
|
+
accelerated with NumPy, SciPy, and JAX (no Cython required).
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
num_parametric_dimensions : int
|
|
39
|
+
The number of parametric dimensions (1 for curve, 2 for surface, 3 for volume).
|
|
40
|
+
degree : Union[int, Tuple[int, ...]]
|
|
41
|
+
Polynomial degree of the B-spline basis in each parametric dimension.
|
|
42
|
+
coefficients_shape : Tuple[int, ...]
|
|
43
|
+
Shape of control points / coefficients in each parametric dimension.
|
|
44
|
+
knots : Optional[Union[Tuple[np.ndarray, ...], np.ndarray]], optional
|
|
45
|
+
Knot vectors for each parametric dimension. If None, open uniform knot
|
|
46
|
+
vectors on [0, 1] are automatically generated.
|
|
47
|
+
knot_indices : Optional[List[np.ndarray]], optional
|
|
48
|
+
Indices of knots per dimension (maintained for backwards compatibility).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
num_parametric_dimensions: int,
|
|
54
|
+
degree: Union[int, Tuple[int, ...]],
|
|
55
|
+
coefficients_shape: Tuple[int, ...],
|
|
56
|
+
knots: Optional[Union[Tuple[np.ndarray, ...], np.ndarray]] = None,
|
|
57
|
+
knot_indices: Optional[Sequence[np.ndarray]] = None,
|
|
58
|
+
):
|
|
59
|
+
self.degree = degree
|
|
60
|
+
self.knots = knots
|
|
61
|
+
self.knot_indices = list(knot_indices) if knot_indices is not None else None
|
|
62
|
+
super().__init__(num_parametric_dimensions, coefficients_shape)
|
|
63
|
+
|
|
64
|
+
if isinstance(self.degree, int):
|
|
65
|
+
self.degree = (self.degree,) * self.num_parametric_dimensions
|
|
66
|
+
|
|
67
|
+
for i in range(self.num_parametric_dimensions):
|
|
68
|
+
if self.degree[i] < 0:
|
|
69
|
+
raise ValueError(f"Degree in axis {i} must be non-negative.")
|
|
70
|
+
if self.degree[i] >= self.coefficients_shape[i]:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Degree in axis {i} must be less than the number of coefficients in each dimension."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Handle 1D concatenated knot vectors for backward compatibility
|
|
76
|
+
if self.knots is not None and isinstance(self.knots, np.ndarray) and self.knots.ndim == 1:
|
|
77
|
+
split_knots = []
|
|
78
|
+
idx = 0
|
|
79
|
+
for i in range(self.num_parametric_dimensions):
|
|
80
|
+
n_knots = self.coefficients_shape[i] + self.degree[i] + 1
|
|
81
|
+
split_knots.append(self.knots[idx : idx + n_knots])
|
|
82
|
+
idx += n_knots
|
|
83
|
+
self.knots = tuple(split_knots)
|
|
84
|
+
elif self.knots is not None and isinstance(self.knots, (list, tuple)):
|
|
85
|
+
self.knots = tuple(np.asarray(k, dtype=float) for k in self.knots)
|
|
86
|
+
|
|
87
|
+
if self.knots is None:
|
|
88
|
+
# Create open uniform knot vectors on [0, 1] for each dimension
|
|
89
|
+
self.knots = tuple(
|
|
90
|
+
np.concatenate([
|
|
91
|
+
np.zeros(self.degree[i]),
|
|
92
|
+
np.linspace(0, 1, self.coefficients_shape[i] - self.degree[i] + 1),
|
|
93
|
+
np.ones(self.degree[i]),
|
|
94
|
+
])
|
|
95
|
+
for i in range(self.num_parametric_dimensions)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.knot_indices is None:
|
|
99
|
+
self.knot_indices = []
|
|
100
|
+
knot_index = 0
|
|
101
|
+
for i in range(self.num_parametric_dimensions):
|
|
102
|
+
num_knots_i = self.coefficients_shape[i] + self.degree[i] + 1
|
|
103
|
+
self.knot_indices.append(np.arange(knot_index, knot_index + num_knots_i))
|
|
104
|
+
knot_index += num_knots_i
|
|
105
|
+
|
|
106
|
+
def _evaluate(
|
|
107
|
+
self,
|
|
108
|
+
coefficients: Union[np.ndarray, csdl.Variable],
|
|
109
|
+
parametric_coordinates: Union[np.ndarray, csdl.Variable],
|
|
110
|
+
parametric_derivative_orders: Optional[Tuple[int, ...]] = None,
|
|
111
|
+
) -> Union[np.ndarray, csdl.Variable]:
|
|
112
|
+
"""Evaluate B-spline functions at the given parametric coordinates."""
|
|
113
|
+
if not isinstance(coefficients, (np.ndarray, csdl.Variable)):
|
|
114
|
+
raise TypeError(
|
|
115
|
+
f"coefficients must be a numpy array or a CSDL variable, "
|
|
116
|
+
f"but got type {type(coefficients)}."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not isinstance(parametric_coordinates, (np.ndarray, csdl.Variable)):
|
|
120
|
+
raise TypeError(
|
|
121
|
+
f"parametric_coordinates must be a numpy array or a CSDL variable, "
|
|
122
|
+
f"but got type {type(parametric_coordinates)}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
|
|
127
|
+
except ValueError:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
|
|
130
|
+
f"but got shape {parametric_coordinates.shape}."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
non_csdl = isinstance(coefficients, np.ndarray)
|
|
134
|
+
|
|
135
|
+
if isinstance(parametric_coordinates, np.ndarray):
|
|
136
|
+
basis_matrix = compute_basis_matrix_numpy(
|
|
137
|
+
us=parametric_coordinates,
|
|
138
|
+
degrees=self.degree,
|
|
139
|
+
knot_vectors=self.knots,
|
|
140
|
+
der_orders=parametric_derivative_orders,
|
|
141
|
+
)
|
|
142
|
+
if coefficients.shape != (basis_matrix.shape[1], coefficients.size // basis_matrix.shape[1]):
|
|
143
|
+
coefficients = coefficients.reshape(
|
|
144
|
+
(basis_matrix.shape[1], coefficients.size // basis_matrix.shape[1])
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if non_csdl:
|
|
148
|
+
values = basis_matrix @ coefficients
|
|
149
|
+
if values.shape[0] == 1:
|
|
150
|
+
values = values.flatten()
|
|
151
|
+
else:
|
|
152
|
+
values = csdl.Variable(value=np.zeros((basis_matrix.shape[0], coefficients.shape[1])))
|
|
153
|
+
for i in csdl.frange(coefficients.shape[1]):
|
|
154
|
+
coefficients_column = coefficients[:, i].reshape((coefficients.shape[0], 1))
|
|
155
|
+
values = values.set(
|
|
156
|
+
csdl.slice[:, i],
|
|
157
|
+
csdl.sparse.matvec(basis_matrix, coefficients_column).reshape(
|
|
158
|
+
(basis_matrix.shape[0],)
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
values = values.reshape((parametric_coordinates.shape[0], coefficients.shape[-1]))
|
|
162
|
+
|
|
163
|
+
return values
|
|
164
|
+
|
|
165
|
+
else:
|
|
166
|
+
b_spline_eval_op = BSplineEvalCustomOp(
|
|
167
|
+
knots=self.knots,
|
|
168
|
+
degree=self.degree,
|
|
169
|
+
coefficients_shape=self.coefficients_shape,
|
|
170
|
+
der_orders=parametric_derivative_orders,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
values = b_spline_eval_op.evaluate(
|
|
174
|
+
parametric_coordinates=parametric_coordinates,
|
|
175
|
+
coefficients=coefficients,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if non_csdl:
|
|
179
|
+
values = values.value
|
|
180
|
+
|
|
181
|
+
return values
|
|
182
|
+
|
|
183
|
+
def _generate_parametric_grid(self, knot_vectors: Sequence[np.ndarray], N: int) -> np.ndarray:
|
|
184
|
+
"""Generate a tensor-grid of parametric sample points."""
|
|
185
|
+
samples_1d = []
|
|
186
|
+
for U in knot_vectors:
|
|
187
|
+
knots = np.unique(U)
|
|
188
|
+
pts = []
|
|
189
|
+
for j in range(len(knots) - 1):
|
|
190
|
+
a, b = knots[j], knots[j + 1]
|
|
191
|
+
pts.append(np.linspace(a, b, N, endpoint=False))
|
|
192
|
+
pts.append(np.array([knots[-1]]))
|
|
193
|
+
samples_1d.append(np.concatenate(pts))
|
|
194
|
+
|
|
195
|
+
mesh = np.meshgrid(*samples_1d, indexing="ij")
|
|
196
|
+
coord_arrays = [m.flatten() for m in mesh]
|
|
197
|
+
grid = np.stack(coord_arrays, axis=-1)
|
|
198
|
+
return grid
|
|
199
|
+
|
|
200
|
+
def _project(
|
|
201
|
+
self,
|
|
202
|
+
points_in_space: Union[np.ndarray, csdl.Variable],
|
|
203
|
+
coefficients: Union[np.ndarray, csdl.Variable],
|
|
204
|
+
plot: bool = False,
|
|
205
|
+
grid_search_density: int = 100,
|
|
206
|
+
) -> np.ndarray:
|
|
207
|
+
"""Project points in physical space onto the B-spline entity."""
|
|
208
|
+
if isinstance(coefficients, csdl.Variable):
|
|
209
|
+
coefficients = coefficients.value
|
|
210
|
+
|
|
211
|
+
if not isinstance(points_in_space, (np.ndarray, csdl.Variable)):
|
|
212
|
+
raise TypeError(
|
|
213
|
+
f"points_in_space must be a numpy array or a CSDL variable, "
|
|
214
|
+
f"but got type {type(points_in_space)}."
|
|
215
|
+
)
|
|
216
|
+
if isinstance(points_in_space, csdl.Variable):
|
|
217
|
+
raise NotImplementedError(
|
|
218
|
+
"Projection of CSDL variables is not implemented yet. "
|
|
219
|
+
"Please provide a numpy array of points in space."
|
|
220
|
+
)
|
|
221
|
+
fun = lfs.Function(
|
|
222
|
+
space=self,
|
|
223
|
+
coefficients=coefficients,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
para_grid = self._generate_parametric_grid(
|
|
227
|
+
knot_vectors=self.knots,
|
|
228
|
+
N=grid_search_density,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
basis_mat = compute_basis_matrix_numpy(
|
|
232
|
+
us=para_grid,
|
|
233
|
+
degrees=self.degree,
|
|
234
|
+
knot_vectors=self.knots,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
surface_grid = basis_mat @ coefficients.reshape(-1, coefficients.shape[-1])
|
|
238
|
+
|
|
239
|
+
kd_tree = cKDTree(surface_grid)
|
|
240
|
+
nearest_index = kd_tree.query(points_in_space, k=1)[1]
|
|
241
|
+
nearest_para_points = para_grid[nearest_index]
|
|
242
|
+
|
|
243
|
+
batched_projection = jax.jit(
|
|
244
|
+
jax.vmap(
|
|
245
|
+
lambda pt, u0, cps: compute_point_to_bspline_projection(
|
|
246
|
+
point=pt,
|
|
247
|
+
degrees=self.degree,
|
|
248
|
+
coefficients=cps,
|
|
249
|
+
para_coords=u0,
|
|
250
|
+
knots=tuple([jnp.array(kv_i) for kv_i in self.knots]),
|
|
251
|
+
),
|
|
252
|
+
in_axes=(0, 0, None),
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
para, res, converged, final_i, J, _, _ = batched_projection(
|
|
257
|
+
points_in_space,
|
|
258
|
+
nearest_para_points,
|
|
259
|
+
coefficients,
|
|
260
|
+
)
|
|
261
|
+
para = np.array(para).reshape(-1, self.num_parametric_dimensions)
|
|
262
|
+
|
|
263
|
+
if not converged.all():
|
|
264
|
+
warnings.warn(
|
|
265
|
+
f"{np.sum(~converged)} out of {len(converged)} projection points did not fully converge.",
|
|
266
|
+
UserWarning,
|
|
267
|
+
stacklevel=2,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if plot:
|
|
271
|
+
point_cloud = lfs.plot_points(
|
|
272
|
+
points=points_in_space,
|
|
273
|
+
color="#00FF1A",
|
|
274
|
+
opacity=0.5,
|
|
275
|
+
size=8,
|
|
276
|
+
show=False,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
projected_points = fun.evaluate(
|
|
280
|
+
parametric_coordinates=para,
|
|
281
|
+
).value
|
|
282
|
+
project_point_cloud = lfs.plot_points(
|
|
283
|
+
points=projected_points,
|
|
284
|
+
color="#FF0000",
|
|
285
|
+
size=4,
|
|
286
|
+
show=False,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
fun.plot(additional_plotting_elements=[point_cloud, project_point_cloud])
|
|
290
|
+
|
|
291
|
+
return para
|
|
292
|
+
|
|
293
|
+
def compute_basis_matrix(
|
|
294
|
+
self,
|
|
295
|
+
parametric_coordinates: Union[np.ndarray, csdl.Variable],
|
|
296
|
+
parametric_derivative_orders: Optional[Tuple[int, ...]] = None,
|
|
297
|
+
expansion_factor: Optional[int] = None,
|
|
298
|
+
) -> Union[sps.coo_matrix, csdl.Variable]:
|
|
299
|
+
"""Compute the B-spline basis matrix for given parametric coordinates."""
|
|
300
|
+
if isinstance(parametric_coordinates, csdl.Variable):
|
|
301
|
+
basis_mat_custom_op = BasisMatrixCustomOp(
|
|
302
|
+
knots=self.knots,
|
|
303
|
+
degree=self.degree,
|
|
304
|
+
coefficients_shape=self.coefficients_shape,
|
|
305
|
+
der_orders=parametric_derivative_orders,
|
|
306
|
+
)
|
|
307
|
+
try:
|
|
308
|
+
parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
|
|
309
|
+
except ValueError:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
|
|
312
|
+
f"but got shape {parametric_coordinates.shape}."
|
|
313
|
+
)
|
|
314
|
+
return basis_mat_custom_op.evaluate(parametric_coordinates)
|
|
315
|
+
|
|
316
|
+
elif isinstance(parametric_coordinates, np.ndarray):
|
|
317
|
+
try:
|
|
318
|
+
parametric_coordinates = parametric_coordinates.reshape(-1, self.num_parametric_dimensions)
|
|
319
|
+
except ValueError:
|
|
320
|
+
raise ValueError(
|
|
321
|
+
f"parametric_coordinates must have shape (num_points, {self.num_parametric_dimensions}), "
|
|
322
|
+
f"but got shape {parametric_coordinates.shape}."
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
res = compute_basis_matrix_numpy(
|
|
326
|
+
us=parametric_coordinates,
|
|
327
|
+
degrees=self.degree,
|
|
328
|
+
knot_vectors=self.knots,
|
|
329
|
+
der_orders=parametric_derivative_orders,
|
|
330
|
+
)
|
|
331
|
+
if expansion_factor is not None and expansion_factor > 1:
|
|
332
|
+
res = sps.kron(res, sps.eye(expansion_factor), format='csr')
|
|
333
|
+
return res
|
|
334
|
+
|
|
335
|
+
else:
|
|
336
|
+
raise TypeError(
|
|
337
|
+
f"parametric_coordinates must be a numpy array or a CSDL variable, "
|
|
338
|
+
f"but got type {type(parametric_coordinates)}."
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _compute_distance_bounds(
|
|
342
|
+
self, point: np.ndarray, function: lfs.Function, direction: Optional[np.ndarray] = None
|
|
343
|
+
) -> float:
|
|
344
|
+
"""Compute distance bounds for a given point relative to function bounding box."""
|
|
345
|
+
if not hasattr(function, "bounding_box"):
|
|
346
|
+
coefficients = function.coefficients.value.reshape((-1, function.num_physical_dimensions))
|
|
347
|
+
function.bounding_box = np.zeros((2, coefficients.shape[-1]))
|
|
348
|
+
if self.num_parametric_dimensions == 1:
|
|
349
|
+
function.bounding_box[0, 0] = np.min(coefficients)
|
|
350
|
+
function.bounding_box[1, 0] = np.max(coefficients)
|
|
351
|
+
else:
|
|
352
|
+
function.bounding_box[0, :] = np.min(coefficients, axis=0)
|
|
353
|
+
function.bounding_box[1, :] = np.max(coefficients, axis=0)
|
|
354
|
+
|
|
355
|
+
if direction is None:
|
|
356
|
+
neg = function.bounding_box[0] - point
|
|
357
|
+
pos = point - function.bounding_box[1]
|
|
358
|
+
distance_vector = np.maximum(np.maximum(neg, pos), 0)
|
|
359
|
+
return float(np.linalg.norm(distance_vector))
|
|
360
|
+
else:
|
|
361
|
+
closest_point = np.zeros((len(point),))
|
|
362
|
+
for i in range(len(point)):
|
|
363
|
+
if point[i] < function.bounding_box[0, i]:
|
|
364
|
+
closest_point[i] = function.bounding_box[0, i]
|
|
365
|
+
elif point[i] > function.bounding_box[1, i]:
|
|
366
|
+
closest_point[i] = function.bounding_box[1, i]
|
|
367
|
+
else:
|
|
368
|
+
closest_point[i] = point[i]
|
|
369
|
+
t = np.dot(direction, (closest_point - point)) / np.dot(direction, direction)
|
|
370
|
+
closest_point_on_line = point + t * direction
|
|
371
|
+
return float(np.linalg.norm(closest_point_on_line - closest_point))
|
|
372
|
+
|
|
373
|
+
def stitch(self, self_face: int, self_coeffs: np.ndarray, other: BSplineSpace, other_face: int, other_coeffs: np.ndarray):
|
|
374
|
+
"""Stitch two B-spline function spaces along adjacent faces."""
|
|
375
|
+
ind_array = np.arange(np.prod(self.coefficients_shape)).reshape(self.coefficients_shape)
|
|
376
|
+
|
|
377
|
+
if len(self_coeffs.shape) > 2:
|
|
378
|
+
self_coeffs = self_coeffs.reshape((-1, self_coeffs.shape[-1]))
|
|
379
|
+
if len(other_coeffs.shape) > 2:
|
|
380
|
+
other_coeffs = other_coeffs.reshape((-1, other_coeffs.shape[-1]))
|
|
381
|
+
|
|
382
|
+
if self_face == 1:
|
|
383
|
+
self_inds = ind_array[:, 0]
|
|
384
|
+
elif self_face == 2:
|
|
385
|
+
self_inds = ind_array[-1, :]
|
|
386
|
+
elif self_face == 3:
|
|
387
|
+
self_inds = ind_array[:, -1]
|
|
388
|
+
elif self_face == 4:
|
|
389
|
+
self_inds = ind_array[0, :]
|
|
390
|
+
self_inds = [int(ind) for ind in self_inds]
|
|
391
|
+
|
|
392
|
+
if other_face == 1:
|
|
393
|
+
other_inds = ind_array[:, 0]
|
|
394
|
+
elif other_face == 2:
|
|
395
|
+
other_inds = ind_array[-1, :]
|
|
396
|
+
elif other_face == 3:
|
|
397
|
+
other_inds = ind_array[:, -1]
|
|
398
|
+
elif other_face == 4:
|
|
399
|
+
other_inds = ind_array[0, :]
|
|
400
|
+
other_inds = [int(ind) for ind in other_inds]
|
|
401
|
+
|
|
402
|
+
return self_inds, other_inds
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class BSplineSpaceNew(BSplineSpace):
|
|
406
|
+
"""Deprecated alias for :class:`BSplineSpace`.
|
|
407
|
+
|
|
408
|
+
.. deprecated:: 1.0.0
|
|
409
|
+
Use :class:`BSplineSpace` instead.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
def __init__(self, *args, **kwargs):
|
|
413
|
+
warnings.warn(
|
|
414
|
+
"BSplineSpaceNew is deprecated; use BSplineSpace instead.",
|
|
415
|
+
DeprecationWarning,
|
|
416
|
+
stacklevel=2,
|
|
417
|
+
)
|
|
418
|
+
super().__init__(*args, **kwargs)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.sparse as sps
|
|
3
|
+
from ..function_space import LinearFunctionSpace
|
|
4
|
+
from scipy.spatial.distance import cdist
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
class ConditionalSpace(LinearFunctionSpace):
|
|
9
|
+
"""
|
|
10
|
+
Conditional Function Space.
|
|
11
|
+
|
|
12
|
+
This function space applies a user-defined condition predicate to
|
|
13
|
+
evaluate basis representations conditionally in parametric space.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
num_parametric_dimensions : int
|
|
18
|
+
The number of parametric dimensions.
|
|
19
|
+
condition : callable
|
|
20
|
+
Condition callable evaluated on coordinates.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, num_parametric_dimensions:int, condition:callable):
|
|
24
|
+
super().__init__(num_parametric_dimensions, (1,))
|
|
25
|
+
self.condition = condition
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compute_basis_matrix(self, parametric_coordinates:np.ndarray, parametric_derivative_orders: np.ndarray=None, expansion_factor:int=None) -> np.ndarray:
|
|
29
|
+
"""
|
|
30
|
+
Compute the basis matrix for the given parametric coordinates.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
parametric_coordinates : np.ndarray
|
|
35
|
+
The parametric coordinates for which to compute the basis matrix.
|
|
36
|
+
parametric_derivative_orders : np.ndarray, optional
|
|
37
|
+
The derivative orders of the parametric coordinates. Default is None.
|
|
38
|
+
expansion_factor : int, optional
|
|
39
|
+
The expansion factor. Default is None.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
np.ndarray
|
|
44
|
+
The computed basis matrix.
|
|
45
|
+
|
|
46
|
+
Raises
|
|
47
|
+
------
|
|
48
|
+
NotImplementedError
|
|
49
|
+
If parametric_derivative_orders or expansion_factor is not None.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
if parametric_derivative_orders is not None:
|
|
53
|
+
raise NotImplementedError('IDWFunctionSpace does not support derivatives')
|
|
54
|
+
if expansion_factor is not None:
|
|
55
|
+
raise NotImplementedError('IDWFunctionSpace does not support expansion factors')
|
|
56
|
+
|
|
57
|
+
if len(parametric_coordinates.shape) == 1:
|
|
58
|
+
parametric_coordinates = parametric_coordinates.reshape(1, -1)
|
|
59
|
+
|
|
60
|
+
weights = self.condition(parametric_coordinates).astype(int)
|
|
61
|
+
|
|
62
|
+
if len(weights.shape) == 1:
|
|
63
|
+
weights = weights.reshape(-1, 1)
|
|
64
|
+
|
|
65
|
+
return weights
|