Module sverchok.utils.curve.nurbs_solver
NURBS Curve Solver: general algorithm to find a curve which meets certain requirements.
The solver can be provided with several goals, which should be reached. Basic curve parameters (number of control points and knotvector) must be provided. Then the algorithm will try to find a curve which meets all these goals.
It is not always possible to find the single solution, given the list of goals. There can be the following situations:
* Well-determined system: specified number of curve control points is equal
to number of equations generated by goals. In most cases, for
well-determined system there is the single possible solution, and the
solver will find it. Although there can be situations when corresponding
system of equation is singular.
* Underdetermined system: specified number of control points is greater
than the number of equations generated by goals. In this case, the system
has an infinite number of solutions. The solver will find the solution
which has all control points as near to origin as possible. Usually, this
case is useful to solve "relative" problems, i.e. problems of adjsting
existing curve to meet the specified goals, by moving control points as
less as possible. For such cases, the solver must be provided with the
initial curve.
* Overdetermined system: specified number of control points is less than
the number of equations generated by goals. In such a case, the system
usually has no exact solutions. However, in such cases the solver will find
the curve which meets the goals approximately, as close as possible. For
such cases, it is possible to set different weights for different goals, to
instruct the solver that some goals are more important than others.
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
"""
NURBS Curve Solver: general algorithm to find a curve which meets certain requirements.
The solver can be provided with several goals, which should be reached. Basic
curve parameters (number of control points and knotvector) must be provided.
Then the algorithm will try to find a curve which meets all these goals.
It is not always possible to find the single solution, given the list of goals.
There can be the following situations:
* Well-determined system: specified number of curve control points is equal
to number of equations generated by goals. In most cases, for
well-determined system there is the single possible solution, and the
solver will find it. Although there can be situations when corresponding
system of equation is singular.
* Underdetermined system: specified number of control points is greater
than the number of equations generated by goals. In this case, the system
has an infinite number of solutions. The solver will find the solution
which has all control points as near to origin as possible. Usually, this
case is useful to solve "relative" problems, i.e. problems of adjsting
existing curve to meet the specified goals, by moving control points as
less as possible. For such cases, the solver must be provided with the
initial curve.
* Overdetermined system: specified number of control points is less than
the number of equations generated by goals. In such a case, the system
usually has no exact solutions. However, in such cases the solver will find
the curve which meets the goals approximately, as close as possible. For
such cases, it is possible to set different weights for different goals, to
instruct the solver that some goals are more important than others.
"""
import numpy as np
from collections import defaultdict
from sverchok.utils.sv_logging import get_logger
from sverchok.utils.curve.core import SvCurve
from sverchok.utils.curve import knotvector as sv_knotvector
from sverchok.utils.nurbs_common import SvNurbsBasisFunctions, SvNurbsMaths, from_homogenous
class SvNurbsCurveGoal(object):
"""
Abstract class for curve goal.
"""
def copy(self):
raise Exception("Not implemented")
def add(self, other):
raise Exception("Not implemented")
def get_equations(self, solver):
raise Exception("Not implemented")
def get_n_defined_control_points(self):
raise Exception("Not implemented")
class SvNurbsCurvePoints(SvNurbsCurveGoal):
"""
Goal which says that the curve must pass through the specified points at
specified values of parameter.
"""
def __init__(self, us, points, weights = None, relative=False):
self.us = np.asarray(us)
if self.us.ndim != 1:
raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}")
self.vectors = np.asarray(points)
if self.vectors.ndim != 2:
raise Exception(f"Points must be 2-dimensional, but got {self.vectors.shape}")
if len(us) != len(self.vectors):
raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}")
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
def __repr__(self):
if self.relative:
return f"<Relative Points, cnt={len(self.us)}>"
else:
return f"<Points, cnt={len(self.us)}>"
@staticmethod
def single(u, point, weight=None, relative=False):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurvePoints([u], [point], weights, relative=relative)
def copy(self):
return SvNurbsCurvePoints(self.us, self.vectors, self.weights, relative=self.relative)
def get_weights(self):
weights = self.weights
n_points = len(self.vectors)
if weights is None:
weights = np.ones((n_points,))
elif isinstance(weights, np.ndarray) and weights.shape == (1,):
weights = np.full((n_points,), weights[0])
elif isinstance(weights, (int,float)):
weights = np.full((n_points,), weights)
return weights
def add(self, other):
if other.relative != self.relative:
return None
g = self.copy()
g.us = np.concatenate((g.us, other.us))
g.vectors = np.concatenate((g.vectors, other.vectors))
g.weights = np.concatenate((g.get_weights(), other.get_weights()))
return g
def calc_alphas(self, solver, us):
p = solver.degree
if solver.is_rational():
alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us) for k in range(solver.n_cpts)]
else:
alphas = [solver.basis.function(k,p)(us) for k in range(solver.n_cpts)]
alphas = np.array(alphas) # (n_cpts, n_points)
return alphas
def get_src_points(self, solver):
return solver.src_curve.evaluate_array(self.us)
def get_n_defined_control_points(self):
return len(self.us)
def get_equations(self, solver):
ndim = solver.ndim
us = self.us
vectors = self.vectors
n_points = len(vectors)
n_equations = ndim * n_points
n_unknowns = ndim * solver.n_cpts
alphas = self.calc_alphas(solver, us)
weights = self.get_weights()
A = np.zeros((n_equations, n_unknowns))
B = np.zeros((n_equations, 1))
#print(f"A: {A.shape}, W {weights.shape}, n_points {n_points}")
for pt_idx in range(n_points):
for cpt_idx in range(solver.n_cpts):
alpha = alphas[cpt_idx][pt_idx]
for dim_idx in range(ndim):
A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * alpha
if solver.src_curve is None:
if self.relative:
raise Exception("Can not solve relative constraint without original curve")
else:
src_points = None
else:
if self.relative:
src_points = None
else:
src_points = self.get_src_points(solver)
for pt_idx, point in enumerate(vectors):
if src_points is not None:
point = point - src_points[pt_idx]
B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis]
return A, B
class SvNurbsCurveTangents(SvNurbsCurvePoints):
"""
Goal which says that the curve must have specified tangent vectors at
specified values of parameter.
"""
def __init__(self, us, tangents, weights = None, relative=False):
self.us = np.asarray(us)
if self.us.ndim != 1:
raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}")
self.vectors = np.asarray(tangents)
if self.vectors.ndim != 2:
raise Exception(f"Points must be 2-dimensional, but got {self.vectors.shape}")
if len(us) != len(self.vectors):
raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}")
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
def __repr__(self):
if self.relative:
return f"<Relative Tangents, cnt={len(self.us)}>"
else:
return f"<Tangents, cnt={len(self.us)}>"
@staticmethod
def single(u, tangent, weight=None, relative=False):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurveTangents([u], [tangent], weights, relative=relative)
def copy(self):
return SvNurbsCurveTangents(self.us, self.vectors, self.weights, relative=self.relative)
def add(self, other):
if self.relative != other.relative:
return None
g = self.copy()
g.us = np.concatenate((g.us, other.us))
g.vectors = np.concatenate((g.vectors, other.vectors))
g.weights = np.concatenate((g.get_weights(), other.get_weights()))
return g
def calc_alphas(self, solver, us):
p = solver.degree
ns = [solver.basis.function(k, p)(us) for k in range(solver.n_cpts)]
ns = np.array(ns) # (n_cpts, n_pts)
derivs = [solver.basis.derivative(k, p, 1)(us) for k in range(solver.n_cpts)]
derivs = np.array(derivs) # (n_cpts, n_pts)
weights = solver.curve_weights[np.newaxis].T # (n_cpts, 1)
sum_ns = (ns * weights).sum(axis=0) # (n_pts,)
sum_derivs = (derivs * weights).sum(axis=0) # (n_pts,)
numerator = weights * (derivs * sum_ns - ns * sum_derivs)
denominator = sum_ns**2
return numerator / denominator
def get_src_points(self, solver):
return solver.src_curve.tangent_array(self.us)
class SvNurbsCurveDerivatives(SvNurbsCurvePoints):
def __init__(self, order, us, vectors, weights = None, relative=False):
self.order = order
self.us = np.asarray(us)
if self.us.ndim != 1:
raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}")
self.vectors = np.asarray(vectors)
if self.vectors.ndim != 2:
raise Exception(f"Vectors must be 2-dimensional, but got {self.vectors.shape}")
if len(us) != len(self.vectors):
raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}")
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
def __repr__(self):
if self.relative:
return f"<Relative Derivatives, order={self.order}, cnt={len(self.us)}>"
else:
return f"<Derivatives, order={self.order}, cnt={len(self.us)}>"
@staticmethod
def single(order, u, tangent, weight=None, relative=False):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurveDerivatives(order, [u], [tangent], weights, relative=relative)
def copy(self):
return SvNurbsCurveDerivatives(self.order, self.us, self.vectors, self.weights, relative=self.relative)
def add(self, other):
if self.relative != other.relative:
return None
if self.order != other.order:
return None
g = self.copy()
g.us = np.concatenate((g.us, other.us))
g.vectors = np.concatenate((g.vectors, other.vectors))
g.weights = np.concatenate((g.get_weights(), other.get_weights()))
return g
def calc_alphas(self, solver, us):
p = solver.degree
betas = [solver.basis.weighted_derivative(k, p, self.order, solver.curve_weights)(us) for k in range(solver.n_cpts)]
betas = np.array(betas) # (n_cpts, n_points)
return betas
def get_src_points(self, solver):
return solver.src_curve.tangent_array(self.us)
class SvNurbsCurveSelfIntersections(SvNurbsCurveGoal):
"""
Goal which says that the curve must have self-intersections at specified
sets of parameter values.
"""
def __init__(self, us1, us2, weights = None, relative_u=False, relative=False):
if len(us1) != len(us2):
raise Exception("Lengths of us1 and us2 must be equal")
self.us1 = np.asarray(us1)
self.us2 = np.asarray(us2)
self.relative_u = relative_u
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
def __repr__(self):
return f"<Self-intersections, cnt={len(self.us1)}>"
@staticmethod
def single(u1, u2, weight=None, relative_u=False, relative=False):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurveSelfIntersections([u1], [u2], weights, relative_u=relative_u, relative=relative)
def copy(self):
return SvNurbsCurveSelfIntersections(self.us1, self.us2, self.weights, self.relative_u, self.relative)
def get_weights(self):
weights = self.weights
if weights is None:
n_points = len(self.us1)
weights = np.ones((n_points,))
return weights
def add(self, other):
if other.relative_u != self.relative_u:
return None
if other.relative != self.relative:
return None
g = self.copy()
g.us1 = np.concatenate((g.us1, other.us1))
g.us2 = np.concatenate((g.us2, other.us2))
g.weights = np.concatenate((g.get_weights(), other.get_weights()))
return g
def calc_alphas(self, solver):
us1 = self.us1
us2 = self.us2
if self.relative_u:
u_min, u_max = solver.knotvector[0], solver.knotvector[-1]
us1 = u_min + (u_max - u_min) * us1
us2 = u_min + (u_max - u_min) * us2
p = solver.degree
alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us1) for k in range(solver.n_cpts)]
alphas = np.array(alphas) # (n_cpts, n_points)
betas = [solver.basis.fraction(k,p, solver.curve_weights)(us2) for k in range(solver.n_cpts)]
betas = np.array(betas) # (n_cpts, n_points)
return alphas, betas
def calc_vectors(self, solver):
points1 = solver.src_curve.evaluate_array(self.us1)
points2 = solver.src_curve.evaluate_array(self.us2)
return points1, points2
def get_n_defined_control_points(self):
return len(self.us1)
def get_equations(self, solver):
ndim = solver.ndim
us1 = self.us1
us2 = self.us2
p = solver.degree
n_points = len(us1)
n_equations = ndim * n_points
n_unknowns = ndim * solver.n_cpts
alphas, betas = self.calc_alphas(solver)
weights = self.get_weights()
A = np.zeros((n_equations, n_unknowns))
B = np.zeros((n_equations, 1))
for pt_idx in range(n_points):
for cpt_idx in range(solver.n_cpts):
alpha = alphas[cpt_idx][pt_idx]
beta = betas[cpt_idx][pt_idx]
for dim_idx in range(ndim):
A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * (alpha - beta)
if self.relative:
if solver.src_curve is None:
raise Exception("Can not solve relative constraint without original curve")
else:
points1, points2 = self.calc_vectors(solver)
for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)):
for dim_idx in range(ndim):
B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * (pt2 - pt1)[np.newaxis]
return A, B
class SvNurbsCurveCotangents(SvNurbsCurveSelfIntersections):
"""
Goal which says that curve must have equal tangent vectors at two sets of
parameter values.
"""
def __init__(self, us1, us2, weights = None, relative_u=False, relative=False):
if len(us1) != len(us2):
raise Exception("Lengths of us1 and us2 must be equal")
self.us1 = np.asarray(us1)
self.us2 = np.asarray(us2)
self.relative_u = relative_u
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
def __repr__(self):
return f"<Equal tangents, cnt={len(self.us1)}>"
@staticmethod
def single(u1, u2, weight=None, relative_u=False, relative=False):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurveCotangents([u1], [u2], weights, relative_u=relative_u, relative=relative)
def copy(self):
return SvNurbsCurveCotangents(self.us1, self.us2, self.weights, self.relative_u, self.relative)
def calc_alphas(self, solver):
us1 = self.us1
us2 = self.us2
if self.relative_u:
u_min, u_max = solver.knotvector[0], solver.knotvector[-1]
us1 = u_min + (u_max - u_min) * us1
us2 = u_min + (u_max - u_min) * us2
p = solver.degree
alphas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us1) for k in range(solver.n_cpts)]
alphas = np.array(alphas) # (n_cpts, n_points)
betas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us2) for k in range(solver.n_cpts)]
betas = np.array(betas) # (n_cpts, n_points)
return alphas, betas
def get_equations(self, solver):
ndim = solver.ndim
us1 = self.us1
us2 = self.us2
p = solver.degree
n_points = len(us1)
n_equations = ndim * n_points
n_unknowns = ndim * solver.n_cpts
alphas, betas = self.calc_alphas(solver)
weights = self.get_weights()
A = np.zeros((n_equations, n_unknowns))
B = np.zeros((n_equations, 1))
weight = 1
for pt_idx in range(n_points):
for cpt_idx in range(solver.n_cpts):
alpha = alphas[cpt_idx][pt_idx]
beta = betas[cpt_idx][pt_idx]
for dim_idx in range(ndim):
A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weight * (alpha - beta)
if self.relative:
if solver.src_curve is None:
raise Exception("Can not solve relative constraint without original curve")
else:
points1, points2 = self.calc_vectors(solver)
for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)):
for dim_idx in range(ndim):
B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weight * (pt2 - pt1)[np.newaxis]
print("A", A)
print("B", B)
return A, B
def calc_vectors(self, solver):
points1 = solver.src_curve.tangent_array(self.us1)
points2 = solver.src_curve.tangent_array(self.us2)
print(f"Tg1: {points1}, Tg2: {points2}")
return points1, points2
class SvNurbsCurveControlPoints(SvNurbsCurveGoal):
"""
Goal which says that the curve must have control points at particular
locations.
"""
def __init__(self, cpt_idxs, cpt_vectors, weights = None, relative=True):
self.cpt_idxs = np.asarray(cpt_idxs)
self.cpt_vectors = np.asarray(cpt_vectors)
self.relative = relative
if weights is None:
self.weights = None
else:
self.weights = np.asarray(weights)
@staticmethod
def single(idx, vector, weight=None, relative=True):
if weight is None:
weights = None
else:
weights = [weight]
return SvNurbsCurveControlPoints([idx], [vector], weights=weights, relative=relative)
def get_weights(self):
weights = self.weights
n_points = len(self.cpt_vectors)
if weights is None:
weights = np.ones((n_points,))
return weights
def copy(self):
return SvNurbsCurveControlPoints(self.cpt_idxs, self.cpt_vectors, self.weights, self.relative)
def add(self, other):
if other.relative != self.relative:
return None
g = self.copy()
g.cpt_idxs = np.concatenate((g.cpt_idxs, other.cpt_idxs))
g.cpt_vectors = np.concatenate((g.cpt_vectors, other.cpt_vectors))
g.weights = np.concatenate((g.get_weights(), other.get_weights()))
return g
def get_n_defined_control_points(self):
return len(self.cpt_idxs)
def get_equations(self, solver):
ndim = solver.ndim
n_points = len(self.cpt_vectors)
n_equations = ndim * n_points
n_unknowns = ndim * solver.n_cpts
weights = self.get_weights()
A = np.zeros((n_equations, n_unknowns))
B = np.zeros((n_equations, 1))
for pt_idx, cpt_idx in enumerate(self.cpt_idxs):
for dim_idx in range(ndim):
A[dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx]
if solver.src_curve is None:
if self.relative:
raise Exception("Can not solve relative constraint without original curve")
else:
src_points = None
else:
if self.relative:
src_points = None
else:
src_points = solver.src_curve.get_control_points()
for pt_idx, (cpt_idx, point) in enumerate(zip(self.cpt_idxs, self.cpt_vectors)):
if src_points is not None:
point = point - src_points[cpt_idx]
B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis]
return A, B
class SvNurbsCurveSolver(SvCurve):
"""
NURBS Curve Solver.
Usually this class is used as follows:
solver = SvNurbsCurveSolver(degree=3)
# Provide goals
solver.add_goal(SvNurbsCurvePoints(...))
solver.add_goal(SvNurbsCurveTangents(...))
# Guess curve parameters so that the system would be well-determined:
solver.guess_curve_params()
# Or the parameters can be provided explicitly:
solver.set_curve_params(n_cpts, knotvector)
# Solve the problem:
curve = solver.solve()
"""
def __init__(self, degree=None, src_curve=None, ndim=3):
if degree is None and src_curve is None:
raise Exception("Either degree or src_curve must be provided")
elif degree is not None and src_curve is not None and src_curve.get_degree() != degree:
raise Exception("If src_curve is provided, then degree must not be provided")
self.src_curve = src_curve
if src_curve is not None and degree is None:
self.degree = src_curve.get_degree()
else:
self.degree = degree
self.ndim = ndim
self.n_cpts = None
self._curve_weights = None
self._is_rational = None
self.knotvector = None
self.goals = []
self.A = self.B = None
@staticmethod
def _check_is_rational(weights):
w, W = weights.min(), weights.max()
return abs(W/w - 1.0) > 1e-6
@property
def curve_weights(self):
return self._curve_weights
@curve_weights.setter
def curve_weights(self, weights):
if weights is None:
self._is_rational = False
else:
weights = np.asarray(weights)
self._is_rational = SvNurbsCurveSolver._check_is_rational(weights)
self._curve_weights = weights
def is_rational(self):
if self._is_rational is None:
self._is_rational = SvNurbsCurveSolver._check_is_rational(self._curve_weights)
return self._is_rational
def copy(self):
solver = SvNurbsCurveSolver(degree=self.degree, src_curve=self.src_curve)
solver.n_cpts = self.n_cpts
solver.curve_weights = self.curve_weights
solver.knotvector = self.knotvector
solver.goals = self.goals[:]
solver.A = self.A
solver.B = self.B
return solver
def evaluate(self, t):
return self.to_nurbs().evaluate(t)
def evaluate_array(self, ts):
return self.to_nurbs().evaluate_array(ts)
def get_u_bounds(self):
return self.to_nurbs().get_u_bounds()
def get_degree(self):
return self.degree
def get_control_points(self):
return self.to_nurbs().get_control_points()
def get_knotvector(self):
return self.knotvector
def set_curve_weights(self, weights):
if len(weights) != self.n_cpts:
raise Exception("Number of weights must be equal to the number of control points")
self.curve_weights = np.asarray(weights)
def set_curve_params(self, n_cpts, knotvector = None, weights = None):
self.n_cpts = n_cpts
if knotvector is not None:
err = sv_knotvector.check(self.degree, knotvector, n_cpts)
if err is not None:
raise Exception(err)
self.knotvector = np.asarray(knotvector)
else:
self.knotvector = sv_knotvector.generate(self.degree, n_cpts)
self.curve_weights = weights
def guess_curve_params(self):
n_equations = sum(g.get_n_defined_control_points() for g in self.goals)
self.n_cpts = n_equations
self.knotvector = sv_knotvector.generate(self.degree, self.n_cpts)
def guess_n_control_points(self):
n_equations = sum(g.get_n_defined_control_points() for g in self.goals)
return n_equations
def set_knotvector(self, knotvector):
self.knotvector = np.asarray(knotvector)
def add_goal(self, goal):
self.goals.append(goal)
def set_goals(self, goals):
self.goals = goals[:]
def _sort_goals(self):
goal_dict = defaultdict(list)
for goal in self.goals:
goal_dict[type(goal)].append(goal)
goals = []
for clazz in goal_dict:
clz_goals = goal_dict[clazz]
#print(f"Merging goals of class {clazz}: {clz_goals}")
merged_goal = clz_goals[0]
g = merged_goal
for other_goal in clz_goals[1:]:
g = merged_goal.add(other_goal)
#print(f"{merged_goal} + {other_goal} = {g}")
if g is not None:
merged_goal = g
else:
goals.append(merged_goal)
merged_goal = other_goal
goals.append(merged_goal)
#print(f"Merge result: {goals}")
self.goals = goals
def _init(self):
if self.n_cpts is None:
raise Exception("Number of control points is not specified; specify it in the constructor, in set_curve_params() call, or call guess_curve_params()")
if self.knotvector is None:
raise Exception("Knotvector is not specified; specify it in the constructor, in set_curve_params() call, or call guess_curve_params()")
ndim = self.ndim
n = self.n_cpts
p = self.degree
if self.curve_weights is None:
self.curve_weights = np.ones((n,))
self.basis = SvNurbsBasisFunctions(self.knotvector)
self._sort_goals()
As = []
Bs = []
for goal in self.goals:
Ai, Bi = goal.get_equations(self)
As.append(Ai)
Bs.append(Bi)
self.A = np.concatenate(As)
self.B = np.concatenate(Bs)
PROBLEM_WELLDETERMINED = 'WELLDETERMINED'
PROBLEM_UNDERDETERMINED = 'UNDERDETERMINED'
PROBLEM_OVERDETERMINED = 'OVERDETERMINED'
PROBLEM_ANY = {PROBLEM_WELLDETERMINED, PROBLEM_UNDERDETERMINED, PROBLEM_OVERDETERMINED}
def solve(self, implementation = SvNurbsMaths.NATIVE, logger = None):
problem_type, residue, curve = self.solve_ex(implementation = implementation, logger = logger)
return curve
def solve_welldetermined(self, implementation = SvNurbsMaths.NATIVE, logger = None):
problem_type, residue, curve = self.solve_ex(problem_types = {SvNurbsCurveSolver.PROBLEM_WELLDETERMINED},
implementation = implementation, logger = logger)
return curve
def solve_ex(self, problem_types = PROBLEM_ANY, implementation = SvNurbsMaths.NATIVE, logger = None):
self._init()
if logger is None:
logger = get_logger()
residue = 0.0
ndim = self.ndim
n = self.n_cpts
n_equations, n_unknowns = self.A.shape
if n_equations == n_unknowns:
#logger.debug(f"Solving well-determined system: #equations = {n_equations}, #unknonwns = {n_unknowns}")
problem_type = SvNurbsCurveSolver.PROBLEM_WELLDETERMINED
if problem_type not in problem_types:
raise Exception("The problem is well-determined")
try:
A1 = np.linalg.inv(self.A)
X = (A1 @ self.B).T
except np.linalg.LinAlgError as e:
logger.error(f"Matrix: {self.A}")
raise Exception(f"Can not solve: #equations = {n_equations}, #unknowns = {n_unknowns}: {e}") from e
elif n_equations < n_unknowns:
#logger.debug(f"Solving underdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}")
problem_type = SvNurbsCurveSolver.PROBLEM_UNDERDETERMINED
if problem_type not in problem_types:
raise Exception("The problem is underdetermined")
A1 = np.linalg.pinv(self.A)
X = (A1 @ self.B).T
else: # n_equations > n_unknowns
#logger.debug(f"Solving overdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}")
problem_type = SvNurbsCurveSolver.PROBLEM_OVERDETERMINED
if problem_type not in problem_types:
raise Exception("The system is overdetermined")
X, residues, rank, singval = np.linalg.lstsq(self.A, self.B)
residue = residues.sum()
d_cpts = X.reshape((n, ndim))
if ndim == 4:
d_cpts, d_weights = from_homogenous(d_cpts)
if self.src_curve is None:
weights = d_weights
else:
weights = self.curve_weights + d_weights
else:
weights = self.curve_weights
if self.src_curve is None:
curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, d_cpts, weights)
else:
cpts = self.src_curve.get_control_points() + d_cpts
curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, cpts, weights)
return problem_type, residue, curve
def to_nurbs(self, implementation = SvNurbsMaths.NATIVE):
solver = self.copy()
solver.guess_curve_params()
return solver.solve(implementation = implementation)
Classes
class SvNurbsCurveControlPoints (cpt_idxs, cpt_vectors, weights=None, relative=True)
-
Goal which says that the curve must have control points at particular locations.
Expand source code
class SvNurbsCurveControlPoints(SvNurbsCurveGoal): """ Goal which says that the curve must have control points at particular locations. """ def __init__(self, cpt_idxs, cpt_vectors, weights = None, relative=True): self.cpt_idxs = np.asarray(cpt_idxs) self.cpt_vectors = np.asarray(cpt_vectors) self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) @staticmethod def single(idx, vector, weight=None, relative=True): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveControlPoints([idx], [vector], weights=weights, relative=relative) def get_weights(self): weights = self.weights n_points = len(self.cpt_vectors) if weights is None: weights = np.ones((n_points,)) return weights def copy(self): return SvNurbsCurveControlPoints(self.cpt_idxs, self.cpt_vectors, self.weights, self.relative) def add(self, other): if other.relative != self.relative: return None g = self.copy() g.cpt_idxs = np.concatenate((g.cpt_idxs, other.cpt_idxs)) g.cpt_vectors = np.concatenate((g.cpt_vectors, other.cpt_vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g def get_n_defined_control_points(self): return len(self.cpt_idxs) def get_equations(self, solver): ndim = solver.ndim n_points = len(self.cpt_vectors) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) for pt_idx, cpt_idx in enumerate(self.cpt_idxs): for dim_idx in range(ndim): A[dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] if solver.src_curve is None: if self.relative: raise Exception("Can not solve relative constraint without original curve") else: src_points = None else: if self.relative: src_points = None else: src_points = solver.src_curve.get_control_points() for pt_idx, (cpt_idx, point) in enumerate(zip(self.cpt_idxs, self.cpt_vectors)): if src_points is not None: point = point - src_points[cpt_idx] B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis] return A, B
Ancestors
Static methods
def single(idx, vector, weight=None, relative=True)
-
Expand source code
@staticmethod def single(idx, vector, weight=None, relative=True): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveControlPoints([idx], [vector], weights=weights, relative=relative)
Methods
def add(self, other)
-
Expand source code
def add(self, other): if other.relative != self.relative: return None g = self.copy() g.cpt_idxs = np.concatenate((g.cpt_idxs, other.cpt_idxs)) g.cpt_vectors = np.concatenate((g.cpt_vectors, other.cpt_vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurveControlPoints(self.cpt_idxs, self.cpt_vectors, self.weights, self.relative)
def get_equations(self, solver)
-
Expand source code
def get_equations(self, solver): ndim = solver.ndim n_points = len(self.cpt_vectors) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) for pt_idx, cpt_idx in enumerate(self.cpt_idxs): for dim_idx in range(ndim): A[dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] if solver.src_curve is None: if self.relative: raise Exception("Can not solve relative constraint without original curve") else: src_points = None else: if self.relative: src_points = None else: src_points = solver.src_curve.get_control_points() for pt_idx, (cpt_idx, point) in enumerate(zip(self.cpt_idxs, self.cpt_vectors)): if src_points is not None: point = point - src_points[cpt_idx] B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis] return A, B
def get_n_defined_control_points(self)
-
Expand source code
def get_n_defined_control_points(self): return len(self.cpt_idxs)
def get_weights(self)
-
Expand source code
def get_weights(self): weights = self.weights n_points = len(self.cpt_vectors) if weights is None: weights = np.ones((n_points,)) return weights
class SvNurbsCurveCotangents (us1, us2, weights=None, relative_u=False, relative=False)
-
Goal which says that curve must have equal tangent vectors at two sets of parameter values.
Expand source code
class SvNurbsCurveCotangents(SvNurbsCurveSelfIntersections): """ Goal which says that curve must have equal tangent vectors at two sets of parameter values. """ def __init__(self, us1, us2, weights = None, relative_u=False, relative=False): if len(us1) != len(us2): raise Exception("Lengths of us1 and us2 must be equal") self.us1 = np.asarray(us1) self.us2 = np.asarray(us2) self.relative_u = relative_u self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) def __repr__(self): return f"<Equal tangents, cnt={len(self.us1)}>" @staticmethod def single(u1, u2, weight=None, relative_u=False, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveCotangents([u1], [u2], weights, relative_u=relative_u, relative=relative) def copy(self): return SvNurbsCurveCotangents(self.us1, self.us2, self.weights, self.relative_u, self.relative) def calc_alphas(self, solver): us1 = self.us1 us2 = self.us2 if self.relative_u: u_min, u_max = solver.knotvector[0], solver.knotvector[-1] us1 = u_min + (u_max - u_min) * us1 us2 = u_min + (u_max - u_min) * us2 p = solver.degree alphas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us1) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) betas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us2) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return alphas, betas def get_equations(self, solver): ndim = solver.ndim us1 = self.us1 us2 = self.us2 p = solver.degree n_points = len(us1) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas, betas = self.calc_alphas(solver) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) weight = 1 for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] beta = betas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weight * (alpha - beta) if self.relative: if solver.src_curve is None: raise Exception("Can not solve relative constraint without original curve") else: points1, points2 = self.calc_vectors(solver) for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)): for dim_idx in range(ndim): B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weight * (pt2 - pt1)[np.newaxis] print("A", A) print("B", B) return A, B def calc_vectors(self, solver): points1 = solver.src_curve.tangent_array(self.us1) points2 = solver.src_curve.tangent_array(self.us2) print(f"Tg1: {points1}, Tg2: {points2}") return points1, points2
Ancestors
Static methods
def single(u1, u2, weight=None, relative_u=False, relative=False)
-
Expand source code
@staticmethod def single(u1, u2, weight=None, relative_u=False, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveCotangents([u1], [u2], weights, relative_u=relative_u, relative=relative)
Methods
def calc_alphas(self, solver)
-
Expand source code
def calc_alphas(self, solver): us1 = self.us1 us2 = self.us2 if self.relative_u: u_min, u_max = solver.knotvector[0], solver.knotvector[-1] us1 = u_min + (u_max - u_min) * us1 us2 = u_min + (u_max - u_min) * us2 p = solver.degree alphas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us1) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) betas = [solver.basis.weighted_derivative(k, p, 1, solver.curve_weights)(us2) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return alphas, betas
def calc_vectors(self, solver)
-
Expand source code
def calc_vectors(self, solver): points1 = solver.src_curve.tangent_array(self.us1) points2 = solver.src_curve.tangent_array(self.us2) print(f"Tg1: {points1}, Tg2: {points2}") return points1, points2
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurveCotangents(self.us1, self.us2, self.weights, self.relative_u, self.relative)
def get_equations(self, solver)
-
Expand source code
def get_equations(self, solver): ndim = solver.ndim us1 = self.us1 us2 = self.us2 p = solver.degree n_points = len(us1) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas, betas = self.calc_alphas(solver) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) weight = 1 for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] beta = betas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weight * (alpha - beta) if self.relative: if solver.src_curve is None: raise Exception("Can not solve relative constraint without original curve") else: points1, points2 = self.calc_vectors(solver) for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)): for dim_idx in range(ndim): B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weight * (pt2 - pt1)[np.newaxis] print("A", A) print("B", B) return A, B
class SvNurbsCurveDerivatives (order, us, vectors, weights=None, relative=False)
-
Goal which says that the curve must pass through the specified points at specified values of parameter.
Expand source code
class SvNurbsCurveDerivatives(SvNurbsCurvePoints): def __init__(self, order, us, vectors, weights = None, relative=False): self.order = order self.us = np.asarray(us) if self.us.ndim != 1: raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}") self.vectors = np.asarray(vectors) if self.vectors.ndim != 2: raise Exception(f"Vectors must be 2-dimensional, but got {self.vectors.shape}") if len(us) != len(self.vectors): raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}") self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) def __repr__(self): if self.relative: return f"<Relative Derivatives, order={self.order}, cnt={len(self.us)}>" else: return f"<Derivatives, order={self.order}, cnt={len(self.us)}>" @staticmethod def single(order, u, tangent, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveDerivatives(order, [u], [tangent], weights, relative=relative) def copy(self): return SvNurbsCurveDerivatives(self.order, self.us, self.vectors, self.weights, relative=self.relative) def add(self, other): if self.relative != other.relative: return None if self.order != other.order: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g def calc_alphas(self, solver, us): p = solver.degree betas = [solver.basis.weighted_derivative(k, p, self.order, solver.curve_weights)(us) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return betas def get_src_points(self, solver): return solver.src_curve.tangent_array(self.us)
Ancestors
Static methods
def single(order, u, tangent, weight=None, relative=False)
-
Expand source code
@staticmethod def single(order, u, tangent, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveDerivatives(order, [u], [tangent], weights, relative=relative)
Methods
def add(self, other)
-
Expand source code
def add(self, other): if self.relative != other.relative: return None if self.order != other.order: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g
def calc_alphas(self, solver, us)
-
Expand source code
def calc_alphas(self, solver, us): p = solver.degree betas = [solver.basis.weighted_derivative(k, p, self.order, solver.curve_weights)(us) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return betas
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurveDerivatives(self.order, self.us, self.vectors, self.weights, relative=self.relative)
def get_src_points(self, solver)
-
Expand source code
def get_src_points(self, solver): return solver.src_curve.tangent_array(self.us)
class SvNurbsCurveGoal
-
Abstract class for curve goal.
Expand source code
class SvNurbsCurveGoal(object): """ Abstract class for curve goal. """ def copy(self): raise Exception("Not implemented") def add(self, other): raise Exception("Not implemented") def get_equations(self, solver): raise Exception("Not implemented") def get_n_defined_control_points(self): raise Exception("Not implemented")
Subclasses
Methods
def add(self, other)
-
Expand source code
def add(self, other): raise Exception("Not implemented")
def copy(self)
-
Expand source code
def copy(self): raise Exception("Not implemented")
def get_equations(self, solver)
-
Expand source code
def get_equations(self, solver): raise Exception("Not implemented")
def get_n_defined_control_points(self)
-
Expand source code
def get_n_defined_control_points(self): raise Exception("Not implemented")
class SvNurbsCurvePoints (us, points, weights=None, relative=False)
-
Goal which says that the curve must pass through the specified points at specified values of parameter.
Expand source code
class SvNurbsCurvePoints(SvNurbsCurveGoal): """ Goal which says that the curve must pass through the specified points at specified values of parameter. """ def __init__(self, us, points, weights = None, relative=False): self.us = np.asarray(us) if self.us.ndim != 1: raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}") self.vectors = np.asarray(points) if self.vectors.ndim != 2: raise Exception(f"Points must be 2-dimensional, but got {self.vectors.shape}") if len(us) != len(self.vectors): raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}") self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) def __repr__(self): if self.relative: return f"<Relative Points, cnt={len(self.us)}>" else: return f"<Points, cnt={len(self.us)}>" @staticmethod def single(u, point, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurvePoints([u], [point], weights, relative=relative) def copy(self): return SvNurbsCurvePoints(self.us, self.vectors, self.weights, relative=self.relative) def get_weights(self): weights = self.weights n_points = len(self.vectors) if weights is None: weights = np.ones((n_points,)) elif isinstance(weights, np.ndarray) and weights.shape == (1,): weights = np.full((n_points,), weights[0]) elif isinstance(weights, (int,float)): weights = np.full((n_points,), weights) return weights def add(self, other): if other.relative != self.relative: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g def calc_alphas(self, solver, us): p = solver.degree if solver.is_rational(): alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us) for k in range(solver.n_cpts)] else: alphas = [solver.basis.function(k,p)(us) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) return alphas def get_src_points(self, solver): return solver.src_curve.evaluate_array(self.us) def get_n_defined_control_points(self): return len(self.us) def get_equations(self, solver): ndim = solver.ndim us = self.us vectors = self.vectors n_points = len(vectors) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas = self.calc_alphas(solver, us) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) #print(f"A: {A.shape}, W {weights.shape}, n_points {n_points}") for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * alpha if solver.src_curve is None: if self.relative: raise Exception("Can not solve relative constraint without original curve") else: src_points = None else: if self.relative: src_points = None else: src_points = self.get_src_points(solver) for pt_idx, point in enumerate(vectors): if src_points is not None: point = point - src_points[pt_idx] B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis] return A, B
Ancestors
Subclasses
Static methods
def single(u, point, weight=None, relative=False)
-
Expand source code
@staticmethod def single(u, point, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurvePoints([u], [point], weights, relative=relative)
Methods
def add(self, other)
-
Expand source code
def add(self, other): if other.relative != self.relative: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g
def calc_alphas(self, solver, us)
-
Expand source code
def calc_alphas(self, solver, us): p = solver.degree if solver.is_rational(): alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us) for k in range(solver.n_cpts)] else: alphas = [solver.basis.function(k,p)(us) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) return alphas
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurvePoints(self.us, self.vectors, self.weights, relative=self.relative)
def get_equations(self, solver)
-
Expand source code
def get_equations(self, solver): ndim = solver.ndim us = self.us vectors = self.vectors n_points = len(vectors) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas = self.calc_alphas(solver, us) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) #print(f"A: {A.shape}, W {weights.shape}, n_points {n_points}") for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * alpha if solver.src_curve is None: if self.relative: raise Exception("Can not solve relative constraint without original curve") else: src_points = None else: if self.relative: src_points = None else: src_points = self.get_src_points(solver) for pt_idx, point in enumerate(vectors): if src_points is not None: point = point - src_points[pt_idx] B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * point[np.newaxis] return A, B
def get_n_defined_control_points(self)
-
Expand source code
def get_n_defined_control_points(self): return len(self.us)
def get_src_points(self, solver)
-
Expand source code
def get_src_points(self, solver): return solver.src_curve.evaluate_array(self.us)
def get_weights(self)
-
Expand source code
def get_weights(self): weights = self.weights n_points = len(self.vectors) if weights is None: weights = np.ones((n_points,)) elif isinstance(weights, np.ndarray) and weights.shape == (1,): weights = np.full((n_points,), weights[0]) elif isinstance(weights, (int,float)): weights = np.full((n_points,), weights) return weights
class SvNurbsCurveSelfIntersections (us1, us2, weights=None, relative_u=False, relative=False)
-
Goal which says that the curve must have self-intersections at specified sets of parameter values.
Expand source code
class SvNurbsCurveSelfIntersections(SvNurbsCurveGoal): """ Goal which says that the curve must have self-intersections at specified sets of parameter values. """ def __init__(self, us1, us2, weights = None, relative_u=False, relative=False): if len(us1) != len(us2): raise Exception("Lengths of us1 and us2 must be equal") self.us1 = np.asarray(us1) self.us2 = np.asarray(us2) self.relative_u = relative_u self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) def __repr__(self): return f"<Self-intersections, cnt={len(self.us1)}>" @staticmethod def single(u1, u2, weight=None, relative_u=False, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveSelfIntersections([u1], [u2], weights, relative_u=relative_u, relative=relative) def copy(self): return SvNurbsCurveSelfIntersections(self.us1, self.us2, self.weights, self.relative_u, self.relative) def get_weights(self): weights = self.weights if weights is None: n_points = len(self.us1) weights = np.ones((n_points,)) return weights def add(self, other): if other.relative_u != self.relative_u: return None if other.relative != self.relative: return None g = self.copy() g.us1 = np.concatenate((g.us1, other.us1)) g.us2 = np.concatenate((g.us2, other.us2)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g def calc_alphas(self, solver): us1 = self.us1 us2 = self.us2 if self.relative_u: u_min, u_max = solver.knotvector[0], solver.knotvector[-1] us1 = u_min + (u_max - u_min) * us1 us2 = u_min + (u_max - u_min) * us2 p = solver.degree alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us1) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) betas = [solver.basis.fraction(k,p, solver.curve_weights)(us2) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return alphas, betas def calc_vectors(self, solver): points1 = solver.src_curve.evaluate_array(self.us1) points2 = solver.src_curve.evaluate_array(self.us2) return points1, points2 def get_n_defined_control_points(self): return len(self.us1) def get_equations(self, solver): ndim = solver.ndim us1 = self.us1 us2 = self.us2 p = solver.degree n_points = len(us1) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas, betas = self.calc_alphas(solver) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] beta = betas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * (alpha - beta) if self.relative: if solver.src_curve is None: raise Exception("Can not solve relative constraint without original curve") else: points1, points2 = self.calc_vectors(solver) for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)): for dim_idx in range(ndim): B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * (pt2 - pt1)[np.newaxis] return A, B
Ancestors
Subclasses
Static methods
def single(u1, u2, weight=None, relative_u=False, relative=False)
-
Expand source code
@staticmethod def single(u1, u2, weight=None, relative_u=False, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveSelfIntersections([u1], [u2], weights, relative_u=relative_u, relative=relative)
Methods
def add(self, other)
-
Expand source code
def add(self, other): if other.relative_u != self.relative_u: return None if other.relative != self.relative: return None g = self.copy() g.us1 = np.concatenate((g.us1, other.us1)) g.us2 = np.concatenate((g.us2, other.us2)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g
def calc_alphas(self, solver)
-
Expand source code
def calc_alphas(self, solver): us1 = self.us1 us2 = self.us2 if self.relative_u: u_min, u_max = solver.knotvector[0], solver.knotvector[-1] us1 = u_min + (u_max - u_min) * us1 us2 = u_min + (u_max - u_min) * us2 p = solver.degree alphas = [solver.basis.fraction(k,p, solver.curve_weights)(us1) for k in range(solver.n_cpts)] alphas = np.array(alphas) # (n_cpts, n_points) betas = [solver.basis.fraction(k,p, solver.curve_weights)(us2) for k in range(solver.n_cpts)] betas = np.array(betas) # (n_cpts, n_points) return alphas, betas
def calc_vectors(self, solver)
-
Expand source code
def calc_vectors(self, solver): points1 = solver.src_curve.evaluate_array(self.us1) points2 = solver.src_curve.evaluate_array(self.us2) return points1, points2
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurveSelfIntersections(self.us1, self.us2, self.weights, self.relative_u, self.relative)
def get_equations(self, solver)
-
Expand source code
def get_equations(self, solver): ndim = solver.ndim us1 = self.us1 us2 = self.us2 p = solver.degree n_points = len(us1) n_equations = ndim * n_points n_unknowns = ndim * solver.n_cpts alphas, betas = self.calc_alphas(solver) weights = self.get_weights() A = np.zeros((n_equations, n_unknowns)) B = np.zeros((n_equations, 1)) for pt_idx in range(n_points): for cpt_idx in range(solver.n_cpts): alpha = alphas[cpt_idx][pt_idx] beta = betas[cpt_idx][pt_idx] for dim_idx in range(ndim): A[ndim*pt_idx + dim_idx, ndim*cpt_idx + dim_idx] = weights[pt_idx] * (alpha - beta) if self.relative: if solver.src_curve is None: raise Exception("Can not solve relative constraint without original curve") else: points1, points2 = self.calc_vectors(solver) for pt_idx, (pt1, pt2) in enumerate(zip(points1, points2)): for dim_idx in range(ndim): B[pt_idx*ndim:pt_idx*ndim+ndim,0] = weights[pt_idx] * (pt2 - pt1)[np.newaxis] return A, B
def get_n_defined_control_points(self)
-
Expand source code
def get_n_defined_control_points(self): return len(self.us1)
def get_weights(self)
-
Expand source code
def get_weights(self): weights = self.weights if weights is None: n_points = len(self.us1) weights = np.ones((n_points,)) return weights
class SvNurbsCurveSolver (degree=None, src_curve=None, ndim=3)
-
NURBS Curve Solver.
Usually this class is used as follows:
solver = SvNurbsCurveSolver(degree=3) # Provide goals solver.add_goal(SvNurbsCurvePoints(...)) solver.add_goal(SvNurbsCurveTangents(...)) # Guess curve parameters so that the system would be well-determined: solver.guess_curve_params() # Or the parameters can be provided explicitly: solver.set_curve_params(n_cpts, knotvector) # Solve the problem: curve = solver.solve()
Expand source code
class SvNurbsCurveSolver(SvCurve): """ NURBS Curve Solver. Usually this class is used as follows: solver = SvNurbsCurveSolver(degree=3) # Provide goals solver.add_goal(SvNurbsCurvePoints(...)) solver.add_goal(SvNurbsCurveTangents(...)) # Guess curve parameters so that the system would be well-determined: solver.guess_curve_params() # Or the parameters can be provided explicitly: solver.set_curve_params(n_cpts, knotvector) # Solve the problem: curve = solver.solve() """ def __init__(self, degree=None, src_curve=None, ndim=3): if degree is None and src_curve is None: raise Exception("Either degree or src_curve must be provided") elif degree is not None and src_curve is not None and src_curve.get_degree() != degree: raise Exception("If src_curve is provided, then degree must not be provided") self.src_curve = src_curve if src_curve is not None and degree is None: self.degree = src_curve.get_degree() else: self.degree = degree self.ndim = ndim self.n_cpts = None self._curve_weights = None self._is_rational = None self.knotvector = None self.goals = [] self.A = self.B = None @staticmethod def _check_is_rational(weights): w, W = weights.min(), weights.max() return abs(W/w - 1.0) > 1e-6 @property def curve_weights(self): return self._curve_weights @curve_weights.setter def curve_weights(self, weights): if weights is None: self._is_rational = False else: weights = np.asarray(weights) self._is_rational = SvNurbsCurveSolver._check_is_rational(weights) self._curve_weights = weights def is_rational(self): if self._is_rational is None: self._is_rational = SvNurbsCurveSolver._check_is_rational(self._curve_weights) return self._is_rational def copy(self): solver = SvNurbsCurveSolver(degree=self.degree, src_curve=self.src_curve) solver.n_cpts = self.n_cpts solver.curve_weights = self.curve_weights solver.knotvector = self.knotvector solver.goals = self.goals[:] solver.A = self.A solver.B = self.B return solver def evaluate(self, t): return self.to_nurbs().evaluate(t) def evaluate_array(self, ts): return self.to_nurbs().evaluate_array(ts) def get_u_bounds(self): return self.to_nurbs().get_u_bounds() def get_degree(self): return self.degree def get_control_points(self): return self.to_nurbs().get_control_points() def get_knotvector(self): return self.knotvector def set_curve_weights(self, weights): if len(weights) != self.n_cpts: raise Exception("Number of weights must be equal to the number of control points") self.curve_weights = np.asarray(weights) def set_curve_params(self, n_cpts, knotvector = None, weights = None): self.n_cpts = n_cpts if knotvector is not None: err = sv_knotvector.check(self.degree, knotvector, n_cpts) if err is not None: raise Exception(err) self.knotvector = np.asarray(knotvector) else: self.knotvector = sv_knotvector.generate(self.degree, n_cpts) self.curve_weights = weights def guess_curve_params(self): n_equations = sum(g.get_n_defined_control_points() for g in self.goals) self.n_cpts = n_equations self.knotvector = sv_knotvector.generate(self.degree, self.n_cpts) def guess_n_control_points(self): n_equations = sum(g.get_n_defined_control_points() for g in self.goals) return n_equations def set_knotvector(self, knotvector): self.knotvector = np.asarray(knotvector) def add_goal(self, goal): self.goals.append(goal) def set_goals(self, goals): self.goals = goals[:] def _sort_goals(self): goal_dict = defaultdict(list) for goal in self.goals: goal_dict[type(goal)].append(goal) goals = [] for clazz in goal_dict: clz_goals = goal_dict[clazz] #print(f"Merging goals of class {clazz}: {clz_goals}") merged_goal = clz_goals[0] g = merged_goal for other_goal in clz_goals[1:]: g = merged_goal.add(other_goal) #print(f"{merged_goal} + {other_goal} = {g}") if g is not None: merged_goal = g else: goals.append(merged_goal) merged_goal = other_goal goals.append(merged_goal) #print(f"Merge result: {goals}") self.goals = goals def _init(self): if self.n_cpts is None: raise Exception("Number of control points is not specified; specify it in the constructor, in set_curve_params() call, or call guess_curve_params()") if self.knotvector is None: raise Exception("Knotvector is not specified; specify it in the constructor, in set_curve_params() call, or call guess_curve_params()") ndim = self.ndim n = self.n_cpts p = self.degree if self.curve_weights is None: self.curve_weights = np.ones((n,)) self.basis = SvNurbsBasisFunctions(self.knotvector) self._sort_goals() As = [] Bs = [] for goal in self.goals: Ai, Bi = goal.get_equations(self) As.append(Ai) Bs.append(Bi) self.A = np.concatenate(As) self.B = np.concatenate(Bs) PROBLEM_WELLDETERMINED = 'WELLDETERMINED' PROBLEM_UNDERDETERMINED = 'UNDERDETERMINED' PROBLEM_OVERDETERMINED = 'OVERDETERMINED' PROBLEM_ANY = {PROBLEM_WELLDETERMINED, PROBLEM_UNDERDETERMINED, PROBLEM_OVERDETERMINED} def solve(self, implementation = SvNurbsMaths.NATIVE, logger = None): problem_type, residue, curve = self.solve_ex(implementation = implementation, logger = logger) return curve def solve_welldetermined(self, implementation = SvNurbsMaths.NATIVE, logger = None): problem_type, residue, curve = self.solve_ex(problem_types = {SvNurbsCurveSolver.PROBLEM_WELLDETERMINED}, implementation = implementation, logger = logger) return curve def solve_ex(self, problem_types = PROBLEM_ANY, implementation = SvNurbsMaths.NATIVE, logger = None): self._init() if logger is None: logger = get_logger() residue = 0.0 ndim = self.ndim n = self.n_cpts n_equations, n_unknowns = self.A.shape if n_equations == n_unknowns: #logger.debug(f"Solving well-determined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_WELLDETERMINED if problem_type not in problem_types: raise Exception("The problem is well-determined") try: A1 = np.linalg.inv(self.A) X = (A1 @ self.B).T except np.linalg.LinAlgError as e: logger.error(f"Matrix: {self.A}") raise Exception(f"Can not solve: #equations = {n_equations}, #unknowns = {n_unknowns}: {e}") from e elif n_equations < n_unknowns: #logger.debug(f"Solving underdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_UNDERDETERMINED if problem_type not in problem_types: raise Exception("The problem is underdetermined") A1 = np.linalg.pinv(self.A) X = (A1 @ self.B).T else: # n_equations > n_unknowns #logger.debug(f"Solving overdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_OVERDETERMINED if problem_type not in problem_types: raise Exception("The system is overdetermined") X, residues, rank, singval = np.linalg.lstsq(self.A, self.B) residue = residues.sum() d_cpts = X.reshape((n, ndim)) if ndim == 4: d_cpts, d_weights = from_homogenous(d_cpts) if self.src_curve is None: weights = d_weights else: weights = self.curve_weights + d_weights else: weights = self.curve_weights if self.src_curve is None: curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, d_cpts, weights) else: cpts = self.src_curve.get_control_points() + d_cpts curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, cpts, weights) return problem_type, residue, curve def to_nurbs(self, implementation = SvNurbsMaths.NATIVE): solver = self.copy() solver.guess_curve_params() return solver.solve(implementation = implementation)
Ancestors
Class variables
var PROBLEM_ANY
var PROBLEM_OVERDETERMINED
var PROBLEM_UNDERDETERMINED
var PROBLEM_WELLDETERMINED
Instance variables
var curve_weights
-
Expand source code
@property def curve_weights(self): return self._curve_weights
Methods
def add_goal(self, goal)
-
Expand source code
def add_goal(self, goal): self.goals.append(goal)
def copy(self)
-
Expand source code
def copy(self): solver = SvNurbsCurveSolver(degree=self.degree, src_curve=self.src_curve) solver.n_cpts = self.n_cpts solver.curve_weights = self.curve_weights solver.knotvector = self.knotvector solver.goals = self.goals[:] solver.A = self.A solver.B = self.B return solver
def get_knotvector(self)
-
Expand source code
def get_knotvector(self): return self.knotvector
def guess_curve_params(self)
-
Expand source code
def guess_curve_params(self): n_equations = sum(g.get_n_defined_control_points() for g in self.goals) self.n_cpts = n_equations self.knotvector = sv_knotvector.generate(self.degree, self.n_cpts)
def guess_n_control_points(self)
-
Expand source code
def guess_n_control_points(self): n_equations = sum(g.get_n_defined_control_points() for g in self.goals) return n_equations
def is_rational(self)
-
Expand source code
def is_rational(self): if self._is_rational is None: self._is_rational = SvNurbsCurveSolver._check_is_rational(self._curve_weights) return self._is_rational
def set_curve_params(self, n_cpts, knotvector=None, weights=None)
-
Expand source code
def set_curve_params(self, n_cpts, knotvector = None, weights = None): self.n_cpts = n_cpts if knotvector is not None: err = sv_knotvector.check(self.degree, knotvector, n_cpts) if err is not None: raise Exception(err) self.knotvector = np.asarray(knotvector) else: self.knotvector = sv_knotvector.generate(self.degree, n_cpts) self.curve_weights = weights
def set_curve_weights(self, weights)
-
Expand source code
def set_curve_weights(self, weights): if len(weights) != self.n_cpts: raise Exception("Number of weights must be equal to the number of control points") self.curve_weights = np.asarray(weights)
def set_goals(self, goals)
-
Expand source code
def set_goals(self, goals): self.goals = goals[:]
def set_knotvector(self, knotvector)
-
Expand source code
def set_knotvector(self, knotvector): self.knotvector = np.asarray(knotvector)
def solve(self, implementation='NATIVE', logger=None)
-
Expand source code
def solve(self, implementation = SvNurbsMaths.NATIVE, logger = None): problem_type, residue, curve = self.solve_ex(implementation = implementation, logger = logger) return curve
def solve_ex(self, problem_types={'WELLDETERMINED', 'UNDERDETERMINED', 'OVERDETERMINED'}, implementation='NATIVE', logger=None)
-
Expand source code
def solve_ex(self, problem_types = PROBLEM_ANY, implementation = SvNurbsMaths.NATIVE, logger = None): self._init() if logger is None: logger = get_logger() residue = 0.0 ndim = self.ndim n = self.n_cpts n_equations, n_unknowns = self.A.shape if n_equations == n_unknowns: #logger.debug(f"Solving well-determined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_WELLDETERMINED if problem_type not in problem_types: raise Exception("The problem is well-determined") try: A1 = np.linalg.inv(self.A) X = (A1 @ self.B).T except np.linalg.LinAlgError as e: logger.error(f"Matrix: {self.A}") raise Exception(f"Can not solve: #equations = {n_equations}, #unknowns = {n_unknowns}: {e}") from e elif n_equations < n_unknowns: #logger.debug(f"Solving underdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_UNDERDETERMINED if problem_type not in problem_types: raise Exception("The problem is underdetermined") A1 = np.linalg.pinv(self.A) X = (A1 @ self.B).T else: # n_equations > n_unknowns #logger.debug(f"Solving overdetermined system: #equations = {n_equations}, #unknonwns = {n_unknowns}") problem_type = SvNurbsCurveSolver.PROBLEM_OVERDETERMINED if problem_type not in problem_types: raise Exception("The system is overdetermined") X, residues, rank, singval = np.linalg.lstsq(self.A, self.B) residue = residues.sum() d_cpts = X.reshape((n, ndim)) if ndim == 4: d_cpts, d_weights = from_homogenous(d_cpts) if self.src_curve is None: weights = d_weights else: weights = self.curve_weights + d_weights else: weights = self.curve_weights if self.src_curve is None: curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, d_cpts, weights) else: cpts = self.src_curve.get_control_points() + d_cpts curve = SvNurbsMaths.build_curve(implementation, self.degree, self.knotvector, cpts, weights) return problem_type, residue, curve
def solve_welldetermined(self, implementation='NATIVE', logger=None)
-
Expand source code
def solve_welldetermined(self, implementation = SvNurbsMaths.NATIVE, logger = None): problem_type, residue, curve = self.solve_ex(problem_types = {SvNurbsCurveSolver.PROBLEM_WELLDETERMINED}, implementation = implementation, logger = logger) return curve
def to_nurbs(self, implementation='NATIVE')
-
Expand source code
def to_nurbs(self, implementation = SvNurbsMaths.NATIVE): solver = self.copy() solver.guess_curve_params() return solver.solve(implementation = implementation)
Inherited members
class SvNurbsCurveTangents (us, tangents, weights=None, relative=False)
-
Goal which says that the curve must have specified tangent vectors at specified values of parameter.
Expand source code
class SvNurbsCurveTangents(SvNurbsCurvePoints): """ Goal which says that the curve must have specified tangent vectors at specified values of parameter. """ def __init__(self, us, tangents, weights = None, relative=False): self.us = np.asarray(us) if self.us.ndim != 1: raise Exception(f"T values array must be 1-dimensional, but got {self.us.shape}") self.vectors = np.asarray(tangents) if self.vectors.ndim != 2: raise Exception(f"Points must be 2-dimensional, but got {self.vectors.shape}") if len(us) != len(self.vectors): raise Exception(f"Number of T values and number of points must be equal, but got #T = {len(us)}, #P = {len(self.vectors)}") self.relative = relative if weights is None: self.weights = None else: self.weights = np.asarray(weights) def __repr__(self): if self.relative: return f"<Relative Tangents, cnt={len(self.us)}>" else: return f"<Tangents, cnt={len(self.us)}>" @staticmethod def single(u, tangent, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveTangents([u], [tangent], weights, relative=relative) def copy(self): return SvNurbsCurveTangents(self.us, self.vectors, self.weights, relative=self.relative) def add(self, other): if self.relative != other.relative: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g def calc_alphas(self, solver, us): p = solver.degree ns = [solver.basis.function(k, p)(us) for k in range(solver.n_cpts)] ns = np.array(ns) # (n_cpts, n_pts) derivs = [solver.basis.derivative(k, p, 1)(us) for k in range(solver.n_cpts)] derivs = np.array(derivs) # (n_cpts, n_pts) weights = solver.curve_weights[np.newaxis].T # (n_cpts, 1) sum_ns = (ns * weights).sum(axis=0) # (n_pts,) sum_derivs = (derivs * weights).sum(axis=0) # (n_pts,) numerator = weights * (derivs * sum_ns - ns * sum_derivs) denominator = sum_ns**2 return numerator / denominator def get_src_points(self, solver): return solver.src_curve.tangent_array(self.us)
Ancestors
Static methods
def single(u, tangent, weight=None, relative=False)
-
Expand source code
@staticmethod def single(u, tangent, weight=None, relative=False): if weight is None: weights = None else: weights = [weight] return SvNurbsCurveTangents([u], [tangent], weights, relative=relative)
Methods
def add(self, other)
-
Expand source code
def add(self, other): if self.relative != other.relative: return None g = self.copy() g.us = np.concatenate((g.us, other.us)) g.vectors = np.concatenate((g.vectors, other.vectors)) g.weights = np.concatenate((g.get_weights(), other.get_weights())) return g
def calc_alphas(self, solver, us)
-
Expand source code
def calc_alphas(self, solver, us): p = solver.degree ns = [solver.basis.function(k, p)(us) for k in range(solver.n_cpts)] ns = np.array(ns) # (n_cpts, n_pts) derivs = [solver.basis.derivative(k, p, 1)(us) for k in range(solver.n_cpts)] derivs = np.array(derivs) # (n_cpts, n_pts) weights = solver.curve_weights[np.newaxis].T # (n_cpts, 1) sum_ns = (ns * weights).sum(axis=0) # (n_pts,) sum_derivs = (derivs * weights).sum(axis=0) # (n_pts,) numerator = weights * (derivs * sum_ns - ns * sum_derivs) denominator = sum_ns**2 return numerator / denominator
def copy(self)
-
Expand source code
def copy(self): return SvNurbsCurveTangents(self.us, self.vectors, self.weights, relative=self.relative)
def get_src_points(self, solver)
-
Expand source code
def get_src_points(self, solver): return solver.src_curve.tangent_array(self.us)