import networkx
from pymead.core.param import LengthDesVar
from pymead.core.constraints import *
from pymead.core.constraint_equations import *
from pymead.core.line import PolyLine
from pymead.core.point import Point
[docs]
class GCS(networkx.DiGraph):
"""
A Geometric Constraint Solver (GCS) used to maintain constraints between points in pymead, implemented using
the directed graph class in ``networkx``. Instances of this
class should not normally be created directly; the geometry collection creates objects of this class when
``GeometryCollection.add_constraint`` is called.
This constraint solver, while not powerful enough to handle arbitrary systems of equations, is nevertheless
sufficient for handling many common types of constraints needed for airfoil systems. The constraints are solved
using a graph-constructive approach and maintained using simple rigid body transformations. These transformations
occur for a given set of points when the parameter value of a constraint "upstream" of this set of points is
modified. All the points downstream of this constraint are handled as a rigid body (even if they are not
fully constrained as such) to preserve each of the relative constraints.
The primary constraints in pymead are distance constraints and relative angle constraints (with specialized options
for perpendicular and antiparallel constraints). The angle constraints are formed between three points
(start, vertex, and end). Two other special kinds of constraints, symmetry constraints and radius of curvature
constraints, are solved following the rigid body transformation.
"""
[docs]
def __init__(self):
"""
Constructor for the geometric constraint solver.
"""
super().__init__()
self.points = {}
self.roots = []
self.cluster_angle_params = {}
self.geo_col = None
self.partial_symmetry_solves = []
[docs]
def add_point(self, point: Point):
"""
Adds a point (node) to the graph (and to the ``"points"`` attribute). Also assigns this graph to the point.
Parameters
----------
point: Point
Point to add
Returns
-------
"""
point.gcs = self
self.add_node(point)
self.points[point.name()] = point
[docs]
def remove_point(self, point: Point):
"""
Removes a point (node) from the graph (and from the ``"points"`` attribute).
Parameters
----------
point: Point
Point to remove
Returns
-------
"""
self.remove_node(point)
self.points.pop(point.name())
def _check_if_constraint_creates_new_cluster(self, constraint: GeoCon):
"""
Determines if a new cluster is formed by adding this constraint by checking if there are any edges connected
in either direction to any of the points associated with the constraint. Only if no edges are found is a new
cluster created.
Parameters
----------
constraint: GeoCon
Constraint to analyze
Returns
-------
bool
``True`` if adding this constraint forms a cluster, ``False`` otherwise
"""
for point in constraint.child_nodes:
# If there is already an edge attached to any of the points in this constraint, do not create a new root
if len(self.in_edges(nbunch=point)) > 0 or len(self.out_edges(nbunch=point)) > 0:
return False
return True
def _set_edge_as_root(self, u: Point, v: Point):
"""
Sets the given edge as a root by setting the ``u`` value as the root and the ``v`` value as the rotation
handle of the constraint cluster.
Parameters
----------
u: Point
Starting node of the edge
v: Point
Terminating node of the edge
Returns
-------
"""
u.root = True
v.rotation_handle = True
if u not in [edge[0] for edge in self.roots]:
self.roots.append((u, v))
cluster_angle_exceptions = [
# Do not add a cluster angle parameter if the root was created as part of an antiparallel constraint between
# a polyline and another curve
any([isinstance(curve, PolyLine) for curve in u.curves]) and not all(
[u in curve.point_sequence().points() for curve in u.curves]),
# Do not add a cluster angle parameter if the root was created as part of an antiparallel constraint between
# a curve defined by symmetric constraints and a normal Bézier curve
(SymmetryConstraint.check_if_point_is_symmetric_target(u) and
SymmetryConstraint.check_if_point_is_symmetric_target(v))
]
if any(cluster_angle_exceptions):
v.rotation_handle = False # TODO: check if this change works for other cases
return
if v.rotation_param is not None:
param = v.rotation_param
else:
param = self.geo_col.add_param(
value=self.geo_col.units.convert_angle_from_base(
u.measure_angle(v),
unit=self.geo_col.units.current_angle_unit()
),
name="ClusterAngle-1",
unit_type="angle",
root=u,
rotation_handle=v
)
self.cluster_angle_params[u] = param
param.gcs = self
def _delete_root_status(self, root_node: Point, rotation_handle_node: Point = None):
"""
Removes root status from the given edge.
Parameters
----------
root_node: Point
``u``-value of the edge
rotation_handle_node: Point
``v``-value of the edge
Returns
-------
"""
root_node.root = False
if rotation_handle_node is not None:
rotation_handle_node.rotation_handle = False
rotation_handle_node.rotation_param = None
if root_node in self.cluster_angle_params:
cluster_angle_param = self.cluster_angle_params.pop(root_node)
if cluster_angle_param is not None:
self.geo_col.remove_pymead_obj(cluster_angle_param, constraint_removal=True)
root_idx = [r[0] for r in self.roots].index(root_node)
self.roots.pop(root_idx)
def _identify_root_from_rotation_handle(self, rotation_handle: Point) -> Point:
"""
Computes the root of the cluster given the rotation handle as the ``u``-value of the edge incident to the
rotation handle.
Parameters
----------
rotation_handle: Point
Rotation handle of the constraint cluster
Returns
-------
Point
Root of the constraint cluster
"""
if not isinstance(rotation_handle, Point):
raise ValueError(f"Detected rotation handle that is not a point ({rotation_handle = })")
in_edges = [edge for edge in self.in_edges(nbunch=rotation_handle)]
if not len(in_edges) == 1:
raise ValueError("Invalid rotation handle. Rotation should have exactly one incident edge "
"(the cluster root)")
return in_edges[0][0]
def _identify_rotation_handle(self, root_node: Point):
"""
Identifies the rotation handle by starting at the root node and testing each of the root node's neighbors
until the rotation handle is found.
Parameters
----------
root_node: Point
Root of the constraint cluster
Returns
-------
Point
Rotation handle
"""
for edge in self.out_edges(nbunch=root_node):
if edge[1].rotation_handle:
return edge[1]
raise ValueError("Could not identify rotation handle")
def _identify_and_delete_root(self, root_node: Point):
"""
Identifies the root edge by starting at the root node and testing each of the root node's neighbors
until the rotation handle is found (the ``v``-value of the root edge). Then, ``self._delete_root_status`` is
applied to demote the edge.
Parameters
----------
root_node: Point
Root of the constraint cluster
Returns
-------
"""
for edge in self.out_edges(nbunch=root_node):
if edge[1].rotation_handle:
self._delete_root_status(root_node, edge[1])
return
self._delete_root_status(root_node=root_node)
def _get_unique_roots_from_constraint(self, constraint: GeoCon) -> typing.List[Point]:
"""
Gets the unique roots associated with the addition of the input constraint. Used to determine whether a cluster
merge should occur.
Parameters
----------
constraint: GeoCon
Constraint from which to determine the unique, connected set of cluster roots
Returns
-------
typing.List[Point]
List of points representing the unique roots found. An error is raised if more than two are found, since
this should not be possible when adding a constraint.
"""
unique_roots = []
for point in constraint.child_nodes:
root = self._discover_root_from_node(point)
if root and root not in unique_roots:
unique_roots.append(root)
if len(unique_roots) > 2:
raise ValueError("Found more than two unique roots connected to the constraint being added")
return unique_roots
def _discover_root_from_node(self, source_node: Point):
"""
Traverse backward along the constraint graph from the ``source_node`` until the root node is reached.
If no root was found when traversing backward, try traversing forward along the constraint graph.
Returns ``None`` if the root was still not found.
Parameters
----------
source_node: Point
Starting point
Returns
-------
Point or None
The root node/point if found, otherwise ``None``
"""
for node in networkx.bfs_tree(self, source=source_node, reverse=True):
if node.root:
return node
for node in networkx.bfs_tree(self, source=source_node, reverse=False):
if node.root:
return node
return None
def _orient_flow_away_from_root(self, root: Point) -> typing.List[GeoCon]:
"""
Orients the flow of the edges away from the given root of a constraint cluster. Critical method that ensures
the rigid body transformation triggered by a given constraint value change applies to the correct set of points
to preserve the constraint state.
Parameters
----------
root: Point
Root of the constraint cluster
Returns
-------
typing.List[GeoCon]
A list of constraints associated with the edges that were affected by this reorientation. These constraints
need re-assigning to apply the constraint data to the flipped edges.
"""
constraints_needing_reassign = []
while True:
constraints_to_flip = []
points_from_root = [point for point in networkx.dfs_preorder_nodes(self, source=root)]
for point in points_from_root:
in_edges = self.in_edges(nbunch=point)
u_values = [in_edge[0] for in_edge in in_edges]
v_values = [in_edge[1] for in_edge in in_edges]
set_difference = list(set(u_values) - set(points_from_root))
for u_value in set_difference:
constraints_to_flip.extend(u_value.geo_cons)
in_edge_idx = u_values.index(u_value)
v = v_values[in_edge_idx]
self.remove_edge(u_value, v)
self.add_edge(v, u_value)
constraints_needing_reassign.extend(constraints_to_flip)
if len(constraints_to_flip) == 0:
break
return list(set(constraints_needing_reassign))
def _check_if_root_flows_into_polyline(self, root_node: Point) -> Point or bool:
"""
Checks if a root point in the constraint graph flows into a polyline.
Parameters
----------
root_node: Point
Root point to check
Returns
-------
Point or bool
If the root does flow into a polyline, the first point encountered that is a member of the polyline
point sequence is returned. Otherwise, a value of ``False`` is returned.
"""
for point in networkx.dfs_preorder_nodes(self, source=root_node):
if any([isinstance(curve, PolyLine) and point not in curve.point_sequence().points()
for curve in point.curves]):
# This is the case where the root is the newly created tangent point on the polyline
return False
if point is root_node:
continue
if any([isinstance(curve, PolyLine) for curve in point.curves]):
return point
return False
[docs]
def move_root(self, new_root: Point):
"""
Moves the root status of a constraint cluster from one point to a new point
Parameters
----------
new_root: Point
Node that will be elevated to root status. The old root is discovered from this new root and the root
status of the old root is removed.
"""
old_root = self._discover_root_from_node(new_root)
self._identify_and_delete_root(old_root)
needs_set_edge = False
for nbr in self.adj[new_root]:
self._set_edge_as_root(new_root, nbr)
break
else:
needs_set_edge = True
constraints_needing_reassign = self._orient_flow_away_from_root(new_root)
self._reassign_constraints(constraints_needing_reassign)
if not needs_set_edge:
return
for nbr in self.adj[new_root]:
self._set_edge_as_root(new_root, nbr)
break
else:
raise ValueError("Failed to move root")
def _reassign_distance_constraint(self, dist_con: DistanceConstraint):
"""
Re-assigns distance constraint data to a flipped edge.
Parameters
----------
dist_con: DistanceConstraint
Constraint to re-assign
"""
edge_data_12 = self.get_edge_data(dist_con.p1, dist_con.p2)
if edge_data_12 is not None:
networkx.set_edge_attributes(self, {(dist_con.p1, dist_con.p2): dist_con}, name="distance")
return
edge_data_21 = self.get_edge_data(dist_con.p2, dist_con.p1)
if edge_data_21 is not None:
networkx.set_edge_attributes(self, {(dist_con.p2, dist_con.p1): dist_con}, name="distance")
return
raise ValueError(f"Could not reassign distance constraint {dist_con}")
def _reassign_angle_constraint(self, angle_con: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint):
"""
Re-assigns angle constraint data to a swapped edge.
Parameters
----------
angle_con: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Constraint to re-assign
"""
edge_data_21 = self.get_edge_data(angle_con.p2, angle_con.p1)
if edge_data_21 is not None and "angle" not in edge_data_21.keys():
networkx.set_edge_attributes(self, {(angle_con.p2, angle_con.p1): angle_con}, name="angle")
return
edge_data_23 = self.get_edge_data(angle_con.p2, angle_con.p3)
if edge_data_23 is not None and "angle" not in edge_data_23.keys():
networkx.set_edge_attributes(self, {(angle_con.p2, angle_con.p3): angle_con}, name="angle")
return
def _reassign_constraint(self, constraint: GeoCon):
"""
Convenience method that applies either ``self._reassign_distance_constraint`` or
``self._reassign_angle_constraint`` depending on the type of the input constraint.
Parameters
----------
constraint: GeoCon
Constraint to re-assign
"""
if isinstance(constraint, DistanceConstraint):
self._reassign_distance_constraint(constraint)
elif isinstance(constraint, RelAngle3Constraint) or isinstance(constraint, Perp3Constraint) or isinstance(
constraint, AntiParallel3Constraint):
self._reassign_angle_constraint(constraint)
else:
raise ValueError(f"Constraint reassignment for constraints of type {constraint} are not implemented")
def _reassign_constraints(self, constraints: typing.List[GeoCon]):
"""
Convenience method that applies ``self._reassign_constraint`` to a list of constraints
Parameters
----------
constraints: typing.List[GeoCon]
List of constraints to re-assign
Returns
-------
"""
for constraint in constraints:
if (isinstance(constraint, DistanceConstraint) or isinstance(constraint, RelAngle3Constraint) or
isinstance(constraint, Perp3Constraint) or isinstance(constraint, AntiParallel3Constraint)):
self._reassign_constraint(constraint)
def _check_distance_constraint_for_duplicates(self, dist_con: DistanceConstraint):
"""
Checks the endpoints of a distance constraint being added for an already present distance constraint. An
exception is raised if a distance constraint is already present.
Parameters
----------
dist_con: DistanceConstraint
The distance constraint that is being added to the constraint graph
"""
for gc in self.geo_col.container()["geocon"].values():
if gc is dist_con:
continue
if not isinstance(gc, DistanceConstraint):
continue
if (dist_con.p1 is gc.p1 and dist_con.p2 is gc.p2) or (dist_con.p1 is gc.p2 and dist_con.p2 is gc.p1):
raise ValueError("A distance constraint already exists between these two points")
def _check_angle_constraint_for_duplicates(self, angle_con: RelAngle3Constraint or AntiParallel3Constraint or
Perp3Constraint):
"""
Checks the vertex and outer points of an angle constraint being added for an already present distance constraint
between the vertex and either combination of the outer points. An exception is raised if a duplicate angle
constraint is found.
Parameters
----------
angle_con: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Angle constraint that is being added to the constraint graph
"""
for gc in self.geo_col.container()["geocon"].values():
if gc is angle_con:
continue
if not isinstance(gc, RelAngle3Constraint) and not isinstance(
gc, AntiParallel3Constraint) and not isinstance(gc, Perp3Constraint):
continue
if (gc.p1 is angle_con.p1 and gc.p3 is angle_con.p3) or (
gc.p1 is angle_con.p3 and gc.p3 is angle_con.p1):
raise ValueError("An angle constraint already exists between these three points")
[docs]
def check_constraint_for_duplicates(self, geo_con: GeoCon):
"""
Wrapper function for ``_check_distance_constraint_for_duplicates`` and
``_check_angle_constraint_for_duplicates`` that applies the duplicate check depending on the constraint type.
Parameters
----------
geo_con: GeoCon
Geometric constraint being added to the graph
"""
if isinstance(geo_con, DistanceConstraint):
self._check_distance_constraint_for_duplicates(geo_con)
elif isinstance(geo_con, RelAngle3Constraint) or isinstance(
geo_con, AntiParallel3Constraint) or isinstance(geo_con, Perp3Constraint):
self._check_angle_constraint_for_duplicates(geo_con)
def _assign_distance_constraint(self, dist_con: DistanceConstraint):
"""
Assigns a distance constraint by first adding an edge from ``p1`` to ``p2`` with no data and subsequently
applying the constraint data.
Parameters
----------
dist_con: DistanceConstraint
Constraint to assign
Returns
-------
"""
if (SymmetryConstraint.check_if_point_is_symmetric_target(dist_con.p1) and
SymmetryConstraint.check_if_point_is_symmetric_target(dist_con.p2)):
raise ValueError("Could not assign distance constraint")
if SymmetryConstraint.check_if_point_is_symmetric_target(dist_con.p1):
self.add_edge(dist_con.p1, dist_con.p2)
elif SymmetryConstraint.check_if_point_is_symmetric_target(dist_con.p2):
self.add_edge(dist_con.p2, dist_con.p1)
else:
self.add_edge(dist_con.p1, dist_con.p2)
self._reassign_distance_constraint(dist_con)
def _assign_angle_constraint(self, angle_con: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint):
"""
Assigns an angle constraint by first adding an edge from either ``p2`` to ``p1`` or ``p2`` to ``p3``
(depending on which edge already has data) with no data and subsequently applying the constraint data.
Parameters
----------
angle_con: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Constraint to assign
Returns
-------
"""
if (SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p1) and
SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p2) and
SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p3)):
raise ValueError("Could not add angle constraint")
if (not (self.get_edge_data(angle_con.p1, angle_con.p2)
or self.get_edge_data(angle_con.p2, angle_con.p1)) and
not (SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p1) and
SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p2))):
self.add_edge(angle_con.p2, angle_con.p1)
if (not (self.get_edge_data(angle_con.p2, angle_con.p3) or
self.get_edge_data(angle_con.p3, angle_con.p2)) and
not (SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p2) and
SymmetryConstraint.check_if_point_is_symmetric_target(angle_con.p3))):
self.add_edge(angle_con.p2, angle_con.p3)
self._reassign_angle_constraint(angle_con)
def _add_ghost_edges_to_angle_constraint(
self, constraint: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint):
"""
Adds any required "ghost" edges (edges with no data) to an angle constraint to ensure that there are edges
connecting both (``p1`` and ``p2``) and (``p2`` and ``p3``). Performing this action ensures that any upstream
changes in constraint parameter value properly flow down to this angle constraint.
Parameters
----------
constraint: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Angle constraint to add ghost edges to (if necessary)
"""
# Only add ghost edges if there is not a concrete edge present (even if facing the wrong direction)
if not (self.get_edge_data(constraint.p1, constraint.p2)
or self.get_edge_data(constraint.p2, constraint.p1)):
if (SymmetryConstraint.check_if_point_is_symmetric_target(constraint.p1) and
SymmetryConstraint.check_if_point_is_symmetric_target(constraint.p2)):
self.add_edge(constraint.p1, constraint.p2)
if constraint.p2.root:
self.move_root(constraint.p1)
else:
self.add_edge(constraint.p2, constraint.p1)
if not (self.get_edge_data(constraint.p2, constraint.p3) or
self.get_edge_data(constraint.p3, constraint.p2)):
if (SymmetryConstraint.check_if_point_is_symmetric_target(constraint.p2) and
SymmetryConstraint.check_if_point_is_symmetric_target(constraint.p3)):
self.add_edge(constraint.p3, constraint.p2)
if constraint.p2.root:
self.move_root(constraint.p3)
else:
self.add_edge(constraint.p2, constraint.p3)
def _test_if_cluster_is_branching(self, root_node: Point) -> bool:
"""
Computes the subgraph corresponding to the constraint cluster given by the input root node, and checks if
this subgraph is branching: each node must have exactly one parent.
Parameters
----------
root_node: Point
Root of the cluster to analyze
Returns
-------
bool
Whether this constraint cluster is branching. If not, an error should be thrown (since closed loops
of constraints are not possible in pymead)
"""
subgraph = self.subgraph([node for node in networkx.dfs_preorder_nodes(self, source=root_node)])
return networkx.is_branching(subgraph)
def _determine_merged_cluster_root(self, unique_roots: typing.List[Point]) -> Point:
"""
Determines which point should be used as the new constraint cluster root after merging two constraint clusters.
Parameters
----------
unique_roots: typing.List[Point]
List of unique cluster roots from which a new single cluster root will be chosen. Most likely should
be the output of a call to ``self._get_unique_roots_from_constraint``.
Returns
-------
Point
The root to use for the single output constraint cluster
"""
current_roots = [r[0] for r in self.roots]
if current_roots.index(unique_roots[0]) < current_roots.index(unique_roots[1]):
return unique_roots[0]
else:
return unique_roots[1]
def _merge_clusters_with_constraint(self, unique_roots: typing.List[Point]) -> Point:
"""
Merges the constraint clusters defined by the input list of unique cluster roots. A single new root is chosen
from this input list by a call to ``_determine_merged_cluster_root``.
Parameters
----------
unique_roots: typing.List[Point]
The list of unique cluster roots associated with the clusters being merged
Returns
-------
Point
The root of the newly merged single constraint cluster
"""
def _delete_root_status_of_other_roots(new_root: Point):
for root in unique_roots:
if root is new_root:
continue
self._identify_and_delete_root(root)
break
merged_cluster_root = self._determine_merged_cluster_root(unique_roots)
_delete_root_status_of_other_roots(merged_cluster_root)
constraints_needing_reassign = self._orient_flow_away_from_root(merged_cluster_root)
self._reassign_constraints(constraints_needing_reassign)
return merged_cluster_root
[docs]
def add_constraint(self, constraint: GeoCon):
"""
Adds a geometric constraint to the graph.
.. warning::
This method should generally not be called directly. Instead, the ``GeometryCollection.add_constraint``
method that calls this method should be used.
Parameters
----------
constraint: GeoCon
Geometric constraint to add
"""
# Check if this constraint creates a new cluster
first_constraint_in_cluster = self._check_if_constraint_creates_new_cluster(constraint)
# If it does, set it as the root constraint
if first_constraint_in_cluster and (
isinstance(constraint, DistanceConstraint) or
isinstance(constraint, RelAngle3Constraint) or
isinstance(constraint, Perp3Constraint) or
isinstance(constraint, AntiParallel3Constraint)):
self._set_edge_as_root(constraint.p1, constraint.p2)
# If the constraint has a Param associated with it, pass the GCS reference to this parameter
if constraint.param() is not None:
constraint.param().gcs = self
if isinstance(constraint, DistanceConstraint):
self._assign_distance_constraint(constraint)
elif isinstance(constraint, RelAngle3Constraint) or isinstance(
constraint, AntiParallel3Constraint) or isinstance(constraint, Perp3Constraint):
self._assign_angle_constraint(constraint)
self._add_ghost_edges_to_angle_constraint(constraint)
# elif isinstance(constraint, SymmetryConstraint):
# self._assign_symmetry_constraint(constraint)
if isinstance(constraint, DistanceConstraint) or isinstance(constraint, AntiParallel3Constraint) or isinstance(
constraint, Perp3Constraint) or isinstance(constraint, RelAngle3Constraint):
# Get a list of child nodes that are both associated with the constraint and promoted to design variables
promoted_child_nodes = []
for child_node in constraint.child_nodes:
if isinstance(child_node.x(), LengthDesVar) or isinstance(child_node.y(), LengthDesVar):
promoted_child_nodes.append(child_node)
unique_roots = self._get_unique_roots_from_constraint(constraint)
merge_clusters = False if len(unique_roots) < 2 else True
if merge_clusters:
if len(promoted_child_nodes) > 0:
potential_new_root = self._determine_merged_cluster_root(unique_roots)
if potential_new_root not in constraint.child_nodes:
raise ValueError(f"The following points associated with this constraint have x- or y-values "
f"that have been promoted to design variables, which is not compatible with "
f"the constraint being added: {[promoted_child_node.name() for promoted_child_node in promoted_child_nodes]}. "
f"Please demote each of these points to add this constraint.")
else:
if len(set(promoted_child_nodes) - {potential_new_root}) > 0:
raise ValueError(
f"The following points associated with this constraint have x- or y-values "
f"that have been promoted to design variables, which is not compatible with "
f"the constraint being added: {[node.name() for node in promoted_child_nodes if node is not potential_new_root]}. "
f"Please demote each of these points to add this constraint."
)
root = self._merge_clusters_with_constraint(unique_roots)
else:
if len(promoted_child_nodes) > 0:
potential_new_root = unique_roots[0]
if potential_new_root not in constraint.child_nodes:
raise ValueError(f"The following points associated with this constraint have x- or y-values "
f"that have been promoted to design variables, which is not compatible with "
f"the constraint being added: {[promoted_child_node.name() for promoted_child_node in promoted_child_nodes]}. "
f"Please demote each of these points to add this constraint.")
else:
if len(set(promoted_child_nodes) - {potential_new_root}) > 0:
raise ValueError(
f"The following points associated with this constraint have x- or y-values "
f"that have been promoted to design variables, which is not compatible with "
f"the constraint being added: {[node.name() for node in promoted_child_nodes if node is not potential_new_root]}. "
f"Please demote each of these points to add this constraint."
)
constraints_to_reassign = self._orient_flow_away_from_root(unique_roots[0])
self._reassign_constraints(constraints_to_reassign)
root = self._discover_root_from_node(constraint.p1)
# Check if the addition of this constraint creates a closed loop
is_branching = self._test_if_cluster_is_branching(root)
if not is_branching:
raise ValueError("Detected a closed loop in the constraint graph. Closed loop sets of constraints "
"are currently not supported in pymead.")
new_root = self._check_if_root_flows_into_polyline(root)
if new_root:
self.move_root(new_root)
elif isinstance(constraint, SymmetryConstraint):
if isinstance(constraint.p4.x(), LengthDesVar):
raise ValueError(f"Cannot create constraint because the x-value of the target point "
f"({constraint.p4.name()}) is a design variable. Demote the x-value to a "
f"parameter to add this constraint.")
if isinstance(constraint.p4.y(), LengthDesVar):
raise ValueError(f"Cannot create constraint because the y-value of the target point "
f"({constraint.p4.name()}) is a design variable. Demote the y-value to a "
f"parameter to add this constraint.")
points_solved = self.solve_symmetry_constraint(constraint)
self.update_canvas_items(points_solved)
elif isinstance(constraint, ROCurvatureConstraint):
points_solved = self.solve_roc_constraint(constraint)
self.update_canvas_items(points_solved)
def _remove_distance_constraint_from_directed_edge(
self, constraint: DistanceConstraint) -> typing.List[typing.Tuple[Point]] or None:
"""
Removes a distance constraint from its associated edge in the graph, removing the edge itself as well
if there is no angle constraint contained in the edge data.
Parameters
----------
constraint: DistanceConstraint
Distance constraint to remove from the edge
Returns
-------
typing.List[typing.Tuple[Point]] or None
The edges removed from the graph (``None`` if no edges were removed)
"""
edges_removed = None
edge_data_12 = self.get_edge_data(constraint.p1, constraint.p2)
if edge_data_12 is not None and "distance" in edge_data_12.keys() and edge_data_12["distance"] is constraint:
angle_constraint_present = False
for geo_con in constraint.p2.geo_cons:
if (isinstance(geo_con, RelAngle3Constraint) or
isinstance(geo_con, AntiParallel3Constraint) or
isinstance(geo_con, Perp3Constraint) and geo_con.p2 is constraint.p2):
angle_constraint_present = True
break
if angle_constraint_present:
edge_data_12.pop("distance")
else:
self.remove_edge(constraint.p1, constraint.p2)
edges_removed = [(constraint.p1, constraint.p2)]
return edges_removed
edge_data_21 = self.get_edge_data(constraint.p2, constraint.p1)
if edge_data_21 is not None and "distance" in edge_data_21.keys() and edge_data_21["distance"] is constraint:
angle_constraint_present = False
for geo_con in constraint.p1.geo_cons:
if (isinstance(geo_con, RelAngle3Constraint) or
isinstance(geo_con, AntiParallel3Constraint) or
isinstance(geo_con, Perp3Constraint) and geo_con.p2 is constraint.p1):
angle_constraint_present = True
break
if angle_constraint_present:
edge_data_21.pop("distance")
else:
self.remove_edge(constraint.p2, constraint.p1)
edges_removed = [(constraint.p2, constraint.p1)]
return edges_removed
def _remove_angle_constraint_from_directed_edge(
self, constraint: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
) -> typing.List[typing.Tuple[Point]]:
"""
Removes an angle constraint from its associated edge(s) in the graph, removing the edge(s) itself as well
if there is no distance constraint in the edge data and the edge(s) are not required as ghost edges
for other angle constraints.
Parameters
----------
constraint: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Angle constraint to remove from the edge
Returns
-------
typing.List[typing.Tuple[Point]] or None
The edges removed from the graph (empty list if no edges were removed)
"""
def _check_if_other_angle_constraint_attached(outer: Point) -> bool:
"""
Determines if there is another angle constraint using the edge ``(vertex, outer)`` as a ghost edge.
Parameters
----------
outer: Point
Main endpoint of the current angle constraint
Returns
-------
bool
Whether there is another angle constraint using the edge ``(vertex, outer)`` as a ghost edge
"""
for nbr, datadict in self.adj[outer].items():
if "angle" not in datadict:
continue
return True
return False
def _remove_edges(vertex: Point, outer: Point, other: Point):
"""
Removes edges as needed depending on the configuration of the angle constraint and whether
there is an attached distance constraint.
Parameters
----------
vertex: Point
Vertex of the angle constraint. Normally ``constraint.p2``
outer: Point
Endpoint of the angle constraint where the target edge is found. Either ``constraint.p1`` or
``constraint.p3``
other: Point
The other endpoint of the angle constraint that may have a ghost constraint attached. Either
``constraint.p1`` or ``constraint.p3``, whichever one is not ``outer``
"""
edge_data = self.get_edge_data(vertex, outer)
if "distance" in edge_data.keys() or _check_if_other_angle_constraint_attached(outer):
# Remove the angle data but keep the edge if there is a distance constraint here or if this edge
# should be used as a ghost edge for another angle constraint
edge_data.pop("angle")
else:
# Otherwise, remove the edge
self.remove_edge(vertex, outer)
edges_removed.append((vertex, outer))
# Remove the inward-facing ghost edge if there is one
ghost_edge_data = self.get_edge_data(other, vertex)
if ghost_edge_data is not None and len(ghost_edge_data) == 0:
self.remove_edge(other, vertex)
edges_removed.append((other, vertex))
# Remove the outward-facing ghost edge if there is one and there are no other angle constraint attached
outward_ghost_edge_data = self.get_edge_data(vertex, other)
if outward_ghost_edge_data is not None and len(
outward_ghost_edge_data) == 0 and not _check_if_other_angle_constraint_attached(other):
self.remove_edge(vertex, other)
edges_removed.append((vertex, other))
edges_removed = []
edge_data_21 = self.get_edge_data(constraint.p2, constraint.p1)
edge_data_23 = self.get_edge_data(constraint.p2, constraint.p3)
if edge_data_21 is not None and "angle" in edge_data_21.keys() and edge_data_21["angle"] is constraint:
_remove_edges(constraint.p2, constraint.p1, constraint.p3)
return edges_removed
if edge_data_23 is not None and "angle" in edge_data_23.keys() and edge_data_23["angle"] is constraint:
_remove_edges(constraint.p2, constraint.p3, constraint.p1)
return edges_removed
def _assign_new_root_if_required(self, v_of_edge_removed: Point):
"""
Assigns a new constraint cluster root if there are any edges flowing from the downstream point of the edge
that was removed.
Parameters
----------
v_of_edge_removed
Downstream point of the removed edge
"""
neighbors_of_v = [nbr for nbr in self.adj[v_of_edge_removed]]
if len(neighbors_of_v) > 0:
self._set_edge_as_root(v_of_edge_removed, neighbors_of_v[0])
else:
pass # This means we trimmed the end of the branch
def _update_roots_based_on_constraint_removal(self, edges_removed: typing.List[typing.Tuple[Point]]):
"""
Updates the constraint cluster roots associated with a constraint removal. Root and constraint handle
privileges may be transferred to another edge or removed entirely, depending on the context.
Parameters
----------
edges_removed: typing.List[typing.Tuple[Point]]
The edges that were removed in the process of deleting a geometric constraint.
"""
for edge_removed in edges_removed:
if edge_removed[1].rotation_handle:
self._delete_root_status(edge_removed[0], edge_removed[1])
for out_edge in self.out_edges(nbunch=edge_removed[0]):
self._set_edge_as_root(out_edge[0], out_edge[1])
break
self._assign_new_root_if_required(edge_removed[1])
[docs]
def remove_constraint(self, constraint: GeoCon):
"""
Removes a geometric constraint from the graph.
.. warning::
This method should generally not be called directly. Instead, the ``GeometryCollection.remove_pymead_obj``
method that calls this method should be used.
Parameters
----------
constraint: GeoCon
Geometric constraint to remove
"""
edges_removed = None
if isinstance(constraint, DistanceConstraint):
edges_removed = self._remove_distance_constraint_from_directed_edge(constraint)
elif isinstance(constraint, RelAngle3Constraint) or isinstance(
constraint, AntiParallel3Constraint) or isinstance(constraint, Perp3Constraint):
edges_removed = self._remove_angle_constraint_from_directed_edge(constraint)
if edges_removed is not None:
self._update_roots_based_on_constraint_removal(edges_removed)
for child_node in constraint.child_nodes:
child_node.geo_cons.remove(constraint)
if constraint in child_node.x().geo_cons:
child_node.x().geo_cons.remove(constraint)
if constraint in child_node.y().geo_cons:
child_node.y().geo_cons.remove(constraint)
def _solve_distance_constraint(self, source: DistanceConstraint) -> typing.List[Point]:
"""
Solves a distance constraint by translating ``p2`` along a fixed angle relative to ``p1``.
Parameters
----------
source: DistanceConstraint
Distance constraint to solve
Returns
-------
typing.List[Point]
List of points solved during the solution process
"""
points_solved = []
edge_data_p12 = self.get_edge_data(source.p1, source.p2)
if edge_data_p12 and edge_data_p12["distance"] is source:
angle = source.p1.measure_angle(source.p2)
start = source.p2
else:
angle = source.p2.measure_angle(source.p1)
start = source.p1
old_distance = source.p1.measure_distance(source.p2)
new_distance = source.param().value()
dx = (new_distance - old_distance) * np.cos(angle)
dy = (new_distance - old_distance) * np.sin(angle)
for point in networkx.bfs_tree(self, source=start):
point.x().set_value(point.x().value() + dx, direct_user_request=False)
point.y().set_value(point.y().value() + dy, direct_user_request=False)
if point not in points_solved:
points_solved.append(point)
return points_solved
def _solve_angle_constraint(self, source: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
) -> typing.List[Point]:
"""
Solves an angle constraint by rotating one of the outer points about the vertex.
Parameters
----------
source: RelAngle3Constraint or AntiParallel3Constraint or Perp3Constraint
Angle constraint to solve
Returns
-------
typing.List[Point]
List of points solved during the solution process
"""
points_solved = []
edge_data_p21 = self.get_edge_data(source.p2, source.p1)
edge_data_p23 = self.get_edge_data(source.p2, source.p3)
old_angle = (source.p2.measure_angle(source.p1) -
source.p2.measure_angle(source.p3)) % (2 * np.pi)
if isinstance(source, RelAngle3Constraint):
new_angle = source.param().rad()
elif isinstance(source, AntiParallel3Constraint):
new_angle = np.pi
elif isinstance(source, Perp3Constraint):
new_angle = np.pi / 2
else:
raise ValueError(f"{type(source)} is not a valid type for angle constraint")
d_angle = new_angle - old_angle
rotation_point = source.p2
if edge_data_p21 and "angle" in edge_data_p21 and edge_data_p21["angle"] is source:
if source.p1.rotation_handle and source.p2.root:
start = source.p3
d_angle *= -1
else:
start = source.p1
elif edge_data_p23 and "angle" in edge_data_p23 and edge_data_p23["angle"] is source:
if source.p3.rotation_handle and source.p2.root:
start = source.p1
else:
start = source.p3
d_angle *= -1
else:
raise ValueError("Somehow no angle constraint found between the three points")
rotation_mat = np.array([[np.cos(d_angle), -np.sin(d_angle)], [np.sin(d_angle), np.cos(d_angle)]])
rotation_point_mat = np.array([[rotation_point.x().value()], [rotation_point.y().value()]])
# Get all the points that might need to rotate
additional_branch_starting_points = []
if start is not source.p2:
for geo_con in start.geo_cons:
if geo_con is source:
continue
if not (isinstance(geo_con, AntiParallel3Constraint) or isinstance(
geo_con, RelAngle3Constraint) or isinstance(geo_con, Perp3Constraint)):
continue
if geo_con.p2 is not source.p2:
continue
# if not geo_con.p1 in self.adj[source.p2] or not geo_con.p3 in self.adj[source.p2]:
# continue
if start is source.p3 and geo_con.p3 is source.p3:
additional_branch_starting_points.append(geo_con.p1)
elif start is source.p3 and geo_con.p1 is source.p3:
additional_branch_starting_points.append(geo_con.p3)
elif start is source.p1 and geo_con.p3 is source.p1:
additional_branch_starting_points.append(geo_con.p1)
elif start is source.p1 and geo_con.p1 is source.p1:
additional_branch_starting_points.append(geo_con.p3)
all_downstream_points = []
rotation_handle = None
for source_point in [start, *additional_branch_starting_points]:
for point in networkx.bfs_tree(self, source=source_point):
# if point not in points_to_exclude:
all_downstream_points.append(point)
if not rotation_handle and point.rotation_handle:
rotation_handle = point
# Get the branch to cut, if there is one
root_rotation_branch = []
if source.p2.root and rotation_handle is not None:
root_rotation_branch = [point for point in networkx.bfs_tree(self, source=rotation_handle)]
for point in all_downstream_points:
if point is source.p2 or point in root_rotation_branch:
continue
dx_dy = np.array([[point.x().value() - rotation_point.x().value()],
[point.y().value() - rotation_point.y().value()]])
new_xy = (rotation_mat @ dx_dy + rotation_point_mat).flatten()
point.x().set_value(new_xy[0], direct_user_request=False)
point.y().set_value(new_xy[1], direct_user_request=False)
if point not in points_solved:
points_solved.append(point)
return points_solved
[docs]
def solve(self, source: GeoCon) -> typing.List[Point]:
"""
Wrapper around the difference constraint solution methods that applies a solution based on the input constraint
type.
Parameters
----------
source: GeoCon
The constraint to solve
Returns
-------
typing.List[Point]
List of points solved during the solution process
"""
points_solved = []
symmetry_points_solved = []
roc_points_solved = []
if isinstance(source, DistanceConstraint):
points_solved.extend(self._solve_distance_constraint(source))
elif (isinstance(source, RelAngle3Constraint) or isinstance(source, AntiParallel3Constraint) or
isinstance(source, Perp3Constraint)):
points_solved.extend(self._solve_angle_constraint(source))
elif isinstance(source, SymmetryConstraint):
points_solved.extend(self.solve_symmetry_constraint(source))
elif isinstance(source, ROCurvatureConstraint):
points_solved.extend(self.solve_roc_constraint(source))
other_points_solved = self.solve_other_constraints(points_solved)
other_points_solved = list(
set(other_points_solved).union(
set(symmetry_points_solved)).union(
set(roc_points_solved))
)
points_solved = list(set(points_solved).union(set(other_points_solved)))
return points_solved
[docs]
def translate_cluster(self, root: Point, dx: float, dy: float) -> typing.List[Point]:
r"""
Translate the constraint cluster (defined by the root point) by a given :math:`\Delta x` and :math:`\Delta y`.
Parameters
----------
root: Point
Root of the constraint cluster
dx: float
Amount to translate in the :math:`x`-direction
dy: float
Amount to translate in the :math:`y`-direction
Returns
-------
typing.List[Point]
List of points solved during the solution process of constraints attached to the points that were translated
"""
if not root.root:
raise ValueError("Cannot move a point that is not a root of a constraint cluster")
points_solved = []
for point in networkx.bfs_tree(self, source=root):
point.x().set_value(point.x().value() + dx, direct_user_request=False)
point.y().set_value(point.y().value() + dy, direct_user_request=False)
if point not in points_solved:
points_solved.append(point)
self.solve_other_constraints(points_solved)
return points_solved
[docs]
def rotate_cluster(self,
rotation_handle: Point,
new_rotation_handle_x: float = None,
new_rotation_handle_y: float = None,
new_rotation_angle: float or AngleParam = None) -> (typing.List[Point], Point):
"""
Rotates the constraint cluster defined by the input rotation handle by either the angle specified
by ``new_rotation_angle`` or by the angle difference specified by the angle between the cluster root and
(``new_rotation_handle_x``, ``new_rotation_handle_y``) and the initial angle between the root and rotation
handle.
Parameters
----------
rotation_handle: Point
Rotation handle of the constraint cluster
new_rotation_handle_x: float or None
:math:`x`-location of the new rotation handle point position. Default: ``None``
new_rotation_handle_y: float or None
:math:`y`-location of the new rotation handle point position. Default: ``None``
new_rotation_angle: float or None
New rotation angle for the cluster. Can only be specified if ``new_rotation_handle_x`` and
``new_rotation_handle_y`` are not specified.
Returns
-------
typing.List[Point], Point
List of points solved after rotating the constraint cluster and the root point of the constraint cluster
"""
if new_rotation_angle is not None and isinstance(new_rotation_angle, AngleParam):
new_rotation_angle = self.geo_col.units.convert_angle_to_base(
new_rotation_angle.value(), self.geo_col.units.current_angle_unit()
)
x_and_y_specified = new_rotation_handle_x is not None and new_rotation_handle_y is not None
if not (x_and_y_specified or new_rotation_angle is not None) and not (
x_and_y_specified and new_rotation_angle is not None):
raise ValueError("Must specify either 'new_rotation_handle_x` and `new_rotation_handle_y` or"
"'new_rotation_angle'")
root = self._identify_root_from_rotation_handle(rotation_handle)
if not root.root:
raise ValueError("Cannot move a point that is not a root of a constraint cluster")
old_rotation_handle_angle = root.measure_angle(rotation_handle)
if new_rotation_angle is None:
new_rotation_handle_angle = root.measure_angle(Point(new_rotation_handle_x, new_rotation_handle_y))
else:
new_rotation_handle_angle = new_rotation_angle
delta_angle = new_rotation_handle_angle - old_rotation_handle_angle
root_x = root.x().value()
root_y = root.y().value()
points_solved = []
for point in networkx.bfs_tree(self, source=root):
if point is root:
continue
old_x = point.x().value()
old_y = point.y().value()
new_x = (old_x - root_x) * np.cos(delta_angle) - (old_y - root_y) * np.sin(delta_angle) + root_x
new_y = (old_x - root_x) * np.sin(delta_angle) + (old_y - root_y) * np.cos(delta_angle) + root_y
point.x().set_value(new_x, direct_user_request=False)
point.y().set_value(new_y, direct_user_request=False)
if point not in points_solved:
points_solved.append(point)
self.solve_other_constraints(points_solved)
return points_solved, root
[docs]
def solve_symmetry_constraint(self, constraint: SymmetryConstraint) -> typing.List[Point]:
"""
Solves a symmetry constraint by modifying the position of the target point to be the mirror of the tool point
about the line defined by the first two points in the constraint.
Parameters
----------
constraint: SymmetryConstraint
The symmetry constraint to solve
Returns
-------
typing.List[Point]
The child nodes of the symmetry constraint (the line-defining points, the tool point, and the target point)
"""
# This code prevents recursion errors for the case where a new cluster is created from a symmetry constraint
# target. The symmetry constraint only gets solved if it has not yet been added to the list of partial solves
if constraint in self.partial_symmetry_solves:
return []
else:
self.partial_symmetry_solves.append(constraint)
x1, y1 = constraint.p1.x().value(), constraint.p1.y().value()
x2, y2 = constraint.p2.x().value(), constraint.p2.y().value()
x3, y3 = constraint.p3.x().value(), constraint.p3.y().value()
line_angle = measure_abs_angle(x1, y1, x2, y2)
tool_angle = measure_rel_angle3(x1, y1, x2, y2, x3, y3)
if tool_angle < np.pi:
mirror_angle = line_angle - np.pi / 2
elif tool_angle > np.pi:
mirror_angle = line_angle + np.pi / 2
else:
# Rare case where the point is coincident with the line: just make p4 = p3
constraint.p4.request_move(constraint.p3.x().value(), constraint.p3.y().value(), force=True)
return constraint.child_nodes
mirror_distance = 2 * measure_point_line_distance_unsigned(x1, y1, x2, y2, x3, y3)
constraint.p4.request_move(
x3 + mirror_distance * np.cos(mirror_angle),
y3 + mirror_distance * np.sin(mirror_angle), force=True
)
self.partial_symmetry_solves.remove(constraint)
return constraint.child_nodes
[docs]
@staticmethod
def solve_roc_constraint(constraint: ROCurvatureConstraint) -> typing.List[Point]:
"""
Solves a radius of curvature constraint by translating the :math:`G^2` points along a fixed angle.
Parameters
----------
constraint: ROCurvatureConstraint
Radius of curvature constraint to solve
Returns
-------
typing.List[Point]
The child nodes of the radius of curvature constraint (the :math:`G^1` and :math:`G^2` points)
"""
def solve_for_single_bezier(p_g1: Point, p_g2: Point, n: int):
"""
Solves a radius of curvature constraint for a single Bézier curve.
Parameters
----------
p_g1: Point
:math:`G^1` (second point or second-to-last point) of the Bézier curve
p_g2: Point
:math:`G^2` (third point or third-to-last-point) of the Bézier curve
n: int
Bézier curve degree (one less than the number of control points)
"""
Lc = measure_curvature_length_bezier(
constraint.curve_joint.x().value(), constraint.curve_joint.y().value(),
p_g1.x().value(), p_g1.y().value(),
p_g2.x().value(), p_g2.y().value(), constraint.param().value(), n
)
angle = p_g1.measure_angle(p_g2)
p_g2.request_move(p_g1.x().value() + Lc * np.cos(angle),
p_g1.y().value() + Lc * np.sin(angle), force=True)
def solve_for_single_curve_zero_curvature(p_g1: Point, p_g2: Point):
"""
Solves a radius of curvature constraint with zero curvature (infinite radius of curvature)
for a single Bézier curve by setting the :math:`G^2` points coincident with the :math:`G^1` points.
Parameters
----------
p_g1: Point
:math:`G^1` (second point or second-to-last point) of the Bézier curve
p_g2: Point
:math:`G^2` (third point or third-to-last-point) of the Bézier curve
"""
p_g2.request_move(p_g1.x().value(), p_g1.y().value(), force=True)
if constraint.curve_type_1 in ("Bezier", "BSpline") and constraint.curve_type_2 == "LineSegment":
solve_for_single_curve_zero_curvature(constraint.g1_point_curve_1, constraint.g2_point_curve_1)
return constraint.child_nodes
if constraint.curve_type_2 in ("Bezier", "BSpline") and constraint.curve_type_1 == "LineSegment":
solve_for_single_curve_zero_curvature(constraint.g1_point_curve_2, constraint.g2_point_curve_2)
return constraint.child_nodes
if constraint.curve_type_1 in ("Bezier", "BSpline"):
if constraint.is_solving_allowed(constraint.g2_point_curve_1):
solve_for_single_bezier(constraint.g1_point_curve_1, constraint.g2_point_curve_1, constraint.curve_1.degree)
else:
R1 = ROCurvatureConstraint.calculate_curvature_data(constraint.curve_joint).R1
constraint.param().set_value(R1, force=True)
if constraint.curve_type_2 in ("Bezier", "BSpline"):
if constraint.is_solving_allowed(constraint.g2_point_curve_2):
solve_for_single_bezier(constraint.g1_point_curve_2, constraint.g2_point_curve_2, constraint.curve_2.degree)
else:
R2 = ROCurvatureConstraint.calculate_curvature_data(constraint.curve_joint).R2
constraint.param().set_value(R2, force=True)
return constraint.child_nodes
[docs]
def solve_symmetry_constraints(self, points: typing.List[Point]) -> typing.List[Point]:
"""
Solves all symmetry constraints associated with a list of points that was modified in some way, either by
direct user input or indirectly via constraint solving.
Parameters
----------
points: typing.List[Point]
Points for which to solve symmetry constraints if needed
Returns
-------
typing.List[Point]
Points solved during the solution process
"""
points_solved = []
symmetry_constraints_solved = []
for point in points:
symmetry_constraints = [geo_con for geo_con in point.geo_cons if isinstance(geo_con, SymmetryConstraint)]
for symmetry_constraint in symmetry_constraints:
if symmetry_constraint in symmetry_constraints_solved:
continue
symmetry_points_solved = self.solve_symmetry_constraint(symmetry_constraint)
symmetry_constraints_solved.append(symmetry_constraint)
for symmetry_point_solved in symmetry_points_solved:
if symmetry_point_solved in points_solved:
continue
points_solved.append(symmetry_point_solved)
return points_solved
[docs]
def solve_roc_constraints(self, points: typing.List[Point]) -> typing.List[Point]:
"""
Solves all radius of curvature constraints associated with a list of points that was modified in some way,
either by direct user input or indirectly via constraint solving.
Parameters
----------
points: typing.List[Point]
Points for which to solve radius of curvature constraints if needed
Returns
-------
typing.List[Point]
Points solved during the solution process
"""
points_solved = []
roc_constraints_solved = []
for point in points:
roc_constraints = [geo_con for geo_con in point.geo_cons if isinstance(geo_con, ROCurvatureConstraint)]
for roc_constraint in roc_constraints:
if roc_constraint in roc_constraints_solved:
continue
roc_points_solved = self.solve_roc_constraint(roc_constraint)
roc_constraints_solved.append(roc_constraint)
for roc_point_solved in roc_points_solved:
if roc_point_solved in points_solved:
continue
points_solved.append(roc_point_solved)
return points_solved
[docs]
def solve_other_constraints(self, points: typing.List[Point]) -> typing.List[Point]:
"""
Solves all radius of curvature and symmetry constraints associated with a list of points that was modified
in some way, either by direct user input or indirectly via constraint solving.
Parameters
----------
points: typing.List[Point]
Points for which to solve radius of curvature or symmetry constraints if needed
Returns
-------
typing.List[Point]
Points solved during the solution process
"""
roc_points_solved = self.solve_roc_constraints(points)
symmetry_points_solved = self.solve_symmetry_constraints(points)
return list(set(symmetry_points_solved).union(roc_points_solved))
[docs]
@staticmethod
def update_canvas_items(points_solved: typing.List[Point]):
"""
Updates the visual representation in the airfoil canvas of all geometric objects modified by the process of
solving the constraint system.
Parameters
----------
points_solved: typing.List[Point]
A list of all the points solved in the constraint system
"""
# Update all the graphical point objects and collect the curves affected
curves_to_update = []
for point in points_solved:
if point.canvas_item is not None:
point.canvas_item.updateCanvasItem(point.x().value(), point.y().value())
for curve in point.curves:
if curve not in curves_to_update:
curves_to_update.append(curve)
# Update all the graphical curve objects and collect the airfoils affected
airfoils_to_update = []
for curve in curves_to_update:
if curve.airfoil is not None and curve.airfoil not in airfoils_to_update:
airfoils_to_update.append(curve.airfoil)
curve.update()
# Update all the graphical airfoil objects
for airfoil in airfoils_to_update:
airfoil.update_coords()
if airfoil.canvas_item is not None:
airfoil.canvas_item.generatePicture()
# Collect all the graphical constraint objects that need to be updated
constraints_to_update = []
for point in points_solved:
for geo_con in point.geo_cons:
if geo_con not in constraints_to_update:
constraints_to_update.append(geo_con)
# Update all the graphical constraint objects than need to be updated
for geo_con in constraints_to_update:
if isinstance(geo_con, GeoCon) and geo_con.canvas_item is not None:
geo_con.canvas_item.update()
[docs]
class ConstraintValidationError(Exception):
pass