Source code for pymead.core.constraints

import typing
from dataclasses import dataclass
from abc import abstractmethod

import numpy as np

from pymead.core.parametric_curve import ParametricCurveEndpoint
from pymead.core.bezier import Bezier
from pymead.core.constraint_equations import measure_rel_angle3, measure_point_line_distance_unsigned
from pymead.core.line import PolyLine
from pymead.core.param import Param, AngleParam, LengthParam
from pymead.core.point import Point
from pymead.core.pymead_obj import PymeadObj


[docs] class GeoCon(PymeadObj): default_name: str = ""
[docs] def __init__(self, param: Param or None, child_nodes: list, kind: str, name: str or None = None, secondary_params: typing.List[Param] = None, geo_col=None): self._param = None self.set_param(param) sub_container = "geocon" super().__init__(sub_container=sub_container, geo_col=geo_col) name = self.default_name if name is None else name self.set_name(name) if self.param() is not None: self.param().geo_cons.append(self) if self.param().name() == "unnamed": self.param().set_name(f"{self.name()}.par") self.child_nodes = child_nodes self.kind = kind self.secondary_params = [] if secondary_params is None else secondary_params self.data = None self.add_constraint_to_points()
def param(self): return self._param def set_param(self, param: Param): self._param = param def add_constraint_to_points(self): for child_node in self.child_nodes: if not isinstance(child_node, Point) or self in child_node.geo_cons: return child_node.geo_cons.append(self) def remove_constraint_from_points(self): for child_node in self.child_nodes: if not isinstance(child_node, Point) or self not in child_node.geo_cons: return child_node.geo_cons.remove(self) @abstractmethod def verify(self) -> bool: pass
[docs] class DistanceConstraint(GeoCon): default_name = "DistCon-1"
[docs] def __init__(self, p1: Point, p2: Point, value: float or LengthParam = None, name: str = None, geo_col=None): if p1.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p1 is airfoil-relative.") if p2.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p2 is airfoil-relative.") self.p1 = p1 self.p2 = p2 value = self.p1.measure_distance(self.p2) if value is None else value param = value if isinstance(value, Param) else LengthParam(value=value, name="Length-1", geo_col=geo_col) self.handle_offset = None super().__init__(param=param, name=name, child_nodes=[self.p1, self.p2], kind="d", geo_col=geo_col)
def __repr__(self): return f"{self.__class__.__name__} {self.name()}<v={self.param().value()}>"
[docs] def get_dict_rep(self) -> dict: return {"p1": self.p1.name(), "p2": self.p2.name(), "value": self.param().name(), "constraint_type": self.__class__.__name__}
# "constraint_type": self.__class__.__name__, "handle_offset": self.handle_offset} def verify(self) -> bool: measured_distance = self.p1.measure_distance(self.p2) return np.isclose(measured_distance, self.param().value(), rtol=1e-14)
[docs] class AntiParallel3Constraint(GeoCon): default_name = "AntiPar3Con-1"
[docs] def __init__(self, p1: Point, p2: Point, p3: Point, name: str = None, polyline: PolyLine = None, point_on_curve: Point = None, geo_col=None): if p1.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p1 is airfoil-relative.") if p2.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p2 is airfoil-relative.") if p3.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p3 is airfoil-relative.") self.p1 = p1 self.polyline = polyline self.point_on_curve = point_on_curve if polyline and polyline not in p1.curves and point_on_curve is p1: p1.curves.append(polyline) self.p2 = p2 self.p3 = p3 if polyline and polyline not in p3.curves and point_on_curve is p3: p3.curves.append(polyline) super().__init__(param=None, name=name, child_nodes=[self.p1, self.p2, self.p3], kind="a3", geo_col=geo_col)
def __repr__(self): return f"{self.__class__.__name__} {self.name()}"
[docs] def get_dict_rep(self) -> dict: return {"p1": self.p1.name(), "p2": self.p2.name(), "p3": self.p3.name(), "constraint_type": self.__class__.__name__, "polyline": self.polyline.name() if self.polyline is not None else None, "point_on_curve": self.point_on_curve.name() if self.point_on_curve is not None else None}
def verify(self) -> bool: a1 = self.p2.measure_angle(self.p1) a2 = self.p2.measure_angle(self.p3) measured_angle = (a1 - a2) % (2 * np.pi) return np.isclose(measured_angle, np.pi, rtol=1e-14)
[docs] class SymmetryConstraint(GeoCon): default_name = "SymCon-1"
[docs] def __init__(self, p1: Point, p2: Point, p3: Point, p4: Point, name: str = None, geo_col=None): self.p1 = p1 # Line start point self.p2 = p2 # Line end point self.p3 = p3 # Tool point self.p4 = p4 # Target point super().__init__(param=None, name=name, child_nodes=[self.p1, self.p2, self.p3, self.p4], kind="a4|d", geo_col=geo_col) for point in self.child_nodes: if self not in point.x().geo_cons: point.x().geo_cons.append(self) if self not in point.y().geo_cons: point.y().geo_cons.append(self)
@staticmethod def check_if_point_is_symmetric_target(p: Point): for geo_con in p.geo_cons: if isinstance(geo_con, SymmetryConstraint) and geo_con.child_nodes[-1] is p: return True return False def __repr__(self): return f"{self.__class__.__name__} {self.name()}"
[docs] def get_dict_rep(self) -> dict: return {"p1": self.p1.name(), "p2": self.p2.name(), "p3": self.p3.name(), "p4": self.p4.name(), "constraint_type": self.__class__.__name__}
def verify(self) -> bool: tool_angle = measure_rel_angle3(self.p3.x().value(), self.p3.y().value(), self.p2.x().value(), self.p2.y().value(), self.p1.x().value(), self.p1.y().value()) target_angle = measure_rel_angle3(self.p1.x().value(), self.p1.y().value(), self.p2.x().value(), self.p2.y().value(), self.p4.x().value(), self.p4.y().value()) if not np.isclose(tool_angle, target_angle, rtol=1e-14): return False tool_distance_to_line = measure_point_line_distance_unsigned( self.p1.x().value(), self.p1.y().value(), self.p2.x().value(), self.p2.y().value(), self.p3.x().value(), self.p3.y().value() ) target_distance_to_line = measure_point_line_distance_unsigned( self.p1.x().value(), self.p1.y().value(), self.p2.x().value(), self.p2.y().value(), self.p4.x().value(), self.p4.y().value() ) return np.isclose(tool_distance_to_line, target_distance_to_line, rtol=1e-14)
[docs] class Perp3Constraint(GeoCon): default_name = "Perp3Con-1"
[docs] def __init__(self, p1: Point, p2: Point, p3: Point, name: str = None, geo_col=None): if p1.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p1 is airfoil-relative.") if p2.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p2 is airfoil-relative.") if p3.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p3 is airfoil-relative.") self.p1 = p1 self.p2 = p2 self.p3 = p3 super().__init__(param=None, name=name, child_nodes=[self.p1, self.p2, self.p3], kind="a3", geo_col=geo_col)
def __repr__(self): return f"{self.__class__.__name__} {self.name()}"
[docs] def get_dict_rep(self) -> dict: return {"p1": self.p1.name(), "p2": self.p2.name(), "p3": self.p3.name(), "constraint_type": self.__class__.__name__}
def verify(self) -> bool: a1 = self.p2.measure_angle(self.p1) a2 = self.p2.measure_angle(self.p3) measured_angle = (a1 - a2) % (2 * np.pi) return np.isclose(measured_angle, 0.5 * np.pi, rtol=1e-14)
[docs] class RelAngle3Constraint(GeoCon): default_name = "RelAng3Con-1"
[docs] def __init__(self, p1: Point, p2: Point, p3: Point, value: float or AngleParam = None, name: str = None, geo_col=None): if p1.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p1 is airfoil-relative.") if p2.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p2 is airfoil-relative.") if p3.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point p3 is airfoil-relative.") self.p1 = p1 self.p2 = p2 self.p3 = p3 if value is None: args = [] for point in [self.p1, self.p2, self.p3]: args.extend([point.x().value(), point.y().value()]) value = measure_rel_angle3(*args) param = value if isinstance(value, Param) else AngleParam( value=geo_col.units.convert_angle_from_base( value, unit=geo_col.units.current_angle_unit() ), name="Angle-1", geo_col=geo_col ) self.handle_offset = None super().__init__(param=param, name=name, child_nodes=[self.p1, self.p2, self.p3], kind="a3", geo_col=geo_col)
def __repr__(self): return f"{self.__class__.__name__} {self.name()}<v={self.param().value()}>"
[docs] def get_dict_rep(self) -> dict: return {"p1": self.p1.name(), "p2": self.p2.name(), "p3": self.p3.name(), "value": self.param().name(), "constraint_type": self.__class__.__name__}
def verify(self) -> bool: a1 = self.p2.measure_angle(self.p1) a2 = self.p2.measure_angle(self.p3) measured_angle = (a1 - a2) % (2 * np.pi) return np.isclose(measured_angle, self.param().rad() % (2 * np.pi), rtol=1e-14)
[docs] @dataclass class CurvatureConstraintData: Lt1: float Lt2: float Lc1: float Lc2: float n1: int n2: int theta1: float theta2: float phi1: float phi2: float psi1: float psi2: float R1: float R2: float
[docs] class ROCurvatureConstraint(GeoCon): default_name = "ROCCon-1"
[docs] def __init__(self, curve_joint: Point, value: float or LengthParam = None, name: str = None, geo_col=None, curve_to_modify: Bezier = None): if curve_joint.relative_airfoil_name is not None: raise InvalidPointError(f"Constraint could not be added because point the curve joint is an " f"airfoil-relative point.") if len(curve_joint.curves) != 2: raise ConstraintValidationError(f"There must be exactly two curves attached to the curve joint. Found " f"{len(curve_joint.curves)} curves") self.curve_joint = curve_joint self.curve_1 = curve_joint.curves[0] self.curve_2 = curve_joint.curves[1] self.curve_type_1 = self.curve_1.__class__.__name__ self.curve_type_2 = self.curve_2.__class__.__name__ if self.curve_type_1 in ("Bezier", "BSpline"): curve_joint_index_curve_1 = self.curve_1.point_sequence().points().index(curve_joint) self.curve_joint_index_curve_1 = -1 if curve_joint_index_curve_1 != 0 else 0 if (self.curve_joint_index_curve_1 == 0 and self.curve_type_1 == "BSpline" and not self.curve_1.is_clamped(ParametricCurveEndpoint.Start)): raise ValueError(f"Curve {self.curve_1} is not clamped at the start. " f"Curvature constraints can only be applied to clamped curves.") elif (self.curve_joint_index_curve_1 == 0 and self.curve_type_1 == "BSpline" and not self.curve_1.is_clamped(ParametricCurveEndpoint.Start)): raise ValueError(f"Curve {self.curve_1} is not clamped at the start. " f"Curvature constraints can only be applied to clamped curves.") self.g2_point_index_curve_1 = 2 if self.curve_joint_index_curve_1 == 0 else -3 self.g1_point_index_curve_1 = 1 if self.g2_point_index_curve_1 == 2 else -2 self.g1_point_curve_1 = self.curve_1.point_sequence().points()[self.g1_point_index_curve_1] self.g2_point_curve_1 = self.curve_1.point_sequence().points()[self.g2_point_index_curve_1] else: (curve_joint_index_curve_1, self.curve_joint_index_curve_1, self.g2_point_index_curve_1, self.g1_point_index_curve_1, self.g1_point_curve_1, self.g2_point_curve_1) = None, None, None, None, None, None if self.curve_type_2 in ("Bezier", "BSpline"): curve_joint_index_curve_2 = self.curve_2.point_sequence().points().index(curve_joint) self.curve_joint_index_curve_2 = -1 if curve_joint_index_curve_2 != 0 else 0 if (self.curve_joint_index_curve_2 == 0 and self.curve_type_1 == "BSpline" and not self.curve_1.is_clamped(ParametricCurveEndpoint.Start)): raise ValueError(f"Curve {self.curve_1} is not clamped at the start. " f"Curvature constraints can only be applied to clamped curves.") self.g2_point_index_curve_2 = 2 if self.curve_joint_index_curve_2 == 0 else -3 self.g1_point_index_curve_2 = 1 if self.g2_point_index_curve_2 == 2 else -2 self.g1_point_curve_2 = self.curve_2.point_sequence().points()[self.g1_point_index_curve_2] self.g2_point_curve_2 = self.curve_2.point_sequence().points()[self.g2_point_index_curve_2] else: (curve_joint_index_curve_2, self.curve_joint_index_curve_2, self.g2_point_index_curve_2, self.g1_point_index_curve_2, self.g1_point_curve_2, self.g2_point_curve_2) = None, None, None, None, None, None points = [self.curve_joint] for point in [self.g2_point_curve_1, self.g1_point_curve_1, self.g1_point_curve_2, self.g2_point_curve_2]: if point is None: continue points.append(point) secondary_params = [] if self.curve_type_1 in ("Bezier", "BSpline"): secondary_params.append(Param(self.curve_1.degree, "n")) if self.curve_type_2 in ("Bezier", "BSpline"): secondary_params.append(Param(self.curve_2.degree, "n")) if self.curve_type_1 == "PolyLine" and self.curve_type_2 == "PolyLine": raise ValueError("Cannot create radius of curvature constraint between two polylines") if self.curve_type_1 == "LineSegment" or self.curve_type_2 == "LineSegment": param = None elif self.curve_type_1 == "PolyLine" or (self.curve_type_1 in ("Bezier", "BSpline") and ( self.curve_1.t_start is not None or self.curve_1.t_end is not None)): data = self.curve_1.evaluate() if (np.isclose(data.xy[0, 0], self.curve_joint.x().value()) and np.isclose(data.xy[0, 1], self.curve_joint.y().value())): R = data.R[0] else: R = data.R[-1] param = LengthParam(value=R, name="ROC-1", enabled=False, geo_col=geo_col) if not isinstance(value, Param) else value elif self.curve_type_2 == "PolyLine" or (self.curve_type_2 in ("Bezier", "BSpline") and ( self.curve_2.t_start is not None or self.curve_2.t_end is not None)): data = self.curve_2.evaluate() if (np.isclose(data.xy[0, 0], self.curve_joint.x().value()) and np.isclose(data.xy[0, 1], self.curve_joint.y().value())): R = data.R[0] else: R = data.R[-1] param = LengthParam(value=R, name="ROC-1", enabled=False, geo_col=geo_col) if not isinstance(value, Param) else value else: enabled = True if value is None: curvature_data = self.calculate_curvature_data(self.curve_joint) if not self.is_solving_allowed(self.g2_point_curve_1) or curve_to_modify is self.curve_2: value = curvature_data.R1 enabled = False elif not self.is_solving_allowed(self.g2_point_curve_2) or curve_to_modify is self.curve_1: value = curvature_data.R2 enabled = False else: value = 0.5 * (curvature_data.R1 + curvature_data.R2) param = value if isinstance(value, Param) else LengthParam(value=value, name="ROC-1", geo_col=geo_col) param.set_enabled(enabled) super().__init__(param=param, child_nodes=points, kind="d", name=name, secondary_params=secondary_params, geo_col=geo_col)
@staticmethod def calculate_curvature_data(curve_joint): Lt1, Lc1, n1, theta1, phi1, psi1, R1 = None, None, None, None, None, None, None Lt2, Lc2, n2, theta2, phi2, psi2, R2 = None, None, None, None, None, None, None curve_1 = curve_joint.curves[0] if curve_1.__class__.__name__ in ("Bezier", "BSpline"): curve_joint_index_curve_1 = curve_1.point_sequence().points().index(curve_joint) curve_joint_index_curve_1 = -1 if curve_joint_index_curve_1 != 0 else 0 g2_point_index_curve_1 = 2 if curve_joint_index_curve_1 == 0 else -3 g1_point_index_curve_1 = 1 if g2_point_index_curve_1 == 2 else -2 g1_point_curve_1 = curve_1.point_sequence().points()[g1_point_index_curve_1] g2_point_curve_1 = curve_1.point_sequence().points()[g2_point_index_curve_1] Lt1 = g1_point_curve_1.measure_distance(curve_joint) Lc1 = g1_point_curve_1.measure_distance(g2_point_curve_1) n1 = curve_1.degree phi1 = curve_joint.measure_angle(g1_point_curve_1) theta1 = g1_point_curve_1.measure_angle(g2_point_curve_1) psi1 = theta1 - phi1 with np.errstate(divide="ignore"): R1 = np.abs(np.true_divide((Lt1 * Lt1), (Lc1 * (1 - 1 / n1) * np.sin(psi1)))) elif curve_1.__class__.__name__ == "LineSegment": R1 = np.inf curve_2 = curve_joint.curves[1] if curve_2.__class__.__name__ in ("Bezier", "BSpline"): curve_joint_index_curve_2 = curve_2.point_sequence().points().index(curve_joint) curve_joint_index_curve_2 = -1 if curve_joint_index_curve_2 != 0 else 0 g2_point_index_curve_2 = 2 if curve_joint_index_curve_2 == 0 else -3 g1_point_index_curve_2 = 1 if g2_point_index_curve_2 == 2 else -2 g1_point_curve_2 = curve_2.point_sequence().points()[g1_point_index_curve_2] g2_point_curve_2 = curve_2.point_sequence().points()[g2_point_index_curve_2] Lt2 = g1_point_curve_2.measure_distance(curve_joint) Lc2 = g1_point_curve_2.measure_distance(g2_point_curve_2) n2 = curve_2.degree phi2 = curve_joint.measure_angle(g1_point_curve_2) theta2 = g1_point_curve_2.measure_angle(g2_point_curve_2) psi2 = theta2 - phi2 with np.errstate(divide="ignore"): R2 = np.abs(np.true_divide((Lt2 * Lt2), (Lc2 * (1 - 1 / n2) * np.sin(psi2)))) elif curve_2.__class__.__name__ == "LineSegment": R2 = np.inf if curve_1.__class__.__name__ == "PolyLine": data = curve_1.evaluate() if (np.isclose(data.xy[0, 0], curve_joint.x().value()) and np.isclose(data.xy[0, 1], curve_joint.y().value())): R1 = data.R[0] else: R1 = data.R[-1] if curve_2.__class__.__name__ == "PolyLine": data = curve_2.evaluate() if (np.isclose(data.xy[0, 0], curve_joint.x().value()) and np.isclose(data.xy[0, 1], curve_joint.y().value())): R2 = data.R[0] else: R2 = data.R[-1] data = CurvatureConstraintData(Lt1=Lt1, Lt2=Lt2, Lc1=Lc1, Lc2=Lc2, n1=n1, n2=n2, theta1=theta1, theta2=theta2, phi1=phi1, phi2=phi2, psi1=psi1, psi2=psi2, R1=R1, R2=R2) return data @staticmethod def is_solving_allowed(g2_point: Point): symmetry_constraints = [geo_con for geo_con in g2_point.geo_cons if isinstance(geo_con, SymmetryConstraint)] # Check if the point is the symmetry target point if any([symmetry_constraint.child_nodes[-1] is g2_point for symmetry_constraint in symmetry_constraints]): return False return True def __repr__(self): return f"{self.__class__.__name__} {self.name()} <C1={self.curve_1.name()}, C2={self.curve_2.name()}>"
[docs] def get_dict_rep(self): value = self.param().name() if self.param() is not None else None return {"curve_joint": self.curve_joint.name(), "value": value, "constraint_type": self.__class__.__name__}
def verify(self) -> bool: data = self.calculate_curvature_data(self.curve_joint) if np.isinf(data.R1) and np.isinf(data.R2): return True return np.isclose(data.R1, data.R2, rtol=1e-10)
[docs] class ConstraintValidationError(Exception): pass
[docs] class InvalidPointError(Exception): pass
[docs] class NoSolutionError(Exception): pass
[docs] class DuplicateConstraintError(Exception): pass
[docs] class MaxWeakConstraintAttemptsError(Exception): pass