Skip to content

Commit

Permalink
Added ClusterAngle parameter functionality, AngleDesVar now no longer…
Browse files Browse the repository at this point in the history
… has angles restricted to 0->2pi
  • Loading branch information
mlauer154 committed Feb 14, 2024
1 parent 431b857 commit 0ad9d8c
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 33 deletions.
13 changes: 11 additions & 2 deletions pymead/core/gcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(self):
super().__init__()
self.points = {}
self.roots = []
self.geo_col = None

def _add_point(self, point: Point):
"""
Expand Down Expand Up @@ -109,6 +110,9 @@ def _set_edge_as_root(self, u: Point, v: Point):
v.rotation_handle = True
if u not in [edge[0] for edge in self.roots]:
self.roots.append((u, v))
param = self.geo_col.add_param(value=u.measure_angle(v), name="ClusterAngle-1", unit_type="angle", root=u,
rotation_handle=v)
param.gcs = self

def _delete_root_status(self, root_node: Point, rotation_handle_node: Point = None):
"""
Expand Down Expand Up @@ -768,12 +772,17 @@ def translate_cluster(self, root: Point, dx: float, dy: float):
self.solve_other_constraints(points_solved)
return points_solved

def rotate_cluster(self, rotation_handle: Point, new_rotation_handle_x: float, new_rotation_handle_y: float):
def rotate_cluster(self, rotation_handle: Point, new_rotation_handle_x: float = None,
new_rotation_handle_y: float = None,
new_rotation_angle: float = None):
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)
new_rotation_handle_angle = root.measure_angle(Point(new_rotation_handle_x, new_rotation_handle_y))
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()
Expand Down
53 changes: 43 additions & 10 deletions pymead/core/geometry_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, gui_obj=None):
"dims": {},
}
self.gcs = GCS()
self.gcs.geo_col = self
self.gui_obj = gui_obj
self.canvas = None
self.tree = None
Expand Down Expand Up @@ -174,7 +175,7 @@ def remove_from_subcontainer(self, pymead_obj: PymeadObj):

