Module sverchok.utils.curve.knotvector
Module: knotvector
:platform: Unix, Windows :synopsis: Provides utility functions related to knot vector generation and validation
Expand source code
# This file is part of project Sverchok. It's copyrighted by the contributors
# recorded in the version control history of the file, available from
# its original location https://github.com/nortikin/sverchok/commit/master
#
# SPDX-License-Identifier: GPL3
# License-Filename: LICENSE
#
# Adopted from Geomdl library: https://raw.githubusercontent.com/orbingol/NURBS-Python/5.x/geomdl/knotvector.py
#
"""
.. module:: knotvector
:platform: Unix, Windows
:synopsis: Provides utility functions related to knot vector generation and validation
.. moduleauthor:: Onur Rauf Bingol <orbingol@gmail.com>
"""
from collections import defaultdict
import numpy as np
from sverchok.utils.geom import CubicSpline, LinearSpline
def knotvector_length(degree, num_ctrlpts):
return degree + num_ctrlpts + 1
def generate(degree, num_ctrlpts, clamped=True):
""" Generates an equally spaced knot vector.
It uses the following equality to generate knot vector: :math:`m = n + p + 1`
where;
* :math:`p`, degree
* :math:`n + 1`, number of control points
* :math:`m + 1`, number of knots
Keyword Arguments:
* ``clamped``: Flag to choose from clamped or unclamped knot vector options. *Default: True*
:param degree: degree
:type degree: int
:param num_ctrlpts: number of control points
:type num_ctrlpts: int
:return: knot vector
:rtype: np.array of shape (m+1,)
"""
if degree == 0 or num_ctrlpts == 0:
raise ValueError("Input values should be different than zero.")
if clamped:
num_repeat = degree
num_segments = num_ctrlpts - (degree + 1)
zeros = np.zeros((num_repeat,))
growing = np.linspace(0.0, 1.0, num = num_segments+2)
ones = np.ones((num_repeat,))
return np.concatenate((zeros, growing, ones))
else:
num_knots = degree + num_ctrlpts + 1
return np.linspace(0.0, 1.0, num=num_knots)
def find_span(knot_vector, num_ctrlpts, knot):
span = 0 # Knot span index starts from zero
while span < num_ctrlpts and knot_vector[span] <= knot:
span += 1
return span - 1
def cubic_resample(tknots, new_count):
tknots = np.asarray(tknots)
old_idxs = np.linspace(0.0, 1.0, num=len(tknots))
new_idxs = np.linspace(0.0, 1.0, num=new_count)
resampled_tknots = CubicSpline.resample(old_idxs, tknots, new_idxs)
return resampled_tknots
def linear_resample(tknots, new_count):
tknots = np.asarray(tknots)
old_idxs = np.linspace(0.0, 1.0, num=len(tknots))
new_idxs = np.linspace(0.0, 1.0, num=new_count)
resampled_tknots = LinearSpline.resample(old_idxs, tknots, new_idxs)
return resampled_tknots
def add_one_by_resampling(knot_vector, index = None, degree = None):
n = len(knot_vector)
if index is None:
return linear_resample(knot_vector, n+1)
else:
if degree is None:
raise Exception("If index is provided, degree must be provided too")
kv_before = knot_vector[:index]
kv_after = knot_vector[index + degree + 1 :]
kv = linear_resample(knot_vector[index : index + degree + 1], degree+2)
return np.concatenate([kv_before, kv, kv_after])
def from_tknots(degree, tknots, include_endpoints=False, n_cpts=None):
n = len(tknots)
if n_cpts is None:
result = [tknots[0]] * (degree+1)
if include_endpoints:
j_min, j_max = 0, n - degree + 1
else:
j_min, j_max = 1, n - degree
for j in range(j_min, j_max):
u = tknots[j:j+degree].sum() / degree
result.append(u)
result.extend([tknots[-1]] * (degree+1))
return np.array(result)
else:
resampled_tknots = linear_resample(tknots, n_cpts)
return from_tknots(degree, resampled_tknots, include_endpoints = include_endpoints)
def normalize(knot_vector):
""" Normalizes the input knot vector to [0, 1] domain.
:param knot_vector: knot vector to be normalized
:type knot_vector: np.array of shape (X,)
:return: normalized knot vector
:rtype: np.array
"""
if not isinstance(knot_vector, np.ndarray):
knot_vector = np.array(knot_vector)
m = knot_vector.min()
M = knot_vector.max()
if m >= M:
raise Exception("All knot values are equal")
return (knot_vector - m) / (M - m)
def concatenate_plain(kv1, kv2):
M = kv1.max()
return np.concatenate((kv1, kv2 + M))
def average(knotvectors):
kvs = np.array(knotvectors)
return kvs.mean(axis=0)
def calc_nodes(degree, n_cpts, knotvector):
nodes = np.zeros((n_cpts,))
for i in range(n_cpts):
nodes[i] = knotvector[i+1:i+degree+1].mean()
return nodes
def to_multiplicity(knot_vector, tolerance=1e-6):
count = 0
prev_u = None
result = []
for u in knot_vector:
if prev_u is None:
last_match = False
else:
last_match = abs(u - prev_u) < tolerance
#print(f"Match: {u} - {prev_u} = {abs(u - prev_u)}, => {last_match}")
if prev_u is None:
count = 1
elif last_match:
count += 1
else:
result.append((prev_u, count))
count = 1
prev_u = u
if last_match:
result.append((u, count))
else:
result.append((u, 1))
return result
def from_multiplicity(pairs):
result = []
for u, count in pairs:
result.extend([u] * count)
return np.array(result)
def is_clamped(knot_vector, degree, check_start=True, check_end=True, tolerance=1e-6):
pairs = to_multiplicity(knot_vector, tolerance)
m1 = pairs[0][1]
m2 = pairs[-1][1]
start_ok = not check_start or m1 == degree+1
end_ok = not check_end or m2 == degree+1
return start_ok and end_ok
def concatenate(kv1, kv2, join_multiplicity):
join_knot = kv1.max()
kv2 = kv2 - kv2.min() + join_knot
kv1_m = to_multiplicity(kv1)
kv2_m = to_multiplicity(kv2)
kv_m = dict(kv1_m[:-1] + kv2_m)
kv_m[join_knot] = join_multiplicity
kv_m = [(k, kv_m[k]) for k in sorted(kv_m.keys())]
return from_multiplicity(kv_m)
def change_degree_pairs(pairs, delta=1):
return [(u, count+delta) for u, count in pairs]
def elevate_degree(knot_vector, delta=1):
pairs = to_multiplicity(knot_vector)
return from_multiplicity(change_degree_pairs(pairs, delta))
def reduce_degree(knot_vector, delta=1):
pairs = to_multiplicity(knot_vector)
return from_multiplicity(change_degree_pairs(pairs, -delta))
def insert(knot_vector, u, count=1):
idx = np.searchsorted(knot_vector, u)
result = knot_vector.tolist()
for i in range(count):
result.insert(idx, u)
#result = np.insert(result, idx, u)
return np.asarray(result)
def rescale(knot_vector, new_t_min, new_t_max):
t_min = knot_vector[0]
t_max = knot_vector[-1]
k = (new_t_max - new_t_min) / (t_max - t_min)
return k * (knot_vector - t_min) + new_t_min
def reverse(knot_vector):
t_max = knot_vector[-1]
t_min = knot_vector[0]
kv = t_max - knot_vector + t_min
return kv[::-1]
def find_multiplicity(knot_vector, u, tolerance=1e-6):
pairs = to_multiplicity(knot_vector, tolerance)
#print(f"kv {knot_vector} => {pairs}")
for k, count in pairs:
if tolerance is None:
if k == u:
return count
else:
if abs(k - u) < tolerance:
return count
return 0
def get_internal_knots(knot_vector, output_multiplicity = False, tolerance=1e-6):
pairs = to_multiplicity(knot_vector)
internal = pairs[1:-1]
if output_multiplicity:
return internal
else:
return [u for u,_ in internal]
def get_min_continuity(knotvector, degree):
ms = to_multiplicity(knotvector)[1:-1]
if not ms:
return degree
multiplicities = [p[1] for p in ms]
max_mult = max(multiplicities)
return degree - max_mult
def difference(src_kv, dst_kv, tolerance=1e-6):
src_pairs = dict(to_multiplicity(src_kv, tolerance))
dst_pairs = to_multiplicity(dst_kv, tolerance)
result = []
for dst_u, dst_multiplicity in dst_pairs:
src_multiplicity = src_pairs.get(dst_u, 0)
diff = dst_multiplicity - src_multiplicity
if diff > 0:
result.append((dst_u, diff))
return result
def equal(kv1, kv2):
if len(kv1) != len(kv2):
return False
return (kv1 == kv2).all()
def merge(kv1, kv2):
kv2 = rescale(kv2, kv1[0], kv1[-1])
kv1_pairs = to_multiplicity(kv1)
kv2_pairs = to_multiplicity(kv2)
pairs = defaultdict(int)
for u, multiplicity in kv1_pairs:
pairs[u] = multiplicity
for u, multiplicity in kv2_pairs:
pairs[u] = max(pairs[u], multiplicity)
result = [(u, pairs[u]) for u in sorted(pairs.keys())]
return from_multiplicity(result)
def check(degree, knot_vector, num_ctrlpts):
""" Checks the validity of the input knot vector.
Please refer to The NURBS Book (2nd Edition), p.50 for details.
:param degree: degree of the curve or the surface
:type degree: int
:param knot_vector: knot vector to be checked
:type knot_vector: np.array of shape (X,)
:param num_ctrlpts: number of control points
:type num_ctrlpts: int
:return: String with error description, if the knotvector is invalid;
None, if the knotvector is valid.
"""
if not isinstance(knot_vector, (list, tuple, np.ndarray)):
raise TypeError("Knot vector must be a list, tuple, or numpy array")
if knot_vector is None or len(knot_vector) == 0:
raise ValueError("Input knot vector cannot be empty")
# Check the formula; m = p + n + 1
m = len(knot_vector)
rhs = degree + num_ctrlpts + 1
if m != rhs:
return f"Knot vector has invalid length {m}; for degree {degree} and {num_ctrlpts} control points it must have {rhs} items"
# Check ascending order
prev_knot = knot_vector[0]
for i, knot in enumerate(knot_vector):
if prev_knot > knot:
print(knot_vector)
return f"Knot vector items are not all non-decreasing: u[{i-1}] = {prev_knot} > u[{i}] = {knot}"
prev_knot = knot
return None
def check_multiplicity(degree, knot_vector, tolerance=1e-6):
ms = to_multiplicity(knot_vector, tolerance)
n = len(ms)
for idx, (u, count) in enumerate(ms):
if idx == 0 or idx == n-1:
if count > degree+1:
return f"First/Last knot u={u} multiplicity {count} is more than degree+1 {degree+1}"
else:
if count > degree:
return f"Inner knot u={u} multiplicity {count} is more than degree {degree}"
return None
Functions
def add_one_by_resampling(knot_vector, index=None, degree=None)
-
Expand source code
def add_one_by_resampling(knot_vector, index = None, degree = None): n = len(knot_vector) if index is None: return linear_resample(knot_vector, n+1) else: if degree is None: raise Exception("If index is provided, degree must be provided too") kv_before = knot_vector[:index] kv_after = knot_vector[index + degree + 1 :] kv = linear_resample(knot_vector[index : index + degree + 1], degree+2) return np.concatenate([kv_before, kv, kv_after])
def average(knotvectors)
-
Expand source code
def average(knotvectors): kvs = np.array(knotvectors) return kvs.mean(axis=0)
def calc_nodes(degree, n_cpts, knotvector)
-
Expand source code
def calc_nodes(degree, n_cpts, knotvector): nodes = np.zeros((n_cpts,)) for i in range(n_cpts): nodes[i] = knotvector[i+1:i+degree+1].mean() return nodes
def change_degree_pairs(pairs, delta=1)
-
Expand source code
def change_degree_pairs(pairs, delta=1): return [(u, count+delta) for u, count in pairs]
def check(degree, knot_vector, num_ctrlpts)
-
Checks the validity of the input knot vector.
Please refer to The NURBS Book (2nd Edition), p.50 for details.
:param degree: degree of the curve or the surface :type degree: int :param knot_vector: knot vector to be checked :type knot_vector: np.array of shape (X,) :param num_ctrlpts: number of control points :type num_ctrlpts: int :return: String with error description, if the knotvector is invalid; None, if the knotvector is valid.
Expand source code
def check(degree, knot_vector, num_ctrlpts): """ Checks the validity of the input knot vector. Please refer to The NURBS Book (2nd Edition), p.50 for details. :param degree: degree of the curve or the surface :type degree: int :param knot_vector: knot vector to be checked :type knot_vector: np.array of shape (X,) :param num_ctrlpts: number of control points :type num_ctrlpts: int :return: String with error description, if the knotvector is invalid; None, if the knotvector is valid. """ if not isinstance(knot_vector, (list, tuple, np.ndarray)): raise TypeError("Knot vector must be a list, tuple, or numpy array") if knot_vector is None or len(knot_vector) == 0: raise ValueError("Input knot vector cannot be empty") # Check the formula; m = p + n + 1 m = len(knot_vector) rhs = degree + num_ctrlpts + 1 if m != rhs: return f"Knot vector has invalid length {m}; for degree {degree} and {num_ctrlpts} control points it must have {rhs} items" # Check ascending order prev_knot = knot_vector[0] for i, knot in enumerate(knot_vector): if prev_knot > knot: print(knot_vector) return f"Knot vector items are not all non-decreasing: u[{i-1}] = {prev_knot} > u[{i}] = {knot}" prev_knot = knot return None
def check_multiplicity(degree, knot_vector, tolerance=1e-06)
-
Expand source code
def check_multiplicity(degree, knot_vector, tolerance=1e-6): ms = to_multiplicity(knot_vector, tolerance) n = len(ms) for idx, (u, count) in enumerate(ms): if idx == 0 or idx == n-1: if count > degree+1: return f"First/Last knot u={u} multiplicity {count} is more than degree+1 {degree+1}" else: if count > degree: return f"Inner knot u={u} multiplicity {count} is more than degree {degree}" return None
def concatenate(kv1, kv2, join_multiplicity)
-
Expand source code
def concatenate(kv1, kv2, join_multiplicity): join_knot = kv1.max() kv2 = kv2 - kv2.min() + join_knot kv1_m = to_multiplicity(kv1) kv2_m = to_multiplicity(kv2) kv_m = dict(kv1_m[:-1] + kv2_m) kv_m[join_knot] = join_multiplicity kv_m = [(k, kv_m[k]) for k in sorted(kv_m.keys())] return from_multiplicity(kv_m)
def concatenate_plain(kv1, kv2)
-
Expand source code
def concatenate_plain(kv1, kv2): M = kv1.max() return np.concatenate((kv1, kv2 + M))
def cubic_resample(tknots, new_count)
-
Expand source code
def cubic_resample(tknots, new_count): tknots = np.asarray(tknots) old_idxs = np.linspace(0.0, 1.0, num=len(tknots)) new_idxs = np.linspace(0.0, 1.0, num=new_count) resampled_tknots = CubicSpline.resample(old_idxs, tknots, new_idxs) return resampled_tknots
def difference(src_kv, dst_kv, tolerance=1e-06)
-
Expand source code
def difference(src_kv, dst_kv, tolerance=1e-6): src_pairs = dict(to_multiplicity(src_kv, tolerance)) dst_pairs = to_multiplicity(dst_kv, tolerance) result = [] for dst_u, dst_multiplicity in dst_pairs: src_multiplicity = src_pairs.get(dst_u, 0) diff = dst_multiplicity - src_multiplicity if diff > 0: result.append((dst_u, diff)) return result
def elevate_degree(knot_vector, delta=1)
-
Expand source code
def elevate_degree(knot_vector, delta=1): pairs = to_multiplicity(knot_vector) return from_multiplicity(change_degree_pairs(pairs, delta))
def equal(kv1, kv2)
-
Expand source code
def equal(kv1, kv2): if len(kv1) != len(kv2): return False return (kv1 == kv2).all()
def find_multiplicity(knot_vector, u, tolerance=1e-06)
-
Expand source code
def find_multiplicity(knot_vector, u, tolerance=1e-6): pairs = to_multiplicity(knot_vector, tolerance) #print(f"kv {knot_vector} => {pairs}") for k, count in pairs: if tolerance is None: if k == u: return count else: if abs(k - u) < tolerance: return count return 0
def find_span(knot_vector, num_ctrlpts, knot)
-
Expand source code
def find_span(knot_vector, num_ctrlpts, knot): span = 0 # Knot span index starts from zero while span < num_ctrlpts and knot_vector[span] <= knot: span += 1 return span - 1
def from_multiplicity(pairs)
-
Expand source code
def from_multiplicity(pairs): result = [] for u, count in pairs: result.extend([u] * count) return np.array(result)
def from_tknots(degree, tknots, include_endpoints=False, n_cpts=None)
-
Expand source code
def from_tknots(degree, tknots, include_endpoints=False, n_cpts=None): n = len(tknots) if n_cpts is None: result = [tknots[0]] * (degree+1) if include_endpoints: j_min, j_max = 0, n - degree + 1 else: j_min, j_max = 1, n - degree for j in range(j_min, j_max): u = tknots[j:j+degree].sum() / degree result.append(u) result.extend([tknots[-1]] * (degree+1)) return np.array(result) else: resampled_tknots = linear_resample(tknots, n_cpts) return from_tknots(degree, resampled_tknots, include_endpoints = include_endpoints)
def generate(degree, num_ctrlpts, clamped=True)
-
Generates an equally spaced knot vector.
It uses the following equality to generate knot vector: :math:
m = n + p + 1
where;
- :math:
p
, degree - :math:
n + 1
, number of control points - :math:
m + 1
, number of knots
Keyword Arguments:
* <code>clamped</code>: Flag to choose from clamped or unclamped knot vector options. *Default: True*
:param degree: degree :type degree: int :param num_ctrlpts: number of control points :type num_ctrlpts: int :return: knot vector :rtype: np.array of shape (m+1,)
Expand source code
def generate(degree, num_ctrlpts, clamped=True): """ Generates an equally spaced knot vector. It uses the following equality to generate knot vector: :math:`m = n + p + 1` where; * :math:`p`, degree * :math:`n + 1`, number of control points * :math:`m + 1`, number of knots Keyword Arguments: * ``clamped``: Flag to choose from clamped or unclamped knot vector options. *Default: True* :param degree: degree :type degree: int :param num_ctrlpts: number of control points :type num_ctrlpts: int :return: knot vector :rtype: np.array of shape (m+1,) """ if degree == 0 or num_ctrlpts == 0: raise ValueError("Input values should be different than zero.") if clamped: num_repeat = degree num_segments = num_ctrlpts - (degree + 1) zeros = np.zeros((num_repeat,)) growing = np.linspace(0.0, 1.0, num = num_segments+2) ones = np.ones((num_repeat,)) return np.concatenate((zeros, growing, ones)) else: num_knots = degree + num_ctrlpts + 1 return np.linspace(0.0, 1.0, num=num_knots)
- :math:
def get_internal_knots(knot_vector, output_multiplicity=False, tolerance=1e-06)
-
Expand source code
def get_internal_knots(knot_vector, output_multiplicity = False, tolerance=1e-6): pairs = to_multiplicity(knot_vector) internal = pairs[1:-1] if output_multiplicity: return internal else: return [u for u,_ in internal]
def get_min_continuity(knotvector, degree)
-
Expand source code
def get_min_continuity(knotvector, degree): ms = to_multiplicity(knotvector)[1:-1] if not ms: return degree multiplicities = [p[1] for p in ms] max_mult = max(multiplicities) return degree - max_mult
def insert(knot_vector, u, count=1)
-
Expand source code
def insert(knot_vector, u, count=1): idx = np.searchsorted(knot_vector, u) result = knot_vector.tolist() for i in range(count): result.insert(idx, u) #result = np.insert(result, idx, u) return np.asarray(result)
def is_clamped(knot_vector, degree, check_start=True, check_end=True, tolerance=1e-06)
-
Expand source code
def is_clamped(knot_vector, degree, check_start=True, check_end=True, tolerance=1e-6): pairs = to_multiplicity(knot_vector, tolerance) m1 = pairs[0][1] m2 = pairs[-1][1] start_ok = not check_start or m1 == degree+1 end_ok = not check_end or m2 == degree+1 return start_ok and end_ok
def knotvector_length(degree, num_ctrlpts)
-
Expand source code
def knotvector_length(degree, num_ctrlpts): return degree + num_ctrlpts + 1
def linear_resample(tknots, new_count)
-
Expand source code
def linear_resample(tknots, new_count): tknots = np.asarray(tknots) old_idxs = np.linspace(0.0, 1.0, num=len(tknots)) new_idxs = np.linspace(0.0, 1.0, num=new_count) resampled_tknots = LinearSpline.resample(old_idxs, tknots, new_idxs) return resampled_tknots
def merge(kv1, kv2)
-
Expand source code
def merge(kv1, kv2): kv2 = rescale(kv2, kv1[0], kv1[-1]) kv1_pairs = to_multiplicity(kv1) kv2_pairs = to_multiplicity(kv2) pairs = defaultdict(int) for u, multiplicity in kv1_pairs: pairs[u] = multiplicity for u, multiplicity in kv2_pairs: pairs[u] = max(pairs[u], multiplicity) result = [(u, pairs[u]) for u in sorted(pairs.keys())] return from_multiplicity(result)
def normalize(knot_vector)
-
Normalizes the input knot vector to [0, 1] domain.
:param knot_vector: knot vector to be normalized :type knot_vector: np.array of shape (X,) :return: normalized knot vector :rtype: np.array
Expand source code
def normalize(knot_vector): """ Normalizes the input knot vector to [0, 1] domain. :param knot_vector: knot vector to be normalized :type knot_vector: np.array of shape (X,) :return: normalized knot vector :rtype: np.array """ if not isinstance(knot_vector, np.ndarray): knot_vector = np.array(knot_vector) m = knot_vector.min() M = knot_vector.max() if m >= M: raise Exception("All knot values are equal") return (knot_vector - m) / (M - m)
def reduce_degree(knot_vector, delta=1)
-
Expand source code
def reduce_degree(knot_vector, delta=1): pairs = to_multiplicity(knot_vector) return from_multiplicity(change_degree_pairs(pairs, -delta))
def rescale(knot_vector, new_t_min, new_t_max)
-
Expand source code
def rescale(knot_vector, new_t_min, new_t_max): t_min = knot_vector[0] t_max = knot_vector[-1] k = (new_t_max - new_t_min) / (t_max - t_min) return k * (knot_vector - t_min) + new_t_min
def reverse(knot_vector)
-
Expand source code
def reverse(knot_vector): t_max = knot_vector[-1] t_min = knot_vector[0] kv = t_max - knot_vector + t_min return kv[::-1]
def to_multiplicity(knot_vector, tolerance=1e-06)
-
Expand source code
def to_multiplicity(knot_vector, tolerance=1e-6): count = 0 prev_u = None result = [] for u in knot_vector: if prev_u is None: last_match = False else: last_match = abs(u - prev_u) < tolerance #print(f"Match: {u} - {prev_u} = {abs(u - prev_u)}, => {last_match}") if prev_u is None: count = 1 elif last_match: count += 1 else: result.append((prev_u, count)) count = 1 prev_u = u if last_match: result.append((u, count)) else: result.append((u, 1)) return result