From 03678e7391b90ba27718ea4590878d742c75eade Mon Sep 17 00:00:00 2001 From: mlauer154 Date: Mon, 29 Jan 2024 13:46:10 -0600 Subject: [PATCH] Edit bounds dialog fixed, added showColoredMessage() to GUI --- pymead/core/constraint_graph.py | 217 -------------------- pymead/core/geometry_collection.py | 11 +- pymead/core/param.py | 6 + pymead/gui/bounds_values_table.py | 173 +++++----------- pymead/gui/gui.py | 42 ++-- pymead/gui/input_dialog.py | 21 +- pymead/tests/core_tests/test_constraints.py | 25 +++ 7 files changed, 107 insertions(+), 388 deletions(-) diff --git a/pymead/core/constraint_graph.py b/pymead/core/constraint_graph.py index 9b578796..a76c36ef 100644 --- a/pymead/core/constraint_graph.py +++ b/pymead/core/constraint_graph.py @@ -720,220 +720,3 @@ def update_points(self, constraint: GeoCon, new_x: np.ndarray): class OverConstrainedError(Exception): pass - - -def main(): - import matplotlib.pyplot as plt - - p1 = Point(0.0, 0.0, "p1") - p2 = Point(0.3, 0.3, "p2") - p3 = Point(0.4, 0.6, "p3") - p4 = Point(0.8, -0.1, "p4") - p5 = Point(0.2, 0.6, "p5") - points = [p1, p2, p3, p4, p5] - - original_x = [p.x().value() for p in points] - original_y = [p.y().value() for p in points] - plt.plot(original_x, original_y, ls="none", marker="o", color="indianred") - plt.gca().set_aspect("equal") - - cnstr1 = DistanceConstraint(p1, p2, 1.0, "d1") - cnstr2 = DistanceConstraint(p2, p3, 0.8, "d2") - cnstr3 = DistanceConstraint(p2, p4, 0.4, "d3") - cnstr4 = DistanceConstraint(p1, p5, 1.2, "d4") - cnstr5 = DistanceConstraint(p3, p1, 0.6, "d5") - graph = ConstraintGraph() - - graph.add_constraint(cnstr1) - graph.add_constraint(cnstr2) - graph.add_constraint(cnstr3) - graph.add_constraint(cnstr4) - graph.add_constraint(cnstr5) - - print(f"{np.hypot(p1.x().value() - p2.x().value(), p1.y().value() - p2.y().value())}") - print(f"{np.hypot(p3.x().value() - p2.x().value(), p3.y().value() - p2.y().value())}") - print(f"{np.hypot(p1.x().value() - p3.x().value(), p1.y().value() - p3.y().value())}") - - print(f"{p2 = }") - - # new_x = [p.x().value() for p in points] - # new_y = [p.y().value() for p in points] - # plt.plot(new_x, new_y, ls="none", marker="s", color="mediumaquamarine", mfc="#aaaaaa88") - # plt.show() - # - # graph.move_point(p2, 0.8, 0.6) - # - # print(f"{np.hypot(p1.x().value() - p2.x().value(), p1.y().value() - p2.y().value())}") - # print(f"{np.hypot(p3.x().value() - p2.x().value(), p3.y().value() - p2.y().value())}") - # print(f"{np.hypot(p1.x().value() - p3.x().value(), p1.y().value() - p3.y().value())}") - - new_x = [p.x().value() for p in points] - new_y = [p.y().value() for p in points] - plt.plot(original_x, original_y, ls="none", marker="o", color="indianred") - plt.plot(new_x, new_y, ls="none", marker="s", color="steelblue", mfc="#aaaaaa88") - plt.show() - - pass - - -def main2(): - import matplotlib.pyplot as plt - # Initialize the graph - g = ConstraintGraph() - - # Add the points - p1 = Point(0.0, 0.0, "p1") - p2 = Point(0.4, 0.1, "p2") - p3 = Point(0.6, 0.0, "p3") - p4 = Point(0.2, 0.8, "p4") - p5 = Point(1.0, 1.3, "p5") - points = [p1, p2, p3, p4, p5] - for point in points: - g.add_point(point) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mec="indianred", mfc="indianred", fillstyle="left", markersize=10) - - # Add the constraints - d1 = DistanceConstraint(p1, p2, 0.2, "d1") - d2 = DistanceConstraint(p2, p4, 1.5, "d2") - d3 = DistanceConstraint(p2, p3, 0.5, "d3") - d4 = DistanceConstraint(p3, p5, 1.4, "d4") - perp3 = Perp3Constraint(p1, p2, p4, "L1") - parl = Parallel4Constraint(p2, p4, p3, p5, "//1") - aparl3 = AntiParallel3Constraint(p1, p2, p3, "ap1") - constraints = [d1, d2, d3, d4, perp3, parl, aparl3] - for constraint in constraints: - g.add_constraint(constraint) - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", - mec="indianred", mfc="indianred", fillstyle="left", markersize=10) - plt.show() - - d4.param().set_value(4.0) - d4.param().set_value(5.0) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", - mfc="steelblue", mec="steelblue", fillstyle="right", markersize=10) - for p in points: - plt.text(p.x().value() + 0.02, p.y().value() + 0.02, p.name()) - plt.gca().set_aspect("equal") - - plt.show() - - labels = {p: p.name() for p in g.nodes} - subax1 = plt.subplot(121) - networkx.draw(g, with_labels=True, labels=labels) - - # for node in networkx.dfs_preorder_nodes(g, source=p1): - # if isinstance(node, GeoCon): - # print(f"{node = }") - - # cycle_basis = networkx.cycle_basis(g) - # # for c in cycle_basis: - # # print(f"{c = }") - # simple_cycles = networkx.chordless_cycles(g) - # for simple_cycle in simple_cycles: - # print(simple_cycle) - - plt.show() - pass - - -def main3(): - import matplotlib.pyplot as plt - # Initialize the graph - g = ConstraintGraph() - - # Add the points - p1 = Point(0.0, 0.0, "p1") - p2 = Point(0.4, 0.1, "p2") - p3 = Point(0.6, 0.0, "p3") - points = [p1, p2, p3] - for point in points: - g.add_point(point) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mec="indianred", mfc="indianred", fillstyle="left", markersize=10) - - # Add the constraints - d1 = DistanceConstraint(p1, p2, 0.6, "d1") - d2 = DistanceConstraint(p2, p3, 1.0, "d2") - d3 = DistanceConstraint(p1, p3, 0.8, "d3") - constraints = [d1, d2, d3] - for constraint in constraints: - g.add_constraint(constraint) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mfc="steelblue", mec="steelblue", fillstyle="right", markersize=10) - for p in points: - plt.text(p.x().value() + 0.02, p.y().value() + 0.02, p.name()) - plt.gca().set_aspect("equal") - - plt.show() - - -def main4(): - import matplotlib.pyplot as plt - # Initialize the graph - g = ConstraintGraph() - - # Add the points - p1 = Point(0.0, 0.0, "p1") - p2 = Point(1.0, 0.0, "p2") - p3 = Point(-0.05, 0.05, "p3") - # p4 = Point(0.5, 0.03, "p4") - points = [p1, p2, p3] - for point in points: - g.add_point(point) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mec="indianred", - mfc="indianred", fillstyle="left", markersize=10) - - # Add the constraints - d1 = DistanceConstraint(p1, p2, 1.0, "d1") - d2 = DistanceConstraint(p1, p3, 0.2, "d2") - pol = PointOnLineConstraint(p3, p1, p2, "pol1") - # d3 = DistanceConstraint(p1, p3, 0.8, "d3") - constraints = [d1, d2, pol] - for constraint in constraints: - g.add_constraint(constraint) - - # d4.param.set_value(2.0) - # # g.compile_equation_for_entity_or_constraint(aparl3) - # x, info = g.initial_solve(d3) - # print(f"{info = }") - # g.update_points(aparl3, new_x=x) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mfc="steelblue", - mec="steelblue", fillstyle="right", markersize=10) - for p in points: - plt.text(p.x().value() + 0.02, p.y().value() + 0.02, p.name()) - plt.gca().set_aspect("equal") - - plt.show() - - -def main5(): - import matplotlib.pyplot as plt - g = ConstraintGraph() - - # Add the points - p1 = Point(0.0, 0.0, "p1") - p2 = Point(0.4, 0.1, "p2") - p3 = Point(0.1, -0.1, "p3") - p4 = Point(0.5, -0.15, "p4") - points = [p1, p2, p3, p4] - for point in points: - g.add_point(point) - - d1 = DistanceConstraint(p1, p2, 0.5, "d1") - d2 = DistanceConstraint(p3, p4, 0.55, "d2") - par = Parallel4Constraint(p1, p2, p3, p4, "par") - for constraint in [d1, d2, par]: - g.add_constraint(constraint) - - plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mfc="indianred") - plt.show() - - g.remove_constraint(par) - - -if __name__ == "__main__": - main2() diff --git a/pymead/core/geometry_collection.py b/pymead/core/geometry_collection.py index d9b7fbaa..e9a7dc7a 100644 --- a/pymead/core/geometry_collection.py +++ b/pymead/core/geometry_collection.py @@ -21,7 +21,7 @@ class GeometryCollection(DualRep): - def __init__(self): + def __init__(self, gui_obj=None): """ The geometry collection is the primary class in pymead for housing all the available fundamental geometry types. Geometry, parameters, and constraints can be added using the nomenclature ``add_()``. @@ -39,6 +39,7 @@ def __init__(self): "dims": {}, } self.gcs = ConstraintGraph() + self.gui_obj = gui_obj self.canvas = None self.tree = None self.selected_objects = {k: [] for k in self._container.keys()} @@ -798,6 +799,10 @@ def add_constraint(self, constraint: GeoCon, assign_unique_name: bool = True, ** self.gcs.add_constraint(constraint, **constraint_kwargs) except (OverConstrainedError, ValueError) as e: self.remove_pymead_obj(constraint) + self.clear_selected_objects() + if self.gui_obj is not None: + self.gui_obj.showColoredMessage("Constraint cluster is over-constrained. Removing constraint...", + 4000, "#eb4034") raise e return constraint @@ -862,8 +867,8 @@ def get_metadata(): } @classmethod - def set_from_dict_rep(cls, d: dict, canvas=None, tree=None): - geo_col = cls() + def set_from_dict_rep(cls, d: dict, canvas=None, tree=None, gui_obj=None): + geo_col = cls(gui_obj=gui_obj) geo_col.canvas = canvas geo_col.tree = tree for name, desvar_dict in d["desvar"].items(): diff --git a/pymead/core/param.py b/pymead/core/param.py index 287f144d..74671ada 100644 --- a/pymead/core/param.py +++ b/pymead/core/param.py @@ -175,6 +175,9 @@ def set_lower(self, lower: float): if self._value is not None and self._value < self._lower: self._value = self._lower + if self.tree_item is not None: + self.tree_item.treeWidget().itemWidget(self.tree_item, 1).setMinimum(self.lower()) + def set_upper(self, upper: float): """ Sets the upper bound for the design variable. If called from outside ``DesVar.__init__()``, adjust the design @@ -189,6 +192,9 @@ def set_upper(self, upper: float): if self._value is not None and self._value > self._upper: self._value = self._upper + if self.tree_item is not None: + self.tree_item.treeWidget().itemWidget(self.tree_item, 1).setMaximum(self.upper()) + def get_dict_rep(self): return {"value": float(self.value()) if self.dtype == "float" else int(self.value()), "lower": self.lower(), "upper": self.upper(), diff --git a/pymead/gui/bounds_values_table.py b/pymead/gui/bounds_values_table.py index 43a02889..62baf240 100644 --- a/pymead/gui/bounds_values_table.py +++ b/pymead/gui/bounds_values_table.py @@ -1,82 +1,66 @@ from copy import deepcopy -import numpy as np -from PyQt5.QtGui import QColor +from PyQt5.QtCore import Qt, QSize from pyqtgraph import TableWidget -import benedict - -def flatten_dict(jmea_dict: dict): - dben = benedict.benedict(jmea_dict) - keypaths = dben.keypaths() - - data = [] - - for k in keypaths: - p = dben[k] - if isinstance(p, dict) and "_value" in p.keys(): - if isinstance(p["_value"], list): - eq1 = "" if p["func_str"] is None else p["func_str"].split(",")[0].strip("{}") - eq2 = "" if p["func_str"] is None else p["func_str"].split(",")[1].strip("{}") - data1 = {"name": k, "value": p["_value"][0], "lower": p["bounds"][0][0], "upper": p["bounds"][0][1], - "active": int(bool(p["active"][0])), "eq": eq1} - data2 = {"name": "", "value": p["_value"][1], "lower": p["bounds"][1][0], "upper": p["bounds"][1][1], - "active": int(bool(p["active"][1])), "eq": eq2} - data.extend([data1, data2]) - else: - eq = "" if p["func_str"] is None else p["func_str"] - data1 = {"name": k, "value": p["_value"], "lower": p["bounds"][0], "upper": p["bounds"][1], - "active": int(bool(p["active"])), "eq": eq} - data.append(data1) - - return data +from pymead.core.geometry_collection import GeometryCollection class BoundsValuesTable(TableWidget): - def __init__(self, jmea_dict: dict): - data = flatten_dict(jmea_dict) + def __init__(self, geo_col: GeometryCollection): super().__init__(editable=True, sortable=False) + data = [[dv.name(), f"{dv.lower():.10f}", f"{dv.value():.10f}", f"{dv.upper():.10f}"] + for dv in geo_col.container()["desvar"].values()] + self.geo_col = geo_col + self.initializing_data = True self.setData(data) - self.highlight_unbounded_params(data=data) - self.original_data = deepcopy(self.data()) + self.setHorizontalHeaderLabels(["DesVar Name", "Lower Bound", "Value", "Upper Bound"]) + for column in (0, 2): + self.makeColumnReadOnly(column) + for column in range(self.columnCount()): + self.setColumnWidth(column, 100) + self.initializing_data = False def handleItemChanged(self, item): super().handleItemChanged(item) - self.highlight_unbounded_params(item=item) - def set_red_cell_background(self, row: int, col: int): - item = self.item(row, col) - item.setBackground(QColor(247, 84, 92, 150)) + if not self.initializing_data: + # Get the design variable from the geometry collection + dv = self.geo_col.container()["desvar"][self.item(item.row(), 0).data(0)] - def reset_cell_background(self, row: int, col: int): - item = self.item(row, col) - bcolor = self.item(row, 0).background() - item.setBackground(bcolor) - - def highlight_unbounded_params(self, item=None, data: list or None = None): - if data is not None: - for row_idx, row in enumerate(data): - if np.isinf(row["lower"]) and bool(row["active"]) and (len(row["eq"]) == 0): - self.set_red_cell_background(row_idx, 2) - if np.isinf(row["upper"]) and bool(row["active"]) and (len(row["eq"]) == 0): - self.set_red_cell_background(row_idx, 3) - if item is not None: - row_idx = item.row() - if self.item(row_idx, 5) is None: - return - lower = self.item(row_idx, 2).value - upper = self.item(row_idx, 3).value - active = bool(self.item(row_idx, 4).value) - linked = bool(len(self.item(row_idx, 5).value) > 0) - - if np.isinf(lower) and active and not linked: - self.set_red_cell_background(row_idx, 2) - else: - self.reset_cell_background(row_idx, 2) - if np.isinf(upper) and active and not linked: - self.set_red_cell_background(row_idx, 3) + # Check if the string input is castable to a float. If not, set the float_val to None + if item.data(0).strip().replace(".", "").isnumeric(): + float_val = float(item.data(0).strip()) else: - self.reset_cell_background(row_idx, 3) + float_val = None + + # Lower bound + if item.column() == 1: + # Only change the design variable's lower bound if the input is castable to a float and is less than + # or equal to the design variable's value + if float_val is not None and float_val <= dv.value(): + dv.set_lower(float_val) + item.setData(0, f"{dv.lower():.10f}") + + # Upper bound + elif item.column() == 3: + # Only change the design variable's upper bound if the input is castable to a float and is greater than + # or equal to the design variable's value + if float_val is not None and float_val >= dv.value(): + dv.set_upper(float_val) + item.setData(0, f"{dv.upper():.10f}") + + def makeColumnReadOnly(self, column: int): + for row in range(self.rowCount()): + item = self.item(row, column) + item.setFlags(item.flags() ^ Qt.ItemIsEditable) + + def sizeHint(self): + width = sum([self.columnWidth(i) for i in range(self.columnCount())]) + width += self.verticalHeader().sizeHint().width() + width += self.verticalScrollBar().sizeHint().width() + width += self.frameWidth() * 2 + return QSize(width + 12, self.height()) def data(self): rows = list(range(self.rowCount())) @@ -85,7 +69,7 @@ def data(self): if self.horizontalHeadersSet: row = [] if self.verticalHeadersSet: - row.append('') + row.append("") for c in columns: row.append(self.horizontalHeaderItem(c).text()) @@ -104,62 +88,3 @@ def data(self): data.append(row) return data - - def compare_data(self): - original_data = self.original_data - - new_data = self.data() - - indices_to_modify = [] - params_to_modify = [] - data_to_modify = {} - - for idx, (row_original, row_new) in enumerate(zip(original_data, new_data)): - if row_original != row_new: - indices_to_modify.append(idx) - - for idx in indices_to_modify: - if new_data[idx][0] == "" and new_data[idx - 1][0] not in params_to_modify: - params_to_modify.append(new_data[idx - 1][0]) - else: - params_to_modify.append(new_data[idx][0]) - if idx + 1 < len(new_data): - if new_data[idx + 1][0] == "" and new_data[idx + 1][0] not in params_to_modify: - params_to_modify.append(new_data[idx + 1][0]) - - for idx in indices_to_modify: - nd = new_data[idx] - # Treat the positional parameters (PosParam) differently than the regular parameters (Param) - if nd[0] is None: - # Identified that this row in the table corresponds to the "y" value of a PosParam - ndy = nd - ndx = new_data[idx - 1] - elif (idx + 1 < len(new_data)) and new_data[idx + 1][0] is None: - # Identified that the next row in the table corresponds to the "y" value of a PosParam - ndx = nd - ndy = new_data[idx + 1] - else: - ndx, ndy = None, None - - if ndx is None and ndy is None: - # For Param - data_to_modify[nd[0]] = { - "value": nd[1], - "bounds": [nd[2], nd[3]], - "active": bool(nd[4]), - "eq": nd[5] - } - else: - # For PosParam - if ndx[5] is None or ndy[5] is None: - eq = None - else: - eq = f"{{{ndx[5]},{ndy[5]}}}" - data_to_modify[ndx[0]] = { - "value": [ndx[1], ndy[1]], - "bounds": [[ndx[2], ndx[3]], [ndy[2], ndy[3]]], - "active": [bool(ndx[4]), bool(ndy[4])], - "eq": eq - } - - return data_to_modify diff --git a/pymead/gui/gui.py b/pymead/gui/gui.py index a9ef50f6..d07d303d 100644 --- a/pymead/gui/gui.py +++ b/pymead/gui/gui.py @@ -204,6 +204,7 @@ def __init__(self, path=None, parent=None): self.setDockNestingEnabled(True) self.status_bar = QStatusBar() + self.status_bar.messageChanged.connect(self.statusChanged) self.setStatusBar(self.status_bar) # self.param_tree_instance = MEAParamTree(self.mea, self.statusBar(), parent=self) # self.design_tree_widget = self.param_tree_instance.t @@ -215,7 +216,7 @@ def __init__(self, path=None, parent=None): # self.main_layout.addWidget(self.dockable_tab_window) # self.dockable_tab_window.add_new_tab_widget(self.w, "Geometry") # self.dockable_tab_window.tab_closed.connect(self.on_tab_closed) - self.geo_col = GeometryCollection() + self.geo_col = GeometryCollection(gui_obj=self) self.airfoil_canvas = AirfoilCanvas(self, geo_col=self.geo_col, gui_obj=self) self.airfoil_canvas.sigStatusBarUpdate.connect(self.setStatusBarText) @@ -720,32 +721,8 @@ def get_grandchild(self, full_param_name: str): return current_param.child(full_param_name) def edit_bounds(self): - jmea_dict = self.mea.copy_as_param_dict(deactivate_airfoil_graphs=True) - bv_dialog = EditBoundsDialog(parent=self, jmea_dict=jmea_dict) - if bv_dialog.exec_(): - data_to_modify = bv_dialog.bv_table.compare_data() - for k, data in data_to_modify.items(): - p = self.get_grandchild(k) - p.airfoil_param.bounds = data["bounds"] - p.setValue(data["value"]) - p.airfoil_param.active = data["active"] - if isinstance(data["value"], list): - p.setReadonly(not data["active"][0]) - else: - p.setReadonly(not data["active"]) - if data["eq"] is not None: - if not p.hasChildren(): - self.param_tree_instance.add_equation_box(p, data["eq"]) - self.param_tree_instance.update_equation(p.child("Equation Definition"), data["eq"]) - else: - self.param_tree_instance.update_equation(p.child("Equation Definition"), data["eq"]) - else: - if not p.hasChildren(): - pass - else: - p.airfoil_param.remove_func() - p.setReadonly(False) - p.child("Equation Definition").remove() + bv_dialog = EditBoundsDialog(geo_col=self.geo_col, theme=self.themes[self.current_theme], parent=self) + bv_dialog.exec_() def auto_range_geometry(self): x_data_range, y_data_range = self.airfoil_canvas.getPointRange() @@ -1046,7 +1023,7 @@ def load_mea_no_dialog(self, file_name): # self.param_tree_instance.t.clear() geo_col_dict = load_data(file_name) self.geo_col = GeometryCollection.set_from_dict_rep(geo_col_dict, canvas=self.airfoil_canvas, - tree=self.parameter_tree) + tree=self.parameter_tree, gui_obj=self) self.permanent_widget.progress_bar.setValue(20) # for idx, airfoil in enumerate(self.mea.airfoils.values()): @@ -1105,6 +1082,15 @@ def output_area_text(self, text: str, mode: str = 'plain', mono: bool = True, li sb = self.text_area.verticalScrollBar() sb.setValue(sb.maximum()) + def showColoredMessage(self, message: str, msecs: int, color: str): + self.status_bar.setStyleSheet(f"color: {color}") + self.permanent_widget.info_label.setStyleSheet(f"color: {self.themes[self.current_theme]['main-color']}") + self.status_bar.showMessage(message, msecs) + + def statusChanged(self, args): + if not args: + self.status_bar.setStyleSheet(f"color: {self.themes[self.current_theme]['main-color']}") + def output_link_text(self, text: str, link: str, line_break: bool = False): previous_cursor = self.text_area.textCursor() self.text_area.moveCursor(QTextCursor.End) diff --git a/pymead/gui/input_dialog.py b/pymead/gui/input_dialog.py index 60fb31a5..a63e7b2f 100644 --- a/pymead/gui/input_dialog.py +++ b/pymead/gui/input_dialog.py @@ -2524,22 +2524,11 @@ def __init__(self, parent=None, window_title: str or None = None, message: str o buttonBox.button(QDialogButtonBox.No).clicked.connect(self.reject) -class EditBoundsDialog(QDialog): - def __init__(self, jmea_dict: dict, parent=None): - super().__init__(parent=parent) - self.setWindowTitle("Edit Bounds") - self.setFont(self.parent().font()) - buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, self) - layout = QFormLayout(self) - - self.bv_table = BoundsValuesTable(jmea_dict=jmea_dict) - - layout.addWidget(self.bv_table) - - layout.addWidget(buttonBox) - - buttonBox.button(QDialogButtonBox.Save).clicked.connect(self.accept) - buttonBox.rejected.connect(self.reject) +class EditBoundsDialog(PymeadDialog): + def __init__(self, geo_col: GeometryCollection, theme: dict, parent=None): + self.bv_table = BoundsValuesTable(geo_col=geo_col) + super().__init__(parent=parent, window_title="Edit Bounds", widget=self.bv_table, theme=theme) + self.resize(self.bv_table.sizeHint()) class OptimizationDialogVTabWidget(PymeadDialogVTabWidget): diff --git a/pymead/tests/core_tests/test_constraints.py b/pymead/tests/core_tests/test_constraints.py index 6f5196a1..2c2e7315 100644 --- a/pymead/tests/core_tests/test_constraints.py +++ b/pymead/tests/core_tests/test_constraints.py @@ -328,3 +328,28 @@ def test_symmetry_constraint(self): plt.plot([p.x().value() for p in points], [p.y().value() for p in points], ls="none", marker="o", mfc="indianred") plt.show() + + def test_complex_case_1(self): + geo_col = GeometryCollection() + p1 = geo_col.add_point(0.0, 0.0) + p2 = geo_col.add_point(1.0, 0.0) + d1 = DistanceConstraint(p1=p1, p2=p2, value=1.0) + geo_col.add_constraint(d1) + p3 = geo_col.add_point(0.3, 0.1) + p4 = geo_col.add_point(0.6, 0.05) + d2 = DistanceConstraint(p1, p3, value=0.25) + d3 = DistanceConstraint(p3, p4, value=0.25) + geo_col.add_constraint(d2) + geo_col.add_constraint(d3) + ap1 = AntiParallel3Constraint(p1, p3, p4) + ap2 = AntiParallel3Constraint(p3, p4, p2) + geo_col.add_constraint(ap1) + geo_col.add_constraint(ap2) + p5 = geo_col.add_point(0.0, 0.1) + p6 = geo_col.add_point(0.0, -0.1) + d4 = DistanceConstraint(p1, p5, value=0.1) + d5 = DistanceConstraint(p1, p6, value=0.1) + geo_col.add_constraint(d4) + geo_col.add_constraint(d5) + ap3 = AntiParallel3Constraint(p6, p1, p5) + geo_col.add_constraint(ap3)