diff --git a/docs/source/_templates/custom-class-template.rst b/docs/source/_templates/custom-class-template.rst index 87bf85bb..58f7f06c 100644 --- a/docs/source/_templates/custom-class-template.rst +++ b/docs/source/_templates/custom-class-template.rst @@ -14,7 +14,7 @@ .. autosummary:: {% for item in methods %} - {%- if item not in inherited_members %} + {%- if item not in inherited_members and item != "__init__" %} ~{{ name }}.{{ item }} {%- endif %} {%- endfor %} diff --git a/pymead/core/airfoil.py b/pymead/core/airfoil.py index 17675309..e0109e92 100644 --- a/pymead/core/airfoil.py +++ b/pymead/core/airfoil.py @@ -12,7 +12,7 @@ class Airfoil(PymeadObj): """ - This a primary class in `pymead`, which defines an airfoil by a leading edge, a trailing edge, + This is a primary class in `pymead`, which defines an airfoil by a leading edge, a trailing edge, and optionally an upper-surface endpoint and a lower-surface endpoint in the case of a blunt airfoil. For the purposes of single-airfoil evaluation method (such as XFOIL or the built-in panel code), instances of this class are sufficient. For multi-element airfoil evaluation (such as MSES), instances of this class are stored in the diff --git a/pymead/core/point.py b/pymead/core/point.py index 9a9f136f..5d571c0d 100644 --- a/pymead/core/point.py +++ b/pymead/core/point.py @@ -9,13 +9,10 @@ class Point(PymeadObj): - def __init__(self, x: float, y: float, name: str or None = None, setting_from_geo_col: bool = False, - fixed: bool = False): + def __init__(self, x: float, y: float, name: str or None = None, setting_from_geo_col: bool = False): super().__init__(sub_container="points") self._x = None self._y = None - self._fixed = fixed - self._fixed_weak = False self.gcs = None self.root = False self.rotation_handle = False @@ -34,12 +31,6 @@ def x(self): def y(self): return self._y - def fixed(self): - return self._fixed - - def fixed_weak(self): - return self._fixed_weak - def set_x(self, x: LengthParam or float): self._x = x if isinstance(x, LengthParam) else LengthParam( value=x, name=self.name() + ".x", setting_from_geo_col=self.setting_from_geo_col, point=self) @@ -54,12 +45,6 @@ def set_y(self, y: LengthParam or float): self._y.geo_objs.append(self) self._y.point = self - def set_fixed(self, fixed: bool): - self._fixed = fixed - - def set_fixed_weak(self, fixed_weak: bool): - self._fixed_weak = fixed_weak - def set_name(self, name: str): # Rename the x and y parameters of the Point if self.x() is not None: @@ -102,77 +87,126 @@ def _is_symmetry_123_and_no_edges(self): return False return symmetry_constraints - def request_move(self, xp: float, yp: float, force: bool = False): + def is_movement_allowed(self) -> bool: + """ + This method determines if movement is allowed for the point. These cases are: + + - Where the constraint solver has not been set or there are no geometric constraints attached + - Where the point is a root or rotation handle of a constraint cluster. If a rotation handle, movement is + allowed, but the movement gets translated to a rotation about the root point with a fixed distance to the + root point + - Where the point is one of the first three out of the four points in a symmetry constraint, and edges are + attached to this point in the constraint graph + + Returns + ------- + bool + ``True`` if movement is allowed for this point, ``False`` otherwise + """ + if self.gcs is None: + return True + if self.gcs is not None and len(self.geo_cons) == 0: + return True + if self.gcs is not None and self.root: + return True + if self.gcs is not None and self.rotation_handle: + # In this case, movement is allowed, but movements get translated to a rotation about the root point with + # a fixed distance to the root point + return True + if self._is_symmetry_123_and_no_edges(): + return True + return False - if (self.gcs is None or (self.gcs is not None and len(self.geo_cons) == 0) or force or - (self.gcs is not None and self.root) or (self.gcs is not None and self.rotation_handle) - or self._is_symmetry_123_and_no_edges()): - - if self.root: - points_to_update = self.gcs.translate_cluster(self, dx=xp - self.x().value(), dy=yp - self.y().value()) - constraints_to_update = [] - for point in networkx.dfs_preorder_nodes(self.gcs, source=self): - 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() - elif self.rotation_handle: - points_to_update, root = self.gcs.rotate_cluster(self, xp, yp) - 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() - else: - self.x().set_value(xp) - self.y().set_value(yp) - points_to_update = [self] - symmetry_constraints = self._is_symmetry_123_and_no_edges() - if symmetry_constraints: - for symmetry_constraint in symmetry_constraints: - self.gcs.solve_symmetry_constraint(symmetry_constraint) - points_to_update.extend(symmetry_constraint.child_nodes) - points_to_update = list(set(points_to_update)) # Get only the unique points - for symmetry_constraint in symmetry_constraints: - if symmetry_constraint.canvas_item is not None: - symmetry_constraint.canvas_item.update() - - # Update the GUI object, if there is one - if self.canvas_item is not None: - self.canvas_item.updateCanvasItem(self.x().value(), self.y().value()) - - 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() + def request_move(self, xp: float, yp: float, force: bool = False): + """ + Updates the location of the point and updates any curves and canvas items associated with the point movement. + + Parameters + ---------- + xp: float + New :math:`x`-value for the point + + yp: float + New :math:`y`-value for the point + + force: bool + Force the movement of this point. Overrides ``is_movement_allowed``. + + Warning + ------- + The ``force`` keyword argument should **never** be called directly from the API, or unexpected behavior + may result. This argument is used in the backend code for the constraint solver in the symmetry and curvature + constraints. + """ + + if not self.is_movement_allowed() and not force: + return + + if self.root: + points_to_update = self.gcs.translate_cluster(self, dx=xp - self.x().value(), dy=yp - self.y().value()) + constraints_to_update = [] + for point in networkx.dfs_preorder_nodes(self.gcs, source=self): + 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() + elif self.rotation_handle: + points_to_update, root = self.gcs.rotate_cluster(self, xp, yp) + 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() + else: + self.x().set_value(xp) + self.y().set_value(yp) + points_to_update = [self] + symmetry_constraints = self._is_symmetry_123_and_no_edges() + if symmetry_constraints: + for symmetry_constraint in symmetry_constraints: + self.gcs.solve_symmetry_constraint(symmetry_constraint) + points_to_update.extend(symmetry_constraint.child_nodes) + points_to_update = list(set(points_to_update)) # Get only the unique points + for symmetry_constraint in symmetry_constraints: + if symmetry_constraint.canvas_item is not None: + symmetry_constraint.canvas_item.update() + + # Update the GUI object, if there is one + if self.canvas_item is not None: + self.canvas_item.updateCanvasItem(self.x().value(), self.y().value()) + + 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() def __repr__(self): return f"Point {self.name()}" def get_dict_rep(self): - return {"x": float(self.x().value()), "y": float(self.y().value()), "fixed": self.fixed()} + return {"x": float(self.x().value()), "y": float(self.y().value())} class PointSequence: diff --git a/pymead/core/pymead_obj.py b/pymead/core/pymead_obj.py index 6caba8f4..f8e06d84 100644 --- a/pymead/core/pymead_obj.py +++ b/pymead/core/pymead_obj.py @@ -31,6 +31,12 @@ class PymeadObj(ABC, DualRep): """ def __init__(self, sub_container: str): + """ + Parameters + ---------- + sub_container: str + Sub-container where this object will be stored in the ``GeometryCollection`` + """ self.sub_container = sub_container self._name = None self.geo_col = None @@ -70,4 +76,13 @@ def set_name(self, name: str): @abstractmethod def get_dict_rep(self) -> dict: + """ + Gets a dictionary representation of the pymead object. In general, this dictionary should consist of only + the required arguments for object instantiation. For example, the dictionary representation of a point looks + something like this: ``{"x": 0.3, "y": 0.5}``. If the argument requires a reference to a ``PymeadObj`` + rather than a string or float value, the ``name()`` method should be the value that is stored. For + an example, see the overridden value of this method in ``pymead.core.airfoil.Airfoil``. + All subclasses of ``PymeadObj`` must implement this method, since it is the way pymead objects are stored in + saved instances of a ``GeometryCollection`` (``.jmea`` files). + """ pass