Skip to content

Commit 0a1f5a5

Browse files
committed
Add: anchor functionality; Fix: vectorize for-loop; Fix: use np.allclose; Fix: catch first trajectory tangent error
1 parent d5d5155 commit 0a1f5a5

1 file changed

Lines changed: 94 additions & 34 deletions

File tree

splinepy/helpme/create.py

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ def swept(
302302
cross_section,
303303
trajectory,
304304
cross_section_normal=None,
305+
anchor="auto",
305306
set_on_trajectory=False,
306307
rotation_adaption=None,
307308
):
@@ -329,6 +330,13 @@ def swept(
329330
cross_section_normal : np.array
330331
Normal vector of the cross-section
331332
Default is [0, 0, 1]
333+
anchor : str
334+
Strategy used to determine which point of the cross-section is
335+
placed on the trajectory. Supported values are:
336+
``"auto"``, ``"parametric"``, ``"control_box"``,
337+
and ``"geometry_box"``.
338+
``"auto"`` uses ``"geometry_box"`` for curve cross-sections and
339+
``"parametric"`` for surface cross-sections.
332340
set_on_trajectory : bool
333341
If True, the cross-section will be placed at the evaluation
334342
points of the trajectory's knots. If False, the cross-section
@@ -376,6 +384,8 @@ def swept(
376384
raise ValueError("trajectory must have a parametric dimension of 1")
377385
if not isinstance(set_on_trajectory, bool):
378386
raise TypeError("set_on_trajectory must be a boolean")
387+
if not isinstance(anchor, str):
388+
raise TypeError("anchor must be a string")
379389

380390
if cross_section_normal is not None and not len(cross_section_normal) == 3:
381391
raise ValueError(
@@ -387,6 +397,17 @@ def swept(
387397
# add debug message
388398
_log.debug("No cross_section_normal given. Defaulting to [0, 0, 1].")
389399

400+
anchor = anchor.lower()
401+
if anchor == "auto":
402+
anchor = (
403+
"geometry_box" if cross_section.para_dim == 1 else "parametric"
404+
)
405+
if anchor not in {"parametric", "control_box", "geometry_box"}:
406+
raise ValueError(
407+
"anchor must be one of 'auto', 'parametric', "
408+
"'control_box', or 'geometry_box'"
409+
)
410+
390411
### STARTING CALCULATIONS ###
391412

392413
# make copies so we can work on it inplace
@@ -401,7 +422,13 @@ def swept(
401422

402423
# tangent vector 'e1' of trajectory at parameter value 0
403424
e1 = trajectory.derivative([par_value[0]], [1])
404-
e1 = (e1 / _np.linalg.norm(e1)).ravel()
425+
e1_norm = _np.linalg.norm(e1)
426+
if e1_norm < _settings.TOLERANCE:
427+
raise ValueError(
428+
"Cannot determine initial sweep direction from trajectory "
429+
"because the first tangent is too small."
430+
)
431+
e1 = (e1 / e1_norm).ravel()
405432

406433
# evaluating a vector normal to e1
407434
vec = [-e1[1], e1[0], -e1[2]]
@@ -429,14 +456,22 @@ def swept(
429456
# calculation according to NURBS Book, eq. 10.27
430457
# tangent vector e1 on trajectory at parameter value i
431458
e1 = trajectory.derivative([par_value[i]], [1])
432-
if _np.linalg.norm(e1) < _settings.TOLERANCE:
433-
e1 = tang_collection[-1]
459+
e1_norm = _np.linalg.norm(e1)
460+
if e1_norm < _settings.TOLERANCE:
461+
if tang_collection:
462+
e1 = tang_collection[-1]
463+
e1_norm = _np.linalg.norm(e1)
464+
else:
465+
raise ValueError(
466+
"Cannot determine sweep direction because the first "
467+
"trajectory tangent is too small."
468+
)
434469
# add debug message
435470
_log.debug(
436471
f"Division by zero occurred. Applying an approximation "
437472
f"by using the previous tangent e1 for parametric value {par_value[i]}"
438473
)
439-
e1 = (e1 / _np.linalg.norm(e1)).ravel()
474+
e1 = (e1 / e1_norm).ravel()
440475
# collecting tangent vectors for later use
441476
tang_collection.append(e1)
442477

@@ -458,15 +493,20 @@ def swept(
458493
T.append(_np.vstack((e1, e2, e3)))
459494

460495
# array of transformation matrices from local to global coordinates
461-
A.append(_np.linalg.inv(T[i]))
496+
A.append(T[i].T)
462497

463498
# separate procedure, if trajectory is closed and B[0] != B[-1]
464499
# recalculate B-vector and middle the values between B and B_rec
465500
# according to NURBS Book, Piegl & Tiller, 2nd edition, p. 483
466-
is_trajectory_closed = _np.array_equal(
467-
trajectory.evaluate([[0]]), trajectory.evaluate([par_value[-1]])
501+
is_trajectory_closed = _np.allclose(
502+
trajectory.evaluate([[0]]),
503+
trajectory.evaluate([par_value[-1]]),
504+
atol=_settings.TOLERANCE,
505+
rtol=1e-8,
506+
)
507+
is_B_start_equal_B_end = _np.allclose(
508+
B[0], B[-1], atol=_settings.TOLERANCE, rtol=1e-8
468509
)
469-
is_B_start_equal_B_end = _np.array_equal(B[0], B[-1])
470510

471511
if is_trajectory_closed and not is_B_start_equal_B_end:
472512
# reset transformation matrices
@@ -503,7 +543,7 @@ def swept(
503543
T.append(_np.vstack((tang_collection[i], e2, e3)))
504544

505545
# array of transformation matrices from local to global coordinates
506-
A.append(_np.linalg.inv(T[i]))
546+
A.append(T[i].T)
507547

508548
# check if the beginning and the end of the B-vector are the same
509549
if not _np.allclose(B_rec[0], B_rec[-1], rtol=1e-3):
@@ -561,38 +601,58 @@ def swept(
561601
### SWEEPING PROCESS ###
562602

563603
# evaluate center of cross-section and translate to origin
564-
cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0)
565-
cs_center = cross_section.evaluate(
566-
cross_para_center.reshape(-1, cross_section.para_dim)
567-
).ravel()
568-
cross_section.control_points -= cs_center
604+
if anchor == "parametric":
605+
cross_para_center = _np.mean(cross_section.parametric_bounds, axis=0)
606+
cs_center = cross_section.evaluate(
607+
cross_para_center.reshape(-1, cross_section.para_dim)
608+
).ravel()
609+
elif anchor == "control_box":
610+
cs_min = cross_section.control_points.min(axis=0)
611+
cs_max = cross_section.control_points.max(axis=0)
612+
cs_center = 0.5 * (cs_min + cs_max)
613+
else:
614+
if cross_section.para_dim == 1:
615+
sample_resolution = max(
616+
101, 4 * cross_section.control_points.shape[0]
617+
)
618+
else:
619+
sample_resolution = int(
620+
_np.ceil(625 ** (1 / cross_section.para_dim))
621+
)
622+
sample_resolution = max(5, min(25, sample_resolution))
623+
624+
sample_axes = [
625+
_np.linspace(
626+
cross_section.parametric_bounds[0, i],
627+
cross_section.parametric_bounds[1, i],
628+
sample_resolution,
629+
)
630+
for i in range(cross_section.para_dim)
631+
]
632+
sample_queries = _np.stack(
633+
_np.meshgrid(*sample_axes, indexing="ij"),
634+
axis=-1,
635+
).reshape(-1, cross_section.para_dim)
636+
sampled_points = cross_section.evaluate(sample_queries)
637+
cs_min = sampled_points.min(axis=0)
638+
cs_max = sampled_points.max(axis=0)
639+
cs_center = 0.5 * (cs_min + cs_max)
640+
641+
centered_cross_section_cps = cross_section.control_points - cs_center
569642

570643
# set cross-section control points along trajectory
571644
swept_spline_cps = []
645+
rotated_cross_section_cps = centered_cross_section_cps @ R.T
572646
for i, par_val in enumerate(par_value):
573-
temp_csp = []
574647
# evaluate trajectory if user wants to set cross-section on trajectory.
575648
if set_on_trajectory:
576-
evals = trajectory.evaluate([par_val]).ravel()
577-
# place every control point of cross-section separately
578-
for cscp in cross_section.control_points:
579-
# rotate cross-section in trajectory direction
580-
normal_cscp = _np.matmul(R, cscp)
581-
# transform cross-section to global coordinates
582-
normal_cscp = _np.matmul(A[i], normal_cscp)
583-
# check if user wants to place cross-section at
584-
# evaluation points or control points of trajectory
585-
if set_on_trajectory:
586-
# translate cross-section to trajectory evaluation point
587-
normal_cscp += evals
588-
else:
589-
# translate cross-section to trajectory control point
590-
normal_cscp += trajectory.control_points[i]
591-
# append control point to list
592-
temp_csp.append(normal_cscp)
649+
section_origin = trajectory.evaluate([par_val]).ravel()
650+
else:
651+
section_origin = trajectory.control_points[i]
593652

594-
# collect all control points
595-
swept_spline_cps.append(_np.array(temp_csp))
653+
swept_spline_cps.append(
654+
rotated_cross_section_cps @ A[i].T + section_origin
655+
)
596656

597657
# create spline dictionary
598658
dict_swept_spline = {

0 commit comments

Comments
 (0)