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,484 @@
|
|
|
1
|
+
"""STEP file importer (patched).
|
|
2
|
+
|
|
3
|
+
Correctness fixes:
|
|
4
|
+
- Robust entity parsing across line wraps (entities end with ';')
|
|
5
|
+
- Correct float parsing for knots/control points (supports E/e and D/d exponents)
|
|
6
|
+
- Correct knot expansion with multiplicities
|
|
7
|
+
- Order-preserving control point lookup via id->xyz dictionary
|
|
8
|
+
|
|
9
|
+
Performance improvements:
|
|
10
|
+
- Single pass read: builds entity dict and point dict once
|
|
11
|
+
- Avoids pandas and global regex scans
|
|
12
|
+
- Caches BSplineSpace objects by a stable hash key
|
|
13
|
+
|
|
14
|
+
Intended primarily for OpenVSP-exported STEP files containing B_SPLINE_SURFACE_WITH_KNOTS.
|
|
15
|
+
|
|
16
|
+
Public API:
|
|
17
|
+
- import_file(...)
|
|
18
|
+
- _check_if_load_stored_import(...)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import pickle
|
|
26
|
+
import hashlib
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
import lsdo_function_spaces as lfs
|
|
32
|
+
import csdl_alpha as csdl
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------- Numeric parsing -----------------------------
|
|
36
|
+
|
|
37
|
+
_FLOAT_RE = re.compile(r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[EeDd][+-]?\d+)?")
|
|
38
|
+
_INT_RE = re.compile(r"[+-]?\d+")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _to_float(tok: str) -> float:
|
|
42
|
+
return float(tok.replace('D', 'E').replace('d', 'e'))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------- STEP parsing -------------------------------
|
|
46
|
+
|
|
47
|
+
def _read_step_entities(file_name: str) -> Dict[int, str]:
|
|
48
|
+
"""Return a dict {id: rhs_text} where rhs_text excludes the trailing ';'.
|
|
49
|
+
|
|
50
|
+
Handles entities that span multiple lines by accumulating until ';'.
|
|
51
|
+
"""
|
|
52
|
+
entities: Dict[int, str] = {}
|
|
53
|
+
cur_id: Optional[int] = None
|
|
54
|
+
buf: List[str] = []
|
|
55
|
+
|
|
56
|
+
with open(file_name, 'r') as f:
|
|
57
|
+
for raw_line in f:
|
|
58
|
+
line = raw_line.strip()
|
|
59
|
+
if not line:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if cur_id is None:
|
|
63
|
+
if not line.startswith('#'):
|
|
64
|
+
continue
|
|
65
|
+
eq = line.find('=')
|
|
66
|
+
if eq < 0:
|
|
67
|
+
continue
|
|
68
|
+
try:
|
|
69
|
+
cur_id = int(line[1:eq].strip())
|
|
70
|
+
except ValueError:
|
|
71
|
+
cur_id = None
|
|
72
|
+
continue
|
|
73
|
+
rest = line[eq+1:].strip()
|
|
74
|
+
buf = [rest]
|
|
75
|
+
else:
|
|
76
|
+
buf.append(line)
|
|
77
|
+
|
|
78
|
+
if buf and buf[-1].endswith(';'):
|
|
79
|
+
joined = ' '.join(buf).strip()
|
|
80
|
+
joined = joined[:-1].strip() # drop trailing ';'
|
|
81
|
+
entities[cur_id] = joined
|
|
82
|
+
cur_id = None
|
|
83
|
+
buf = []
|
|
84
|
+
|
|
85
|
+
return entities
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _split_top_level_commas(s: str) -> List[str]:
|
|
89
|
+
"""Split by commas not inside parentheses or quoted strings."""
|
|
90
|
+
out: List[str] = []
|
|
91
|
+
depth = 0
|
|
92
|
+
start = 0
|
|
93
|
+
in_quote = False
|
|
94
|
+
i = 0
|
|
95
|
+
while i < len(s):
|
|
96
|
+
ch = s[i]
|
|
97
|
+
if ch == "'":
|
|
98
|
+
if in_quote:
|
|
99
|
+
if i + 1 < len(s) and s[i + 1] == "'":
|
|
100
|
+
i += 2
|
|
101
|
+
continue
|
|
102
|
+
in_quote = False
|
|
103
|
+
else:
|
|
104
|
+
in_quote = True
|
|
105
|
+
elif not in_quote:
|
|
106
|
+
if ch == '(':
|
|
107
|
+
depth += 1
|
|
108
|
+
elif ch == ')':
|
|
109
|
+
depth -= 1
|
|
110
|
+
elif ch == ',' and depth == 0:
|
|
111
|
+
out.append(s[start:i].strip())
|
|
112
|
+
start = i + 1
|
|
113
|
+
i += 1
|
|
114
|
+
out.append(s[start:].strip())
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _strip_outer_parens(s: str) -> str:
|
|
119
|
+
s = s.strip()
|
|
120
|
+
if s.startswith('(') and s.endswith(')'):
|
|
121
|
+
return s[1:-1].strip()
|
|
122
|
+
return s
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_int_list(step_list: str) -> List[int]:
|
|
126
|
+
body = _strip_outer_parens(step_list)
|
|
127
|
+
toks = _INT_RE.findall(body)
|
|
128
|
+
return [int(t) for t in toks]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _parse_float_list(step_list: str) -> List[float]:
|
|
132
|
+
body = _strip_outer_parens(step_list)
|
|
133
|
+
toks = _FLOAT_RE.findall(body)
|
|
134
|
+
return [_to_float(t) for t in toks]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _parse_cartesian_point(rhs: str) -> Optional[np.ndarray]:
|
|
138
|
+
"""Parse CARTESIAN_POINT('',(x,y,z)) -> np.array([x,y,z])"""
|
|
139
|
+
if not rhs.startswith('CARTESIAN_POINT'):
|
|
140
|
+
return None
|
|
141
|
+
# grab all floats; take the last 3
|
|
142
|
+
vals = [_to_float(t) for t in _FLOAT_RE.findall(rhs)]
|
|
143
|
+
if len(vals) < 3:
|
|
144
|
+
return None
|
|
145
|
+
return np.array(vals[-3:], dtype=float)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _parse_control_point_grid(field: str) -> np.ndarray:
|
|
149
|
+
"""Parse control point grid '((#1,#2),(#3,#4))' -> int array shape (nu,nv)."""
|
|
150
|
+
body = _strip_outer_parens(field)
|
|
151
|
+
rows = _split_top_level_commas(body)
|
|
152
|
+
grid: List[List[int]] = []
|
|
153
|
+
for r in rows:
|
|
154
|
+
r_body = _strip_outer_parens(r)
|
|
155
|
+
ids = [int(x[1:]) for x in re.findall(r"#\d+", r_body)]
|
|
156
|
+
if ids:
|
|
157
|
+
grid.append(ids)
|
|
158
|
+
if not grid:
|
|
159
|
+
raise ValueError('Failed to parse control point grid.')
|
|
160
|
+
# Ensure rectangular
|
|
161
|
+
n0 = len(grid[0])
|
|
162
|
+
for rr in grid:
|
|
163
|
+
if len(rr) != n0:
|
|
164
|
+
raise ValueError('Control point grid is not rectangular.')
|
|
165
|
+
return np.array(grid, dtype=int)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _detect_knot_fields(fields: List[str]) -> Tuple[List[int], List[int], List[float], List[float]]:
|
|
169
|
+
"""Identify (u_mults, v_mults, u_knots, v_knots) from argument fields.
|
|
170
|
+
|
|
171
|
+
OpenVSP typically uses indices 8..11, but exporters vary. We look for
|
|
172
|
+
two int-lists followed by two float-lists among the parenthesized fields.
|
|
173
|
+
"""
|
|
174
|
+
# First try the OpenVSP expected slots
|
|
175
|
+
try:
|
|
176
|
+
u_mults = _parse_int_list(fields[8])
|
|
177
|
+
v_mults = _parse_int_list(fields[9])
|
|
178
|
+
u_knots = _parse_float_list(fields[10])
|
|
179
|
+
v_knots = _parse_float_list(fields[11])
|
|
180
|
+
if len(u_mults) == len(u_knots) and len(v_mults) == len(v_knots):
|
|
181
|
+
return u_mults, v_mults, u_knots, v_knots
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
# Otherwise scan for pattern: int list, int list, float list, float list
|
|
186
|
+
parenth_idxs = [i for i, f in enumerate(fields) if f.strip().startswith('(')]
|
|
187
|
+
parsed_int: Dict[int, List[int]] = {}
|
|
188
|
+
parsed_float: Dict[int, List[float]] = {}
|
|
189
|
+
|
|
190
|
+
for i in parenth_idxs:
|
|
191
|
+
txt = fields[i]
|
|
192
|
+
# Heuristic: if contains '.' or 'E'/'D' treat as float list
|
|
193
|
+
if any(c in txt for c in ['.', 'E', 'e', 'D', 'd']):
|
|
194
|
+
try:
|
|
195
|
+
parsed_float[i] = _parse_float_list(txt)
|
|
196
|
+
except Exception:
|
|
197
|
+
continue
|
|
198
|
+
else:
|
|
199
|
+
try:
|
|
200
|
+
parsed_int[i] = _parse_int_list(txt)
|
|
201
|
+
except Exception:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
# Find int,int,float,float sequence
|
|
205
|
+
idxs = sorted(parenth_idxs)
|
|
206
|
+
for a in idxs:
|
|
207
|
+
if a not in parsed_int:
|
|
208
|
+
continue
|
|
209
|
+
for b in idxs:
|
|
210
|
+
if b <= a or b not in parsed_int:
|
|
211
|
+
continue
|
|
212
|
+
for c in idxs:
|
|
213
|
+
if c <= b or c not in parsed_float:
|
|
214
|
+
continue
|
|
215
|
+
for d in idxs:
|
|
216
|
+
if d <= c or d not in parsed_float:
|
|
217
|
+
continue
|
|
218
|
+
u_mults = parsed_int[a]
|
|
219
|
+
v_mults = parsed_int[b]
|
|
220
|
+
u_knots = parsed_float[c]
|
|
221
|
+
v_knots = parsed_float[d]
|
|
222
|
+
if len(u_mults) == len(u_knots) and len(v_mults) == len(v_knots):
|
|
223
|
+
return u_mults, v_mults, u_knots, v_knots
|
|
224
|
+
|
|
225
|
+
raise ValueError('Could not locate knot and multiplicity fields in B_SPLINE_SURFACE_WITH_KNOTS.')
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _parse_bspline_surface_with_knots(rhs: str) -> Optional[Tuple[str, int, int, np.ndarray, List[int], List[int], List[float], List[float]]]:
|
|
229
|
+
"""Parse B_SPLINE_SURFACE_WITH_KNOTS entity RHS.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
(name, deg_u, deg_v, cp_id_grid, u_mults, v_mults, u_knots, v_knots)
|
|
233
|
+
"""
|
|
234
|
+
if not rhs.startswith('B_SPLINE_SURFACE_WITH_KNOTS'):
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
args = rhs[len('B_SPLINE_SURFACE_WITH_KNOTS'):].strip()
|
|
238
|
+
args = _strip_outer_parens(args)
|
|
239
|
+
fields = _split_top_level_commas(args)
|
|
240
|
+
if len(fields) < 8:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
name = fields[0].strip().strip("'")
|
|
244
|
+
deg_u = int(fields[1].strip())
|
|
245
|
+
deg_v = int(fields[2].strip())
|
|
246
|
+
|
|
247
|
+
cp_grid = _parse_control_point_grid(fields[3])
|
|
248
|
+
|
|
249
|
+
u_mults, v_mults, u_knots, v_knots = _detect_knot_fields(fields)
|
|
250
|
+
|
|
251
|
+
return name, deg_u, deg_v, cp_grid, u_mults, v_mults, u_knots, v_knots
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# ---------------------------- Knot helpers -------------------------------
|
|
255
|
+
|
|
256
|
+
def _expand_knots(base_knots: List[float], mults: List[int]) -> np.ndarray:
|
|
257
|
+
base = np.asarray(base_knots, dtype=float)
|
|
258
|
+
m = np.asarray(mults, dtype=int)
|
|
259
|
+
if base.shape[0] != m.shape[0]:
|
|
260
|
+
raise ValueError('Knot values and multiplicities must have same length.')
|
|
261
|
+
return np.repeat(base, m)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _normalize_knots_affine(k: np.ndarray, eps: float = 1e-14) -> np.ndarray:
|
|
265
|
+
"""Affine-map knots to [0,1] using endpoints. If degenerate, returns copy."""
|
|
266
|
+
k = np.asarray(k, dtype=float)
|
|
267
|
+
denom = k[-1] - k[0]
|
|
268
|
+
if abs(denom) < eps:
|
|
269
|
+
return k.copy()
|
|
270
|
+
return (k - k[0]) / denom
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _space_key(deg_u: int, deg_v: int, u_mults: List[int], v_mults: List[int], u_knots: List[float], v_knots: List[float]) -> str:
|
|
274
|
+
"""Create a stable key for caching BSplineSpace objects."""
|
|
275
|
+
payload = (
|
|
276
|
+
str(deg_u) + '|' + str(deg_v) + '|'
|
|
277
|
+
+ ','.join(map(str, u_mults)) + '|' + ','.join(map(str, v_mults)) + '|'
|
|
278
|
+
+ ','.join(f'{x:.17g}' for x in u_knots) + '|' + ','.join(f'{x:.17g}' for x in v_knots)
|
|
279
|
+
).encode('utf-8')
|
|
280
|
+
return hashlib.md5(payload).hexdigest()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# -----------------------------------------------------------------------------
|
|
284
|
+
# Stored import helper
|
|
285
|
+
# -----------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def _check_if_load_stored_import(
|
|
288
|
+
file_name: str,
|
|
289
|
+
name: str = 'geometry',
|
|
290
|
+
parallelize: bool = True,
|
|
291
|
+
) -> Optional[lfs.FunctionSet]:
|
|
292
|
+
"""Load a previously stored import if it exists.
|
|
293
|
+
|
|
294
|
+
Note: parallelize is retained for backward-compatibility with the old API.
|
|
295
|
+
"""
|
|
296
|
+
fn = os.path.basename(file_name)
|
|
297
|
+
fn_wo_ext = fn[:fn.rindex('.')]
|
|
298
|
+
file_path = f"stored_files/imports/{fn_wo_ext}_stored_import.pickle"
|
|
299
|
+
path = Path(file_path)
|
|
300
|
+
|
|
301
|
+
if not path.is_file():
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
with open(file_path, 'rb') as handle:
|
|
305
|
+
function_set = pickle.load(handle)
|
|
306
|
+
|
|
307
|
+
# Re-wrap coefficients as csdl Variables (pickle stores numpy arrays)
|
|
308
|
+
for function in function_set.functions.values():
|
|
309
|
+
function.coefficients = csdl.Variable(value=np.asarray(function.coefficients))
|
|
310
|
+
|
|
311
|
+
# Invalidate cache if it was built with an incompatible class.
|
|
312
|
+
for function in function_set.functions.values():
|
|
313
|
+
if not isinstance(function.space, lfs.BSplineSpace):
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
# Optionally rename set
|
|
317
|
+
if hasattr(function_set, 'name'):
|
|
318
|
+
function_set.name = name
|
|
319
|
+
|
|
320
|
+
return function_set
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# -----------------------------------------------------------------------------
|
|
324
|
+
# Public API
|
|
325
|
+
# -----------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
def import_file(
|
|
328
|
+
file_name: str,
|
|
329
|
+
parallelize: bool = True,
|
|
330
|
+
normalize_knots: bool = True,
|
|
331
|
+
name: str = 'imported_geometry',
|
|
332
|
+
) -> lfs.FunctionSet:
|
|
333
|
+
"""Import OpenVSP STEP file containing B_SPLINE_SURFACE_WITH_KNOTS.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
file_name : str
|
|
338
|
+
STEP file path.
|
|
339
|
+
parallelize : bool
|
|
340
|
+
Retained for compatibility; parsing is typically faster single-threaded.
|
|
341
|
+
normalize_knots : bool
|
|
342
|
+
If True, affine-normalize each knot vector to [0,1].
|
|
343
|
+
name : str
|
|
344
|
+
Name for the returned FunctionSet.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
lfs.FunctionSet
|
|
349
|
+
A set of lfs.Function objects, one per B-spline surface.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
# Quick existence check
|
|
353
|
+
with open(file_name, 'r') as f:
|
|
354
|
+
content = f.read(200000) # partial read for quick check
|
|
355
|
+
if 'B_SPLINE_SURFACE_WITH_KNOTS' not in content:
|
|
356
|
+
raise ValueError('No B_SPLINE_SURFACE_WITH_KNOTS found in file (or file not compatible).')
|
|
357
|
+
|
|
358
|
+
# Stored import
|
|
359
|
+
loaded = _check_if_load_stored_import(file_name, name=name, parallelize=parallelize)
|
|
360
|
+
if loaded is not None:
|
|
361
|
+
return loaded
|
|
362
|
+
|
|
363
|
+
print('Importing OpenVSP file:', file_name)
|
|
364
|
+
|
|
365
|
+
entities = _read_step_entities(file_name)
|
|
366
|
+
|
|
367
|
+
# Build point map: STEP id -> xyz
|
|
368
|
+
point_map: Dict[int, np.ndarray] = {}
|
|
369
|
+
for eid, rhs in entities.items():
|
|
370
|
+
if rhs.startswith('CARTESIAN_POINT'):
|
|
371
|
+
xyz = _parse_cartesian_point(rhs)
|
|
372
|
+
if xyz is not None:
|
|
373
|
+
point_map[eid] = xyz
|
|
374
|
+
|
|
375
|
+
# Parse surfaces
|
|
376
|
+
surfaces: List[Tuple[str, int, int, np.ndarray, List[int], List[int], List[float], List[float]]] = []
|
|
377
|
+
for eid, rhs in entities.items():
|
|
378
|
+
if rhs.startswith('B_SPLINE_SURFACE_WITH_KNOTS'):
|
|
379
|
+
parsed = _parse_bspline_surface_with_knots(rhs)
|
|
380
|
+
if parsed is not None:
|
|
381
|
+
surfaces.append(parsed)
|
|
382
|
+
|
|
383
|
+
if not surfaces:
|
|
384
|
+
raise ValueError('No parsable B_SPLINE_SURFACE_WITH_KNOTS entities found.')
|
|
385
|
+
|
|
386
|
+
# Cache spaces
|
|
387
|
+
space_cache: Dict[str, lfs.BSplineSpace] = {}
|
|
388
|
+
functions: List[lfs.Function] = []
|
|
389
|
+
|
|
390
|
+
for (surf_name, deg_u, deg_v, cp_ids, u_mults, v_mults, u_knots_base, v_knots_base) in surfaces:
|
|
391
|
+
# Expand and optionally normalize knots
|
|
392
|
+
ku = _expand_knots(u_knots_base, u_mults)
|
|
393
|
+
kv = _expand_knots(v_knots_base, v_mults)
|
|
394
|
+
if normalize_knots:
|
|
395
|
+
ku = _normalize_knots_affine(ku)
|
|
396
|
+
kv = _normalize_knots_affine(kv)
|
|
397
|
+
|
|
398
|
+
order_u = deg_u + 1
|
|
399
|
+
order_v = deg_v + 1
|
|
400
|
+
# Control point grid gives coefficient shape directly (nu, nv)
|
|
401
|
+
coeff_shape = (cp_ids.shape[0], cp_ids.shape[1])
|
|
402
|
+
|
|
403
|
+
# Basic consistency check: for clamped open knot vectors,
|
|
404
|
+
# len(knots) = n + p + 1, where p=degree, n=#ctrlpts-1
|
|
405
|
+
# Here: n_ctrl = coeff_shape[0] etc.
|
|
406
|
+
expected_ku = coeff_shape[0] + deg_u + 1
|
|
407
|
+
expected_kv = coeff_shape[1] + deg_v + 1
|
|
408
|
+
if ku.size != expected_ku or kv.size != expected_kv:
|
|
409
|
+
# Don't hard-fail: some exporters may include different specs.
|
|
410
|
+
# But warn to surface possible mismatches.
|
|
411
|
+
print(
|
|
412
|
+
f"[WARN] Knot length mismatch for '{surf_name}': "
|
|
413
|
+
f"len(ku)={ku.size} expected={expected_ku}; "
|
|
414
|
+
f"len(kv)={kv.size} expected={expected_kv}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Create/get space
|
|
418
|
+
skey = _space_key(deg_u, deg_v, u_mults, v_mults, u_knots_base, v_knots_base)
|
|
419
|
+
if skey in space_cache:
|
|
420
|
+
space = space_cache[skey]
|
|
421
|
+
else:
|
|
422
|
+
space = lfs.BSplineSpace(
|
|
423
|
+
num_parametric_dimensions=2,
|
|
424
|
+
degree=(deg_u, deg_v),
|
|
425
|
+
coefficients_shape=coeff_shape,
|
|
426
|
+
knots=tuple([ku, kv]),
|
|
427
|
+
)
|
|
428
|
+
space_cache[skey] = space
|
|
429
|
+
|
|
430
|
+
# Assemble control points in exact order
|
|
431
|
+
nu, nv = coeff_shape
|
|
432
|
+
ctrl = np.zeros((nu, nv, 3), dtype=float)
|
|
433
|
+
missing = 0
|
|
434
|
+
for i in range(nu):
|
|
435
|
+
for j in range(nv):
|
|
436
|
+
pid = int(cp_ids[i, j])
|
|
437
|
+
p = point_map.get(pid)
|
|
438
|
+
if p is None:
|
|
439
|
+
missing += 1
|
|
440
|
+
continue
|
|
441
|
+
ctrl[i, j, :] = p
|
|
442
|
+
if missing:
|
|
443
|
+
raise ValueError(f"Missing {missing} CARTESIAN_POINT references while building '{surf_name}'.")
|
|
444
|
+
|
|
445
|
+
coeffs = csdl.Variable(value=ctrl)
|
|
446
|
+
fn = lfs.Function(space=space, coefficients=coeffs, name=f"{surf_name}")
|
|
447
|
+
functions.append(fn)
|
|
448
|
+
|
|
449
|
+
fset = lfs.FunctionSet(functions, name=name)
|
|
450
|
+
|
|
451
|
+
# Store import to disk (convert csdl vars to numpy arrays)
|
|
452
|
+
fn_base = os.path.basename(file_name)
|
|
453
|
+
fn_wo_ext = fn_base[:fn_base.rindex('.')]
|
|
454
|
+
store_path = f"stored_files/imports/{fn_wo_ext}_stored_import.pickle"
|
|
455
|
+
Path('stored_files/imports').mkdir(parents=True, exist_ok=True)
|
|
456
|
+
|
|
457
|
+
with open(store_path, 'wb+') as handle:
|
|
458
|
+
fset_copy = fset.copy()
|
|
459
|
+
for key, function in fset.functions.items():
|
|
460
|
+
function_copy = function.copy()
|
|
461
|
+
# csdl.Variable -> ndarray
|
|
462
|
+
val = function.coefficients.value if hasattr(function.coefficients, 'value') else function.coefficients
|
|
463
|
+
function_copy.coefficients = np.asarray(val).copy()
|
|
464
|
+
fset_copy.functions[key] = function_copy
|
|
465
|
+
pickle.dump(fset_copy, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
|
466
|
+
|
|
467
|
+
print('Complete import')
|
|
468
|
+
return fset
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def import_file_patched(*args, **kwargs):
|
|
472
|
+
"""Deprecated alias for import_file.
|
|
473
|
+
|
|
474
|
+
.. deprecated:: 1.0.0
|
|
475
|
+
Use :func:`import_file` instead.
|
|
476
|
+
"""
|
|
477
|
+
import warnings
|
|
478
|
+
warnings.warn(
|
|
479
|
+
"import_file_patched is deprecated; use import_file instead.",
|
|
480
|
+
DeprecationWarning,
|
|
481
|
+
stacklevel=2,
|
|
482
|
+
)
|
|
483
|
+
return import_file(*args, **kwargs)
|
|
484
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def get_projection_squared_distances(points_1, points_2, direction):
|
|
4
|
+
difference = points_2 - points_1
|
|
5
|
+
if direction is None:
|
|
6
|
+
squared_distances = np.sum((difference)**2, axis=-1)
|
|
7
|
+
else:
|
|
8
|
+
direction = direction/np.linalg.norm(direction)
|
|
9
|
+
distances_along_axis = np.dot(difference, direction)
|
|
10
|
+
squared_distances = np.sum((difference)**2, axis=-1) - distances_along_axis**2
|
|
11
|
+
return squared_distances
|