def add_param(self, value: float, name: str or None = None, lower: float or None = None,
upper: float or None = None, unit_type: str or None = None, assign_unique_name: bool = True,
point: Point = None):
point: Point = None, root: Point = None, rotation_handle: Point = None):
"""
Adds a parameter to the geometry collection sub-container ``"params"``, and modifies the name to make it
unique if necessary.
Expand All @@ -201,7 +202,8 @@ def add_param(self, value: float, name: str or None = None, lower: float or None
Param
The generated parameter
"""
kwargs = dict(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=True, point=point)
kwargs = dict(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=True, point=point,
root=root, rotation_handle=rotation_handle)
if unit_type is None:
param = Param(**kwargs)
elif unit_type == "length":
Expand Down Expand Up @@ -347,7 +349,8 @@ def add_pymead_obj_by_ref(self, pymead_obj: PymeadObj, assign_unique_name: bool

return pymead_obj

def remove_pymead_obj(self, pymead_obj: PymeadObj, promotion_demotion: bool = False):
def remove_pymead_obj(self, pymead_obj: PymeadObj, promotion_demotion: bool = False,
constraint_removal: bool = False):
"""
Removes a pymead object from the geometry collection.
Expand All @@ -359,12 +362,28 @@ def remove_pymead_obj(self, pymead_obj: PymeadObj, promotion_demotion: bool = Fa
promotion_demotion: bool
When this flag is set to ``True``, the ``ValueError`` normally raised when directly deleting a ``Param``
associated with a ``GeoCon`` is ignored. Default: ``False``
constraint_removal: bool
When this flag is set to ``True``, the ``ValueError`` normally raise when directly deleting a ``Param``
associated with a constraint cluster rotation is ignored. Default: ``False``
"""
# Type-specific actions
if isinstance(pymead_obj, Param):
if len(pymead_obj.geo_cons) != 0 and not promotion_demotion:
raise ValueError(f"Please delete each constraint associated with this parameter ({pymead_obj.geo_cons})"
f" before deleting this parameter")
error_message = (f"Please delete each constraint associated with this parameter ({pymead_obj.geo_cons}) "
f"before deleting this parameter")
if self.gui_obj is None:
raise ValueError(error_message)
else:
self.gui_obj.disp_message_box(error_message, message_mode="error")
return
if pymead_obj.rotation_handle and not constraint_removal and not promotion_demotion:
error_message = f"This parameter can only be removed by deleting its associated constraint cluster"
if self.gui_obj is None:
raise ValueError(error_message)
else:
self.gui_obj.disp_message_box(error_message, message_mode="error")
return

elif isinstance(pymead_obj, Bezier) or isinstance(pymead_obj, LineSegment):
# Remove all the references to this curve in each of the curve's points
Expand All @@ -380,8 +399,13 @@ def remove_pymead_obj(self, pymead_obj: PymeadObj, promotion_demotion: bool = Fa
elif isinstance(pymead_obj, Point):

if len(pymead_obj.geo_cons) != 0:
raise ValueError(f"Please delete each constraint associated with this point ({pymead_obj.geo_cons})"
error_message = (f"Please delete each constraint associated with this point ({pymead_obj.geo_cons})"
f" before deleting this point")
if self.gui_obj is None:
raise ValueError(error_message)
else:
self.gui_obj.disp_message_box(error_message, message_mode="error")
return

# Loop through the curves associated with this point to see which ones need to be deleted if one point
# is removed from their point sequence
Expand Down Expand Up @@ -432,6 +456,11 @@ def remove_pymead_obj(self, pymead_obj: PymeadObj, promotion_demotion: bool = Fa
if len(pymead_obj.param().geo_cons) == 0:
self.remove_pymead_obj(pymead_obj.param())

if (isinstance(pymead_obj, DistanceConstraint) or isinstance(pymead_obj, RelAngle3Constraint) or
isinstance(pymead_obj, Perp3Constraint) or isinstance(pymead_obj, AntiParallel3Constraint)):
if pymead_obj.p2.rotation_handle:
self.remove_pymead_obj(pymead_obj.p2.rotation_param, constraint_removal=True)

# Remove the constraint from the ConstraintGraph
self.gcs.remove_constraint(pymead_obj)

Expand Down Expand Up @@ -489,7 +518,8 @@ def add_line(self, point_sequence: PointSequence, name: str or None = None, assi
return self.add_pymead_obj_by_ref(line, assign_unique_name=assign_unique_name)

def add_desvar(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
unit_type: str or None = None, assign_unique_name: bool = True, point: Point = None):
unit_type: str or None = None, assign_unique_name: bool = True, point: Point = None,
root: Point = None, rotation_handle: Point = None):
"""
Directly adds a design variable value to the geometry collection.
Expand Down Expand Up @@ -517,7 +547,8 @@ def add_desvar(self, value: float, name: str, lower: float or None = None, upper
DesVar
The generated design variable
"""
kwargs = dict(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=True, point=point)
kwargs = dict(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=True, point=point,
root=root, rotation_handle=rotation_handle)
if unit_type is None:
desvar = DesVar(**kwargs)
elif unit_type == "length":
Expand Down Expand Up @@ -583,7 +614,8 @@ def promote_param_to_desvar(self, param: Param or str, lower: float or None = No
unit_type = None

desvar = self.add_desvar(value=param.value(), name=param.name(), lower=lower, upper=upper, unit_type=unit_type,
point=copy(param.point))
point=copy(param.point), root=param.root,
rotation_handle=param.rotation_handle)

# Replace the corresponding x() or y() in parameter with the new design variable
self.replace_geo_objs(tool=param, target=desvar)
Expand Down Expand Up @@ -630,7 +662,8 @@ def demote_desvar_to_param(self, desvar: DesVar):
else:
unit_type = None

param = self.add_param(value=desvar.value(), name=desvar.name(), unit_type=unit_type, point=copy(desvar.point))
param = self.add_param(value=desvar.value(), name=desvar.name(), unit_type=unit_type, point=copy(desvar.point),
root=desvar.root, rotation_handle=desvar.rotation_handle)

# Replace the corresponding x() or y() in parameter with the new parameter
self.replace_geo_objs(tool=desvar, target=param)
Expand Down
79 changes: 68 additions & 11 deletions pymead/core/param.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing

import networkx
import numpy as np

from pymead.core import UNITS
Expand All @@ -13,7 +14,8 @@ class Param(PymeadObj):
"""

def __init__(self, value: float or int, name: str, lower: float or None = None, upper: float or None = None,
sub_container: str = "params", setting_from_geo_col: bool = False, point=None):
sub_container: str = "params", setting_from_geo_col: bool = False, point=None, root=None,
rotation_handle=None):
"""
Parameters
==========
Expand All @@ -37,6 +39,10 @@ def __init__(self, value: float or int, name: str, lower: float or None = None,
self._upper = None
self.at_boundary = False
self.point = point
self.root = root
self.rotation_handle = rotation_handle
if rotation_handle is not None:
self.rotation_handle.rotation_param = self
self.geo_objs = []
self.gcs = None
self.geo_cons = []
Expand Down Expand Up @@ -105,6 +111,40 @@ def set_value(self, value: float or int, updated_objs: typing.List[PymeadObj] =
1 if the value is equal to the upper bound, or a float between 0.0 and 1.0 if the value is somewhere
between the bounds). Default: ``False``
"""
def rotate_cluster(new_v):
if self.gcs is None:
return
points_to_update, root = self.gcs.rotate_cluster(self.rotation_handle, new_rotation_angle=new_v)
constraints_to_update = []
for point in networkx.dfs_preorder_nodes(self.gcs, source=root):
for geo_con in point.geo_cons:
if geo_con not in constraints_to_update:
constraints_to_update.append(geo_con)

for geo_con in constraints_to_update:
if geo_con.canvas_item is not None:
geo_con.canvas_item.update()

curves_to_update = []
for point in points_to_update:
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)

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()

for airfoil in airfoils_to_update:
airfoil.update_coords()
if airfoil.canvas_item is not None:
airfoil.canvas_item.generatePicture()

if bounds_normalized:
if self.lower() is None or self.upper() is None:
raise ValueError("Lower and upper bounds must be set to assign a bounds-normalized value.")
Expand All @@ -122,6 +162,9 @@ def set_value(self, value: float or int, updated_objs: typing.List[PymeadObj] =
self._value = value
self.at_boundary = False

if self.rotation_handle is not None:
rotate_cluster(self._value)

if self.at_boundary:
return

Expand Down Expand Up @@ -255,13 +298,13 @@ def __pow__(self, power, modulo=None):
class LengthParam(Param):
def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
sub_container: str = "params",
setting_from_geo_col: bool = False, point=None):
setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None):
self._unit = None
self.set_unit(UNITS.current_length_unit())
name = "Length-1" if name is None else name
super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container,
setting_from_geo_col=setting_from_geo_col,
point=point)
point=point, root=root, rotation_handle=rotation_handle)

def unit(self):
return self._unit
Expand Down Expand Up @@ -311,12 +354,13 @@ def get_dict_rep(self):
class AngleParam(Param):
def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
sub_container: str = "params",
setting_from_geo_col: bool = False, point=None):
setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None):
self._unit = None
self.set_unit(UNITS.current_angle_unit())
name = "Angle-1" if name is None else name
super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container,
setting_from_geo_col=setting_from_geo_col, point=point)
setting_from_geo_col=setting_from_geo_col, point=point, root=root,
rotation_handle=rotation_handle)

def unit(self):
return self._unit
Expand Down Expand Up @@ -426,7 +470,8 @@ class DesVar(Param):
Design variable class; subclasses the base-level Param. Adds lower and upper bound default behavior.
"""
def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
sub_container: str = "desvar", setting_from_geo_col: bool = False, point=None):
sub_container: str = "desvar", setting_from_geo_col: bool = False, point=None, root=None,
rotation_handle=None):
"""
Parameters
==========
Expand Down Expand Up @@ -455,7 +500,8 @@ def __init__(self, value: float, name: str, lower: float or None = None, upper:
upper = default_upper(value)

super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container=sub_container,
setting_from_geo_col=setting_from_geo_col, point=point)
setting_from_geo_col=setting_from_geo_col, point=point, root=root,
rotation_handle=rotation_handle)


class LengthDesVar(LengthParam):
Expand All @@ -464,7 +510,7 @@ class LengthDesVar(LengthParam):
default behavior.
"""
def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
setting_from_geo_col: bool = False, point=None):
setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None):
"""
Parameters
==========
Expand Down Expand Up @@ -493,7 +539,7 @@ def __init__(self, value: float, name: str, lower: float or None = None, upper:
upper = default_upper(value)

super().__init__(value=value, name=name, lower=lower, upper=upper, setting_from_geo_col=setting_from_geo_col,
sub_container="desvar", point=point)
sub_container="desvar", point=point, root=root, rotation_handle=rotation_handle)

def get_dict_rep(self):
return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(),
Expand All @@ -506,7 +552,7 @@ class AngleDesVar(AngleParam):
default behavior.
"""
def __init__(self, value: float, name: str, lower: float or None = None, upper: float or None = None,
setting_from_geo_col: bool = False, point=None):
setting_from_geo_col: bool = False, point=None, root=None, rotation_handle=None):
"""
Parameters
==========
Expand Down Expand Up @@ -535,7 +581,18 @@ def __init__(self, value: float, name: str, lower: float or None = None, upper:
upper = default_upper(value)

super().__init__(value=value, name=name, lower=lower, upper=upper, sub_container="desvar",
setting_from_geo_col=setting_from_geo_col, point=point)
setting_from_geo_col=setting_from_geo_col, point=point, root=root,
rotation_handle=rotation_handle)

def set_value(self, value: float, updated_objs: typing.List[PymeadObj] = None, bounds_normalized: bool = False):
r"""
In this special case of ``set_value`` for an ``AngleDesVar``, we skip over the call to the ``set_value``
method in ``AngleParam`` and directly call the ``set_value`` method in ``Param`` (the grandparent class).
The reason for this is that ``AngleParam`` always keeps the angle between 0 and :math:`2 \pi`, which is not
logical behavior for a bounded variable. This method eliminates that restriction.
"""
new_value = UNITS.convert_angle_to_base(value, self.unit())
return Param.set_value(self, new_value, updated_objs=updated_objs, bounds_normalized=bounds_normalized)

def get_dict_rep(self):
return {"value": float(self.value()), "lower": self.lower(), "upper": self.upper(),
Expand Down
Loading

0 comments on commit 0ad9d8c

Please sign in to comment.