From f0c28675e005e9c06fb3a6de3de53fe6652a6fdc Mon Sep 17 00:00:00 2001 From: ropercha Date: Fri, 30 Aug 2024 16:42:57 +0200 Subject: [PATCH 01/22] New function collapse. Some issues remain --- actions/triangular_actions.py | 92 ++++++++++++++++++---- mesh_display.py | 14 ++-- model/mesh_analysis.py | 77 ++++++++++++++----- model/mesh_struct/mesh.py | 115 ++++++++++++++++++++++++---- model/mesh_struct/mesh_elements.py | 10 ++- model/random_trimesh.py | 12 +-- plots/mesh_plotter.py | 4 +- test_modules/test_actions.py | 110 ++++++++++++++++++++++++++ test_modules/test_mesh_structure.py | 54 ------------- test_modules/test_random_trimesh.py | 9 ++- user_game.py | 2 +- view/graph.py | 13 ++-- view/window.py | 7 +- 13 files changed, 392 insertions(+), 127 deletions(-) create mode 100644 test_modules/test_actions.py diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 1c458e7..837b3f2 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,9 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk +from model.mesh_analysis import degree, isFlipOk, isCollapseOk + +import numpy as np def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -15,7 +17,7 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isFlipOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = active_triangles(mesh, d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) test_degree(n3) test_degree(n4) @@ -57,7 +59,7 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found: return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = active_triangles(mesh, d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) test_degree(n3) test_degree(n4) @@ -86,24 +88,82 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: return True -def active_triangles(mesh: Mesh, d: Dart) -> tuple[Dart, Dart, Dart, Dart, Dart, Node, Node, Node, Node]: - """ - Return the darts and nodes around selected dart - :param mesh: the mesh - :param d: selected dart - :return: a tuple of darts and nodes - """ +def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: + return collapse_edge(mesh, Node(mesh, id1), Node(mesh, id2)) + + +def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: + found, d = mesh.find_inner_edge(n1, n2) + if not found or not isCollapseOk(d): + return False + + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + #T1 + d212 = d21.get_beta(2) + + #T2 + d2112 = d211.get_beta(2) + + #T3 + d12 = d1.get_beta(2) + + #T4 + d112 = d11.get_beta(2) + + #Delete the darts around selected dart + delete_triangles(mesh, d) + + #move n1 node in the middle of [n1, n2] + n1.set_xy((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) + + #update node relations + if d12 is not None: + d121 = d12.get_beta(1) + d121.set_node(n1) + ds = d121 + while ds is not None and ds != d2112: + d2s = ds.get_beta(2) + if d2s is None: + ds = d2112 + while ds is not None: + ds.set_node(n1) + ds1 = ds.get_beta(1) + ds11 = ds1.get_beta(1) + ds = ds11.get_beta(2) + else: + ds = d2s.get_beta(1) + ds.set_node(n1) + + #update beta2 relations + if d112 is not None: + d112.set_beta(2, d12) + if d12 is not None: + d12.set_beta(2, d112) + + if d212 is not None: + d212.set_beta(2, d2112) + if d2112 is not None: + d2112.set_beta(2, d212) + + #delete n2 node + mesh.del_node(n2) + + return True + +def delete_triangles(mesh: Mesh, d: Dart) -> None: d2 = d.get_beta(2) d1 = d.get_beta(1) d11 = d1.get_beta(1) d21 = d2.get_beta(1) d211 = d21.get_beta(1) - n1 = d.get_node() - n2 = d2.get_node() - n3 = d11.get_node() - n4 = d211.get_node() - - return d2, d1, d11, d21, d211, n1, n2, n3, n4 + + f1 = d.get_face() + f2 = d2.get_face() + + mesh.del_triangle(d, d1, d11, f1) + mesh.del_triangle(d2, d21, d211, f2) + def test_degree(n: Node) -> bool: diff --git a/mesh_display.py b/mesh_display.py index 1e09b67..d15247d 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -12,8 +12,9 @@ def get_nodes_coordinates(self): :return: a list of coordinates (x,y) """ node_list = [] - for n in self.mesh.nodes: - node_list.append((n[0], n[1])) + for idx, n in enumerate(self.mesh.nodes): + if n[2] >= 0 : + node_list.append((idx, n[0], n[1])) return node_list def get_edges(self): @@ -22,9 +23,9 @@ def get_edges(self): :return: a list of coordinates (x,y) """ edge_list = [] - for d in self.mesh.dart_info: + for d in self.mesh.active_darts(): n1_id = d[3] - n2_id = self.mesh.dart_info[d[1],3] + n2_id = self.mesh.dart_info[d[1], 3] if (d[2] != -1 and n1_id < n2_id) or d[2] == -1: edge_list.append((n1_id, n2_id)) return edge_list @@ -34,5 +35,6 @@ def get_scores(self): Calculates the irregularities of each node and the real and ideal score of the mesh :return: a list of three elements (nodes_score, mesh_score, ideal_mesh_score) """ - scores = global_score(self.mesh) - return scores + nodes_score, mesh_score, ideal_mesh_score = global_score(self.mesh) + nodes_score = [score for score in nodes_score if score is not None] + return [nodes_score, mesh_score, ideal_mesh_score] diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 2120603..c618a14 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -16,12 +16,14 @@ def global_score(m: Mesh) -> (int, int): mesh_score = 0 nodes_score = [] for i in range(len(m.nodes)): - n_id = i - node = Node(m, n_id) - n_score = score_calculation(node) - nodes_score.append(n_score) - mesh_ideal_score += n_score - mesh_score += abs(n_score) + if m.nodes[i, 2] >= 0: + n_id = i + node = Node(m, n_id) + n_score = score_calculation(node) + nodes_score.append(n_score) + mesh_ideal_score += n_score + mesh_score += abs(n_score) + nodes_score.append(None) return nodes_score, mesh_score, mesh_ideal_score @@ -82,7 +84,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) > 3: + if len(boundary_darts) >= 4: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -109,7 +111,7 @@ def adjacent_darts(n: Node) -> list[Dart]: :return: the list of adjacent darts """ adj_darts = [] - for d_info in n.mesh.dart_info: + for d_info in n.mesh.active_darts(): d = Dart(n.mesh, d_info[0]) d_nfrom = d.get_node() d_nto = d.get_beta(1) @@ -138,6 +140,8 @@ def degree(n: Node) -> int: else: adjacency += 0.5 if adjacency != int(adjacency): + print(adjacency) + print(n.id) raise ValueError("Adjacency error") return adjacency @@ -149,7 +153,7 @@ def get_boundary_darts(m: Mesh) -> list[Dart]: :return: a list of all boundary darts """ boundary_darts = [] - for d_info in m.dart_info: + for d_info in m.active_darts(): d = Dart(m, d_info[0]) d_twin = d.get_beta(2) if d_twin is None : @@ -164,11 +168,11 @@ def get_boundary_nodes(m: Mesh) -> list[Node]: :return: a list of all boundary nodes """ boundary_nodes = [] - nb_nodes = len(m.nodes) - for n_id in range(0, nb_nodes): - n = Node(m, n_id) - if on_boundary(n): - boundary_nodes.append(n) + for n_id in range(0, len(m.nodes)): + if m.nodes[n_id, 2] >= 0: + n = Node(m, n_id) + if on_boundary(n): + boundary_nodes.append(n) return boundary_nodes @@ -220,16 +224,26 @@ def node_in_mesh(mesh: Mesh, x: float, y: float) -> (bool, int): """ n_id = 0 for n in mesh.nodes: - if abs(x - n[0]) <= 0.1 and abs(y - n[1]) <= 0.1: - return True, n_id - n_id = n_id + 1 + if n[2] >= 0 : + if abs(x - n[0]) <= 0.1 and abs(y - n[1]) <= 0.1: + return True, n_id + n_id += 1 return False, None -def isValidAction(mesh, dart_id: int) -> bool: +def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: + flip = 0 + split = 1 + collapse = 2 d = Dart(mesh, dart_id) boundary_darts = get_boundary_darts(mesh) - if d in boundary_darts or not isFlipOk(d): + if d in boundary_darts: + return False + elif action == flip and isFlipOk(d) is not True: + return False + elif action == split and isFlipOk(d) is not True: + return False + elif action == collapse and isCollapseOk(d) is not True: return False else: return True @@ -245,7 +259,7 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: return deg -def isFlipOk(d:Dart) -> bool: +def isFlipOk(d: Dart) -> bool: d1 = d.get_beta(1) d11 = d1.get_beta(1) A = d.get_node() @@ -267,3 +281,26 @@ def isFlipOk(d:Dart) -> bool: return False else: return True + + +def isCollapseOk(d: Dart) -> bool: + mesh = d.mesh + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + if d112 is None and d12 is None: + return False + elif d212 is None and d2112 is None: + return False + elif d212 is None and d12 is None: + return False + elif d112 is None and d2112 is None: + return False + else: + return True + diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index b54a444..d8225a3 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -20,6 +20,9 @@ def __init__(self, nodes=[], faces=[]): self.nodes = numpy.empty((0, 3)) self.faces = numpy.empty(0, dtype=int) self.dart_info = numpy.empty((0, 5), dtype=int) + self.first_free_dart = 1 + self.first_free_node = 1 + self.first_free_face = 1 for n in nodes: self.add_node(n[0], n[1]) @@ -46,14 +49,14 @@ def nb_nodes(self) -> int: :return: the number of vertices in the mesh """ # We filter the vertices having the x-coordinate equals to max float. Such vertices were removed - return len(self.nodes[self.nodes[:, 0] != sys.float_info.max]) + return len(self.active_nodes()) def nb_faces(self) -> int: """ :return: the number of faces in the mesh """ # We filter the faces having the -1 value. An item with this value is a deleted face - return len(self.faces[self.faces[:] != -1]) + return len(self.active_faces()) def add_node(self, x: float, y: float) -> Node: """ @@ -62,10 +65,28 @@ def add_node(self, x: float, y: float) -> Node: :param y: Y coordinate :return: the created node """ - self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) - return Node(self, len(self.nodes) - 1) - - def del_vertex(self, ni: int) -> None: + if len(self.nodes) < self.first_free_node: + self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) + self.first_free_node += 1 + return Node(self, len(self.nodes) - 1) + elif self.first_free_node >= 0: + n_id = int(self.first_free_node) + if isinstance(n_id, int): + self.first_free_node = abs(self.nodes[n_id, 2] + 1 ) + self.nodes[n_id] = [x, y, -1] + else: + print(n_id) + print(type(n_id)) + raise ValueError("n_id not integer") + return Node(self, n_id) + else: + raise ValueError("Try to add a node outside the array") + + def del_node(self, n: Node) -> None: + self.nodes[n.id, 2] = -self.first_free_node - 1 + self.first_free_node = n.id + + def del_vertex(self, ni: Node) -> None: """ Removes the node ni. Warning all the darts that point to this node will be invalid (but not automatically updated) @@ -96,14 +117,32 @@ def add_triangle(self, n1: Node, n2: Node, n3: Node) -> Face: darts[k].set_node(nodes[k]) nodes[k].set_dart(darts[k]) - self.faces = numpy.append(self.faces, [darts[0].id]) - tri = Face(self, len(self.faces)-1) + if len(self.faces) < self.first_free_face: + self.faces = numpy.append(self.faces, [darts[0].id]) + self.first_free_face += 1 + tri = Face(self, len(self.faces) - 1) + elif self.first_free_face >= 0: + f_id = self.first_free_face + self.first_free_face = abs(self.faces[f_id]+1) + self.faces[f_id] = darts[0].id + tri = Face(self, f_id) + else: + raise ValueError("Try to add a node outside the array") for d in darts: d.set_face(tri) return tri + def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: + self.del_dart(d1) + self.del_dart(d2) + self.del_dart(d3) + + self.faces[f.id] = -self.first_free_face -1 + self.first_free_face = f.id + + def add_quad(self, n1: Node, n2: Node, n3: Node, n4: Node) -> Face: """ Add a quad defined by nodes of indices n1, n2, n3 and n4. @@ -141,7 +180,7 @@ def set_twin_pointers(self) -> None: """ This function search for the inner darts to connect and connect them with beta2. """ - for d_info in self.dart_info: + for d_info in self.active_darts(): d = Dart(self, d_info[0]) if d.get_beta(2) is None: # d is not 2-sew, we look for a dart to connect. If we don'f find one, @@ -150,7 +189,7 @@ def set_twin_pointers(self) -> None: d_nfrom = d.get_node() d_nto = d.get_beta(1).get_node() - for d2_info in self.dart_info: + for d2_info in self.active_darts(): d2 = Dart(self, d2_info[0]) if d2.get_beta(2) is None: d2_nfrom = d2.get_node() @@ -168,7 +207,7 @@ def find_inner_edge(self, n1: Node, n2: Node) -> (bool, Dart): :param n2: Second node :return: the inner dart connecting n1 to n2 if it exists """ - for d_info in self.dart_info: + for d_info in self.active_darts(): d = Dart(self, d_info[0]) d2 = d.get_beta(2) if d2 is not None: @@ -208,8 +247,27 @@ def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1) -> Dart :param v: vertex index this dart point to :return: the created dart """ - self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) - return Dart(self, len(self.dart_info) - 1) + if len(self.dart_info) < self.first_free_dart: + self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) + self.first_free_dart += 1 + return Dart(self, len(self.dart_info) - 1) + elif len(self.dart_info) > self.first_free_dart: + next_free_dart = abs(self.dart_info[self.first_free_dart][0]+1) + dart_id = self.first_free_dart + self.dart_info[dart_id][0] = dart_id + self.dart_info[dart_id][1] = a1 + self.dart_info[dart_id][2] = a2 + self.dart_info[dart_id][3] = v + self.dart_info[dart_id][4] = f + self.first_free_dart = next_free_dart + return Dart(self, dart_id) + else: + raise IndexError('Dart index out of range') + + def del_dart(self, d: Dart): + self.dart_info[d.id][0] = -self.first_free_dart - 1 + self.first_free_dart = d.id + def set_beta2(self, dart: Dart) -> None: """ @@ -218,7 +276,7 @@ def set_beta2(self, dart: Dart) -> None: """ dart_nfrom = dart.get_node() dart_nto = dart.get_beta(1) - for d_info in dart.mesh.dart_info: + for d_info in self.active_darts(): d = Dart(dart.mesh, d_info[0]) d_nfrom = d.get_node() d_nto = d.get_beta(1) @@ -226,3 +284,32 @@ def set_beta2(self, dart: Dart) -> None: d.set_beta(2, dart) dart.set_beta(2, d) + def active_nodes(self): + return self.nodes[self.nodes[:, 2] >= 0] + + def active_darts(self): + return self.dart_info[self.dart_info[:, 0] >= 0] + + def active_faces(self): + return self.faces[self.faces[:] >= 0] + + def active_triangles(self, d: Dart) -> tuple[Dart, Dart, Dart, Dart, Dart, Node, Node, Node, Node]: + """ + Return the darts and nodes around selected dart + :param mesh: the mesh + :param d: selected dart + :return: a tuple of darts and nodes + """ + d2 = d.get_beta(2) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d21 = d2.get_beta(1) + d211 = d21.get_beta(1) + n1 = d.get_node() + n2 = d2.get_node() + n3 = d11.get_node() + n4 = d211.get_node() + + return d2, d1, d11, d21, d211, n1, n2, n3, n4 + + diff --git a/model/mesh_struct/mesh_elements.py b/model/mesh_struct/mesh_elements.py index 4b02e71..4e857c1 100644 --- a/model/mesh_struct/mesh_elements.py +++ b/model/mesh_struct/mesh_elements.py @@ -27,7 +27,10 @@ def __eq__(self, a_dart: Dart) -> bool: :param a_dart: another dart :return: true if the darts are equal, false otherwise """ - return self.mesh == a_dart.mesh and self.id == a_dart.id + if a_dart is None: + return False + else: + return self.mesh == a_dart.mesh and self.id == a_dart.id def get_beta(self, i: int) -> Dart: """ @@ -52,7 +55,10 @@ def set_beta(self, i: int, dart_to: Dart) -> None: """ if i < 1 or i > 2: raise ValueError("Wrong alpha dimension") - self.mesh.dart_info[self.id, i] = dart_to.id + elif dart_to is None: + self.mesh.dart_info[self.id, i] = -1 + else: + self.mesh.dart_info[self.id, i] = dart_to.id def get_node(self) -> Node: """ diff --git a/model/random_trimesh.py b/model/random_trimesh.py index aaddac6..6e322f9 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -4,7 +4,7 @@ from model.mesh_struct.mesh_elements import Dart, Node from model.mesh_struct.mesh import Mesh from model.mesh_analysis import find_opposite_node, node_in_mesh -from actions.triangular_actions import flip_edge_ids, split_edge_ids +from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids def regular_mesh(num_nodes_max: int) -> Mesh: @@ -91,16 +91,18 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_flip = len(mesh.dart_info) - nb_action =nb_flip * 2 + nb_action = len(mesh.dart_info) nb_nodes = len(mesh.nodes) for i in range(nb_action): + action = np.random.randint(1, 4) i1 = np.random.randint(nb_nodes) i2 = np.random.randint(nb_nodes) - if i1 != i2 and i%2 == 0: + if action == 1: flip_edge_ids(mesh, i1, i2) - elif i1 !=i2 : + elif action == 2: split_edge_ids(mesh, i1, i2) + elif action == 3: + collapse_edge_ids(mesh, i1, i2) return mesh diff --git a/plots/mesh_plotter.py b/plots/mesh_plotter.py index 6bad764..8dc9352 100644 --- a/plots/mesh_plotter.py +++ b/plots/mesh_plotter.py @@ -19,8 +19,8 @@ def subplot_mesh(mesh: Mesh) -> None: Plot a mesh using matplotlib for subplots with many meshes :param mesh: a Mesh """ - faces = mesh.faces - nodes = mesh.nodes + faces = mesh.active_faces() + nodes = mesh.active_nodes() nodes = np.array([list[:2] for list in nodes]) for dart_id in faces: diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py new file mode 100644 index 0000000..72501ea --- /dev/null +++ b/test_modules/test_actions.py @@ -0,0 +1,110 @@ +import unittest +import model.mesh_struct.mesh as mesh +from model.mesh_struct.mesh_elements import Dart, Node, Face +import numpy.testing +from plots.mesh_plotter import plot_mesh + +from actions.triangular_actions import split_edge, flip_edge, collapse_edge + +class TestActions(unittest.TestCase): + + def test_flip(self): + cmap = mesh.Mesh() + n00 = cmap.add_node(0, 0) + n01 = cmap.add_node(0, 1) + n10 = cmap.add_node(1, 0) + n11 = cmap.add_node(1, 1) + + t1 = cmap.add_triangle(n00, n10, n11) + t2 = cmap.add_triangle(n00, n11, n01) + + d1 = t1.get_dart() + # d1 goes from n00 to n10 + self.assertEqual(d1.get_node(), n00) + d1 = d1.get_beta(1).get_beta(1) + # now d1 goes from n11 to n00 + self.assertEqual(d1.get_node(), n11) + + d2 = t2.get_dart() # goes from n00 to n11 + self.assertEqual(d2.get_node(), n00) + # We sew on both directions + d1.set_beta(2, d2) + d2.set_beta(2, d1) + + flip_edge(cmap, n00, n11) + self.assertEqual(2, cmap.nb_faces()) + self.assertEqual(4, cmap.nb_nodes()) + + def test_split(self): + cmap = mesh.Mesh() + n00 = cmap.add_node(0, 0) + n01 = cmap.add_node(0, 1) + n10 = cmap.add_node(1, 0) + n11 = cmap.add_node(1, 1) + + t1 = cmap.add_triangle(n00, n10, n11) + t2 = cmap.add_triangle(n00, n11, n01) + + split_edge(cmap, n00, n11) + d1 = t1.get_dart() + # d1 goes from n00 to n10 + self.assertEqual(d1.get_node(), n00) + d1 = d1.get_beta(1).get_beta(1) + # now d1 goes from n11 to n00 + self.assertEqual(d1.get_node(), n11) + + d2 = t2.get_dart() # goes from n00 to n11 + self.assertEqual(d2.get_node(), n00) + # We sew on both directions + d1.set_beta(2, d2) + d2.set_beta(2, d1) + + def test_collapse(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] + faces = [[0, 1, 2], [0, 2, 3]] + cmap = mesh.Mesh(nodes, faces) + n00 = Node(cmap, 0) + n11 = Node(cmap, 2) + split_edge(cmap, n00, n11) + n5 = Node(cmap, 4) + plot_mesh(cmap) + collapse_edge(cmap, n00, n5) + d1_to_test = Dart(cmap, 7) + d2_to_test = Dart(cmap, 0) + self.assertEqual(d1_to_test.get_beta(2), None) + self.assertEqual(d2_to_test.get_beta(2), None) + plot_mesh(cmap) + + def test_split_collapse_split(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] + faces = [[0, 1, 2], [0, 2, 3]] + cmap = mesh.Mesh(nodes, faces) + n0 = Node(cmap, 0) + n1 = Node(cmap, 1) + n2 = Node(cmap, 2) + n3 = Node(cmap, 3) + split_edge(cmap, n0, n2) + n4 = Node(cmap, 4) + collapse_edge(cmap, n0, n4) + split_edge(cmap, n0, n2) + n5 = Node(cmap, 5) + collapse_edge(cmap, n0, n5) + split_edge(cmap, n4, n2) + collapse_edge(cmap, n4, n5) + collapse_edge(cmap, n2, n4) + split_edge(cmap, n0, n2) + split_edge(cmap, n0, n4) + split_edge(cmap, n4, n3) + split_edge(cmap, n4, n1) + split_edge(cmap, n5, n1) + plot_mesh(cmap) + n7 = Node(cmap, 7) + n8 = Node(cmap, 8) + collapse_edge(cmap, n7, n8) + plot_mesh(cmap) + collapse_edge(cmap, n5, n7) + plot_mesh(cmap) + + + + diff --git a/test_modules/test_mesh_structure.py b/test_modules/test_mesh_structure.py index 2d49484..6828fe0 100644 --- a/test_modules/test_mesh_structure.py +++ b/test_modules/test_mesh_structure.py @@ -2,8 +2,6 @@ import model.mesh_struct.mesh as mesh import numpy.testing -from actions.triangular_actions import split_edge, flip_edge - class TestMeshStructure(unittest.TestCase): @@ -85,58 +83,6 @@ def test_single_quad(self): self.assertEqual(n3, nodes_of_t[2]) self.assertEqual(n4, nodes_of_t[3]) - def test_flip(self): - cmap = mesh.Mesh() - n00 = cmap.add_node(0, 0) - n01 = cmap.add_node(0, 1) - n10 = cmap.add_node(1, 0) - n11 = cmap.add_node(1, 1) - - t1 = cmap.add_triangle(n00, n10, n11) - t2 = cmap.add_triangle(n00, n11, n01) - - d1 = t1.get_dart() - # d1 goes from n00 to n10 - self.assertEqual(d1.get_node(), n00) - d1 = d1.get_beta(1).get_beta(1) - # now d1 goes from n11 to n00 - self.assertEqual(d1.get_node(), n11) - - d2 = t2.get_dart() # goes from n00 to n11 - self.assertEqual(d2.get_node(), n00) - # We sew on both directions - d1.set_beta(2, d2) - d2.set_beta(2, d1) - - flip_edge(cmap, n00, n11) - self.assertEqual(2, cmap.nb_faces()) - self.assertEqual(4, cmap.nb_nodes()) - - def test_split(self): - cmap = mesh.Mesh() - n00 = cmap.add_node(0, 0) - n01 = cmap.add_node(0, 1) - n10 = cmap.add_node(1, 0) - n11 = cmap.add_node(1, 1) - - t1 = cmap.add_triangle(n00, n10, n11) - t2 = cmap.add_triangle(n00, n11, n01) - - d1 = t1.get_dart() - # d1 goes from n00 to n10 - self.assertEqual(d1.get_node(), n00) - d1 = d1.get_beta(1).get_beta(1) - # now d1 goes from n11 to n00 - self.assertEqual(d1.get_node(), n11) - - d2 = t2.get_dart() # goes from n00 to n11 - self.assertEqual(d2.get_node(), n00) - # We sew on both directions - d1.set_beta(2, d2) - d2.set_beta(2, d1) - - split_edge(cmap, n00, n11) - if __name__ == '__main__': unittest.main() diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 09d249e..e7cf2b4 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -1,7 +1,9 @@ import unittest -from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh +from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle from model.mesh_struct.mesh import Mesh +from plots.mesh_plotter import plot_mesh + class TestRandomTrimesh(unittest.TestCase): @@ -29,6 +31,11 @@ def test_random_flip_mesh(self): m = random_flip_mesh(60) self.assertEqual(m.nb_nodes(), 60) + def test_mesh_suffle(self): + m = regular_mesh(30) + mesh = mesh_shuffle(m) + plot_mesh(mesh) + if __name__ == '__main__': diff --git a/user_game.py b/user_game.py index 470b51d..558e628 100644 --- a/user_game.py +++ b/user_game.py @@ -18,7 +18,7 @@ def user_game(mesh_size): g = Game(cmap, mesh_disp) g.run() """ - cmap = TM.random_mesh(mesh_size) + cmap = TM.regular_mesh(mesh_size) mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() diff --git a/view/graph.py b/view/graph.py index 4c9b0e2..e2eff08 100644 --- a/view/graph.py +++ b/view/graph.py @@ -18,7 +18,7 @@ def __init__(self, idx, x, y, value=0): self.selected = False self.color = vertex_color_normal self.obj = None - self.value = round(value,0) + self.value = round(value, 0) def switch_selection(self): if self.selected: @@ -111,9 +111,9 @@ def clear(self): def update(self, vertices, edges, scores): for idx, n in enumerate(vertices): - nodes_scores= scores[0] + nodes_scores = scores[0] n_value = nodes_scores[idx] - self.create_vertex(idx, n[0], n[1], n_value) + self.create_vertex(n[0], n[1], n[2], n_value) for e in edges: self.create_edge(e[0], e[1]) @@ -123,8 +123,11 @@ def create_vertex(self, id: int, x: int, y: int, n_value) -> int: return len(self.vertices) - 1 def create_edge(self, i1: int, i2: int) -> int: - n1 = self.vertices[i1] - n2 = self.vertices[i2] + for v in self.vertices: + if v.idx == i1: + n1 = v + elif v.idx == i2: + n2 = v self.add_edge(Edge(n1, n2)) return len(self.edges) - 1 diff --git a/view/window.py b/view/window.py index 9acea1a..93c4dbf 100644 --- a/view/window.py +++ b/view/window.py @@ -3,7 +3,7 @@ from pygame.locals import * from view import graph from mesh_display import MeshDisplay -from actions.triangular_actions import split_edge_ids, flip_edge_ids +from actions.triangular_actions import split_edge_ids, flip_edge_ids, collapse_edge_ids import sys color1 = pygame.Color(30, 30, 30) # Dark Grey @@ -97,6 +97,11 @@ def control_events(self): self.graph.clear() self.graph.update(self.mesh_disp.get_nodes_coordinates(),self.mesh_disp.get_edges(), self.mesh_disp.get_scores()) already_selected = True + elif pygame.key.get_pressed()[pygame.K_c]: + if collapse_edge_ids(self.model, e.start.idx, e.end.idx): + self.graph.clear() + self.graph.update(self.mesh_disp.get_nodes_coordinates(),self.mesh_disp.get_edges(), self.mesh_disp.get_scores()) + already_selected = True def run(self): print("TriGame is starting!!") From e607687d5e648a45da4529f538e32438be5c9e88 Mon Sep 17 00:00:00 2001 From: ropercha Date: Tue, 3 Sep 2024 09:54:39 +0200 Subject: [PATCH 02/22] Clipping the cosinus of the theta angle to ensure it remains within valid bounds with rounding errors (boundary angle calculation). Incorporation of the PR changes to the gmsh reader --- environment/trimesh_env.py | 10 +++++----- main.py | 9 ++++++++- model/mesh_analysis.py | 29 ++++++++++++++++++++++++++--- model/random_trimesh.py | 4 ++-- model/reader.py | 2 ++ test_modules/test_random_trimesh.py | 2 +- train.py | 6 +++--- 7 files changed, 47 insertions(+), 15 deletions(-) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 412ae2d..2e2286f 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -4,7 +4,7 @@ from model.mesh_struct.mesh_elements import Dart from model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge -from model.random_trimesh import random_flip_mesh +from model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 @@ -14,7 +14,7 @@ class TriMesh: def __init__(self, mesh=None, mesh_size: int = None, max_steps: int = 50, feat: int = 0): self.mesh = mesh if mesh is not None else random_flip_mesh(mesh_size) - self.mesh_size = len(self.mesh.nodes) + self.mesh_size = len(self.mesh.active_nodes()) self.size = len(self.mesh.dart_info) self.actions = np.array([FLIP]) self.reward = 0 @@ -30,7 +30,7 @@ def reset(self, mesh=None): self.reward = 0 self.steps = 0 self.terminal = False - self.mesh = mesh if mesh is not None else random_flip_mesh(self.mesh_size) + self.mesh = mesh if mesh is not None else random_mesh(self.mesh_size) self.size = len(self.mesh.dart_info) self.nodes_scores = global_score(self.mesh)[0] self.ideal_score = global_score(self.mesh)[2] @@ -75,10 +75,10 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: """ mesh = state nodes_scores = global_score(mesh)[0] - size = len(mesh.dart_info) + size = len(mesh.active_darts()) template = np.zeros((size, 6)) - for d_info in mesh.dart_info: + for d_info in mesh.active_darts(): d = Dart(mesh, d_info[0]) A = d.get_node() diff --git a/main.py b/main.py index 21b2812..936120e 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,9 @@ from user_game import user_game from train import train +from model.reader import read_gmsh +from view.window import Game +from mesh_display import MeshDisplay # Press the green button in the gutter to run the script. @@ -10,4 +13,8 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - train() + #train() + cmap = read_gmsh("mesh_files/irr_losange.msh") + mesh_disp = MeshDisplay(cmap) + g = Game(cmap, mesh_disp) + g.run() diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index c618a14..bf491b3 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -68,7 +68,9 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: vect_AC = (C.x() - A.x(), C.y() - A.y()) dist_AB = sqrt(vect_AB[0]**2 + vect_AB[1]**2) dist_AC = sqrt(vect_AC[0]**2 + vect_AC[1]**2) - angle = np.arccos(np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC)) + cos_theta = np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC) + cos_theta = np.clip(cos_theta, -1, 1) + angle = np.arccos(cos_theta) return degrees(angle) @@ -84,7 +86,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) >= 4: + if len(boundary_darts) > 10: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -197,7 +199,8 @@ def find_opposite_node(d: Dart) -> (int, int): return x_C, y_C -def find_template_opposite_node(d: Dart) -> (int): + +def find_template_opposite_node(d: Dart) -> int: """ Find the the vertex opposite in the adjacent triangle :param d: a dart @@ -293,6 +296,24 @@ def isCollapseOk(d: Dart) -> bool: d212 = d21.get_beta(2) d2112 = d211.get_beta(2) + if d112 is None and d12 is None and d2112 is None and d212 is None: + return False + elif d112 is not None and d212 is not None: + if d12 is None and d2112 is None: + return True + elif d12 is not None: + ds = d12.get_beta(1) + while ds != d2112: + ds2 = ds.get_beta(2) + if ds2 is None: + return False + ds = ds2.get_beta(1) + return True + else: + return False + + """ + #Old condition if d112 is None and d12 is None: return False elif d212 is None and d2112 is None: @@ -303,4 +324,6 @@ def isCollapseOk(d: Dart) -> bool: return False else: return True + + """ diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 6e322f9..95fe3b9 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -66,7 +66,7 @@ def random_mesh(num_nodes_max: int) -> Mesh: :return: a random mesh """ mesh = regular_mesh(num_nodes_max) - mesh_shuffle_flip(mesh) + mesh_shuffle(mesh) return mesh @@ -91,7 +91,7 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action = len(mesh.dart_info) + nb_action = 2*len(mesh.dart_info) nb_nodes = len(mesh.nodes) for i in range(nb_action): action = np.random.randint(1, 4) diff --git a/model/reader.py b/model/reader.py index 97c4341..d1d4e20 100644 --- a/model/reader.py +++ b/model/reader.py @@ -103,6 +103,8 @@ def read_gmsh(filename: string) -> Mesh: faces.append([n0 - 1, n1 - 1, n2 - 1, n3 - 1]) elif elem_type == 1: # skip 2-node line elements continue + elif elem_type == 15: # skip 1-node point elements + continue else: print("element_type " + str(elem_type) + " not handled") exit(1) diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index e7cf2b4..0f6bca8 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -32,7 +32,7 @@ def test_random_flip_mesh(self): self.assertEqual(m.nb_nodes(), 60) def test_mesh_suffle(self): - m = regular_mesh(30) + m = regular_mesh(40) mesh = mesh_shuffle(m) plot_mesh(mesh) diff --git a/train.py b/train.py index ae7c0ae..5ffb301 100644 --- a/train.py +++ b/train.py @@ -20,7 +20,7 @@ def train(): gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_flip_mesh(30) for _ in range(16)] + dataset = [TM.random_mesh(30) for _ in range(16)] plot_dataset(dataset) env = TriMesh(None, mesh_size, max_steps=30, feat=feature) @@ -30,7 +30,7 @@ def train(): # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=100, nb_epochs=1, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=50, nb_epochs=1, batch_size=8) actor, rewards, wins, steps = model.train() avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 10, dataset, 60) @@ -38,4 +38,4 @@ def train(): if rewards is not None: plot_training_results(rewards, wins, steps) plot_test_results(avg_rewards, avg_wins, avg_steps) - plot_dataset(final_meshes) \ No newline at end of file + plot_dataset(final_meshes) From d4e2ce45099345b172ba861b4bf802719ace10b9 Mon Sep 17 00:00:00 2001 From: ropercha Date: Sat, 7 Sep 2024 01:18:33 +0200 Subject: [PATCH 03/22] Working environment with collapse action --- actions/triangular_actions.py | 85 +++++++++- environment/trimesh_env.py | 92 +++++----- main.py | 4 +- mesh_display.py | 9 +- model/mesh_analysis.py | 176 ++++++++++++++++++-- model/mesh_struct/mesh.py | 17 +- model/mesh_struct/mesh_elements.py | 11 +- model/random_trimesh.py | 35 +++- model_RL/PPO_model.py | 27 +-- model_RL/evaluate_model.py | 12 +- model_RL/utilities/actor_critic_networks.py | 32 +++- test_modules/test_mesh_analysis.py | 5 +- test_modules/test_random_trimesh.py | 11 +- train.py | 14 +- view/graph.py | 4 +- 15 files changed, 413 insertions(+), 121 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 837b3f2..37dd919 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, isCollapseOk +from model.mesh_analysis import degree, isFlipOk, newIsCollapseOk, adjacent_darts import numpy as np @@ -60,8 +60,11 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - test_degree(n3) - test_degree(n4) + + if not test_degree(n3): + return False + elif not test_degree(n4): + return False # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) @@ -94,7 +97,12 @@ def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not isCollapseOk(d): + + if not found or not newIsCollapseOk(d): + return False + elif not test_degree(n1): + return False + elif not test_boundary(n1, n2): return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -134,6 +142,15 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: else: ds = d2s.get_beta(1) ds.set_node(n1) + elif d12 is None and d2112 is not None: + d2112.set_node(n1) + ds = (d2112.get_beta(1)).get_beta(1) + ds2 = ds.get_beta(2) + while ds2 is not None: + ds2.set_node(n1) + ds = (ds2.get_beta(1)).get_beta(1) + ds2 = ds.get_beta(2) + #update beta2 relations if d112 is not None: @@ -148,7 +165,10 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) - + if not check_double(mesh): + raise ValueError("double error") + if not check_beta2_relation(mesh): + raise ValueError("error beta2") return True def delete_triangles(mesh: Mesh, d: Dart) -> None: @@ -175,4 +195,57 @@ def test_degree(n: Node) -> bool: if degree(n) > 10: return False else: - return True \ No newline at end of file + return True + + +def test_boundary(n1: Node, n2: Node) -> bool: + boundary_darts_n1 = [] + boundary_darts_n2 = [] + for d in adjacent_darts(n1): + if d.get_beta(2) is None: + boundary_darts_n1.append(d) + for d in adjacent_darts(n2): + if d.get_beta(2) is None: + boundary_darts_n2.append(d) + if (len(boundary_darts_n1) + len(boundary_darts_n2)) > 3: + return False + else: + return True + +def check_beta2_relation(mesh: Mesh) -> bool: + for dart_info in mesh.active_darts(): + d = dart_info[0] + d2 = dart_info[2] + if d2 >= 0 and mesh.dart_info[d2, 0] < 0: + return False + elif d2 >= 0 and mesh.dart_info[d2, 2] != d: + return False + return True + + +def check_double(mesh: Mesh) -> bool: + for dart_info in mesh.active_darts(): + d = Dart(mesh, dart_info[0]) + d2 = Dart(mesh, dart_info[2]) if dart_info[2] >= 0 else None + n1 = dart_info[3] + if d2 is None: + d1 = d.get_beta(1) + n2 = d1.get_node().id + else : + n2 = d2.get_node().id + for dart_info2 in mesh.active_darts(): + ds = Dart(mesh, dart_info2[0]) + ds2 = Dart(mesh, dart_info2[2]) if dart_info2[2] >= 0 else None + if d != ds and d != ds2: + ns1 = dart_info2[3] + if ds2 is None: + ds1 = ds.get_beta(1) + ns2 = ds1.get_node().id + else: + ns2 = ds2.get_node().id + + if n1 == ns1 and n2 == ns2: + return False + elif n2 == ns1 and n1 == ns2: + return False + return True diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 2e2286f..f1f07a7 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,13 +1,16 @@ from typing import Any +import math import numpy as np from model.mesh_analysis import global_score, isValidAction, find_template_opposite_node from model.mesh_struct.mesh_elements import Dart from model.mesh_struct.mesh import Mesh -from actions.triangular_actions import flip_edge +from actions.triangular_actions import flip_edge, split_edge, collapse_edge from model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 +SPLIT = 1 +COLLAPSE = 2 GLOBAL = 0 @@ -16,12 +19,11 @@ def __init__(self, mesh=None, mesh_size: int = None, max_steps: int = 50, feat: self.mesh = mesh if mesh is not None else random_flip_mesh(mesh_size) self.mesh_size = len(self.mesh.active_nodes()) self.size = len(self.mesh.dart_info) - self.actions = np.array([FLIP]) + self.actions = np.array([FLIP, SPLIT, COLLAPSE]) self.reward = 0 self.steps = 0 self.max_steps = max_steps - self.nodes_scores = global_score(self.mesh)[0] - self.ideal_score = global_score(self.mesh)[2] + self.nodes_scores, self.mesh_score, self.ideal_score = global_score(self.mesh) self.terminal = False self.feat = feat self.won = 0 @@ -32,26 +34,30 @@ def reset(self, mesh=None): self.terminal = False self.mesh = mesh if mesh is not None else random_mesh(self.mesh_size) self.size = len(self.mesh.dart_info) - self.nodes_scores = global_score(self.mesh)[0] - self.ideal_score = global_score(self.mesh)[2] + self.nodes_scores, self.mesh_score, self.ideal_score = global_score(self.mesh) self.won = 0 def step(self, action): dart_id = action[1] - _, mesh_score, mesh_ideal_score = global_score(self.mesh) d = Dart(self.mesh, dart_id) d1 = d.get_beta(1) n1 = d.get_node() n2 = d1.get_node() - flip_edge(self.mesh, n1, n2) + if action[2] == FLIP: + flip_edge(self.mesh, n1, n2) + elif action[2] == SPLIT: + split_edge(self.mesh, n1, n2) + elif action[2] == COLLAPSE: + collapse_edge(self.mesh, n1, n2) self.steps += 1 next_nodes_score, next_mesh_score, _ = global_score(self.mesh) self.nodes_scores = next_nodes_score - self.reward = (mesh_score - next_mesh_score)*10 - if self.steps >= self.max_steps or next_mesh_score == mesh_ideal_score: - if next_mesh_score == mesh_ideal_score: + self.reward = (self.mesh_score - next_mesh_score)*10 + if self.steps >= self.max_steps or next_mesh_score == self.ideal_score: + if next_mesh_score == self.ideal_score: self.won = True self.terminal = True + self.nodes_scores, self.mesh_score = next_nodes_score, next_mesh_score def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: """ @@ -74,11 +80,33 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: :return: the feature vector """ mesh = state + template = get_template_2(mesh) + darts_to_delete = [] + darts_id = [] + all_action_type = 3 + + for i, d_info in enumerate(mesh.active_darts()): + d_id = d_info[0] + d = Dart(mesh, d_id) + if d_info[2] == -1: #test the validity of all action type + darts_to_delete.append(i) + else: + darts_id.append(d_id) + valid_template = np.delete(template, darts_to_delete, axis=0) + score_sum = np.sum(np.abs(valid_template), axis=1) + indices_top_10 = np.argsort(score_sum)[-5:][::-1] + valid_dart_ids = [darts_id[i] for i in indices_top_10] + X = valid_template[indices_top_10, :] + X = X.flatten() + return X, valid_dart_ids + + +def get_template_2(mesh: Mesh): nodes_scores = global_score(mesh)[0] size = len(mesh.active_darts()) template = np.zeros((size, 6)) - for d_info in mesh.active_darts(): + for i, d_info in enumerate(mesh.active_darts()): d = Dart(mesh, d_info[0]) A = d.get_node() @@ -87,35 +115,21 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: d11 = d1.get_beta(1) C = d11.get_node() - #Template niveau 1 - template[d_info[0], 0] = nodes_scores[C.id] - template[d_info[0], 1] = nodes_scores[A.id] - template[d_info[0], 2] = nodes_scores[B.id] + # Template niveau 1 + template[i, 0] = nodes_scores[C.id] if not math.isnan(nodes_scores[C.id]) else 0 + template[i, 1] = nodes_scores[A.id] if not math.isnan(nodes_scores[A.id]) else 0 + template[i, 2] = nodes_scores[B.id] if not math.isnan(nodes_scores[B.id]) else 0 - #template niveau 2 + # template niveau 2 n_id = find_template_opposite_node(d) - if n_id is not None: - template[d_info[0], 3] = nodes_scores[n_id] + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 3] = nodes_scores[n_id] n_id = find_template_opposite_node(d1) - if n_id is not None: - template[d_info[0], 4] = nodes_scores[n_id] + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 4] = nodes_scores[n_id] n_id = find_template_opposite_node(d11) - if n_id is not None: - template[d_info[0], 5] = nodes_scores[n_id] - - dart_to_delete = [] - dart_ids = [] - for i in range(size): - d = Dart(mesh, i) - if not isValidAction(mesh, d.id): - dart_to_delete.append(i) - else : - dart_ids.append(i) - valid_template = np.delete(template, dart_to_delete, axis=0) - score_sum = np.sum(np.abs(valid_template), axis=1) - indices_top_10 = np.argsort(score_sum)[-5:][::-1] - valid_dart_ids = [dart_ids[i] for i in indices_top_10] - X = valid_template[indices_top_10, :] - X = X.flatten() - return X, valid_dart_ids + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 5] = nodes_scores[n_id] + + return template diff --git a/main.py b/main.py index 936120e..0caa05c 100644 --- a/main.py +++ b/main.py @@ -13,8 +13,10 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - #train() + train() + """ cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() + """ diff --git a/mesh_display.py b/mesh_display.py index d15247d..b4edb3c 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -9,18 +9,18 @@ def __init__(self, m: Mesh): def get_nodes_coordinates(self): """ Build a list containing the coordinates of the all the mesh nodes - :return: a list of coordinates (x,y) + :return: a list of coordinates (id, x, y) """ node_list = [] for idx, n in enumerate(self.mesh.nodes): - if n[2] >= 0 : + if n[2] >= 0: node_list.append((idx, n[0], n[1])) return node_list def get_edges(self): """ - Build a list containing the coordinates of the all the mesh nodes - :return: a list of coordinates (x,y) + Build a list containing the id of the nodes of the mesh edges + :return: a list of nodes id (n1_id, n2_id) """ edge_list = [] for d in self.mesh.active_darts(): @@ -36,5 +36,4 @@ def get_scores(self): :return: a list of three elements (nodes_score, mesh_score, ideal_mesh_score) """ nodes_score, mesh_score, ideal_mesh_score = global_score(self.mesh) - nodes_score = [score for score in nodes_score if score is not None] return [nodes_score, mesh_score, ideal_mesh_score] diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index bf491b3..5cc1ad2 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -1,11 +1,11 @@ from math import sqrt, degrees, radians, cos, sin, acos import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node +from model.mesh_struct.mesh_elements import Dart, Node, Face from model.mesh_struct.mesh import Mesh -def global_score(m: Mesh) -> (int, int): +def global_score(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -15,15 +15,18 @@ def global_score(m: Mesh) -> (int, int): mesh_ideal_score = 0 mesh_score = 0 nodes_score = [] + active_nodes_score = [] for i in range(len(m.nodes)): if m.nodes[i, 2] >= 0: n_id = i node = Node(m, n_id) n_score = score_calculation(node) nodes_score.append(n_score) + active_nodes_score.append(n_score) mesh_ideal_score += n_score mesh_score += abs(n_score) - nodes_score.append(None) + else: + nodes_score.append(0) return nodes_score, mesh_score, mesh_ideal_score @@ -71,6 +74,8 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: cos_theta = np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC) cos_theta = np.clip(cos_theta, -1, 1) angle = np.arccos(cos_theta) + if np.isnan(angle): + raise(ValueError("Angle error")) return degrees(angle) @@ -86,7 +91,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) > 10: + if len(boundary_darts) > 7: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -158,7 +163,7 @@ def get_boundary_darts(m: Mesh) -> list[Dart]: for d_info in m.active_darts(): d = Dart(m, d_info[0]) d_twin = d.get_beta(2) - if d_twin is None : + if d_twin is None: boundary_darts.append(d) return boundary_darts @@ -238,25 +243,29 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: flip = 0 split = 1 collapse = 2 + test_all = 3 d = Dart(mesh, dart_id) boundary_darts = get_boundary_darts(mesh) if d in boundary_darts: return False - elif action == flip and isFlipOk(d) is not True: - return False - elif action == split and isFlipOk(d) is not True: - return False - elif action == collapse and isCollapseOk(d) is not True: - return False + elif action == flip: + return isFlipOk(d) + elif action == split: + return isFlipOk(d) + elif action == collapse: + return newIsCollapseOk(d) + elif action == test_all: + return isFlipOk(d) and newIsCollapseOk(d) else: - return True + raise ValueError("No valid action") + def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3:float) -> float: BAx, BAy = x1 - x2, y1 - y2 BCx, BCy = x3 - x2, y3 - y2 cos_ABC = (BAx * BCx + BAy * BCy) / (sqrt(BAx ** 2 + BAy ** 2) * sqrt(BCx ** 2 + BCy ** 2)) - + cos_ABC = np.clip(cos_ABC, -1, 1) rad = acos(cos_ABC) deg = degrees(rad) return deg @@ -286,7 +295,7 @@ def isFlipOk(d: Dart) -> bool: return True -def isCollapseOk(d: Dart) -> bool: +def newIsCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -296,12 +305,124 @@ def isCollapseOk(d: Dart) -> bool: d212 = d21.get_beta(2) d2112 = d211.get_beta(2) - if d112 is None and d12 is None and d2112 is None and d212 is None: + newNode_x, newNode_y = (n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2 + + if d112 is None or d12 is None or d2112 is None or d212 is None: + return False + elif on_boundary(n1) or on_boundary(n2): return False + else: + # search for all adjacent faces to n1 and n2 + if d12 is None and d2112 is None: + adj_faces_n1 = get_adjacent_faces(n1, d212, d112) + return valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) + elif d212 is None and d112 is None: + adj_faces_n2 = get_adjacent_faces(n2, d12, d2112) + return valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y) + else: + adj_faces_n1 = get_adjacent_faces(n1, d212, d112) + adj_faces_n2 = get_adjacent_faces(n2, d12, d2112) + if not valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) or not valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y): + return False + else: + return True + + +def get_adjacent_faces(n: Node, d_from: Dart, d_to: Dart) -> list: + adj_faces = [] + d2 = d_from + d = None if d2 is None else d_from.get_beta(1) + while d != d_to: + if d2 is None and d_to is not None: + # chercher dans l'autre sens + d = d_to + adj_faces.append(d.get_face()) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d = d11.get_beta(2) + while d is not None: + adj_faces.append(d.get_face()) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d = d11.get_beta(2) + break + elif d2 is None and d_to is None: + break + elif d2 is not None: + d = d2.get_beta(1) + adj_faces.append(d.get_face()) + d2 = d.get_beta(2) + else: + break + return adj_faces + +def discontinue(d_from, d_to) -> bool: + if d_from is None or d_to is None: + raise ValueError("Discontinue condition") + + ds = d_from.get_beta(1) + while ds != d_to: + ds2 = ds.get_beta(2) + if ds2 is None: + return True + ds = ds2.get_beta(1) + d1 = d_from.get_beta(1) + ds = d1.get_beta(1) + i = 0 + while ds != d_to: + ds2 = ds.get_beta(2) + if ds2 is None or i > 10: + return True + ds21 = ds2.get_beta(1) + ds = ds21.get_beta(1) + i += 1 + return False + +def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float) -> bool: + """ + Check the orientation of triangles adjacent to node n = Node(mesh, n_id) if the latter is moved to coordinates new_x, new_y. + Also checks that no triangle will become flat + :param mesh: + :param faces: adjacents faces to node of id n_id + :param n_id: + :param new_x: + :param new_y: + :return: + """ + for f in faces: + d, d1, d11, A, B, C = f.get_surrounding() + if A.id == n_id: + vect_AB = (B.x() - new_x, B.y() - new_y) + vect_AC = (C.x() - new_x, C.y() - new_y) + elif B.id == n_id: + vect_AB = (new_x - A.x(), new_y - A.y()) + vect_AC = (C.x() - A.x(), C.y() - A.y()) + elif C.id == n_id: + vect_AB = (B.x() - A.x(), B.y() - A.y()) + vect_AC = (new_x - A.x(), new_y - A.y()) + else: + print("Erreur face non adjacente") + continue + + cross_product = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] + + if cross_product <= 0: + return False # Une face n'est pas orientée correctement ou est plate + return True + + """ elif d112 is not None and d212 is not None: if d12 is None and d2112 is None: + # search for discontinuities + ds = d212.get_beta(1) + while ds != d112: + ds2 = ds.get_beta(2) + if ds2 is None: + return False + ds = ds2.get_beta(1) return True elif d12 is not None: + # search for discontinuities ds = d12.get_beta(1) while ds != d2112: ds2 = ds.get_beta(2) @@ -312,7 +433,6 @@ def isCollapseOk(d: Dart) -> bool: else: return False - """ #Old condition if d112 is None and d12 is None: return False @@ -324,6 +444,26 @@ def isCollapseOk(d: Dart) -> bool: return False else: return True - - """ + +def isCollapseOk(d: Dart) -> bool: + mesh = d.mesh + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + if d112 is None or d12 is None or d2112 is None or d212 is None: + return False + else: + # search for discontinuities on right side (d12 and d2112) + if discontinue(d12, d2112): + return False + if discontinue(d212, d112): + return False + else: + return True + """ \ No newline at end of file diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index d8225a3..912e004 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -20,9 +20,9 @@ def __init__(self, nodes=[], faces=[]): self.nodes = numpy.empty((0, 3)) self.faces = numpy.empty(0, dtype=int) self.dart_info = numpy.empty((0, 5), dtype=int) - self.first_free_dart = 1 - self.first_free_node = 1 - self.first_free_face = 1 + self.first_free_dart = 0 + self.first_free_node = 0 + self.first_free_face = 0 for n in nodes: self.add_node(n[0], n[1]) @@ -65,14 +65,14 @@ def add_node(self, x: float, y: float) -> Node: :param y: Y coordinate :return: the created node """ - if len(self.nodes) < self.first_free_node: + if len(self.nodes) <= self.first_free_node: self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) self.first_free_node += 1 return Node(self, len(self.nodes) - 1) elif self.first_free_node >= 0: n_id = int(self.first_free_node) if isinstance(n_id, int): - self.first_free_node = abs(self.nodes[n_id, 2] + 1 ) + self.first_free_node = abs(self.nodes[n_id, 2] + 1) self.nodes[n_id] = [x, y, -1] else: print(n_id) @@ -117,7 +117,7 @@ def add_triangle(self, n1: Node, n2: Node, n3: Node) -> Face: darts[k].set_node(nodes[k]) nodes[k].set_dart(darts[k]) - if len(self.faces) < self.first_free_face: + if len(self.faces) <= self.first_free_face: self.faces = numpy.append(self.faces, [darts[0].id]) self.first_free_face += 1 tri = Face(self, len(self.faces) - 1) @@ -139,7 +139,7 @@ def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: self.del_dart(d2) self.del_dart(d3) - self.faces[f.id] = -self.first_free_face -1 + self.faces[f.id] = -self.first_free_face - 1 self.first_free_face = f.id @@ -245,9 +245,10 @@ def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1) -> Dart :param a1: dart index to connect by alpha1 :param a2: dart index to connect by alpha2 :param v: vertex index this dart point to + :param f: face to connect :return: the created dart """ - if len(self.dart_info) < self.first_free_dart: + if len(self.dart_info) <= self.first_free_dart: self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) self.first_free_dart += 1 return Dart(self, len(self.dart_info) - 1) diff --git a/model/mesh_struct/mesh_elements.py b/model/mesh_struct/mesh_elements.py index 4e857c1..970a3e0 100644 --- a/model/mesh_struct/mesh_elements.py +++ b/model/mesh_struct/mesh_elements.py @@ -223,4 +223,13 @@ def set_dart(self, d: Dart) -> None: """ if d is None: raise ValueError("Try to connect a face to a non-existing dart") - self.mesh.faces[self.id] = d.id \ No newline at end of file + self.mesh.faces[self.id] = d.id + + def get_surrounding(self) -> [Dart, Dart, Dart, Node, Node, Node]: + d = self.get_dart() + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + A = d.get_node() + B = d1.get_node() + C = d11.get_node() + return d, d1, d11, A, B, C diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 95fe3b9..50b2489 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -3,7 +3,7 @@ from model.mesh_struct.mesh_elements import Dart, Node from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import find_opposite_node, node_in_mesh +from model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids @@ -91,7 +91,38 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action = 2*len(mesh.dart_info) + nb_action_max = 5 + nb_action = 0 + active_darts_list = mesh.active_darts() + i = 0 + while nb_action < nb_action_max and i < 20: + action_type = np.random.randint(1, 4) + d_id = np.random.randint(len(active_darts_list)) + d_id = active_darts_list[d_id][0] + dart = Dart(mesh, d_id) + i1 = dart.get_node() + i2 = ((dart.get_beta(1)).get_beta(1)).get_node() + if action_type == 1 and isValidAction(mesh, d_id, action_type): + flip_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + elif action_type == 2 and isValidAction(mesh, d_id, action_type): + split_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + elif action_type == 3 and isValidAction(mesh, d_id, action_type): + collapse_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + i += 1 + active_darts_list = mesh.active_darts() + return mesh + + +def old_mesh_shuffle(mesh: Mesh) -> Mesh: + """ + Performs random flip actions on mesh darts. + :param mesh: the mesh to work with + :return: a mesh with randomly flipped darts. + """ + nb_action = int(len(mesh.dart_info)/2) nb_nodes = len(mesh.nodes) for i in range(nb_action): action = np.random.randint(1, 4) diff --git a/model_RL/PPO_model.py b/model_RL/PPO_model.py index d1ae1ce..d0b9258 100644 --- a/model_RL/PPO_model.py +++ b/model_RL/PPO_model.py @@ -9,7 +9,7 @@ class PPO: def __init__(self, env, lr, gamma, nb_iterations, nb_episodes_per_iteration, nb_epochs, batch_size): self.env = env - self.actor = Actor(env, 30, 5, lr=0.0001) + self.actor = Actor(env, 30, 15, lr=0.0001) self.critic = Critic(30, lr=0.0001) self.lr = lr self.gamma = gamma @@ -43,8 +43,9 @@ def train_epoch(self, dataset): next_value = torch.tensor(0.0, dtype=torch.float32) if done else self.critic(next_X) delta = r + 0.9 * next_value - value G = (r + 0.9 * G) / 10 - st = global_score(s)[1] - ideal_s = global_score(s)[2] + _, st, ideal_s = global_score(s) + if st == ideal_s: + continue advantage = 1 if done else G / (st - ideal_s) ratio = torch.exp(log_prob - torch.log(old_prob).detach()) actor_loss1 = advantage * ratio @@ -80,18 +81,17 @@ def train(self): print('ITERATION', iteration) rollouts = [] dataset = [] - for _ in range(self.nb_episodes_per_iteration): + for _ in tqdm(range(self.nb_episodes_per_iteration)): self.env.reset() trajectory = [] ep_reward = 0 done = False while True: state = copy.deepcopy(self.env.mesh) - action = self.actor.select_action(state) - X, _ = self.env.get_x(state, None) - X = torch.tensor(X, dtype=torch.float32) - pmf = self.actor.forward(X) - prob = pmf[action[0]] + action, prob = self.actor.select_action(state) + if action is None: + wins.append(0) + break self.env.step(action) next_state = copy.deepcopy(self.env.mesh) R = self.env.reward @@ -106,10 +106,11 @@ def train(self): trajectory.append((state, action, R, prob, next_state, done)) break trajectory.append((state, action, R, prob, next_state, done)) - rewards.append(ep_reward) - rollouts.append(trajectory) - dataset.extend(trajectory) - len_ep.append(len(trajectory)) + if len(trajectory) != 0: + rewards.append(ep_reward) + rollouts.append(trajectory) + dataset.extend(trajectory) + len_ep.append(len(trajectory)) self.train_epoch(dataset) diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 6359be9..700ce44 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,7 +1,7 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from model.mesh_analysis import global_score +from model.mesh_analysis import global_score, get_boundary_darts from model.mesh_struct.mesh import Mesh import numpy as np import copy @@ -34,8 +34,12 @@ def testPolicy( ep_rewards: int = 0 ep_length: int = 0 env.reset(mesh) - while env.won == 0 and ep_length < 30: - action = policy.select_action(env.mesh) + while env.won == 0 and ep_length < max_steps: + action, _ = policy.select_action(env.mesh) + if action is None: + env.terminal = True + break + boundary_darts = get_boundary_darts(env.mesh) env.step(action) ep_rewards += env.reward ep_length += 1 @@ -57,7 +61,7 @@ def isBetterPolicy(actual_best_policy, policy_to_test): def isBetterMesh(best_mesh, actual_mesh): - if actual_mesh is not None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: + if best_mesh is None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: return True else: return False diff --git a/model_RL/utilities/actor_critic_networks.py b/model_RL/utilities/actor_critic_networks.py index 4d24d04..6cf8de0 100644 --- a/model_RL/utilities/actor_critic_networks.py +++ b/model_RL/utilities/actor_critic_networks.py @@ -33,11 +33,20 @@ def reset(self, env=None): self.optimizer = Adam(self.parameters(), lr=self.optimizer.defaults['lr'], weight_decay=self.optimizer.defaults['weight_decay']) def select_action(self, state): - if np.random.rand() < self.eps: X, dart_indices = self.env.get_x(state, None) - action = np.random.randint(5) - dart_id = dart_indices[action] + action = np.random.randint(5*3) # random choice of 3 actions on 3 darts + dart_id = dart_indices[int(action/3)] + action_type = action % 3 + prob = 1/3 + i = 0 + while not isValidAction(state, dart_id, action_type): + if i > 15: + return None, None + action = np.random.randint(5 * 3) # random choice of 3 actions on 3 darts + dart_id = dart_indices[int(action / 3)] + action_type = action % 3 + i += 1 else: X, dart_indices = self.env.get_x(state, None) X = torch.tensor(X, dtype=torch.float32) @@ -45,16 +54,25 @@ def select_action(self, state): dist = Categorical(pmf) action = dist.sample() action = action.tolist() - dart_id = dart_indices[action] + prob = pmf[action] + action_darts = int(action/3) + action_type = action % 3 + dart_id = dart_indices[action_darts] i = 0 - while not isValidAction(state, dart_id) and i < 10: + while not isValidAction(state, dart_id, action_type): + if i > 15: + return None, None pmf = self.forward(X) dist = Categorical(pmf) action = dist.sample() action = action.tolist() - dart_id = dart_indices[action] + prob = pmf[action] + action_darts = int(action/3) + action_type = action % 3 + dart_id = dart_indices[action_darts] i += 1 - return action, dart_id + action_list = [action, dart_id, action_type] + return action_list, prob def forward(self, x): x = torch.relu(self.fc1(x)) diff --git a/test_modules/test_mesh_analysis.py b/test_modules/test_mesh_analysis.py index 3ff7580..a28321a 100644 --- a/test_modules/test_mesh_analysis.py +++ b/test_modules/test_mesh_analysis.py @@ -57,8 +57,9 @@ def test_is_valid_action(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [2.0, 0.0]] faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] cmap = Mesh(nodes, faces) - self.assertEqual(Mesh_analysis.isValidAction(cmap, 0), False) - self.assertEqual(Mesh_analysis.isValidAction(cmap, 2), True) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 0, 3), False) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 2, 0), True) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 2, 3), False) def test_isFlipOk(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [2.0, 0.0]] diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 0f6bca8..9c87657 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -16,12 +16,9 @@ def test_regular_trimesh(self): self.assertEqual(m.nb_nodes(), 60) def test_random_trimesh(self): - m = random_mesh(44) - self.assertIsInstance(m, Mesh) - m = random_mesh(30) - self.assertIsInstance(m, Mesh) - m = random_mesh(60) - self.assertIsInstance(m, Mesh) + for i in range(10): + m = random_mesh(30) + self.assertIsInstance(m, Mesh) def test_random_flip_mesh(self): m = random_flip_mesh(44) @@ -32,7 +29,7 @@ def test_random_flip_mesh(self): self.assertEqual(m.nb_nodes(), 60) def test_mesh_suffle(self): - m = regular_mesh(40) + m = regular_mesh(15) mesh = mesh_shuffle(m) plot_mesh(mesh) diff --git a/train.py b/train.py index 5ffb301..0200d0b 100644 --- a/train.py +++ b/train.py @@ -1,5 +1,5 @@ import model.random_trimesh as TM - +import torch from environment.trimesh_env import TriMesh from plots.create_plots import plot_training_results, plot_test_results @@ -20,22 +20,24 @@ def train(): gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(30) for _ in range(16)] + dataset = [TM.random_mesh(12) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=30, feat=feature) + env = TriMesh(None, mesh_size, max_steps=15, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=50, nb_epochs=1, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=3, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) actor, rewards, wins, steps = model.train() + if rewards is not None: + plot_training_results(rewards, wins, steps) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 10, dataset, 60) + torch.save(actor.state_dict(), 'policy saved/actor_network.pth') + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) if rewards is not None: - plot_training_results(rewards, wins, steps) plot_test_results(avg_rewards, avg_wins, avg_steps) plot_dataset(final_meshes) diff --git a/view/graph.py b/view/graph.py index e2eff08..46e95ac 100644 --- a/view/graph.py +++ b/view/graph.py @@ -110,9 +110,9 @@ def clear(self): self.edges = [] def update(self, vertices, edges, scores): - for idx, n in enumerate(vertices): + for n in vertices: nodes_scores = scores[0] - n_value = nodes_scores[idx] + n_value = nodes_scores[n[0]] self.create_vertex(n[0], n[1], n[2], n_value) for e in edges: self.create_edge(e[0], e[1]) From 0708ecee82f538f0ca95c1ff084037b115a35d4b Mon Sep 17 00:00:00 2001 From: ropercha Date: Thu, 12 Sep 2024 09:56:53 +0200 Subject: [PATCH 04/22] Working environment with collapse action, the agent has learned to collapse a maximum of nodes --- actions/triangular_actions.py | 7 +- environment/trimesh_env.py | 2 +- exploit.py | 32 ++++++++ main.py | 1 + model/mesh_analysis.py | 142 ++++++++++++++++------------------ model/mesh_struct/mesh.py | 1 - model/random_trimesh.py | 7 +- train.py | 10 +-- 8 files changed, 115 insertions(+), 87 deletions(-) create mode 100644 exploit.py diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 37dd919..a641af6 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, newIsCollapseOk, adjacent_darts +from model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts import numpy as np @@ -98,7 +98,7 @@ def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not newIsCollapseOk(d): + if not found or not isCollapseOk(d): return False elif not test_degree(n1): return False @@ -212,6 +212,7 @@ def test_boundary(n1: Node, n2: Node) -> bool: else: return True + def check_beta2_relation(mesh: Mesh) -> bool: for dart_info in mesh.active_darts(): d = dart_info[0] @@ -231,7 +232,7 @@ def check_double(mesh: Mesh) -> bool: if d2 is None: d1 = d.get_beta(1) n2 = d1.get_node().id - else : + else: n2 = d2.get_node().id for dart_info2 in mesh.active_darts(): ds = Dart(mesh, dart_info2[0]) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index f1f07a7..3c08a81 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -52,7 +52,7 @@ def step(self, action): self.steps += 1 next_nodes_score, next_mesh_score, _ = global_score(self.mesh) self.nodes_scores = next_nodes_score - self.reward = (self.mesh_score - next_mesh_score)*10 + self.reward = (self.mesh_score - next_mesh_score) * 10 if self.steps >= self.max_steps or next_mesh_score == self.ideal_score: if next_mesh_score == self.ideal_score: self.won = True diff --git a/exploit.py b/exploit.py new file mode 100644 index 0000000..6c1d2e3 --- /dev/null +++ b/exploit.py @@ -0,0 +1,32 @@ +import model.random_trimesh as TM +import torch +from environment.trimesh_env import TriMesh +from model_RL.utilities.actor_critic_networks import Actor + +from plots.create_plots import plot_training_results, plot_test_results +from plots.mesh_plotter import plot_dataset + +from model_RL.evaluate_model import testPolicy + +from model_RL.PPO_model import PPO + +LOCAL_MESH_FEAT = 0 + + +def exploit(): + mesh_size = 12 + feature = LOCAL_MESH_FEAT + + dataset = [TM.random_mesh(12) for _ in range(9)] + plot_dataset(dataset) + + env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + + actor = Actor(env, 30, 15, lr=0.0001) + actor.load_state_dict(torch.load('policy saved/actor_network_nul.pth')) + + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + + if avg_steps is not None: + plot_test_results(avg_rewards, avg_wins, avg_steps) + plot_dataset(final_meshes) diff --git a/main.py b/main.py index 0caa05c..934f988 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ from user_game import user_game from train import train +from exploit import exploit from model.reader import read_gmsh from view.window import Game from mesh_display import MeshDisplay diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 5cc1ad2..2acb070 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -6,6 +6,33 @@ def global_score(m: Mesh): + """ + Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. + And the current score is the mesh score. + :param m: the mesh to be analyzed + :return: three return values: a list of the nodes score, the current mesh score and the ideal mesh score + """ + mesh_ideal_score = 0 + mesh_score = 0 + nodes_score = [] + active_nodes_score = [] + for i in range(len(m.nodes)): + if m.nodes[i, 2] >= 0: + n_id = i + node = Node(m, n_id) + real_n_score = score_calculation(node) + #n_score = real_n_score ** 2 if real_n_score > 0 else -1 * (real_n_score ** 2) + n_score = real_n_score + nodes_score.append(n_score) + active_nodes_score.append(n_score) + mesh_ideal_score += n_score + mesh_score += abs(n_score) + else: + nodes_score.append(0) + return nodes_score, mesh_score, mesh_ideal_score + + +def global_score_old(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -253,9 +280,9 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: elif action == split: return isFlipOk(d) elif action == collapse: - return newIsCollapseOk(d) + return isCollapseOk(d) elif action == test_all: - return isFlipOk(d) and newIsCollapseOk(d) + return isFlipOk(d) and isCollapseOk(d) else: raise ValueError("No valid action") @@ -295,7 +322,12 @@ def isFlipOk(d: Dart) -> bool: return True -def newIsCollapseOk(d: Dart) -> bool: +def isSplitOk(d: Dart) -> bool: + mesh = d.mesh + + return True + +def isCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -356,27 +388,6 @@ def get_adjacent_faces(n: Node, d_from: Dart, d_to: Dart) -> list: break return adj_faces -def discontinue(d_from, d_to) -> bool: - if d_from is None or d_to is None: - raise ValueError("Discontinue condition") - - ds = d_from.get_beta(1) - while ds != d_to: - ds2 = ds.get_beta(2) - if ds2 is None: - return True - ds = ds2.get_beta(1) - d1 = d_from.get_beta(1) - ds = d1.get_beta(1) - i = 0 - while ds != d_to: - ds2 = ds.get_beta(2) - if ds2 is None or i > 10: - return True - ds21 = ds2.get_beta(1) - ds = ds21.get_beta(1) - i += 1 - return False def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float) -> bool: """ @@ -394,12 +405,15 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float if A.id == n_id: vect_AB = (B.x() - new_x, B.y() - new_y) vect_AC = (C.x() - new_x, C.y() - new_y) + vect_BC = (C.x() - B.x(), C.y() - B.y()) elif B.id == n_id: vect_AB = (new_x - A.x(), new_y - A.y()) vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_BC = (C.x() - new_x, C.y() - new_y) elif C.id == n_id: vect_AB = (B.x() - A.x(), B.y() - A.y()) vect_AC = (new_x - A.x(), new_y - A.y()) + vect_BC = (new_x - B.x(), new_y - B.y()) else: print("Erreur face non adjacente") continue @@ -408,62 +422,42 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float if cross_product <= 0: return False # Une face n'est pas orientée correctement ou est plate + elif not valid_triangle(vect_AB, vect_AC, vect_BC): + return False return True - """ - elif d112 is not None and d212 is not None: - if d12 is None and d2112 is None: - # search for discontinuities - ds = d212.get_beta(1) - while ds != d112: - ds2 = ds.get_beta(2) - if ds2 is None: - return False - ds = ds2.get_beta(1) - return True - elif d12 is not None: - # search for discontinuities - ds = d12.get_beta(1) - while ds != d2112: - ds2 = ds.get_beta(2) - if ds2 is None: - return False - ds = ds2.get_beta(1) - return True - else: - return False - #Old condition - if d112 is None and d12 is None: - return False - elif d212 is None and d2112 is None: - return False - elif d212 is None and d12 is None: - return False - elif d112 is None and d2112 is None: - return False +def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: + dist_AB = sqrt(vect_AB[0] ** 2 + vect_AB[1] ** 2) + dist_AC = sqrt(vect_AC[0] ** 2 + vect_AC[1] ** 2) + dist_BC = sqrt(vect_BC[0] ** 2 + vect_BC[1] ** 2) + l_min = 0.1 + l_max = 1.5 + + if l_min < dist_AB < l_max or l_min < dist_AC < l_max or l_min < dist_BC < l_max: + pass else: - return True + return False + # Calcul des angles avec le théorème du cosinus + angle_A = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A + angle_B = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B + angle_C = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C -def isCollapseOk(d: Dart) -> bool: - mesh = d.mesh - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + # Vérification que tous les angles sont supérieurs à 5° + if angle_A <= 5 or angle_B <= 5 or angle_C <= 5: + return False + return True - d112 = d11.get_beta(2) - d12 = d1.get_beta(2) - d212 = d21.get_beta(2) - d2112 = d211.get_beta(2) +def angle_from_sides(a, b, c): + # Théorème du cosinus pour obtenir l'angle en radians entre les côtés a, b, c + cosA = (b**2 + c**2 - a**2) / (2 * b * c) + if 1 <= cosA < 1.01: + cosA = 1 + elif -1.01 <= cosA < -1: + cosA = -1 + elif cosA > 1.01 or cosA < -1.01: + raise ValueError("Math domain error : cos>1.01") + return acos(cosA) - if d112 is None or d12 is None or d2112 is None or d212 is None: - return False - else: - # search for discontinuities on right side (d12 and d2112) - if discontinue(d12, d2112): - return False - if discontinue(d212, d112): - return False - else: - return True - """ \ No newline at end of file diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index 912e004..323fff4 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -269,7 +269,6 @@ def del_dart(self, d: Dart): self.dart_info[d.id][0] = -self.first_free_dart - 1 self.first_free_dart = d.id - def set_beta2(self, dart: Dart) -> None: """ Search for a dart to connect with beta2 relation when possible. diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 50b2489..f7d4bd1 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -59,6 +59,7 @@ def random_flip_mesh(num_nodes_max: int) -> Mesh: mesh_shuffle_flip(mesh) return mesh + def random_mesh(num_nodes_max: int) -> Mesh: """ Create a random mesh with a fixed number of nodes. @@ -66,7 +67,7 @@ def random_mesh(num_nodes_max: int) -> Mesh: :return: a random mesh """ mesh = regular_mesh(num_nodes_max) - mesh_shuffle(mesh) + mesh_shuffle(mesh, num_nodes_max) return mesh @@ -85,13 +86,13 @@ def mesh_shuffle_flip(mesh: Mesh) -> Mesh: flip_edge_ids(mesh, i1, i2) return mesh -def mesh_shuffle(mesh: Mesh) -> Mesh: +def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: """ Performs random flip actions on mesh darts. :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action_max = 5 + nb_action_max = int(num_nodes / 2) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 diff --git a/train.py b/train.py index 0200d0b..0827401 100644 --- a/train.py +++ b/train.py @@ -15,28 +15,28 @@ def train(): - mesh_size = 12 + mesh_size = 16 lr = 0.0001 gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(12) for _ in range(9)] + dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + env = TriMesh(None, mesh_size, max_steps=30, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=3, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=10, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) torch.save(actor.state_dict(), 'policy saved/actor_network.pth') - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 40) if rewards is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) From 0dfe3d868a875fe756d1eb9f16418749cddcb5ad Mon Sep 17 00:00:00 2001 From: ropercha Date: Mon, 16 Sep 2024 09:40:14 +0200 Subject: [PATCH 05/22] New "exploit" function to exploit saved policies (already trained). New function isSplitOk, to verifiy some geometric constraints. --- exploit.py | 9 +++-- main.py | 2 +- model/mesh_analysis.py | 90 ++++++++++++++++++++++++++++++++++++----- model/random_trimesh.py | 12 +++--- train.py | 8 ++-- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/exploit.py b/exploit.py index 6c1d2e3..5b94dd8 100644 --- a/exploit.py +++ b/exploit.py @@ -17,15 +17,16 @@ def exploit(): mesh_size = 12 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(12) for _ in range(9)] + dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + env = TriMesh(None, mesh_size, max_steps=60, feat=feature) + actor = Actor(env, 30, 15, lr=0.0001) - actor.load_state_dict(torch.load('policy saved/actor_network_nul.pth')) + actor.load_state_dict(torch.load('policy saved/actor_30.pth')) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 15, dataset, 60) if avg_steps is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) diff --git a/main.py b/main.py index 934f988..5d95221 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,7 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - train() + exploit() """ cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 2acb070..5dfba9c 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -278,11 +278,11 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: elif action == flip: return isFlipOk(d) elif action == split: - return isFlipOk(d) + return isSplitOk(d) elif action == collapse: return isCollapseOk(d) elif action == test_all: - return isFlipOk(d) and isCollapseOk(d) + return isFlipOk(d) and isCollapseOk(d) and isSplitOk(d) else: raise ValueError("No valid action") @@ -299,6 +299,35 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: def isFlipOk(d: Dart) -> bool: + mesh = d.mesh + if d.get_beta(2) is None: + return False + else: + d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + + # Check angle at d limits to avoid edge reversal + angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) + angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) + if angle_B >= 180 or angle_A >= 180: + return False + + #Check if new triangle will be valid + + #Triangle ACD + vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_DC = (C.x() - D.x(), C.y() - D.y()) + + #Triangle CBD + vect_BC = (C.x() - B.x(), C.y() - B.y()) + vect_BD = (D.x() - B.x(), D.y() - B.y()) + + if not valid_triangle(vect_AC, vect_AD, vect_DC) or not valid_triangle(vect_BC, vect_BD, vect_DC): + return False + + return True + +def isFlipOk_old(d: Dart) -> bool: d1 = d.get_beta(1) d11 = d1.get_beta(1) A = d.get_node() @@ -325,8 +354,50 @@ def isFlipOk(d: Dart) -> bool: def isSplitOk(d: Dart) -> bool: mesh = d.mesh + if d.get_beta(2) is None: + return False + else: + d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + + newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 + + # Check angle at d limits to avoid edge reversal + angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) + angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) + if angle_B >= 180 or angle_A >= 180: + return False + + #Check if new triangle will be valid + + # Triangle AEC + vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_AE = (newNode_x - A.x(), newNode_y - A.y()) + vect_EC = (C.x() - newNode_x, C.y() - newNode_y) + if not valid_triangle(vect_AE, vect_AC, vect_EC): + return False + + # Triangle ADE + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not valid_triangle(vect_AD, vect_AE, vect_ED): + return False + + # Triangle BCE + vect_BC = (C.x() - B.x(), C.y() - B.y()) + vect_BE = (newNode_x - B.x(), newNode_y - B.y()) + vect_EC = (C.x() - newNode_x, C.y() - newNode_y) + if not valid_triangle(vect_BC, vect_BE, vect_EC): + return False + + # Triangle BDE + vect_BD = (D.x() - B.x(), D.y() - B.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not valid_triangle(vect_BD, vect_BE, vect_ED): + return False + return True + def isCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -431,18 +502,19 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: dist_AB = sqrt(vect_AB[0] ** 2 + vect_AB[1] ** 2) dist_AC = sqrt(vect_AC[0] ** 2 + vect_AC[1] ** 2) dist_BC = sqrt(vect_BC[0] ** 2 + vect_BC[1] ** 2) - l_min = 0.1 - l_max = 1.5 + target_mesh_size = 1 + + L_max = max(dist_AB, dist_AC, dist_BC) - if l_min < dist_AB < l_max or l_min < dist_AC < l_max or l_min < dist_BC < l_max: + if target_mesh_size/sqrt(2) < L_max < target_mesh_size*sqrt(2): pass else: return False # Calcul des angles avec le théorème du cosinus - angle_A = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A - angle_B = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B - angle_C = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C + angle_B = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A + angle_C = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B + angle_A = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C # Vérification que tous les angles sont supérieurs à 5° if angle_A <= 5 or angle_B <= 5 or angle_C <= 5: @@ -451,7 +523,7 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: def angle_from_sides(a, b, c): - # Théorème du cosinus pour obtenir l'angle en radians entre les côtés a, b, c + # Calculate angle A, with a the opposite side and b and c the adjacent sides cosA = (b**2 + c**2 - a**2) / (2 * b * c) if 1 <= cosA < 1.01: cosA = 1 diff --git a/model/random_trimesh.py b/model/random_trimesh.py index f7d4bd1..969da0b 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -92,24 +92,24 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action_max = int(num_nodes / 2) + nb_action_max = int(num_nodes*3) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 - while nb_action < nb_action_max and i < 20: - action_type = np.random.randint(1, 4) + while nb_action < nb_action_max and i < (nb_action_max + 30): + action_type = np.random.randint(0, 3) d_id = np.random.randint(len(active_darts_list)) d_id = active_darts_list[d_id][0] dart = Dart(mesh, d_id) i1 = dart.get_node() i2 = ((dart.get_beta(1)).get_beta(1)).get_node() - if action_type == 1 and isValidAction(mesh, d_id, action_type): + if action_type == 0 and isValidAction(mesh, d_id, action_type): flip_edge_ids(mesh, i1.id, i2.id) nb_action += 1 - elif action_type == 2 and isValidAction(mesh, d_id, action_type): + elif action_type == 1 and isValidAction(mesh, d_id, action_type): split_edge_ids(mesh, i1.id, i2.id) nb_action += 1 - elif action_type == 3 and isValidAction(mesh, d_id, action_type): + elif action_type == 2 and isValidAction(mesh, d_id, action_type): collapse_edge_ids(mesh, i1.id, i2.id) nb_action += 1 i += 1 diff --git a/train.py b/train.py index 0827401..c1e619f 100644 --- a/train.py +++ b/train.py @@ -15,7 +15,7 @@ def train(): - mesh_size = 16 + mesh_size = 30 lr = 0.0001 gamma = 0.9 feature = LOCAL_MESH_FEAT @@ -23,20 +23,20 @@ def train(): dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=30, feat=feature) + env = TriMesh(None, mesh_size, max_steps=80, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=10, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=8, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) torch.save(actor.state_dict(), 'policy saved/actor_network.pth') - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 40) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 60) if rewards is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) From 989be5f58edb884488b00212879839d7cd2d0728 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 10:39:05 +0100 Subject: [PATCH 06/22] Cleaning up and organization --- .github/workflows/model-view-ci.yml | 2 +- actions/triangular_actions.py | 31 ++------ environment/trimesh_env.py | 12 +-- exploit.py | 6 +- main.py | 3 +- mesh_display.py | 4 +- {model => mesh_model}/__init__.py | 0 {model => mesh_model}/mesh_analysis.py | 79 +++---------------- {model => mesh_model}/mesh_struct/__init__.py | 0 {model => mesh_model}/mesh_struct/mesh.py | 19 ++++- .../mesh_struct/mesh_elements.py | 0 {model => mesh_model}/random_trimesh.py | 33 ++------ {model => mesh_model}/reader.py | 2 +- model_RL/AC_model.py | 2 +- model_RL/PPO_model.py | 4 +- model_RL/evaluate_model.py | 4 +- model_RL/utilities/actor_critic_networks.py | 2 +- model_RL/utilities/nnPolicy.py | 2 +- plots/mesh_plotter.py | 4 +- test_modules/test_actions.py | 4 +- test_modules/test_mesh_analysis.py | 6 +- test_modules/test_mesh_structure.py | 2 +- test_modules/test_random_trimesh.py | 4 +- test_modules/test_reader.py | 4 +- train.py | 6 +- user_game.py | 2 +- 26 files changed, 82 insertions(+), 155 deletions(-) rename {model => mesh_model}/__init__.py (100%) rename {model => mesh_model}/mesh_analysis.py (85%) rename {model => mesh_model}/mesh_struct/__init__.py (100%) rename {model => mesh_model}/mesh_struct/mesh.py (95%) rename {model => mesh_model}/mesh_struct/mesh_elements.py (100%) rename {model => mesh_model}/random_trimesh.py (79%) rename {model => mesh_model}/reader.py (98%) diff --git a/.github/workflows/model-view-ci.yml b/.github/workflows/model-view-ci.yml index 9041aa0..42c071f 100644 --- a/.github/workflows/model-view-ci.yml +++ b/.github/workflows/model-view-ci.yml @@ -1,4 +1,4 @@ -name: model-ci +name: mesh_model-ci on: push: diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index a641af6..6bff48f 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -1,10 +1,8 @@ from __future__ import annotations -from model.mesh_struct.mesh import Mesh -from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts - -import numpy as np +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -120,12 +118,12 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d112 = d11.get_beta(2) #Delete the darts around selected dart - delete_triangles(mesh, d) + mesh.del_adj_triangles(d) - #move n1 node in the middle of [n1, n2] + #Move n1 node in the middle of [n1, n2] n1.set_xy((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) - #update node relations + #Update node relations if d12 is not None: d121 = d12.get_beta(1) d121.set_node(n1) @@ -165,27 +163,14 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) + + #check for duplicate darts if not check_double(mesh): raise ValueError("double error") if not check_beta2_relation(mesh): raise ValueError("error beta2") return True -def delete_triangles(mesh: Mesh, d: Dart) -> None: - d2 = d.get_beta(2) - d1 = d.get_beta(1) - d11 = d1.get_beta(1) - d21 = d2.get_beta(1) - d211 = d21.get_beta(1) - - f1 = d.get_face() - f2 = d2.get_face() - - mesh.del_triangle(d, d1, d11, f1) - mesh.del_triangle(d2, d21, d211, f2) - - - def test_degree(n: Node) -> bool: """ Verify that the degree of a vertex is lower than 10 diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 3c08a81..6475f98 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,11 +1,11 @@ from typing import Any import math import numpy as np -from model.mesh_analysis import global_score, isValidAction, find_template_opposite_node -from model.mesh_struct.mesh_elements import Dart -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score, isValidAction, find_template_opposite_node +from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge, split_edge, collapse_edge -from model.random_trimesh import random_flip_mesh, random_mesh +from mesh_model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 @@ -59,7 +59,7 @@ def step(self, action): self.terminal = True self.nodes_scores, self.mesh_score = next_nodes_score, next_mesh_score - def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: + def get_x(self, s: Mesh, a: int): """ Get the feature vector of the state-action pair :param s: the state @@ -72,7 +72,7 @@ def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: return get_x_global_4(self, s) -def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: +def get_x_global_4(env, state: Mesh): """ Get the feature vector of the state. :param state: the state diff --git a/exploit.py b/exploit.py index 5b94dd8..fa8cdf8 100644 --- a/exploit.py +++ b/exploit.py @@ -1,4 +1,4 @@ -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM import torch from environment.trimesh_env import TriMesh from model_RL.utilities.actor_critic_networks import Actor @@ -24,9 +24,9 @@ def exploit(): actor = Actor(env, 30, 15, lr=0.0001) - actor.load_state_dict(torch.load('policy saved/actor_30.pth')) + actor.load_state_dict(torch.load('policy_saved/actor_network.pth')) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 15, dataset, 60) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 30, dataset, 100) if avg_steps is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) diff --git a/main.py b/main.py index 5d95221..077c326 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,10 @@ from user_game import user_game from train import train from exploit import exploit -from model.reader import read_gmsh +from mesh_model.reader import read_gmsh from view.window import Game from mesh_display import MeshDisplay +from mesh_model.random_trimesh import random_mesh # Press the green button in the gutter to run the script. diff --git a/mesh_display.py b/mesh_display.py index b4edb3c..3ea4521 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -1,5 +1,5 @@ -from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import global_score +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score class MeshDisplay: diff --git a/model/__init__.py b/mesh_model/__init__.py similarity index 100% rename from model/__init__.py rename to mesh_model/__init__.py diff --git a/model/mesh_analysis.py b/mesh_model/mesh_analysis.py similarity index 85% rename from model/mesh_analysis.py rename to mesh_model/mesh_analysis.py index 5dfba9c..34e67a0 100644 --- a/model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -1,38 +1,11 @@ from math import sqrt, degrees, radians, cos, sin, acos import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node, Face -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh import Mesh def global_score(m: Mesh): - """ - Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. - And the current score is the mesh score. - :param m: the mesh to be analyzed - :return: three return values: a list of the nodes score, the current mesh score and the ideal mesh score - """ - mesh_ideal_score = 0 - mesh_score = 0 - nodes_score = [] - active_nodes_score = [] - for i in range(len(m.nodes)): - if m.nodes[i, 2] >= 0: - n_id = i - node = Node(m, n_id) - real_n_score = score_calculation(node) - #n_score = real_n_score ** 2 if real_n_score > 0 else -1 * (real_n_score ** 2) - n_score = real_n_score - nodes_score.append(n_score) - active_nodes_score.append(n_score) - mesh_ideal_score += n_score - mesh_score += abs(n_score) - else: - nodes_score.append(0) - return nodes_score, mesh_score, mesh_ideal_score - - -def global_score_old(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -300,6 +273,8 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: def isFlipOk(d: Dart) -> bool: mesh = d.mesh + + #if d is on boundary, flip is not possible if d.get_beta(2) is None: return False else: @@ -327,30 +302,6 @@ def isFlipOk(d: Dart) -> bool: return True -def isFlipOk_old(d: Dart) -> bool: - d1 = d.get_beta(1) - d11 = d1.get_beta(1) - A = d.get_node() - B = d1.get_node() - C = d11.get_node() - d2 = d.get_beta(2) - if d2 is None: - return False - else: - d21 = d2.get_beta(1) - d211 = d21.get_beta(1) - D = d211.get_node() - - # Calcul angle at d limits - angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) - angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) - - if angle_B >= 180 or angle_A >= 180: - return False - else: - return True - - def isSplitOk(d: Dart) -> bool: mesh = d.mesh @@ -361,12 +312,6 @@ def isSplitOk(d: Dart) -> bool: newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 - # Check angle at d limits to avoid edge reversal - angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) - angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) - if angle_B >= 180 or angle_A >= 180: - return False - #Check if new triangle will be valid # Triangle AEC @@ -464,12 +409,12 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float """ Check the orientation of triangles adjacent to node n = Node(mesh, n_id) if the latter is moved to coordinates new_x, new_y. Also checks that no triangle will become flat - :param mesh: + :param mesh: a mesh :param faces: adjacents faces to node of id n_id - :param n_id: - :param new_x: - :param new_y: - :return: + :param n_id: node id + :param new_x: new x coordinate + :param new_y: new y coordinate + :return: True if valid, False otherwise """ for f in faces: d, d1, d11, A, B, C = f.get_surrounding() @@ -486,13 +431,13 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float vect_AC = (new_x - A.x(), new_y - A.y()) vect_BC = (new_x - B.x(), new_y - B.y()) else: - print("Erreur face non adjacente") + print("Non-adjacent face error") continue cross_product = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] if cross_product <= 0: - return False # Une face n'est pas orientée correctement ou est plate + return False # One face is not correctly oriented or is flat elif not valid_triangle(vect_AB, vect_AC, vect_BC): return False return True @@ -506,7 +451,7 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: L_max = max(dist_AB, dist_AC, dist_BC) - if target_mesh_size/sqrt(2) < L_max < target_mesh_size*sqrt(2): + if target_mesh_size/1.5*sqrt(2) < L_max < target_mesh_size*1.5*sqrt(2): pass else: return False diff --git a/model/mesh_struct/__init__.py b/mesh_model/mesh_struct/__init__.py similarity index 100% rename from model/mesh_struct/__init__.py rename to mesh_model/mesh_struct/__init__.py diff --git a/model/mesh_struct/mesh.py b/mesh_model/mesh_struct/mesh.py similarity index 95% rename from model/mesh_struct/mesh.py rename to mesh_model/mesh_struct/mesh.py index 323fff4..b35b819 100644 --- a/model/mesh_struct/mesh.py +++ b/mesh_model/mesh_struct/mesh.py @@ -2,7 +2,7 @@ import sys import numpy -from model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face """ Classes Dart, Node and Face must be seen as handlers on data that are stored in the @@ -142,6 +142,23 @@ def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: self.faces[f.id] = -self.first_free_face - 1 self.first_free_face = f.id + def del_adj_triangles(self, d: Dart) -> None: + """ + Delete the two adjacent triangles of the given dart d + :param mesh: a mesh + :param d: the dart to be deleted + """ + d2 = d.get_beta(2) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d21 = d2.get_beta(1) + d211 = d21.get_beta(1) + + f1 = d.get_face() + f2 = d2.get_face() + + self.del_triangle(d, d1, d11, f1) + self.del_triangle(d2, d21, d211, f2) def add_quad(self, n1: Node, n2: Node, n3: Node, n4: Node) -> Face: """ diff --git a/model/mesh_struct/mesh_elements.py b/mesh_model/mesh_struct/mesh_elements.py similarity index 100% rename from model/mesh_struct/mesh_elements.py rename to mesh_model/mesh_struct/mesh_elements.py diff --git a/model/random_trimesh.py b/mesh_model/random_trimesh.py similarity index 79% rename from model/random_trimesh.py rename to mesh_model/random_trimesh.py index 969da0b..2e1b367 100644 --- a/model/random_trimesh.py +++ b/mesh_model/random_trimesh.py @@ -1,9 +1,9 @@ from __future__ import annotations import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids @@ -88,11 +88,12 @@ def mesh_shuffle_flip(mesh: Mesh) -> Mesh: def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: """ - Performs random flip actions on mesh darts. + Performs random actions on mesh darts. :param mesh: the mesh to work with + :param num_nodes: number nodes of the mesh :return: a mesh with randomly flipped darts. """ - nb_action_max = int(num_nodes*3) + nb_action_max = int(num_nodes) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 @@ -117,26 +118,4 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: return mesh -def old_mesh_shuffle(mesh: Mesh) -> Mesh: - """ - Performs random flip actions on mesh darts. - :param mesh: the mesh to work with - :return: a mesh with randomly flipped darts. - """ - nb_action = int(len(mesh.dart_info)/2) - nb_nodes = len(mesh.nodes) - for i in range(nb_action): - action = np.random.randint(1, 4) - i1 = np.random.randint(nb_nodes) - i2 = np.random.randint(nb_nodes) - if action == 1: - flip_edge_ids(mesh, i1, i2) - elif action == 2: - split_edge_ids(mesh, i1, i2) - elif action == 3: - collapse_edge_ids(mesh, i1, i2) - return mesh - - - diff --git a/model/reader.py b/mesh_model/reader.py similarity index 98% rename from model/reader.py rename to mesh_model/reader.py index d1d4e20..4828a74 100644 --- a/model/reader.py +++ b/mesh_model/reader.py @@ -1,6 +1,6 @@ import string -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh import Mesh def read_medit(filename: string) -> Mesh: diff --git a/model_RL/AC_model.py b/model_RL/AC_model.py index fe11daa..f48582f 100644 --- a/model_RL/AC_model.py +++ b/model_RL/AC_model.py @@ -23,7 +23,7 @@ def __init__(self, env, lr, gamma, nb_episodes): def train(self) -> [Actor, list, list, list]: """ - Train the model over nb episodes. Both Actor and Critic networks are updated at the end of each episode. + Train the mesh_model over nb episodes. Both Actor and Critic networks are updated at the end of each episode. :return: the final actor policy, rewards history, wins history and number of steps history """ rewards = [] diff --git a/model_RL/PPO_model.py b/model_RL/PPO_model.py index d0b9258..533856e 100644 --- a/model_RL/PPO_model.py +++ b/model_RL/PPO_model.py @@ -1,5 +1,5 @@ from model_RL.utilities.actor_critic_networks import NaNExceptionActor, NaNExceptionCritic, Actor, Critic -from model.mesh_analysis import global_score +from mesh_model.mesh_analysis import global_score import copy import torch import random @@ -69,7 +69,7 @@ def train_epoch(self, dataset): def train(self): """ - Train the PPO model + Train the PPO mesh_model :return: the actor policy, training rewards, training wins, len of episodes """ rewards = [] diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 700ce44..21542a8 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,8 +1,8 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from model.mesh_analysis import global_score, get_boundary_darts -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score, get_boundary_darts +from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy from tqdm import tqdm diff --git a/model_RL/utilities/actor_critic_networks.py b/model_RL/utilities/actor_critic_networks.py index 6cf8de0..ccbb779 100644 --- a/model_RL/utilities/actor_critic_networks.py +++ b/model_RL/utilities/actor_critic_networks.py @@ -3,7 +3,7 @@ import torch.nn as nn from torch.optim import Adam from torch.distributions import Categorical -from model.mesh_analysis import isValidAction +from mesh_model.mesh_analysis import isValidAction class NaNExceptionActor(Exception): diff --git a/model_RL/utilities/nnPolicy.py b/model_RL/utilities/nnPolicy.py index 53d8874..643049d 100644 --- a/model_RL/utilities/nnPolicy.py +++ b/model_RL/utilities/nnPolicy.py @@ -3,7 +3,7 @@ import torch.nn as nn from torch.optim import Adam from torch.distributions import Categorical -from model.mesh_analysis_old import isValidAction +from mesh_model.mesh_analysis_old import isValidAction class NaNException(Exception): diff --git a/plots/mesh_plotter.py b/plots/mesh_plotter.py index 8dc9352..2ddda3f 100644 --- a/plots/mesh_plotter.py +++ b/plots/mesh_plotter.py @@ -1,6 +1,6 @@ import matplotlib.pyplot as plt -from model.mesh_struct.mesh_elements import Dart -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.mesh_struct.mesh import Mesh import numpy as np diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index 72501ea..ab845d0 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,6 +1,6 @@ import unittest -import model.mesh_struct.mesh as mesh -from model.mesh_struct.mesh_elements import Dart, Node, Face +import mesh_model.mesh_struct.mesh as mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face import numpy.testing from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_mesh_analysis.py b/test_modules/test_mesh_analysis.py index a28321a..5710b67 100644 --- a/test_modules/test_mesh_analysis.py +++ b/test_modules/test_mesh_analysis.py @@ -1,8 +1,8 @@ import unittest -from model.mesh_struct.mesh import Mesh -from model.mesh_struct.mesh_elements import Dart -import model.mesh_analysis as Mesh_analysis +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart +import mesh_model.mesh_analysis as Mesh_analysis from actions.triangular_actions import split_edge_ids diff --git a/test_modules/test_mesh_structure.py b/test_modules/test_mesh_structure.py index 6828fe0..5a48163 100644 --- a/test_modules/test_mesh_structure.py +++ b/test_modules/test_mesh_structure.py @@ -1,5 +1,5 @@ import unittest -import model.mesh_struct.mesh as mesh +import mesh_model.mesh_struct.mesh as mesh import numpy.testing diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 9c87657..ebe3cf7 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -1,6 +1,6 @@ import unittest -from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle -from model.mesh_struct.mesh import Mesh +from mesh_model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle +from mesh_model.mesh_struct.mesh import Mesh from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_reader.py b/test_modules/test_reader.py index d9a2184..0302829 100644 --- a/test_modules/test_reader.py +++ b/test_modules/test_reader.py @@ -1,6 +1,6 @@ import unittest -from model.reader import read_medit -from model.reader import read_gmsh +from mesh_model.reader import read_medit +from mesh_model.reader import read_gmsh import os TESTFILE_FOLDER = os.path.join(os.path.dirname(__file__), '../mesh_files/') diff --git a/train.py b/train.py index c1e619f..c6a8c73 100644 --- a/train.py +++ b/train.py @@ -1,4 +1,4 @@ -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM import torch from environment.trimesh_env import TriMesh @@ -30,12 +30,12 @@ def train(): # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=8, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=7, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) - torch.save(actor.state_dict(), 'policy saved/actor_network.pth') + torch.save(actor.state_dict(), 'policy_saved/actor_network.pth') avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 60) if rewards is not None: diff --git a/user_game.py b/user_game.py index 558e628..aea31bb 100644 --- a/user_game.py +++ b/user_game.py @@ -1,6 +1,6 @@ from view.window import Game -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM from mesh_display import MeshDisplay From ffb7c98ba71c370313e38ae19ff9904087f8ab74 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 11:07:12 +0100 Subject: [PATCH 07/22] Fixed some Codacy issues --- environment/trimesh_env.py | 4 +--- exploit.py | 4 +--- main.py | 8 +++----- mesh_model/mesh_analysis.py | 6 +++--- model_RL/evaluate_model.py | 3 +-- test_modules/test_actions.py | 2 +- test_modules/test_random_trimesh.py | 4 ++-- 7 files changed, 12 insertions(+), 19 deletions(-) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 6475f98..ea8167f 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,7 +1,7 @@ from typing import Any import math import numpy as np -from mesh_model.mesh_analysis import global_score, isValidAction, find_template_opposite_node +from mesh_model.mesh_analysis import global_score, find_template_opposite_node from mesh_model.mesh_struct.mesh_elements import Dart from mesh_model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge, split_edge, collapse_edge @@ -83,11 +83,9 @@ def get_x_global_4(env, state: Mesh): template = get_template_2(mesh) darts_to_delete = [] darts_id = [] - all_action_type = 3 for i, d_info in enumerate(mesh.active_darts()): d_id = d_info[0] - d = Dart(mesh, d_id) if d_info[2] == -1: #test the validity of all action type darts_to_delete.append(i) else: diff --git a/exploit.py b/exploit.py index fa8cdf8..208c8f6 100644 --- a/exploit.py +++ b/exploit.py @@ -3,13 +3,11 @@ from environment.trimesh_env import TriMesh from model_RL.utilities.actor_critic_networks import Actor -from plots.create_plots import plot_training_results, plot_test_results +from plots.create_plots import plot_test_results from plots.mesh_plotter import plot_dataset from model_RL.evaluate_model import testPolicy -from model_RL.PPO_model import PPO - LOCAL_MESH_FEAT = 0 diff --git a/main.py b/main.py index 077c326..21646c9 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,10 @@ import sys from user_game import user_game -from train import train +#from train import train from exploit import exploit -from mesh_model.reader import read_gmsh -from view.window import Game -from mesh_display import MeshDisplay -from mesh_model.random_trimesh import random_mesh +#from mesh_model.reader import read_gmsh +#from mesh_display import MeshDisplay # Press the green button in the gutter to run the script. diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index 34e67a0..73fc21c 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -75,7 +75,7 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: cos_theta = np.clip(cos_theta, -1, 1) angle = np.arccos(cos_theta) if np.isnan(angle): - raise(ValueError("Angle error")) + raise ValueError("Angle error") return degrees(angle) @@ -345,7 +345,7 @@ def isSplitOk(d: Dart) -> bool: def isCollapseOk(d: Dart) -> bool: mesh = d.mesh - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + _, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) d112 = d11.get_beta(2) d12 = d1.get_beta(2) @@ -417,7 +417,7 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float :return: True if valid, False otherwise """ for f in faces: - d, d1, d11, A, B, C = f.get_surrounding() + _, _, _, A, B, C = f.get_surrounding() if A.id == n_id: vect_AB = (B.x() - new_x, B.y() - new_y) vect_AC = (C.x() - new_x, C.y() - new_y) diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 21542a8..a433b16 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,7 +1,7 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from mesh_model.mesh_analysis import global_score, get_boundary_darts +from mesh_model.mesh_analysis import global_score from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy @@ -39,7 +39,6 @@ def testPolicy( if action is None: env.terminal = True break - boundary_darts = get_boundary_darts(env.mesh) env.step(action) ep_rewards += env.reward ep_length += 1 diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index ab845d0..a922ba0 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,6 +1,6 @@ import unittest import mesh_model.mesh_struct.mesh as mesh -from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh_elements import Dart, Node import numpy.testing from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index ebe3cf7..a529f79 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -16,7 +16,7 @@ def test_regular_trimesh(self): self.assertEqual(m.nb_nodes(), 60) def test_random_trimesh(self): - for i in range(10): + for _ in range(10): m = random_mesh(30) self.assertIsInstance(m, Mesh) @@ -30,7 +30,7 @@ def test_random_flip_mesh(self): def test_mesh_suffle(self): m = regular_mesh(15) - mesh = mesh_shuffle(m) + mesh = mesh_shuffle(m, 15) plot_mesh(mesh) From da9529f72e414dadbabf58efb87b307d008d77eb Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 13:38:01 +0100 Subject: [PATCH 08/22] Fixed Codacy complexity issue --- actions/triangular_actions.py | 44 +++++++---------------------------- main.py | 5 ++-- mesh_model/mesh_analysis.py | 22 ++++++++++++++++-- test_modules/test_actions.py | 1 - 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 6bff48f..a357131 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts +from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts, isSplitOk def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -17,9 +17,6 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - test_degree(n3) - test_degree(n4) - f1 = d.get_face() f2 = d2.get_face() @@ -54,16 +51,11 @@ def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found: + if not found or not isFlipOk(d): return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - if not test_degree(n3): - return False - elif not test_degree(n4): - return False - # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) @@ -98,10 +90,6 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - elif not test_degree(n1): - return False - elif not test_boundary(n1, n2): - return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -149,7 +137,6 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: ds = (ds2.get_beta(1)).get_beta(1) ds2 = ds.get_beta(2) - #update beta2 relations if d112 is not None: d112.set_beta(2, d12) @@ -164,23 +151,7 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) - #check for duplicate darts - if not check_double(mesh): - raise ValueError("double error") - if not check_beta2_relation(mesh): - raise ValueError("error beta2") - return True - -def test_degree(n: Node) -> bool: - """ - Verify that the degree of a vertex is lower than 10 - :param n: a Node - :return: True if the degree is lower than 10, False otherwise - """ - if degree(n) > 10: - return False - else: - return True + return mesh_check(mesh) def test_boundary(n1: Node, n2: Node) -> bool: @@ -203,9 +174,9 @@ def check_beta2_relation(mesh: Mesh) -> bool: d = dart_info[0] d2 = dart_info[2] if d2 >= 0 and mesh.dart_info[d2, 0] < 0: - return False + raise ValueError("error beta2") elif d2 >= 0 and mesh.dart_info[d2, 2] != d: - return False + raise ValueError("error beta2") return True @@ -231,7 +202,10 @@ def check_double(mesh: Mesh) -> bool: ns2 = ds2.get_node().id if n1 == ns1 and n2 == ns2: - return False + raise ValueError("double error") elif n2 == ns1 and n1 == ns2: return False return True + +def mesh_check(mesh: Mesh) -> bool: + return check_double(mesh) and check_beta2_relation(mesh) \ No newline at end of file diff --git a/main.py b/main.py index 21646c9..ad9cd7b 100644 --- a/main.py +++ b/main.py @@ -14,9 +14,10 @@ user_game(int(sys.argv[1])) else: exploit() - """ + +""" cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() - """ +""" \ No newline at end of file diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index 73fc21c..ebba650 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -278,7 +278,10 @@ def isFlipOk(d: Dart) -> bool: if d.get_beta(2) is None: return False else: - d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) + + if not test_degree(A) or not test_degree(B): + return False # Check angle at d limits to avoid edge reversal angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) @@ -310,6 +313,9 @@ def isSplitOk(d: Dart) -> bool: else: d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + if not test_degree(A) or not test_degree(B): + return False + newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 #Check if new triangle will be valid @@ -345,7 +351,7 @@ def isSplitOk(d: Dart) -> bool: def isCollapseOk(d: Dart) -> bool: mesh = d.mesh - _, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) d112 = d11.get_beta(2) d12 = d1.get_beta(2) @@ -359,6 +365,8 @@ def isCollapseOk(d: Dart) -> bool: return False elif on_boundary(n1) or on_boundary(n2): return False + elif not test_degree(n1): + return False else: # search for all adjacent faces to n1 and n2 if d12 is None and d2112 is None: @@ -478,3 +486,13 @@ def angle_from_sides(a, b, c): raise ValueError("Math domain error : cos>1.01") return acos(cosA) +def test_degree(n: Node) -> bool: + """ + Verify that the degree of a vertex is lower than 10 + :param n: a Node + :return: True if the degree is lower than 10, False otherwise + """ + if degree(n) > 10: + return False + else: + return True \ No newline at end of file diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index a922ba0..28e70ca 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,7 +1,6 @@ import unittest import mesh_model.mesh_struct.mesh as mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -import numpy.testing from plots.mesh_plotter import plot_mesh from actions.triangular_actions import split_edge, flip_edge, collapse_edge From 3a42ebdfbe5499bd2d21d92ad4dc0c734d338979 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 13:46:19 +0100 Subject: [PATCH 09/22] Fixed Codacy issues --- actions/triangular_actions.py | 6 +++--- main.py | 11 +++++------ mesh_model/mesh_analysis.py | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index a357131..1e2c7b4 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts, isSplitOk +from mesh_model.mesh_analysis import isFlipOk, isCollapseOk, adjacent_darts, isSplitOk def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -51,10 +51,10 @@ def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not isFlipOk(d): + if not found or not isSplitOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d2, d1, _, d21, _, n1, n2, n3, n4 = mesh.active_triangles(d) # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) diff --git a/main.py b/main.py index ad9cd7b..8176b0e 100644 --- a/main.py +++ b/main.py @@ -15,9 +15,8 @@ else: exploit() -""" - cmap = read_gmsh("mesh_files/irr_losange.msh") - mesh_disp = MeshDisplay(cmap) - g = Game(cmap, mesh_disp) - g.run() -""" \ No newline at end of file + + #cmap = read_gmsh("mesh_files/irr_losange.msh") + #mesh_disp = MeshDisplay(cmap) + #g = Game(cmap, mesh_disp) + #g.run() diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index ebba650..dfb8a21 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -311,7 +311,7 @@ def isSplitOk(d: Dart) -> bool: if d.get_beta(2) is None: return False else: - d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) if not test_degree(A) or not test_degree(B): return False From 275e6b0008ad8b764a23b6aefc7c4f4228180883 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 16:26:21 +0100 Subject: [PATCH 10/22] Fixed Codacy issues --- actions/triangular_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 1e2c7b4..d69ab87 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -91,8 +91,8 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - + d2, d1, dbeta1, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d11 = dbeta1 #T1 d212 = d21.get_beta(2) From 11a13b0f8d3242f1acc1d518ed08744b0a1de537 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 16:33:13 +0100 Subject: [PATCH 11/22] Fix Codacy issues --- actions/triangular_actions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index d69ab87..3df7fe3 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -91,19 +91,12 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - d2, d1, dbeta1, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - d11 = dbeta1 - #T1 - d212 = d21.get_beta(2) + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) - #T2 - d2112 = d211.get_beta(2) - - #T3 - d12 = d1.get_beta(2) - - #T4 - d112 = d11.get_beta(2) + d212 = d21.get_beta(2) #T1 + d2112 = d211.get_beta(2) #T2 + d12 = d1.get_beta(2) #T3 + d112 = d11.get_beta(2) #T4 #Delete the darts around selected dart mesh.del_adj_triangles(d) From 85880013035f7c3bf4fef45a3262d54f8b2dc15a Mon Sep 17 00:00:00 2001 From: ropercha Date: Fri, 30 Aug 2024 16:42:57 +0200 Subject: [PATCH 12/22] New function collapse. Some issues remain --- actions/triangular_actions.py | 92 ++++++++++++++++++---- mesh_display.py | 14 ++-- model/mesh_analysis.py | 77 ++++++++++++++----- model/mesh_struct/mesh.py | 115 ++++++++++++++++++++++++---- model/mesh_struct/mesh_elements.py | 10 ++- model/random_trimesh.py | 12 +-- plots/mesh_plotter.py | 4 +- test_modules/test_actions.py | 110 ++++++++++++++++++++++++++ test_modules/test_mesh_structure.py | 54 ------------- test_modules/test_random_trimesh.py | 9 ++- user_game.py | 2 +- view/graph.py | 13 ++-- view/window.py | 7 +- 13 files changed, 392 insertions(+), 127 deletions(-) create mode 100644 test_modules/test_actions.py diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 1c458e7..837b3f2 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,9 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk +from model.mesh_analysis import degree, isFlipOk, isCollapseOk + +import numpy as np def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -15,7 +17,7 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isFlipOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = active_triangles(mesh, d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) test_degree(n3) test_degree(n4) @@ -57,7 +59,7 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found: return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = active_triangles(mesh, d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) test_degree(n3) test_degree(n4) @@ -86,24 +88,82 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: return True -def active_triangles(mesh: Mesh, d: Dart) -> tuple[Dart, Dart, Dart, Dart, Dart, Node, Node, Node, Node]: - """ - Return the darts and nodes around selected dart - :param mesh: the mesh - :param d: selected dart - :return: a tuple of darts and nodes - """ +def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: + return collapse_edge(mesh, Node(mesh, id1), Node(mesh, id2)) + + +def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: + found, d = mesh.find_inner_edge(n1, n2) + if not found or not isCollapseOk(d): + return False + + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + #T1 + d212 = d21.get_beta(2) + + #T2 + d2112 = d211.get_beta(2) + + #T3 + d12 = d1.get_beta(2) + + #T4 + d112 = d11.get_beta(2) + + #Delete the darts around selected dart + delete_triangles(mesh, d) + + #move n1 node in the middle of [n1, n2] + n1.set_xy((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) + + #update node relations + if d12 is not None: + d121 = d12.get_beta(1) + d121.set_node(n1) + ds = d121 + while ds is not None and ds != d2112: + d2s = ds.get_beta(2) + if d2s is None: + ds = d2112 + while ds is not None: + ds.set_node(n1) + ds1 = ds.get_beta(1) + ds11 = ds1.get_beta(1) + ds = ds11.get_beta(2) + else: + ds = d2s.get_beta(1) + ds.set_node(n1) + + #update beta2 relations + if d112 is not None: + d112.set_beta(2, d12) + if d12 is not None: + d12.set_beta(2, d112) + + if d212 is not None: + d212.set_beta(2, d2112) + if d2112 is not None: + d2112.set_beta(2, d212) + + #delete n2 node + mesh.del_node(n2) + + return True + +def delete_triangles(mesh: Mesh, d: Dart) -> None: d2 = d.get_beta(2) d1 = d.get_beta(1) d11 = d1.get_beta(1) d21 = d2.get_beta(1) d211 = d21.get_beta(1) - n1 = d.get_node() - n2 = d2.get_node() - n3 = d11.get_node() - n4 = d211.get_node() - - return d2, d1, d11, d21, d211, n1, n2, n3, n4 + + f1 = d.get_face() + f2 = d2.get_face() + + mesh.del_triangle(d, d1, d11, f1) + mesh.del_triangle(d2, d21, d211, f2) + def test_degree(n: Node) -> bool: diff --git a/mesh_display.py b/mesh_display.py index 1e09b67..d15247d 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -12,8 +12,9 @@ def get_nodes_coordinates(self): :return: a list of coordinates (x,y) """ node_list = [] - for n in self.mesh.nodes: - node_list.append((n[0], n[1])) + for idx, n in enumerate(self.mesh.nodes): + if n[2] >= 0 : + node_list.append((idx, n[0], n[1])) return node_list def get_edges(self): @@ -22,9 +23,9 @@ def get_edges(self): :return: a list of coordinates (x,y) """ edge_list = [] - for d in self.mesh.dart_info: + for d in self.mesh.active_darts(): n1_id = d[3] - n2_id = self.mesh.dart_info[d[1],3] + n2_id = self.mesh.dart_info[d[1], 3] if (d[2] != -1 and n1_id < n2_id) or d[2] == -1: edge_list.append((n1_id, n2_id)) return edge_list @@ -34,5 +35,6 @@ def get_scores(self): Calculates the irregularities of each node and the real and ideal score of the mesh :return: a list of three elements (nodes_score, mesh_score, ideal_mesh_score) """ - scores = global_score(self.mesh) - return scores + nodes_score, mesh_score, ideal_mesh_score = global_score(self.mesh) + nodes_score = [score for score in nodes_score if score is not None] + return [nodes_score, mesh_score, ideal_mesh_score] diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 2120603..c618a14 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -16,12 +16,14 @@ def global_score(m: Mesh) -> (int, int): mesh_score = 0 nodes_score = [] for i in range(len(m.nodes)): - n_id = i - node = Node(m, n_id) - n_score = score_calculation(node) - nodes_score.append(n_score) - mesh_ideal_score += n_score - mesh_score += abs(n_score) + if m.nodes[i, 2] >= 0: + n_id = i + node = Node(m, n_id) + n_score = score_calculation(node) + nodes_score.append(n_score) + mesh_ideal_score += n_score + mesh_score += abs(n_score) + nodes_score.append(None) return nodes_score, mesh_score, mesh_ideal_score @@ -82,7 +84,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) > 3: + if len(boundary_darts) >= 4: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -109,7 +111,7 @@ def adjacent_darts(n: Node) -> list[Dart]: :return: the list of adjacent darts """ adj_darts = [] - for d_info in n.mesh.dart_info: + for d_info in n.mesh.active_darts(): d = Dart(n.mesh, d_info[0]) d_nfrom = d.get_node() d_nto = d.get_beta(1) @@ -138,6 +140,8 @@ def degree(n: Node) -> int: else: adjacency += 0.5 if adjacency != int(adjacency): + print(adjacency) + print(n.id) raise ValueError("Adjacency error") return adjacency @@ -149,7 +153,7 @@ def get_boundary_darts(m: Mesh) -> list[Dart]: :return: a list of all boundary darts """ boundary_darts = [] - for d_info in m.dart_info: + for d_info in m.active_darts(): d = Dart(m, d_info[0]) d_twin = d.get_beta(2) if d_twin is None : @@ -164,11 +168,11 @@ def get_boundary_nodes(m: Mesh) -> list[Node]: :return: a list of all boundary nodes """ boundary_nodes = [] - nb_nodes = len(m.nodes) - for n_id in range(0, nb_nodes): - n = Node(m, n_id) - if on_boundary(n): - boundary_nodes.append(n) + for n_id in range(0, len(m.nodes)): + if m.nodes[n_id, 2] >= 0: + n = Node(m, n_id) + if on_boundary(n): + boundary_nodes.append(n) return boundary_nodes @@ -220,16 +224,26 @@ def node_in_mesh(mesh: Mesh, x: float, y: float) -> (bool, int): """ n_id = 0 for n in mesh.nodes: - if abs(x - n[0]) <= 0.1 and abs(y - n[1]) <= 0.1: - return True, n_id - n_id = n_id + 1 + if n[2] >= 0 : + if abs(x - n[0]) <= 0.1 and abs(y - n[1]) <= 0.1: + return True, n_id + n_id += 1 return False, None -def isValidAction(mesh, dart_id: int) -> bool: +def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: + flip = 0 + split = 1 + collapse = 2 d = Dart(mesh, dart_id) boundary_darts = get_boundary_darts(mesh) - if d in boundary_darts or not isFlipOk(d): + if d in boundary_darts: + return False + elif action == flip and isFlipOk(d) is not True: + return False + elif action == split and isFlipOk(d) is not True: + return False + elif action == collapse and isCollapseOk(d) is not True: return False else: return True @@ -245,7 +259,7 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: return deg -def isFlipOk(d:Dart) -> bool: +def isFlipOk(d: Dart) -> bool: d1 = d.get_beta(1) d11 = d1.get_beta(1) A = d.get_node() @@ -267,3 +281,26 @@ def isFlipOk(d:Dart) -> bool: return False else: return True + + +def isCollapseOk(d: Dart) -> bool: + mesh = d.mesh + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + if d112 is None and d12 is None: + return False + elif d212 is None and d2112 is None: + return False + elif d212 is None and d12 is None: + return False + elif d112 is None and d2112 is None: + return False + else: + return True + diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index b54a444..d8225a3 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -20,6 +20,9 @@ def __init__(self, nodes=[], faces=[]): self.nodes = numpy.empty((0, 3)) self.faces = numpy.empty(0, dtype=int) self.dart_info = numpy.empty((0, 5), dtype=int) + self.first_free_dart = 1 + self.first_free_node = 1 + self.first_free_face = 1 for n in nodes: self.add_node(n[0], n[1]) @@ -46,14 +49,14 @@ def nb_nodes(self) -> int: :return: the number of vertices in the mesh """ # We filter the vertices having the x-coordinate equals to max float. Such vertices were removed - return len(self.nodes[self.nodes[:, 0] != sys.float_info.max]) + return len(self.active_nodes()) def nb_faces(self) -> int: """ :return: the number of faces in the mesh """ # We filter the faces having the -1 value. An item with this value is a deleted face - return len(self.faces[self.faces[:] != -1]) + return len(self.active_faces()) def add_node(self, x: float, y: float) -> Node: """ @@ -62,10 +65,28 @@ def add_node(self, x: float, y: float) -> Node: :param y: Y coordinate :return: the created node """ - self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) - return Node(self, len(self.nodes) - 1) - - def del_vertex(self, ni: int) -> None: + if len(self.nodes) < self.first_free_node: + self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) + self.first_free_node += 1 + return Node(self, len(self.nodes) - 1) + elif self.first_free_node >= 0: + n_id = int(self.first_free_node) + if isinstance(n_id, int): + self.first_free_node = abs(self.nodes[n_id, 2] + 1 ) + self.nodes[n_id] = [x, y, -1] + else: + print(n_id) + print(type(n_id)) + raise ValueError("n_id not integer") + return Node(self, n_id) + else: + raise ValueError("Try to add a node outside the array") + + def del_node(self, n: Node) -> None: + self.nodes[n.id, 2] = -self.first_free_node - 1 + self.first_free_node = n.id + + def del_vertex(self, ni: Node) -> None: """ Removes the node ni. Warning all the darts that point to this node will be invalid (but not automatically updated) @@ -96,14 +117,32 @@ def add_triangle(self, n1: Node, n2: Node, n3: Node) -> Face: darts[k].set_node(nodes[k]) nodes[k].set_dart(darts[k]) - self.faces = numpy.append(self.faces, [darts[0].id]) - tri = Face(self, len(self.faces)-1) + if len(self.faces) < self.first_free_face: + self.faces = numpy.append(self.faces, [darts[0].id]) + self.first_free_face += 1 + tri = Face(self, len(self.faces) - 1) + elif self.first_free_face >= 0: + f_id = self.first_free_face + self.first_free_face = abs(self.faces[f_id]+1) + self.faces[f_id] = darts[0].id + tri = Face(self, f_id) + else: + raise ValueError("Try to add a node outside the array") for d in darts: d.set_face(tri) return tri + def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: + self.del_dart(d1) + self.del_dart(d2) + self.del_dart(d3) + + self.faces[f.id] = -self.first_free_face -1 + self.first_free_face = f.id + + def add_quad(self, n1: Node, n2: Node, n3: Node, n4: Node) -> Face: """ Add a quad defined by nodes of indices n1, n2, n3 and n4. @@ -141,7 +180,7 @@ def set_twin_pointers(self) -> None: """ This function search for the inner darts to connect and connect them with beta2. """ - for d_info in self.dart_info: + for d_info in self.active_darts(): d = Dart(self, d_info[0]) if d.get_beta(2) is None: # d is not 2-sew, we look for a dart to connect. If we don'f find one, @@ -150,7 +189,7 @@ def set_twin_pointers(self) -> None: d_nfrom = d.get_node() d_nto = d.get_beta(1).get_node() - for d2_info in self.dart_info: + for d2_info in self.active_darts(): d2 = Dart(self, d2_info[0]) if d2.get_beta(2) is None: d2_nfrom = d2.get_node() @@ -168,7 +207,7 @@ def find_inner_edge(self, n1: Node, n2: Node) -> (bool, Dart): :param n2: Second node :return: the inner dart connecting n1 to n2 if it exists """ - for d_info in self.dart_info: + for d_info in self.active_darts(): d = Dart(self, d_info[0]) d2 = d.get_beta(2) if d2 is not None: @@ -208,8 +247,27 @@ def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1) -> Dart :param v: vertex index this dart point to :return: the created dart """ - self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) - return Dart(self, len(self.dart_info) - 1) + if len(self.dart_info) < self.first_free_dart: + self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) + self.first_free_dart += 1 + return Dart(self, len(self.dart_info) - 1) + elif len(self.dart_info) > self.first_free_dart: + next_free_dart = abs(self.dart_info[self.first_free_dart][0]+1) + dart_id = self.first_free_dart + self.dart_info[dart_id][0] = dart_id + self.dart_info[dart_id][1] = a1 + self.dart_info[dart_id][2] = a2 + self.dart_info[dart_id][3] = v + self.dart_info[dart_id][4] = f + self.first_free_dart = next_free_dart + return Dart(self, dart_id) + else: + raise IndexError('Dart index out of range') + + def del_dart(self, d: Dart): + self.dart_info[d.id][0] = -self.first_free_dart - 1 + self.first_free_dart = d.id + def set_beta2(self, dart: Dart) -> None: """ @@ -218,7 +276,7 @@ def set_beta2(self, dart: Dart) -> None: """ dart_nfrom = dart.get_node() dart_nto = dart.get_beta(1) - for d_info in dart.mesh.dart_info: + for d_info in self.active_darts(): d = Dart(dart.mesh, d_info[0]) d_nfrom = d.get_node() d_nto = d.get_beta(1) @@ -226,3 +284,32 @@ def set_beta2(self, dart: Dart) -> None: d.set_beta(2, dart) dart.set_beta(2, d) + def active_nodes(self): + return self.nodes[self.nodes[:, 2] >= 0] + + def active_darts(self): + return self.dart_info[self.dart_info[:, 0] >= 0] + + def active_faces(self): + return self.faces[self.faces[:] >= 0] + + def active_triangles(self, d: Dart) -> tuple[Dart, Dart, Dart, Dart, Dart, Node, Node, Node, Node]: + """ + Return the darts and nodes around selected dart + :param mesh: the mesh + :param d: selected dart + :return: a tuple of darts and nodes + """ + d2 = d.get_beta(2) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d21 = d2.get_beta(1) + d211 = d21.get_beta(1) + n1 = d.get_node() + n2 = d2.get_node() + n3 = d11.get_node() + n4 = d211.get_node() + + return d2, d1, d11, d21, d211, n1, n2, n3, n4 + + diff --git a/model/mesh_struct/mesh_elements.py b/model/mesh_struct/mesh_elements.py index 4b02e71..4e857c1 100644 --- a/model/mesh_struct/mesh_elements.py +++ b/model/mesh_struct/mesh_elements.py @@ -27,7 +27,10 @@ def __eq__(self, a_dart: Dart) -> bool: :param a_dart: another dart :return: true if the darts are equal, false otherwise """ - return self.mesh == a_dart.mesh and self.id == a_dart.id + if a_dart is None: + return False + else: + return self.mesh == a_dart.mesh and self.id == a_dart.id def get_beta(self, i: int) -> Dart: """ @@ -52,7 +55,10 @@ def set_beta(self, i: int, dart_to: Dart) -> None: """ if i < 1 or i > 2: raise ValueError("Wrong alpha dimension") - self.mesh.dart_info[self.id, i] = dart_to.id + elif dart_to is None: + self.mesh.dart_info[self.id, i] = -1 + else: + self.mesh.dart_info[self.id, i] = dart_to.id def get_node(self) -> Node: """ diff --git a/model/random_trimesh.py b/model/random_trimesh.py index aaddac6..6e322f9 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -4,7 +4,7 @@ from model.mesh_struct.mesh_elements import Dart, Node from model.mesh_struct.mesh import Mesh from model.mesh_analysis import find_opposite_node, node_in_mesh -from actions.triangular_actions import flip_edge_ids, split_edge_ids +from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids def regular_mesh(num_nodes_max: int) -> Mesh: @@ -91,16 +91,18 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_flip = len(mesh.dart_info) - nb_action =nb_flip * 2 + nb_action = len(mesh.dart_info) nb_nodes = len(mesh.nodes) for i in range(nb_action): + action = np.random.randint(1, 4) i1 = np.random.randint(nb_nodes) i2 = np.random.randint(nb_nodes) - if i1 != i2 and i%2 == 0: + if action == 1: flip_edge_ids(mesh, i1, i2) - elif i1 !=i2 : + elif action == 2: split_edge_ids(mesh, i1, i2) + elif action == 3: + collapse_edge_ids(mesh, i1, i2) return mesh diff --git a/plots/mesh_plotter.py b/plots/mesh_plotter.py index 6bad764..8dc9352 100644 --- a/plots/mesh_plotter.py +++ b/plots/mesh_plotter.py @@ -19,8 +19,8 @@ def subplot_mesh(mesh: Mesh) -> None: Plot a mesh using matplotlib for subplots with many meshes :param mesh: a Mesh """ - faces = mesh.faces - nodes = mesh.nodes + faces = mesh.active_faces() + nodes = mesh.active_nodes() nodes = np.array([list[:2] for list in nodes]) for dart_id in faces: diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py new file mode 100644 index 0000000..72501ea --- /dev/null +++ b/test_modules/test_actions.py @@ -0,0 +1,110 @@ +import unittest +import model.mesh_struct.mesh as mesh +from model.mesh_struct.mesh_elements import Dart, Node, Face +import numpy.testing +from plots.mesh_plotter import plot_mesh + +from actions.triangular_actions import split_edge, flip_edge, collapse_edge + +class TestActions(unittest.TestCase): + + def test_flip(self): + cmap = mesh.Mesh() + n00 = cmap.add_node(0, 0) + n01 = cmap.add_node(0, 1) + n10 = cmap.add_node(1, 0) + n11 = cmap.add_node(1, 1) + + t1 = cmap.add_triangle(n00, n10, n11) + t2 = cmap.add_triangle(n00, n11, n01) + + d1 = t1.get_dart() + # d1 goes from n00 to n10 + self.assertEqual(d1.get_node(), n00) + d1 = d1.get_beta(1).get_beta(1) + # now d1 goes from n11 to n00 + self.assertEqual(d1.get_node(), n11) + + d2 = t2.get_dart() # goes from n00 to n11 + self.assertEqual(d2.get_node(), n00) + # We sew on both directions + d1.set_beta(2, d2) + d2.set_beta(2, d1) + + flip_edge(cmap, n00, n11) + self.assertEqual(2, cmap.nb_faces()) + self.assertEqual(4, cmap.nb_nodes()) + + def test_split(self): + cmap = mesh.Mesh() + n00 = cmap.add_node(0, 0) + n01 = cmap.add_node(0, 1) + n10 = cmap.add_node(1, 0) + n11 = cmap.add_node(1, 1) + + t1 = cmap.add_triangle(n00, n10, n11) + t2 = cmap.add_triangle(n00, n11, n01) + + split_edge(cmap, n00, n11) + d1 = t1.get_dart() + # d1 goes from n00 to n10 + self.assertEqual(d1.get_node(), n00) + d1 = d1.get_beta(1).get_beta(1) + # now d1 goes from n11 to n00 + self.assertEqual(d1.get_node(), n11) + + d2 = t2.get_dart() # goes from n00 to n11 + self.assertEqual(d2.get_node(), n00) + # We sew on both directions + d1.set_beta(2, d2) + d2.set_beta(2, d1) + + def test_collapse(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] + faces = [[0, 1, 2], [0, 2, 3]] + cmap = mesh.Mesh(nodes, faces) + n00 = Node(cmap, 0) + n11 = Node(cmap, 2) + split_edge(cmap, n00, n11) + n5 = Node(cmap, 4) + plot_mesh(cmap) + collapse_edge(cmap, n00, n5) + d1_to_test = Dart(cmap, 7) + d2_to_test = Dart(cmap, 0) + self.assertEqual(d1_to_test.get_beta(2), None) + self.assertEqual(d2_to_test.get_beta(2), None) + plot_mesh(cmap) + + def test_split_collapse_split(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] + faces = [[0, 1, 2], [0, 2, 3]] + cmap = mesh.Mesh(nodes, faces) + n0 = Node(cmap, 0) + n1 = Node(cmap, 1) + n2 = Node(cmap, 2) + n3 = Node(cmap, 3) + split_edge(cmap, n0, n2) + n4 = Node(cmap, 4) + collapse_edge(cmap, n0, n4) + split_edge(cmap, n0, n2) + n5 = Node(cmap, 5) + collapse_edge(cmap, n0, n5) + split_edge(cmap, n4, n2) + collapse_edge(cmap, n4, n5) + collapse_edge(cmap, n2, n4) + split_edge(cmap, n0, n2) + split_edge(cmap, n0, n4) + split_edge(cmap, n4, n3) + split_edge(cmap, n4, n1) + split_edge(cmap, n5, n1) + plot_mesh(cmap) + n7 = Node(cmap, 7) + n8 = Node(cmap, 8) + collapse_edge(cmap, n7, n8) + plot_mesh(cmap) + collapse_edge(cmap, n5, n7) + plot_mesh(cmap) + + + + diff --git a/test_modules/test_mesh_structure.py b/test_modules/test_mesh_structure.py index 2d49484..6828fe0 100644 --- a/test_modules/test_mesh_structure.py +++ b/test_modules/test_mesh_structure.py @@ -2,8 +2,6 @@ import model.mesh_struct.mesh as mesh import numpy.testing -from actions.triangular_actions import split_edge, flip_edge - class TestMeshStructure(unittest.TestCase): @@ -85,58 +83,6 @@ def test_single_quad(self): self.assertEqual(n3, nodes_of_t[2]) self.assertEqual(n4, nodes_of_t[3]) - def test_flip(self): - cmap = mesh.Mesh() - n00 = cmap.add_node(0, 0) - n01 = cmap.add_node(0, 1) - n10 = cmap.add_node(1, 0) - n11 = cmap.add_node(1, 1) - - t1 = cmap.add_triangle(n00, n10, n11) - t2 = cmap.add_triangle(n00, n11, n01) - - d1 = t1.get_dart() - # d1 goes from n00 to n10 - self.assertEqual(d1.get_node(), n00) - d1 = d1.get_beta(1).get_beta(1) - # now d1 goes from n11 to n00 - self.assertEqual(d1.get_node(), n11) - - d2 = t2.get_dart() # goes from n00 to n11 - self.assertEqual(d2.get_node(), n00) - # We sew on both directions - d1.set_beta(2, d2) - d2.set_beta(2, d1) - - flip_edge(cmap, n00, n11) - self.assertEqual(2, cmap.nb_faces()) - self.assertEqual(4, cmap.nb_nodes()) - - def test_split(self): - cmap = mesh.Mesh() - n00 = cmap.add_node(0, 0) - n01 = cmap.add_node(0, 1) - n10 = cmap.add_node(1, 0) - n11 = cmap.add_node(1, 1) - - t1 = cmap.add_triangle(n00, n10, n11) - t2 = cmap.add_triangle(n00, n11, n01) - - d1 = t1.get_dart() - # d1 goes from n00 to n10 - self.assertEqual(d1.get_node(), n00) - d1 = d1.get_beta(1).get_beta(1) - # now d1 goes from n11 to n00 - self.assertEqual(d1.get_node(), n11) - - d2 = t2.get_dart() # goes from n00 to n11 - self.assertEqual(d2.get_node(), n00) - # We sew on both directions - d1.set_beta(2, d2) - d2.set_beta(2, d1) - - split_edge(cmap, n00, n11) - if __name__ == '__main__': unittest.main() diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 09d249e..e7cf2b4 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -1,7 +1,9 @@ import unittest -from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh +from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle from model.mesh_struct.mesh import Mesh +from plots.mesh_plotter import plot_mesh + class TestRandomTrimesh(unittest.TestCase): @@ -29,6 +31,11 @@ def test_random_flip_mesh(self): m = random_flip_mesh(60) self.assertEqual(m.nb_nodes(), 60) + def test_mesh_suffle(self): + m = regular_mesh(30) + mesh = mesh_shuffle(m) + plot_mesh(mesh) + if __name__ == '__main__': diff --git a/user_game.py b/user_game.py index 470b51d..558e628 100644 --- a/user_game.py +++ b/user_game.py @@ -18,7 +18,7 @@ def user_game(mesh_size): g = Game(cmap, mesh_disp) g.run() """ - cmap = TM.random_mesh(mesh_size) + cmap = TM.regular_mesh(mesh_size) mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() diff --git a/view/graph.py b/view/graph.py index 4c9b0e2..e2eff08 100644 --- a/view/graph.py +++ b/view/graph.py @@ -18,7 +18,7 @@ def __init__(self, idx, x, y, value=0): self.selected = False self.color = vertex_color_normal self.obj = None - self.value = round(value,0) + self.value = round(value, 0) def switch_selection(self): if self.selected: @@ -111,9 +111,9 @@ def clear(self): def update(self, vertices, edges, scores): for idx, n in enumerate(vertices): - nodes_scores= scores[0] + nodes_scores = scores[0] n_value = nodes_scores[idx] - self.create_vertex(idx, n[0], n[1], n_value) + self.create_vertex(n[0], n[1], n[2], n_value) for e in edges: self.create_edge(e[0], e[1]) @@ -123,8 +123,11 @@ def create_vertex(self, id: int, x: int, y: int, n_value) -> int: return len(self.vertices) - 1 def create_edge(self, i1: int, i2: int) -> int: - n1 = self.vertices[i1] - n2 = self.vertices[i2] + for v in self.vertices: + if v.idx == i1: + n1 = v + elif v.idx == i2: + n2 = v self.add_edge(Edge(n1, n2)) return len(self.edges) - 1 diff --git a/view/window.py b/view/window.py index 9acea1a..93c4dbf 100644 --- a/view/window.py +++ b/view/window.py @@ -3,7 +3,7 @@ from pygame.locals import * from view import graph from mesh_display import MeshDisplay -from actions.triangular_actions import split_edge_ids, flip_edge_ids +from actions.triangular_actions import split_edge_ids, flip_edge_ids, collapse_edge_ids import sys color1 = pygame.Color(30, 30, 30) # Dark Grey @@ -97,6 +97,11 @@ def control_events(self): self.graph.clear() self.graph.update(self.mesh_disp.get_nodes_coordinates(),self.mesh_disp.get_edges(), self.mesh_disp.get_scores()) already_selected = True + elif pygame.key.get_pressed()[pygame.K_c]: + if collapse_edge_ids(self.model, e.start.idx, e.end.idx): + self.graph.clear() + self.graph.update(self.mesh_disp.get_nodes_coordinates(),self.mesh_disp.get_edges(), self.mesh_disp.get_scores()) + already_selected = True def run(self): print("TriGame is starting!!") From 4dd5ebd24667cf0a7b45fec939c5f7ebd4f8d0d9 Mon Sep 17 00:00:00 2001 From: ropercha Date: Tue, 3 Sep 2024 09:54:39 +0200 Subject: [PATCH 13/22] Clipping the cosinus of the theta angle to ensure it remains within valid bounds with rounding errors (boundary angle calculation). Incorporation of the PR changes to the gmsh reader --- environment/trimesh_env.py | 10 +++++----- main.py | 9 ++++++++- model/mesh_analysis.py | 29 ++++++++++++++++++++++++++--- model/random_trimesh.py | 4 ++-- model/reader.py | 2 +- test_modules/test_random_trimesh.py | 2 +- train.py | 6 +++--- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 412ae2d..2e2286f 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -4,7 +4,7 @@ from model.mesh_struct.mesh_elements import Dart from model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge -from model.random_trimesh import random_flip_mesh +from model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 @@ -14,7 +14,7 @@ class TriMesh: def __init__(self, mesh=None, mesh_size: int = None, max_steps: int = 50, feat: int = 0): self.mesh = mesh if mesh is not None else random_flip_mesh(mesh_size) - self.mesh_size = len(self.mesh.nodes) + self.mesh_size = len(self.mesh.active_nodes()) self.size = len(self.mesh.dart_info) self.actions = np.array([FLIP]) self.reward = 0 @@ -30,7 +30,7 @@ def reset(self, mesh=None): self.reward = 0 self.steps = 0 self.terminal = False - self.mesh = mesh if mesh is not None else random_flip_mesh(self.mesh_size) + self.mesh = mesh if mesh is not None else random_mesh(self.mesh_size) self.size = len(self.mesh.dart_info) self.nodes_scores = global_score(self.mesh)[0] self.ideal_score = global_score(self.mesh)[2] @@ -75,10 +75,10 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: """ mesh = state nodes_scores = global_score(mesh)[0] - size = len(mesh.dart_info) + size = len(mesh.active_darts()) template = np.zeros((size, 6)) - for d_info in mesh.dart_info: + for d_info in mesh.active_darts(): d = Dart(mesh, d_info[0]) A = d.get_node() diff --git a/main.py b/main.py index 21b2812..936120e 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,9 @@ from user_game import user_game from train import train +from model.reader import read_gmsh +from view.window import Game +from mesh_display import MeshDisplay # Press the green button in the gutter to run the script. @@ -10,4 +13,8 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - train() + #train() + cmap = read_gmsh("mesh_files/irr_losange.msh") + mesh_disp = MeshDisplay(cmap) + g = Game(cmap, mesh_disp) + g.run() diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index c618a14..bf491b3 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -68,7 +68,9 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: vect_AC = (C.x() - A.x(), C.y() - A.y()) dist_AB = sqrt(vect_AB[0]**2 + vect_AB[1]**2) dist_AC = sqrt(vect_AC[0]**2 + vect_AC[1]**2) - angle = np.arccos(np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC)) + cos_theta = np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC) + cos_theta = np.clip(cos_theta, -1, 1) + angle = np.arccos(cos_theta) return degrees(angle) @@ -84,7 +86,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) >= 4: + if len(boundary_darts) > 10: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -197,7 +199,8 @@ def find_opposite_node(d: Dart) -> (int, int): return x_C, y_C -def find_template_opposite_node(d: Dart) -> (int): + +def find_template_opposite_node(d: Dart) -> int: """ Find the the vertex opposite in the adjacent triangle :param d: a dart @@ -293,6 +296,24 @@ def isCollapseOk(d: Dart) -> bool: d212 = d21.get_beta(2) d2112 = d211.get_beta(2) + if d112 is None and d12 is None and d2112 is None and d212 is None: + return False + elif d112 is not None and d212 is not None: + if d12 is None and d2112 is None: + return True + elif d12 is not None: + ds = d12.get_beta(1) + while ds != d2112: + ds2 = ds.get_beta(2) + if ds2 is None: + return False + ds = ds2.get_beta(1) + return True + else: + return False + + """ + #Old condition if d112 is None and d12 is None: return False elif d212 is None and d2112 is None: @@ -303,4 +324,6 @@ def isCollapseOk(d: Dart) -> bool: return False else: return True + + """ diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 6e322f9..95fe3b9 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -66,7 +66,7 @@ def random_mesh(num_nodes_max: int) -> Mesh: :return: a random mesh """ mesh = regular_mesh(num_nodes_max) - mesh_shuffle_flip(mesh) + mesh_shuffle(mesh) return mesh @@ -91,7 +91,7 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action = len(mesh.dart_info) + nb_action = 2*len(mesh.dart_info) nb_nodes = len(mesh.nodes) for i in range(nb_action): action = np.random.randint(1, 4) diff --git a/model/reader.py b/model/reader.py index c9326dd..d1d4e20 100644 --- a/model/reader.py +++ b/model/reader.py @@ -103,7 +103,7 @@ def read_gmsh(filename: string) -> Mesh: faces.append([n0 - 1, n1 - 1, n2 - 1, n3 - 1]) elif elem_type == 1: # skip 2-node line elements continue - elif elem_type == 15: # skip 1-node point elements + elif elem_type == 15: # skip 1-node point elements continue else: print("element_type " + str(elem_type) + " not handled") diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index e7cf2b4..0f6bca8 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -32,7 +32,7 @@ def test_random_flip_mesh(self): self.assertEqual(m.nb_nodes(), 60) def test_mesh_suffle(self): - m = regular_mesh(30) + m = regular_mesh(40) mesh = mesh_shuffle(m) plot_mesh(mesh) diff --git a/train.py b/train.py index ae7c0ae..5ffb301 100644 --- a/train.py +++ b/train.py @@ -20,7 +20,7 @@ def train(): gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_flip_mesh(30) for _ in range(16)] + dataset = [TM.random_mesh(30) for _ in range(16)] plot_dataset(dataset) env = TriMesh(None, mesh_size, max_steps=30, feat=feature) @@ -30,7 +30,7 @@ def train(): # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=100, nb_epochs=1, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=50, nb_epochs=1, batch_size=8) actor, rewards, wins, steps = model.train() avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 10, dataset, 60) @@ -38,4 +38,4 @@ def train(): if rewards is not None: plot_training_results(rewards, wins, steps) plot_test_results(avg_rewards, avg_wins, avg_steps) - plot_dataset(final_meshes) \ No newline at end of file + plot_dataset(final_meshes) From a3db24db47d326ea87b083b47bcd712617e9eed9 Mon Sep 17 00:00:00 2001 From: ropercha Date: Sat, 7 Sep 2024 01:18:33 +0200 Subject: [PATCH 14/22] Working environment with collapse action --- actions/triangular_actions.py | 85 +++++++++- environment/trimesh_env.py | 92 +++++----- main.py | 4 +- mesh_display.py | 9 +- model/mesh_analysis.py | 176 ++++++++++++++++++-- model/mesh_struct/mesh.py | 17 +- model/mesh_struct/mesh_elements.py | 11 +- model/random_trimesh.py | 35 +++- model_RL/PPO_model.py | 27 +-- model_RL/evaluate_model.py | 12 +- model_RL/utilities/actor_critic_networks.py | 32 +++- test_modules/test_mesh_analysis.py | 5 +- test_modules/test_random_trimesh.py | 11 +- train.py | 14 +- view/graph.py | 4 +- 15 files changed, 413 insertions(+), 121 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 837b3f2..37dd919 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, isCollapseOk +from model.mesh_analysis import degree, isFlipOk, newIsCollapseOk, adjacent_darts import numpy as np @@ -60,8 +60,11 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - test_degree(n3) - test_degree(n4) + + if not test_degree(n3): + return False + elif not test_degree(n4): + return False # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) @@ -94,7 +97,12 @@ def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not isCollapseOk(d): + + if not found or not newIsCollapseOk(d): + return False + elif not test_degree(n1): + return False + elif not test_boundary(n1, n2): return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -134,6 +142,15 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: else: ds = d2s.get_beta(1) ds.set_node(n1) + elif d12 is None and d2112 is not None: + d2112.set_node(n1) + ds = (d2112.get_beta(1)).get_beta(1) + ds2 = ds.get_beta(2) + while ds2 is not None: + ds2.set_node(n1) + ds = (ds2.get_beta(1)).get_beta(1) + ds2 = ds.get_beta(2) + #update beta2 relations if d112 is not None: @@ -148,7 +165,10 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) - + if not check_double(mesh): + raise ValueError("double error") + if not check_beta2_relation(mesh): + raise ValueError("error beta2") return True def delete_triangles(mesh: Mesh, d: Dart) -> None: @@ -175,4 +195,57 @@ def test_degree(n: Node) -> bool: if degree(n) > 10: return False else: - return True \ No newline at end of file + return True + + +def test_boundary(n1: Node, n2: Node) -> bool: + boundary_darts_n1 = [] + boundary_darts_n2 = [] + for d in adjacent_darts(n1): + if d.get_beta(2) is None: + boundary_darts_n1.append(d) + for d in adjacent_darts(n2): + if d.get_beta(2) is None: + boundary_darts_n2.append(d) + if (len(boundary_darts_n1) + len(boundary_darts_n2)) > 3: + return False + else: + return True + +def check_beta2_relation(mesh: Mesh) -> bool: + for dart_info in mesh.active_darts(): + d = dart_info[0] + d2 = dart_info[2] + if d2 >= 0 and mesh.dart_info[d2, 0] < 0: + return False + elif d2 >= 0 and mesh.dart_info[d2, 2] != d: + return False + return True + + +def check_double(mesh: Mesh) -> bool: + for dart_info in mesh.active_darts(): + d = Dart(mesh, dart_info[0]) + d2 = Dart(mesh, dart_info[2]) if dart_info[2] >= 0 else None + n1 = dart_info[3] + if d2 is None: + d1 = d.get_beta(1) + n2 = d1.get_node().id + else : + n2 = d2.get_node().id + for dart_info2 in mesh.active_darts(): + ds = Dart(mesh, dart_info2[0]) + ds2 = Dart(mesh, dart_info2[2]) if dart_info2[2] >= 0 else None + if d != ds and d != ds2: + ns1 = dart_info2[3] + if ds2 is None: + ds1 = ds.get_beta(1) + ns2 = ds1.get_node().id + else: + ns2 = ds2.get_node().id + + if n1 == ns1 and n2 == ns2: + return False + elif n2 == ns1 and n1 == ns2: + return False + return True diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 2e2286f..f1f07a7 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,13 +1,16 @@ from typing import Any +import math import numpy as np from model.mesh_analysis import global_score, isValidAction, find_template_opposite_node from model.mesh_struct.mesh_elements import Dart from model.mesh_struct.mesh import Mesh -from actions.triangular_actions import flip_edge +from actions.triangular_actions import flip_edge, split_edge, collapse_edge from model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 +SPLIT = 1 +COLLAPSE = 2 GLOBAL = 0 @@ -16,12 +19,11 @@ def __init__(self, mesh=None, mesh_size: int = None, max_steps: int = 50, feat: self.mesh = mesh if mesh is not None else random_flip_mesh(mesh_size) self.mesh_size = len(self.mesh.active_nodes()) self.size = len(self.mesh.dart_info) - self.actions = np.array([FLIP]) + self.actions = np.array([FLIP, SPLIT, COLLAPSE]) self.reward = 0 self.steps = 0 self.max_steps = max_steps - self.nodes_scores = global_score(self.mesh)[0] - self.ideal_score = global_score(self.mesh)[2] + self.nodes_scores, self.mesh_score, self.ideal_score = global_score(self.mesh) self.terminal = False self.feat = feat self.won = 0 @@ -32,26 +34,30 @@ def reset(self, mesh=None): self.terminal = False self.mesh = mesh if mesh is not None else random_mesh(self.mesh_size) self.size = len(self.mesh.dart_info) - self.nodes_scores = global_score(self.mesh)[0] - self.ideal_score = global_score(self.mesh)[2] + self.nodes_scores, self.mesh_score, self.ideal_score = global_score(self.mesh) self.won = 0 def step(self, action): dart_id = action[1] - _, mesh_score, mesh_ideal_score = global_score(self.mesh) d = Dart(self.mesh, dart_id) d1 = d.get_beta(1) n1 = d.get_node() n2 = d1.get_node() - flip_edge(self.mesh, n1, n2) + if action[2] == FLIP: + flip_edge(self.mesh, n1, n2) + elif action[2] == SPLIT: + split_edge(self.mesh, n1, n2) + elif action[2] == COLLAPSE: + collapse_edge(self.mesh, n1, n2) self.steps += 1 next_nodes_score, next_mesh_score, _ = global_score(self.mesh) self.nodes_scores = next_nodes_score - self.reward = (mesh_score - next_mesh_score)*10 - if self.steps >= self.max_steps or next_mesh_score == mesh_ideal_score: - if next_mesh_score == mesh_ideal_score: + self.reward = (self.mesh_score - next_mesh_score)*10 + if self.steps >= self.max_steps or next_mesh_score == self.ideal_score: + if next_mesh_score == self.ideal_score: self.won = True self.terminal = True + self.nodes_scores, self.mesh_score = next_nodes_score, next_mesh_score def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: """ @@ -74,11 +80,33 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: :return: the feature vector """ mesh = state + template = get_template_2(mesh) + darts_to_delete = [] + darts_id = [] + all_action_type = 3 + + for i, d_info in enumerate(mesh.active_darts()): + d_id = d_info[0] + d = Dart(mesh, d_id) + if d_info[2] == -1: #test the validity of all action type + darts_to_delete.append(i) + else: + darts_id.append(d_id) + valid_template = np.delete(template, darts_to_delete, axis=0) + score_sum = np.sum(np.abs(valid_template), axis=1) + indices_top_10 = np.argsort(score_sum)[-5:][::-1] + valid_dart_ids = [darts_id[i] for i in indices_top_10] + X = valid_template[indices_top_10, :] + X = X.flatten() + return X, valid_dart_ids + + +def get_template_2(mesh: Mesh): nodes_scores = global_score(mesh)[0] size = len(mesh.active_darts()) template = np.zeros((size, 6)) - for d_info in mesh.active_darts(): + for i, d_info in enumerate(mesh.active_darts()): d = Dart(mesh, d_info[0]) A = d.get_node() @@ -87,35 +115,21 @@ def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: d11 = d1.get_beta(1) C = d11.get_node() - #Template niveau 1 - template[d_info[0], 0] = nodes_scores[C.id] - template[d_info[0], 1] = nodes_scores[A.id] - template[d_info[0], 2] = nodes_scores[B.id] + # Template niveau 1 + template[i, 0] = nodes_scores[C.id] if not math.isnan(nodes_scores[C.id]) else 0 + template[i, 1] = nodes_scores[A.id] if not math.isnan(nodes_scores[A.id]) else 0 + template[i, 2] = nodes_scores[B.id] if not math.isnan(nodes_scores[B.id]) else 0 - #template niveau 2 + # template niveau 2 n_id = find_template_opposite_node(d) - if n_id is not None: - template[d_info[0], 3] = nodes_scores[n_id] + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 3] = nodes_scores[n_id] n_id = find_template_opposite_node(d1) - if n_id is not None: - template[d_info[0], 4] = nodes_scores[n_id] + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 4] = nodes_scores[n_id] n_id = find_template_opposite_node(d11) - if n_id is not None: - template[d_info[0], 5] = nodes_scores[n_id] - - dart_to_delete = [] - dart_ids = [] - for i in range(size): - d = Dart(mesh, i) - if not isValidAction(mesh, d.id): - dart_to_delete.append(i) - else : - dart_ids.append(i) - valid_template = np.delete(template, dart_to_delete, axis=0) - score_sum = np.sum(np.abs(valid_template), axis=1) - indices_top_10 = np.argsort(score_sum)[-5:][::-1] - valid_dart_ids = [dart_ids[i] for i in indices_top_10] - X = valid_template[indices_top_10, :] - X = X.flatten() - return X, valid_dart_ids + if n_id is not None and not math.isnan(nodes_scores[n_id]): + template[i, 5] = nodes_scores[n_id] + + return template diff --git a/main.py b/main.py index 936120e..0caa05c 100644 --- a/main.py +++ b/main.py @@ -13,8 +13,10 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - #train() + train() + """ cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() + """ diff --git a/mesh_display.py b/mesh_display.py index d15247d..b4edb3c 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -9,18 +9,18 @@ def __init__(self, m: Mesh): def get_nodes_coordinates(self): """ Build a list containing the coordinates of the all the mesh nodes - :return: a list of coordinates (x,y) + :return: a list of coordinates (id, x, y) """ node_list = [] for idx, n in enumerate(self.mesh.nodes): - if n[2] >= 0 : + if n[2] >= 0: node_list.append((idx, n[0], n[1])) return node_list def get_edges(self): """ - Build a list containing the coordinates of the all the mesh nodes - :return: a list of coordinates (x,y) + Build a list containing the id of the nodes of the mesh edges + :return: a list of nodes id (n1_id, n2_id) """ edge_list = [] for d in self.mesh.active_darts(): @@ -36,5 +36,4 @@ def get_scores(self): :return: a list of three elements (nodes_score, mesh_score, ideal_mesh_score) """ nodes_score, mesh_score, ideal_mesh_score = global_score(self.mesh) - nodes_score = [score for score in nodes_score if score is not None] return [nodes_score, mesh_score, ideal_mesh_score] diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index bf491b3..5cc1ad2 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -1,11 +1,11 @@ from math import sqrt, degrees, radians, cos, sin, acos import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node +from model.mesh_struct.mesh_elements import Dart, Node, Face from model.mesh_struct.mesh import Mesh -def global_score(m: Mesh) -> (int, int): +def global_score(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -15,15 +15,18 @@ def global_score(m: Mesh) -> (int, int): mesh_ideal_score = 0 mesh_score = 0 nodes_score = [] + active_nodes_score = [] for i in range(len(m.nodes)): if m.nodes[i, 2] >= 0: n_id = i node = Node(m, n_id) n_score = score_calculation(node) nodes_score.append(n_score) + active_nodes_score.append(n_score) mesh_ideal_score += n_score mesh_score += abs(n_score) - nodes_score.append(None) + else: + nodes_score.append(0) return nodes_score, mesh_score, mesh_ideal_score @@ -71,6 +74,8 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: cos_theta = np.dot(vect_AB, vect_AC)/(dist_AB*dist_AC) cos_theta = np.clip(cos_theta, -1, 1) angle = np.arccos(cos_theta) + if np.isnan(angle): + raise(ValueError("Angle error")) return degrees(angle) @@ -86,7 +91,7 @@ def get_boundary_angle(n: Node) -> float: d_twin = d.get_beta(2) if d_twin is None: boundary_darts.append(d) - if len(boundary_darts) > 10: + if len(boundary_darts) > 7: raise ValueError("Boundary error") angle = get_angle(boundary_darts[0], boundary_darts[1], n) return angle @@ -158,7 +163,7 @@ def get_boundary_darts(m: Mesh) -> list[Dart]: for d_info in m.active_darts(): d = Dart(m, d_info[0]) d_twin = d.get_beta(2) - if d_twin is None : + if d_twin is None: boundary_darts.append(d) return boundary_darts @@ -238,25 +243,29 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: flip = 0 split = 1 collapse = 2 + test_all = 3 d = Dart(mesh, dart_id) boundary_darts = get_boundary_darts(mesh) if d in boundary_darts: return False - elif action == flip and isFlipOk(d) is not True: - return False - elif action == split and isFlipOk(d) is not True: - return False - elif action == collapse and isCollapseOk(d) is not True: - return False + elif action == flip: + return isFlipOk(d) + elif action == split: + return isFlipOk(d) + elif action == collapse: + return newIsCollapseOk(d) + elif action == test_all: + return isFlipOk(d) and newIsCollapseOk(d) else: - return True + raise ValueError("No valid action") + def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3:float) -> float: BAx, BAy = x1 - x2, y1 - y2 BCx, BCy = x3 - x2, y3 - y2 cos_ABC = (BAx * BCx + BAy * BCy) / (sqrt(BAx ** 2 + BAy ** 2) * sqrt(BCx ** 2 + BCy ** 2)) - + cos_ABC = np.clip(cos_ABC, -1, 1) rad = acos(cos_ABC) deg = degrees(rad) return deg @@ -286,7 +295,7 @@ def isFlipOk(d: Dart) -> bool: return True -def isCollapseOk(d: Dart) -> bool: +def newIsCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -296,12 +305,124 @@ def isCollapseOk(d: Dart) -> bool: d212 = d21.get_beta(2) d2112 = d211.get_beta(2) - if d112 is None and d12 is None and d2112 is None and d212 is None: + newNode_x, newNode_y = (n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2 + + if d112 is None or d12 is None or d2112 is None or d212 is None: + return False + elif on_boundary(n1) or on_boundary(n2): return False + else: + # search for all adjacent faces to n1 and n2 + if d12 is None and d2112 is None: + adj_faces_n1 = get_adjacent_faces(n1, d212, d112) + return valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) + elif d212 is None and d112 is None: + adj_faces_n2 = get_adjacent_faces(n2, d12, d2112) + return valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y) + else: + adj_faces_n1 = get_adjacent_faces(n1, d212, d112) + adj_faces_n2 = get_adjacent_faces(n2, d12, d2112) + if not valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) or not valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y): + return False + else: + return True + + +def get_adjacent_faces(n: Node, d_from: Dart, d_to: Dart) -> list: + adj_faces = [] + d2 = d_from + d = None if d2 is None else d_from.get_beta(1) + while d != d_to: + if d2 is None and d_to is not None: + # chercher dans l'autre sens + d = d_to + adj_faces.append(d.get_face()) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d = d11.get_beta(2) + while d is not None: + adj_faces.append(d.get_face()) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d = d11.get_beta(2) + break + elif d2 is None and d_to is None: + break + elif d2 is not None: + d = d2.get_beta(1) + adj_faces.append(d.get_face()) + d2 = d.get_beta(2) + else: + break + return adj_faces + +def discontinue(d_from, d_to) -> bool: + if d_from is None or d_to is None: + raise ValueError("Discontinue condition") + + ds = d_from.get_beta(1) + while ds != d_to: + ds2 = ds.get_beta(2) + if ds2 is None: + return True + ds = ds2.get_beta(1) + d1 = d_from.get_beta(1) + ds = d1.get_beta(1) + i = 0 + while ds != d_to: + ds2 = ds.get_beta(2) + if ds2 is None or i > 10: + return True + ds21 = ds2.get_beta(1) + ds = ds21.get_beta(1) + i += 1 + return False + +def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float) -> bool: + """ + Check the orientation of triangles adjacent to node n = Node(mesh, n_id) if the latter is moved to coordinates new_x, new_y. + Also checks that no triangle will become flat + :param mesh: + :param faces: adjacents faces to node of id n_id + :param n_id: + :param new_x: + :param new_y: + :return: + """ + for f in faces: + d, d1, d11, A, B, C = f.get_surrounding() + if A.id == n_id: + vect_AB = (B.x() - new_x, B.y() - new_y) + vect_AC = (C.x() - new_x, C.y() - new_y) + elif B.id == n_id: + vect_AB = (new_x - A.x(), new_y - A.y()) + vect_AC = (C.x() - A.x(), C.y() - A.y()) + elif C.id == n_id: + vect_AB = (B.x() - A.x(), B.y() - A.y()) + vect_AC = (new_x - A.x(), new_y - A.y()) + else: + print("Erreur face non adjacente") + continue + + cross_product = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] + + if cross_product <= 0: + return False # Une face n'est pas orientée correctement ou est plate + return True + + """ elif d112 is not None and d212 is not None: if d12 is None and d2112 is None: + # search for discontinuities + ds = d212.get_beta(1) + while ds != d112: + ds2 = ds.get_beta(2) + if ds2 is None: + return False + ds = ds2.get_beta(1) return True elif d12 is not None: + # search for discontinuities ds = d12.get_beta(1) while ds != d2112: ds2 = ds.get_beta(2) @@ -312,7 +433,6 @@ def isCollapseOk(d: Dart) -> bool: else: return False - """ #Old condition if d112 is None and d12 is None: return False @@ -324,6 +444,26 @@ def isCollapseOk(d: Dart) -> bool: return False else: return True - - """ + +def isCollapseOk(d: Dart) -> bool: + mesh = d.mesh + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + if d112 is None or d12 is None or d2112 is None or d212 is None: + return False + else: + # search for discontinuities on right side (d12 and d2112) + if discontinue(d12, d2112): + return False + if discontinue(d212, d112): + return False + else: + return True + """ \ No newline at end of file diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index d8225a3..912e004 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -20,9 +20,9 @@ def __init__(self, nodes=[], faces=[]): self.nodes = numpy.empty((0, 3)) self.faces = numpy.empty(0, dtype=int) self.dart_info = numpy.empty((0, 5), dtype=int) - self.first_free_dart = 1 - self.first_free_node = 1 - self.first_free_face = 1 + self.first_free_dart = 0 + self.first_free_node = 0 + self.first_free_face = 0 for n in nodes: self.add_node(n[0], n[1]) @@ -65,14 +65,14 @@ def add_node(self, x: float, y: float) -> Node: :param y: Y coordinate :return: the created node """ - if len(self.nodes) < self.first_free_node: + if len(self.nodes) <= self.first_free_node: self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) self.first_free_node += 1 return Node(self, len(self.nodes) - 1) elif self.first_free_node >= 0: n_id = int(self.first_free_node) if isinstance(n_id, int): - self.first_free_node = abs(self.nodes[n_id, 2] + 1 ) + self.first_free_node = abs(self.nodes[n_id, 2] + 1) self.nodes[n_id] = [x, y, -1] else: print(n_id) @@ -117,7 +117,7 @@ def add_triangle(self, n1: Node, n2: Node, n3: Node) -> Face: darts[k].set_node(nodes[k]) nodes[k].set_dart(darts[k]) - if len(self.faces) < self.first_free_face: + if len(self.faces) <= self.first_free_face: self.faces = numpy.append(self.faces, [darts[0].id]) self.first_free_face += 1 tri = Face(self, len(self.faces) - 1) @@ -139,7 +139,7 @@ def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: self.del_dart(d2) self.del_dart(d3) - self.faces[f.id] = -self.first_free_face -1 + self.faces[f.id] = -self.first_free_face - 1 self.first_free_face = f.id @@ -245,9 +245,10 @@ def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1) -> Dart :param a1: dart index to connect by alpha1 :param a2: dart index to connect by alpha2 :param v: vertex index this dart point to + :param f: face to connect :return: the created dart """ - if len(self.dart_info) < self.first_free_dart: + if len(self.dart_info) <= self.first_free_dart: self.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f]], axis=0) self.first_free_dart += 1 return Dart(self, len(self.dart_info) - 1) diff --git a/model/mesh_struct/mesh_elements.py b/model/mesh_struct/mesh_elements.py index 4e857c1..970a3e0 100644 --- a/model/mesh_struct/mesh_elements.py +++ b/model/mesh_struct/mesh_elements.py @@ -223,4 +223,13 @@ def set_dart(self, d: Dart) -> None: """ if d is None: raise ValueError("Try to connect a face to a non-existing dart") - self.mesh.faces[self.id] = d.id \ No newline at end of file + self.mesh.faces[self.id] = d.id + + def get_surrounding(self) -> [Dart, Dart, Dart, Node, Node, Node]: + d = self.get_dart() + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + A = d.get_node() + B = d1.get_node() + C = d11.get_node() + return d, d1, d11, A, B, C diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 95fe3b9..50b2489 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -3,7 +3,7 @@ from model.mesh_struct.mesh_elements import Dart, Node from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import find_opposite_node, node_in_mesh +from model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids @@ -91,7 +91,38 @@ def mesh_shuffle(mesh: Mesh) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action = 2*len(mesh.dart_info) + nb_action_max = 5 + nb_action = 0 + active_darts_list = mesh.active_darts() + i = 0 + while nb_action < nb_action_max and i < 20: + action_type = np.random.randint(1, 4) + d_id = np.random.randint(len(active_darts_list)) + d_id = active_darts_list[d_id][0] + dart = Dart(mesh, d_id) + i1 = dart.get_node() + i2 = ((dart.get_beta(1)).get_beta(1)).get_node() + if action_type == 1 and isValidAction(mesh, d_id, action_type): + flip_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + elif action_type == 2 and isValidAction(mesh, d_id, action_type): + split_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + elif action_type == 3 and isValidAction(mesh, d_id, action_type): + collapse_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + i += 1 + active_darts_list = mesh.active_darts() + return mesh + + +def old_mesh_shuffle(mesh: Mesh) -> Mesh: + """ + Performs random flip actions on mesh darts. + :param mesh: the mesh to work with + :return: a mesh with randomly flipped darts. + """ + nb_action = int(len(mesh.dart_info)/2) nb_nodes = len(mesh.nodes) for i in range(nb_action): action = np.random.randint(1, 4) diff --git a/model_RL/PPO_model.py b/model_RL/PPO_model.py index d1ae1ce..d0b9258 100644 --- a/model_RL/PPO_model.py +++ b/model_RL/PPO_model.py @@ -9,7 +9,7 @@ class PPO: def __init__(self, env, lr, gamma, nb_iterations, nb_episodes_per_iteration, nb_epochs, batch_size): self.env = env - self.actor = Actor(env, 30, 5, lr=0.0001) + self.actor = Actor(env, 30, 15, lr=0.0001) self.critic = Critic(30, lr=0.0001) self.lr = lr self.gamma = gamma @@ -43,8 +43,9 @@ def train_epoch(self, dataset): next_value = torch.tensor(0.0, dtype=torch.float32) if done else self.critic(next_X) delta = r + 0.9 * next_value - value G = (r + 0.9 * G) / 10 - st = global_score(s)[1] - ideal_s = global_score(s)[2] + _, st, ideal_s = global_score(s) + if st == ideal_s: + continue advantage = 1 if done else G / (st - ideal_s) ratio = torch.exp(log_prob - torch.log(old_prob).detach()) actor_loss1 = advantage * ratio @@ -80,18 +81,17 @@ def train(self): print('ITERATION', iteration) rollouts = [] dataset = [] - for _ in range(self.nb_episodes_per_iteration): + for _ in tqdm(range(self.nb_episodes_per_iteration)): self.env.reset() trajectory = [] ep_reward = 0 done = False while True: state = copy.deepcopy(self.env.mesh) - action = self.actor.select_action(state) - X, _ = self.env.get_x(state, None) - X = torch.tensor(X, dtype=torch.float32) - pmf = self.actor.forward(X) - prob = pmf[action[0]] + action, prob = self.actor.select_action(state) + if action is None: + wins.append(0) + break self.env.step(action) next_state = copy.deepcopy(self.env.mesh) R = self.env.reward @@ -106,10 +106,11 @@ def train(self): trajectory.append((state, action, R, prob, next_state, done)) break trajectory.append((state, action, R, prob, next_state, done)) - rewards.append(ep_reward) - rollouts.append(trajectory) - dataset.extend(trajectory) - len_ep.append(len(trajectory)) + if len(trajectory) != 0: + rewards.append(ep_reward) + rollouts.append(trajectory) + dataset.extend(trajectory) + len_ep.append(len(trajectory)) self.train_epoch(dataset) diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 6359be9..700ce44 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,7 +1,7 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from model.mesh_analysis import global_score +from model.mesh_analysis import global_score, get_boundary_darts from model.mesh_struct.mesh import Mesh import numpy as np import copy @@ -34,8 +34,12 @@ def testPolicy( ep_rewards: int = 0 ep_length: int = 0 env.reset(mesh) - while env.won == 0 and ep_length < 30: - action = policy.select_action(env.mesh) + while env.won == 0 and ep_length < max_steps: + action, _ = policy.select_action(env.mesh) + if action is None: + env.terminal = True + break + boundary_darts = get_boundary_darts(env.mesh) env.step(action) ep_rewards += env.reward ep_length += 1 @@ -57,7 +61,7 @@ def isBetterPolicy(actual_best_policy, policy_to_test): def isBetterMesh(best_mesh, actual_mesh): - if actual_mesh is not None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: + if best_mesh is None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: return True else: return False diff --git a/model_RL/utilities/actor_critic_networks.py b/model_RL/utilities/actor_critic_networks.py index 4d24d04..6cf8de0 100644 --- a/model_RL/utilities/actor_critic_networks.py +++ b/model_RL/utilities/actor_critic_networks.py @@ -33,11 +33,20 @@ def reset(self, env=None): self.optimizer = Adam(self.parameters(), lr=self.optimizer.defaults['lr'], weight_decay=self.optimizer.defaults['weight_decay']) def select_action(self, state): - if np.random.rand() < self.eps: X, dart_indices = self.env.get_x(state, None) - action = np.random.randint(5) - dart_id = dart_indices[action] + action = np.random.randint(5*3) # random choice of 3 actions on 3 darts + dart_id = dart_indices[int(action/3)] + action_type = action % 3 + prob = 1/3 + i = 0 + while not isValidAction(state, dart_id, action_type): + if i > 15: + return None, None + action = np.random.randint(5 * 3) # random choice of 3 actions on 3 darts + dart_id = dart_indices[int(action / 3)] + action_type = action % 3 + i += 1 else: X, dart_indices = self.env.get_x(state, None) X = torch.tensor(X, dtype=torch.float32) @@ -45,16 +54,25 @@ def select_action(self, state): dist = Categorical(pmf) action = dist.sample() action = action.tolist() - dart_id = dart_indices[action] + prob = pmf[action] + action_darts = int(action/3) + action_type = action % 3 + dart_id = dart_indices[action_darts] i = 0 - while not isValidAction(state, dart_id) and i < 10: + while not isValidAction(state, dart_id, action_type): + if i > 15: + return None, None pmf = self.forward(X) dist = Categorical(pmf) action = dist.sample() action = action.tolist() - dart_id = dart_indices[action] + prob = pmf[action] + action_darts = int(action/3) + action_type = action % 3 + dart_id = dart_indices[action_darts] i += 1 - return action, dart_id + action_list = [action, dart_id, action_type] + return action_list, prob def forward(self, x): x = torch.relu(self.fc1(x)) diff --git a/test_modules/test_mesh_analysis.py b/test_modules/test_mesh_analysis.py index 3ff7580..a28321a 100644 --- a/test_modules/test_mesh_analysis.py +++ b/test_modules/test_mesh_analysis.py @@ -57,8 +57,9 @@ def test_is_valid_action(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [2.0, 0.0]] faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] cmap = Mesh(nodes, faces) - self.assertEqual(Mesh_analysis.isValidAction(cmap, 0), False) - self.assertEqual(Mesh_analysis.isValidAction(cmap, 2), True) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 0, 3), False) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 2, 0), True) + self.assertEqual(Mesh_analysis.isValidAction(cmap, 2, 3), False) def test_isFlipOk(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [2.0, 0.0]] diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 0f6bca8..9c87657 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -16,12 +16,9 @@ def test_regular_trimesh(self): self.assertEqual(m.nb_nodes(), 60) def test_random_trimesh(self): - m = random_mesh(44) - self.assertIsInstance(m, Mesh) - m = random_mesh(30) - self.assertIsInstance(m, Mesh) - m = random_mesh(60) - self.assertIsInstance(m, Mesh) + for i in range(10): + m = random_mesh(30) + self.assertIsInstance(m, Mesh) def test_random_flip_mesh(self): m = random_flip_mesh(44) @@ -32,7 +29,7 @@ def test_random_flip_mesh(self): self.assertEqual(m.nb_nodes(), 60) def test_mesh_suffle(self): - m = regular_mesh(40) + m = regular_mesh(15) mesh = mesh_shuffle(m) plot_mesh(mesh) diff --git a/train.py b/train.py index 5ffb301..0200d0b 100644 --- a/train.py +++ b/train.py @@ -1,5 +1,5 @@ import model.random_trimesh as TM - +import torch from environment.trimesh_env import TriMesh from plots.create_plots import plot_training_results, plot_test_results @@ -20,22 +20,24 @@ def train(): gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(30) for _ in range(16)] + dataset = [TM.random_mesh(12) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=30, feat=feature) + env = TriMesh(None, mesh_size, max_steps=15, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=2, nb_episodes_per_iteration=50, nb_epochs=1, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=3, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) actor, rewards, wins, steps = model.train() + if rewards is not None: + plot_training_results(rewards, wins, steps) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 10, dataset, 60) + torch.save(actor.state_dict(), 'policy saved/actor_network.pth') + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) if rewards is not None: - plot_training_results(rewards, wins, steps) plot_test_results(avg_rewards, avg_wins, avg_steps) plot_dataset(final_meshes) diff --git a/view/graph.py b/view/graph.py index e2eff08..46e95ac 100644 --- a/view/graph.py +++ b/view/graph.py @@ -110,9 +110,9 @@ def clear(self): self.edges = [] def update(self, vertices, edges, scores): - for idx, n in enumerate(vertices): + for n in vertices: nodes_scores = scores[0] - n_value = nodes_scores[idx] + n_value = nodes_scores[n[0]] self.create_vertex(n[0], n[1], n[2], n_value) for e in edges: self.create_edge(e[0], e[1]) From 5770085e3af7866ebff88c4157eda6ce5f062168 Mon Sep 17 00:00:00 2001 From: ropercha Date: Thu, 12 Sep 2024 09:56:53 +0200 Subject: [PATCH 15/22] Working environment with collapse action, the agent has learned to collapse a maximum of nodes --- actions/triangular_actions.py | 7 +- environment/trimesh_env.py | 2 +- exploit.py | 32 ++++++++ main.py | 1 + model/mesh_analysis.py | 142 ++++++++++++++++------------------ model/mesh_struct/mesh.py | 1 - model/random_trimesh.py | 7 +- train.py | 10 +-- 8 files changed, 115 insertions(+), 87 deletions(-) create mode 100644 exploit.py diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 37dd919..a641af6 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from model.mesh_struct.mesh import Mesh from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, newIsCollapseOk, adjacent_darts +from model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts import numpy as np @@ -98,7 +98,7 @@ def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not newIsCollapseOk(d): + if not found or not isCollapseOk(d): return False elif not test_degree(n1): return False @@ -212,6 +212,7 @@ def test_boundary(n1: Node, n2: Node) -> bool: else: return True + def check_beta2_relation(mesh: Mesh) -> bool: for dart_info in mesh.active_darts(): d = dart_info[0] @@ -231,7 +232,7 @@ def check_double(mesh: Mesh) -> bool: if d2 is None: d1 = d.get_beta(1) n2 = d1.get_node().id - else : + else: n2 = d2.get_node().id for dart_info2 in mesh.active_darts(): ds = Dart(mesh, dart_info2[0]) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index f1f07a7..3c08a81 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -52,7 +52,7 @@ def step(self, action): self.steps += 1 next_nodes_score, next_mesh_score, _ = global_score(self.mesh) self.nodes_scores = next_nodes_score - self.reward = (self.mesh_score - next_mesh_score)*10 + self.reward = (self.mesh_score - next_mesh_score) * 10 if self.steps >= self.max_steps or next_mesh_score == self.ideal_score: if next_mesh_score == self.ideal_score: self.won = True diff --git a/exploit.py b/exploit.py new file mode 100644 index 0000000..6c1d2e3 --- /dev/null +++ b/exploit.py @@ -0,0 +1,32 @@ +import model.random_trimesh as TM +import torch +from environment.trimesh_env import TriMesh +from model_RL.utilities.actor_critic_networks import Actor + +from plots.create_plots import plot_training_results, plot_test_results +from plots.mesh_plotter import plot_dataset + +from model_RL.evaluate_model import testPolicy + +from model_RL.PPO_model import PPO + +LOCAL_MESH_FEAT = 0 + + +def exploit(): + mesh_size = 12 + feature = LOCAL_MESH_FEAT + + dataset = [TM.random_mesh(12) for _ in range(9)] + plot_dataset(dataset) + + env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + + actor = Actor(env, 30, 15, lr=0.0001) + actor.load_state_dict(torch.load('policy saved/actor_network_nul.pth')) + + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + + if avg_steps is not None: + plot_test_results(avg_rewards, avg_wins, avg_steps) + plot_dataset(final_meshes) diff --git a/main.py b/main.py index 0caa05c..934f988 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ from user_game import user_game from train import train +from exploit import exploit from model.reader import read_gmsh from view.window import Game from mesh_display import MeshDisplay diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 5cc1ad2..2acb070 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -6,6 +6,33 @@ def global_score(m: Mesh): + """ + Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. + And the current score is the mesh score. + :param m: the mesh to be analyzed + :return: three return values: a list of the nodes score, the current mesh score and the ideal mesh score + """ + mesh_ideal_score = 0 + mesh_score = 0 + nodes_score = [] + active_nodes_score = [] + for i in range(len(m.nodes)): + if m.nodes[i, 2] >= 0: + n_id = i + node = Node(m, n_id) + real_n_score = score_calculation(node) + #n_score = real_n_score ** 2 if real_n_score > 0 else -1 * (real_n_score ** 2) + n_score = real_n_score + nodes_score.append(n_score) + active_nodes_score.append(n_score) + mesh_ideal_score += n_score + mesh_score += abs(n_score) + else: + nodes_score.append(0) + return nodes_score, mesh_score, mesh_ideal_score + + +def global_score_old(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -253,9 +280,9 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: elif action == split: return isFlipOk(d) elif action == collapse: - return newIsCollapseOk(d) + return isCollapseOk(d) elif action == test_all: - return isFlipOk(d) and newIsCollapseOk(d) + return isFlipOk(d) and isCollapseOk(d) else: raise ValueError("No valid action") @@ -295,7 +322,12 @@ def isFlipOk(d: Dart) -> bool: return True -def newIsCollapseOk(d: Dart) -> bool: +def isSplitOk(d: Dart) -> bool: + mesh = d.mesh + + return True + +def isCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -356,27 +388,6 @@ def get_adjacent_faces(n: Node, d_from: Dart, d_to: Dart) -> list: break return adj_faces -def discontinue(d_from, d_to) -> bool: - if d_from is None or d_to is None: - raise ValueError("Discontinue condition") - - ds = d_from.get_beta(1) - while ds != d_to: - ds2 = ds.get_beta(2) - if ds2 is None: - return True - ds = ds2.get_beta(1) - d1 = d_from.get_beta(1) - ds = d1.get_beta(1) - i = 0 - while ds != d_to: - ds2 = ds.get_beta(2) - if ds2 is None or i > 10: - return True - ds21 = ds2.get_beta(1) - ds = ds21.get_beta(1) - i += 1 - return False def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float) -> bool: """ @@ -394,12 +405,15 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float if A.id == n_id: vect_AB = (B.x() - new_x, B.y() - new_y) vect_AC = (C.x() - new_x, C.y() - new_y) + vect_BC = (C.x() - B.x(), C.y() - B.y()) elif B.id == n_id: vect_AB = (new_x - A.x(), new_y - A.y()) vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_BC = (C.x() - new_x, C.y() - new_y) elif C.id == n_id: vect_AB = (B.x() - A.x(), B.y() - A.y()) vect_AC = (new_x - A.x(), new_y - A.y()) + vect_BC = (new_x - B.x(), new_y - B.y()) else: print("Erreur face non adjacente") continue @@ -408,62 +422,42 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float if cross_product <= 0: return False # Une face n'est pas orientée correctement ou est plate + elif not valid_triangle(vect_AB, vect_AC, vect_BC): + return False return True - """ - elif d112 is not None and d212 is not None: - if d12 is None and d2112 is None: - # search for discontinuities - ds = d212.get_beta(1) - while ds != d112: - ds2 = ds.get_beta(2) - if ds2 is None: - return False - ds = ds2.get_beta(1) - return True - elif d12 is not None: - # search for discontinuities - ds = d12.get_beta(1) - while ds != d2112: - ds2 = ds.get_beta(2) - if ds2 is None: - return False - ds = ds2.get_beta(1) - return True - else: - return False - #Old condition - if d112 is None and d12 is None: - return False - elif d212 is None and d2112 is None: - return False - elif d212 is None and d12 is None: - return False - elif d112 is None and d2112 is None: - return False +def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: + dist_AB = sqrt(vect_AB[0] ** 2 + vect_AB[1] ** 2) + dist_AC = sqrt(vect_AC[0] ** 2 + vect_AC[1] ** 2) + dist_BC = sqrt(vect_BC[0] ** 2 + vect_BC[1] ** 2) + l_min = 0.1 + l_max = 1.5 + + if l_min < dist_AB < l_max or l_min < dist_AC < l_max or l_min < dist_BC < l_max: + pass else: - return True + return False + # Calcul des angles avec le théorème du cosinus + angle_A = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A + angle_B = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B + angle_C = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C -def isCollapseOk(d: Dart) -> bool: - mesh = d.mesh - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + # Vérification que tous les angles sont supérieurs à 5° + if angle_A <= 5 or angle_B <= 5 or angle_C <= 5: + return False + return True - d112 = d11.get_beta(2) - d12 = d1.get_beta(2) - d212 = d21.get_beta(2) - d2112 = d211.get_beta(2) +def angle_from_sides(a, b, c): + # Théorème du cosinus pour obtenir l'angle en radians entre les côtés a, b, c + cosA = (b**2 + c**2 - a**2) / (2 * b * c) + if 1 <= cosA < 1.01: + cosA = 1 + elif -1.01 <= cosA < -1: + cosA = -1 + elif cosA > 1.01 or cosA < -1.01: + raise ValueError("Math domain error : cos>1.01") + return acos(cosA) - if d112 is None or d12 is None or d2112 is None or d212 is None: - return False - else: - # search for discontinuities on right side (d12 and d2112) - if discontinue(d12, d2112): - return False - if discontinue(d212, d112): - return False - else: - return True - """ \ No newline at end of file diff --git a/model/mesh_struct/mesh.py b/model/mesh_struct/mesh.py index 912e004..323fff4 100644 --- a/model/mesh_struct/mesh.py +++ b/model/mesh_struct/mesh.py @@ -269,7 +269,6 @@ def del_dart(self, d: Dart): self.dart_info[d.id][0] = -self.first_free_dart - 1 self.first_free_dart = d.id - def set_beta2(self, dart: Dart) -> None: """ Search for a dart to connect with beta2 relation when possible. diff --git a/model/random_trimesh.py b/model/random_trimesh.py index 50b2489..f7d4bd1 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -59,6 +59,7 @@ def random_flip_mesh(num_nodes_max: int) -> Mesh: mesh_shuffle_flip(mesh) return mesh + def random_mesh(num_nodes_max: int) -> Mesh: """ Create a random mesh with a fixed number of nodes. @@ -66,7 +67,7 @@ def random_mesh(num_nodes_max: int) -> Mesh: :return: a random mesh """ mesh = regular_mesh(num_nodes_max) - mesh_shuffle(mesh) + mesh_shuffle(mesh, num_nodes_max) return mesh @@ -85,13 +86,13 @@ def mesh_shuffle_flip(mesh: Mesh) -> Mesh: flip_edge_ids(mesh, i1, i2) return mesh -def mesh_shuffle(mesh: Mesh) -> Mesh: +def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: """ Performs random flip actions on mesh darts. :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action_max = 5 + nb_action_max = int(num_nodes / 2) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 diff --git a/train.py b/train.py index 0200d0b..0827401 100644 --- a/train.py +++ b/train.py @@ -15,28 +15,28 @@ def train(): - mesh_size = 12 + mesh_size = 16 lr = 0.0001 gamma = 0.9 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(12) for _ in range(9)] + dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + env = TriMesh(None, mesh_size, max_steps=30, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=3, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=10, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) torch.save(actor.state_dict(), 'policy saved/actor_network.pth') - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 40) if rewards is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) From a40194299784759cb21dc6c0701392f0f15583c9 Mon Sep 17 00:00:00 2001 From: ropercha Date: Mon, 16 Sep 2024 09:40:14 +0200 Subject: [PATCH 16/22] New "exploit" function to exploit saved policies (already trained). New function isSplitOk, to verifiy some geometric constraints. --- exploit.py | 9 +++-- main.py | 2 +- model/mesh_analysis.py | 90 ++++++++++++++++++++++++++++++++++++----- model/random_trimesh.py | 12 +++--- train.py | 8 ++-- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/exploit.py b/exploit.py index 6c1d2e3..5b94dd8 100644 --- a/exploit.py +++ b/exploit.py @@ -17,15 +17,16 @@ def exploit(): mesh_size = 12 feature = LOCAL_MESH_FEAT - dataset = [TM.random_mesh(12) for _ in range(9)] + dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=15, feat=feature) + env = TriMesh(None, mesh_size, max_steps=60, feat=feature) + actor = Actor(env, 30, 15, lr=0.0001) - actor.load_state_dict(torch.load('policy saved/actor_network_nul.pth')) + actor.load_state_dict(torch.load('policy saved/actor_30.pth')) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 15) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 15, dataset, 60) if avg_steps is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) diff --git a/main.py b/main.py index 934f988..5d95221 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,7 @@ if len(sys.argv) == 2: user_game(int(sys.argv[1])) else: - train() + exploit() """ cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) diff --git a/model/mesh_analysis.py b/model/mesh_analysis.py index 2acb070..5dfba9c 100644 --- a/model/mesh_analysis.py +++ b/model/mesh_analysis.py @@ -278,11 +278,11 @@ def isValidAction(mesh: Mesh, dart_id: int, action: int) -> bool: elif action == flip: return isFlipOk(d) elif action == split: - return isFlipOk(d) + return isSplitOk(d) elif action == collapse: return isCollapseOk(d) elif action == test_all: - return isFlipOk(d) and isCollapseOk(d) + return isFlipOk(d) and isCollapseOk(d) and isSplitOk(d) else: raise ValueError("No valid action") @@ -299,6 +299,35 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: def isFlipOk(d: Dart) -> bool: + mesh = d.mesh + if d.get_beta(2) is None: + return False + else: + d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + + # Check angle at d limits to avoid edge reversal + angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) + angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) + if angle_B >= 180 or angle_A >= 180: + return False + + #Check if new triangle will be valid + + #Triangle ACD + vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_DC = (C.x() - D.x(), C.y() - D.y()) + + #Triangle CBD + vect_BC = (C.x() - B.x(), C.y() - B.y()) + vect_BD = (D.x() - B.x(), D.y() - B.y()) + + if not valid_triangle(vect_AC, vect_AD, vect_DC) or not valid_triangle(vect_BC, vect_BD, vect_DC): + return False + + return True + +def isFlipOk_old(d: Dart) -> bool: d1 = d.get_beta(1) d11 = d1.get_beta(1) A = d.get_node() @@ -325,8 +354,50 @@ def isFlipOk(d: Dart) -> bool: def isSplitOk(d: Dart) -> bool: mesh = d.mesh + if d.get_beta(2) is None: + return False + else: + d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + + newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 + + # Check angle at d limits to avoid edge reversal + angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) + angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) + if angle_B >= 180 or angle_A >= 180: + return False + + #Check if new triangle will be valid + + # Triangle AEC + vect_AC = (C.x() - A.x(), C.y() - A.y()) + vect_AE = (newNode_x - A.x(), newNode_y - A.y()) + vect_EC = (C.x() - newNode_x, C.y() - newNode_y) + if not valid_triangle(vect_AE, vect_AC, vect_EC): + return False + + # Triangle ADE + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not valid_triangle(vect_AD, vect_AE, vect_ED): + return False + + # Triangle BCE + vect_BC = (C.x() - B.x(), C.y() - B.y()) + vect_BE = (newNode_x - B.x(), newNode_y - B.y()) + vect_EC = (C.x() - newNode_x, C.y() - newNode_y) + if not valid_triangle(vect_BC, vect_BE, vect_EC): + return False + + # Triangle BDE + vect_BD = (D.x() - B.x(), D.y() - B.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not valid_triangle(vect_BD, vect_BE, vect_ED): + return False + return True + def isCollapseOk(d: Dart) -> bool: mesh = d.mesh d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -431,18 +502,19 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: dist_AB = sqrt(vect_AB[0] ** 2 + vect_AB[1] ** 2) dist_AC = sqrt(vect_AC[0] ** 2 + vect_AC[1] ** 2) dist_BC = sqrt(vect_BC[0] ** 2 + vect_BC[1] ** 2) - l_min = 0.1 - l_max = 1.5 + target_mesh_size = 1 + + L_max = max(dist_AB, dist_AC, dist_BC) - if l_min < dist_AB < l_max or l_min < dist_AC < l_max or l_min < dist_BC < l_max: + if target_mesh_size/sqrt(2) < L_max < target_mesh_size*sqrt(2): pass else: return False # Calcul des angles avec le théorème du cosinus - angle_A = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A - angle_B = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B - angle_C = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C + angle_B = degrees(angle_from_sides(dist_AC, dist_AB, dist_BC)) # Angle au point A + angle_C = degrees(angle_from_sides(dist_AB, dist_BC, dist_AC)) # Angle au point B + angle_A = degrees(angle_from_sides(dist_BC, dist_AC, dist_AB)) # Angle au point C # Vérification que tous les angles sont supérieurs à 5° if angle_A <= 5 or angle_B <= 5 or angle_C <= 5: @@ -451,7 +523,7 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: def angle_from_sides(a, b, c): - # Théorème du cosinus pour obtenir l'angle en radians entre les côtés a, b, c + # Calculate angle A, with a the opposite side and b and c the adjacent sides cosA = (b**2 + c**2 - a**2) / (2 * b * c) if 1 <= cosA < 1.01: cosA = 1 diff --git a/model/random_trimesh.py b/model/random_trimesh.py index f7d4bd1..969da0b 100644 --- a/model/random_trimesh.py +++ b/model/random_trimesh.py @@ -92,24 +92,24 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: :param mesh: the mesh to work with :return: a mesh with randomly flipped darts. """ - nb_action_max = int(num_nodes / 2) + nb_action_max = int(num_nodes*3) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 - while nb_action < nb_action_max and i < 20: - action_type = np.random.randint(1, 4) + while nb_action < nb_action_max and i < (nb_action_max + 30): + action_type = np.random.randint(0, 3) d_id = np.random.randint(len(active_darts_list)) d_id = active_darts_list[d_id][0] dart = Dart(mesh, d_id) i1 = dart.get_node() i2 = ((dart.get_beta(1)).get_beta(1)).get_node() - if action_type == 1 and isValidAction(mesh, d_id, action_type): + if action_type == 0 and isValidAction(mesh, d_id, action_type): flip_edge_ids(mesh, i1.id, i2.id) nb_action += 1 - elif action_type == 2 and isValidAction(mesh, d_id, action_type): + elif action_type == 1 and isValidAction(mesh, d_id, action_type): split_edge_ids(mesh, i1.id, i2.id) nb_action += 1 - elif action_type == 3 and isValidAction(mesh, d_id, action_type): + elif action_type == 2 and isValidAction(mesh, d_id, action_type): collapse_edge_ids(mesh, i1.id, i2.id) nb_action += 1 i += 1 diff --git a/train.py b/train.py index 0827401..c1e619f 100644 --- a/train.py +++ b/train.py @@ -15,7 +15,7 @@ def train(): - mesh_size = 16 + mesh_size = 30 lr = 0.0001 gamma = 0.9 feature = LOCAL_MESH_FEAT @@ -23,20 +23,20 @@ def train(): dataset = [TM.random_mesh(30) for _ in range(9)] plot_dataset(dataset) - env = TriMesh(None, mesh_size, max_steps=30, feat=feature) + env = TriMesh(None, mesh_size, max_steps=80, feat=feature) # Choix de la politique Actor Critic # actor = Actor(env, 30, 5, lr=0.0001) # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=10, nb_episodes_per_iteration=100, nb_epochs=3, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=8, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) torch.save(actor.state_dict(), 'policy saved/actor_network.pth') - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 40) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 60) if rewards is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) From 8b98a8f4511f0f1ed50366e8f5ba1b6e9b62b8a8 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 10:39:05 +0100 Subject: [PATCH 17/22] Cleaning up and organization --- .github/workflows/model-view-ci.yml | 2 +- actions/triangular_actions.py | 31 ++------ environment/trimesh_env.py | 12 +-- exploit.py | 6 +- main.py | 3 +- mesh_display.py | 4 +- {model => mesh_model}/__init__.py | 0 {model => mesh_model}/mesh_analysis.py | 79 +++---------------- {model => mesh_model}/mesh_struct/__init__.py | 0 {model => mesh_model}/mesh_struct/mesh.py | 19 ++++- .../mesh_struct/mesh_elements.py | 0 {model => mesh_model}/random_trimesh.py | 33 ++------ {model => mesh_model}/reader.py | 2 +- model_RL/AC_model.py | 2 +- model_RL/PPO_model.py | 4 +- model_RL/evaluate_model.py | 4 +- model_RL/utilities/actor_critic_networks.py | 2 +- model_RL/utilities/nnPolicy.py | 2 +- plots/mesh_plotter.py | 4 +- test_modules/test_actions.py | 4 +- test_modules/test_mesh_analysis.py | 6 +- test_modules/test_mesh_structure.py | 2 +- test_modules/test_random_trimesh.py | 4 +- test_modules/test_reader.py | 4 +- train.py | 6 +- user_game.py | 2 +- 26 files changed, 82 insertions(+), 155 deletions(-) rename {model => mesh_model}/__init__.py (100%) rename {model => mesh_model}/mesh_analysis.py (85%) rename {model => mesh_model}/mesh_struct/__init__.py (100%) rename {model => mesh_model}/mesh_struct/mesh.py (95%) rename {model => mesh_model}/mesh_struct/mesh_elements.py (100%) rename {model => mesh_model}/random_trimesh.py (79%) rename {model => mesh_model}/reader.py (98%) diff --git a/.github/workflows/model-view-ci.yml b/.github/workflows/model-view-ci.yml index 9041aa0..42c071f 100644 --- a/.github/workflows/model-view-ci.yml +++ b/.github/workflows/model-view-ci.yml @@ -1,4 +1,4 @@ -name: model-ci +name: mesh_model-ci on: push: diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index a641af6..6bff48f 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -1,10 +1,8 @@ from __future__ import annotations -from model.mesh_struct.mesh import Mesh -from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts - -import numpy as np +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -120,12 +118,12 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d112 = d11.get_beta(2) #Delete the darts around selected dart - delete_triangles(mesh, d) + mesh.del_adj_triangles(d) - #move n1 node in the middle of [n1, n2] + #Move n1 node in the middle of [n1, n2] n1.set_xy((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) - #update node relations + #Update node relations if d12 is not None: d121 = d12.get_beta(1) d121.set_node(n1) @@ -165,27 +163,14 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) + + #check for duplicate darts if not check_double(mesh): raise ValueError("double error") if not check_beta2_relation(mesh): raise ValueError("error beta2") return True -def delete_triangles(mesh: Mesh, d: Dart) -> None: - d2 = d.get_beta(2) - d1 = d.get_beta(1) - d11 = d1.get_beta(1) - d21 = d2.get_beta(1) - d211 = d21.get_beta(1) - - f1 = d.get_face() - f2 = d2.get_face() - - mesh.del_triangle(d, d1, d11, f1) - mesh.del_triangle(d2, d21, d211, f2) - - - def test_degree(n: Node) -> bool: """ Verify that the degree of a vertex is lower than 10 diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 3c08a81..6475f98 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,11 +1,11 @@ from typing import Any import math import numpy as np -from model.mesh_analysis import global_score, isValidAction, find_template_opposite_node -from model.mesh_struct.mesh_elements import Dart -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score, isValidAction, find_template_opposite_node +from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge, split_edge, collapse_edge -from model.random_trimesh import random_flip_mesh, random_mesh +from mesh_model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 @@ -59,7 +59,7 @@ def step(self, action): self.terminal = True self.nodes_scores, self.mesh_score = next_nodes_score, next_mesh_score - def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: + def get_x(self, s: Mesh, a: int): """ Get the feature vector of the state-action pair :param s: the state @@ -72,7 +72,7 @@ def get_x(self, s: Mesh, a: int) -> tuple[Any, list[int | list[int]]]: return get_x_global_4(self, s) -def get_x_global_4(env, state: Mesh) -> tuple[Any, list[int | list[int]]]: +def get_x_global_4(env, state: Mesh): """ Get the feature vector of the state. :param state: the state diff --git a/exploit.py b/exploit.py index 5b94dd8..fa8cdf8 100644 --- a/exploit.py +++ b/exploit.py @@ -1,4 +1,4 @@ -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM import torch from environment.trimesh_env import TriMesh from model_RL.utilities.actor_critic_networks import Actor @@ -24,9 +24,9 @@ def exploit(): actor = Actor(env, 30, 15, lr=0.0001) - actor.load_state_dict(torch.load('policy saved/actor_30.pth')) + actor.load_state_dict(torch.load('policy_saved/actor_network.pth')) - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 15, dataset, 60) + avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 30, dataset, 100) if avg_steps is not None: plot_test_results(avg_rewards, avg_wins, avg_steps) diff --git a/main.py b/main.py index 5d95221..077c326 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,10 @@ from user_game import user_game from train import train from exploit import exploit -from model.reader import read_gmsh +from mesh_model.reader import read_gmsh from view.window import Game from mesh_display import MeshDisplay +from mesh_model.random_trimesh import random_mesh # Press the green button in the gutter to run the script. diff --git a/mesh_display.py b/mesh_display.py index b4edb3c..3ea4521 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -1,5 +1,5 @@ -from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import global_score +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score class MeshDisplay: diff --git a/model/__init__.py b/mesh_model/__init__.py similarity index 100% rename from model/__init__.py rename to mesh_model/__init__.py diff --git a/model/mesh_analysis.py b/mesh_model/mesh_analysis.py similarity index 85% rename from model/mesh_analysis.py rename to mesh_model/mesh_analysis.py index 5dfba9c..34e67a0 100644 --- a/model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -1,38 +1,11 @@ from math import sqrt, degrees, radians, cos, sin, acos import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node, Face -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh import Mesh def global_score(m: Mesh): - """ - Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. - And the current score is the mesh score. - :param m: the mesh to be analyzed - :return: three return values: a list of the nodes score, the current mesh score and the ideal mesh score - """ - mesh_ideal_score = 0 - mesh_score = 0 - nodes_score = [] - active_nodes_score = [] - for i in range(len(m.nodes)): - if m.nodes[i, 2] >= 0: - n_id = i - node = Node(m, n_id) - real_n_score = score_calculation(node) - #n_score = real_n_score ** 2 if real_n_score > 0 else -1 * (real_n_score ** 2) - n_score = real_n_score - nodes_score.append(n_score) - active_nodes_score.append(n_score) - mesh_ideal_score += n_score - mesh_score += abs(n_score) - else: - nodes_score.append(0) - return nodes_score, mesh_score, mesh_ideal_score - - -def global_score_old(m: Mesh): """ Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. And the current score is the mesh score. @@ -300,6 +273,8 @@ def get_angle_by_coord(x1: float, y1: float, x2: float, y2: float, x3:float, y3: def isFlipOk(d: Dart) -> bool: mesh = d.mesh + + #if d is on boundary, flip is not possible if d.get_beta(2) is None: return False else: @@ -327,30 +302,6 @@ def isFlipOk(d: Dart) -> bool: return True -def isFlipOk_old(d: Dart) -> bool: - d1 = d.get_beta(1) - d11 = d1.get_beta(1) - A = d.get_node() - B = d1.get_node() - C = d11.get_node() - d2 = d.get_beta(2) - if d2 is None: - return False - else: - d21 = d2.get_beta(1) - d211 = d21.get_beta(1) - D = d211.get_node() - - # Calcul angle at d limits - angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) - angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) - - if angle_B >= 180 or angle_A >= 180: - return False - else: - return True - - def isSplitOk(d: Dart) -> bool: mesh = d.mesh @@ -361,12 +312,6 @@ def isSplitOk(d: Dart) -> bool: newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 - # Check angle at d limits to avoid edge reversal - angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) - angle_A = get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), D.x(), D.y()) - if angle_B >= 180 or angle_A >= 180: - return False - #Check if new triangle will be valid # Triangle AEC @@ -464,12 +409,12 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float """ Check the orientation of triangles adjacent to node n = Node(mesh, n_id) if the latter is moved to coordinates new_x, new_y. Also checks that no triangle will become flat - :param mesh: + :param mesh: a mesh :param faces: adjacents faces to node of id n_id - :param n_id: - :param new_x: - :param new_y: - :return: + :param n_id: node id + :param new_x: new x coordinate + :param new_y: new y coordinate + :return: True if valid, False otherwise """ for f in faces: d, d1, d11, A, B, C = f.get_surrounding() @@ -486,13 +431,13 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float vect_AC = (new_x - A.x(), new_y - A.y()) vect_BC = (new_x - B.x(), new_y - B.y()) else: - print("Erreur face non adjacente") + print("Non-adjacent face error") continue cross_product = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] if cross_product <= 0: - return False # Une face n'est pas orientée correctement ou est plate + return False # One face is not correctly oriented or is flat elif not valid_triangle(vect_AB, vect_AC, vect_BC): return False return True @@ -506,7 +451,7 @@ def valid_triangle(vect_AB, vect_AC, vect_BC) -> bool: L_max = max(dist_AB, dist_AC, dist_BC) - if target_mesh_size/sqrt(2) < L_max < target_mesh_size*sqrt(2): + if target_mesh_size/1.5*sqrt(2) < L_max < target_mesh_size*1.5*sqrt(2): pass else: return False diff --git a/model/mesh_struct/__init__.py b/mesh_model/mesh_struct/__init__.py similarity index 100% rename from model/mesh_struct/__init__.py rename to mesh_model/mesh_struct/__init__.py diff --git a/model/mesh_struct/mesh.py b/mesh_model/mesh_struct/mesh.py similarity index 95% rename from model/mesh_struct/mesh.py rename to mesh_model/mesh_struct/mesh.py index 323fff4..b35b819 100644 --- a/model/mesh_struct/mesh.py +++ b/mesh_model/mesh_struct/mesh.py @@ -2,7 +2,7 @@ import sys import numpy -from model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face """ Classes Dart, Node and Face must be seen as handlers on data that are stored in the @@ -142,6 +142,23 @@ def del_triangle(self, d1: Dart, d2: Dart, d3: Dart, f: Face) -> None: self.faces[f.id] = -self.first_free_face - 1 self.first_free_face = f.id + def del_adj_triangles(self, d: Dart) -> None: + """ + Delete the two adjacent triangles of the given dart d + :param mesh: a mesh + :param d: the dart to be deleted + """ + d2 = d.get_beta(2) + d1 = d.get_beta(1) + d11 = d1.get_beta(1) + d21 = d2.get_beta(1) + d211 = d21.get_beta(1) + + f1 = d.get_face() + f2 = d2.get_face() + + self.del_triangle(d, d1, d11, f1) + self.del_triangle(d2, d21, d211, f2) def add_quad(self, n1: Node, n2: Node, n3: Node, n4: Node) -> Face: """ diff --git a/model/mesh_struct/mesh_elements.py b/mesh_model/mesh_struct/mesh_elements.py similarity index 100% rename from model/mesh_struct/mesh_elements.py rename to mesh_model/mesh_struct/mesh_elements.py diff --git a/model/random_trimesh.py b/mesh_model/random_trimesh.py similarity index 79% rename from model/random_trimesh.py rename to mesh_model/random_trimesh.py index 969da0b..2e1b367 100644 --- a/model/random_trimesh.py +++ b/mesh_model/random_trimesh.py @@ -1,9 +1,9 @@ from __future__ import annotations import numpy as np -from model.mesh_struct.mesh_elements import Dart, Node -from model.mesh_struct.mesh import Mesh -from model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import find_opposite_node, node_in_mesh, isValidAction from actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids @@ -88,11 +88,12 @@ def mesh_shuffle_flip(mesh: Mesh) -> Mesh: def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: """ - Performs random flip actions on mesh darts. + Performs random actions on mesh darts. :param mesh: the mesh to work with + :param num_nodes: number nodes of the mesh :return: a mesh with randomly flipped darts. """ - nb_action_max = int(num_nodes*3) + nb_action_max = int(num_nodes) nb_action = 0 active_darts_list = mesh.active_darts() i = 0 @@ -117,26 +118,4 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: return mesh -def old_mesh_shuffle(mesh: Mesh) -> Mesh: - """ - Performs random flip actions on mesh darts. - :param mesh: the mesh to work with - :return: a mesh with randomly flipped darts. - """ - nb_action = int(len(mesh.dart_info)/2) - nb_nodes = len(mesh.nodes) - for i in range(nb_action): - action = np.random.randint(1, 4) - i1 = np.random.randint(nb_nodes) - i2 = np.random.randint(nb_nodes) - if action == 1: - flip_edge_ids(mesh, i1, i2) - elif action == 2: - split_edge_ids(mesh, i1, i2) - elif action == 3: - collapse_edge_ids(mesh, i1, i2) - return mesh - - - diff --git a/model/reader.py b/mesh_model/reader.py similarity index 98% rename from model/reader.py rename to mesh_model/reader.py index d1d4e20..4828a74 100644 --- a/model/reader.py +++ b/mesh_model/reader.py @@ -1,6 +1,6 @@ import string -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh import Mesh def read_medit(filename: string) -> Mesh: diff --git a/model_RL/AC_model.py b/model_RL/AC_model.py index fe11daa..f48582f 100644 --- a/model_RL/AC_model.py +++ b/model_RL/AC_model.py @@ -23,7 +23,7 @@ def __init__(self, env, lr, gamma, nb_episodes): def train(self) -> [Actor, list, list, list]: """ - Train the model over nb episodes. Both Actor and Critic networks are updated at the end of each episode. + Train the mesh_model over nb episodes. Both Actor and Critic networks are updated at the end of each episode. :return: the final actor policy, rewards history, wins history and number of steps history """ rewards = [] diff --git a/model_RL/PPO_model.py b/model_RL/PPO_model.py index d0b9258..533856e 100644 --- a/model_RL/PPO_model.py +++ b/model_RL/PPO_model.py @@ -1,5 +1,5 @@ from model_RL.utilities.actor_critic_networks import NaNExceptionActor, NaNExceptionCritic, Actor, Critic -from model.mesh_analysis import global_score +from mesh_model.mesh_analysis import global_score import copy import torch import random @@ -69,7 +69,7 @@ def train_epoch(self, dataset): def train(self): """ - Train the PPO model + Train the PPO mesh_model :return: the actor policy, training rewards, training wins, len of episodes """ rewards = [] diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 700ce44..21542a8 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,8 +1,8 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from model.mesh_analysis import global_score, get_boundary_darts -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score, get_boundary_darts +from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy from tqdm import tqdm diff --git a/model_RL/utilities/actor_critic_networks.py b/model_RL/utilities/actor_critic_networks.py index 6cf8de0..ccbb779 100644 --- a/model_RL/utilities/actor_critic_networks.py +++ b/model_RL/utilities/actor_critic_networks.py @@ -3,7 +3,7 @@ import torch.nn as nn from torch.optim import Adam from torch.distributions import Categorical -from model.mesh_analysis import isValidAction +from mesh_model.mesh_analysis import isValidAction class NaNExceptionActor(Exception): diff --git a/model_RL/utilities/nnPolicy.py b/model_RL/utilities/nnPolicy.py index 53d8874..643049d 100644 --- a/model_RL/utilities/nnPolicy.py +++ b/model_RL/utilities/nnPolicy.py @@ -3,7 +3,7 @@ import torch.nn as nn from torch.optim import Adam from torch.distributions import Categorical -from model.mesh_analysis_old import isValidAction +from mesh_model.mesh_analysis_old import isValidAction class NaNException(Exception): diff --git a/plots/mesh_plotter.py b/plots/mesh_plotter.py index 8dc9352..2ddda3f 100644 --- a/plots/mesh_plotter.py +++ b/plots/mesh_plotter.py @@ -1,6 +1,6 @@ import matplotlib.pyplot as plt -from model.mesh_struct.mesh_elements import Dart -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.mesh_struct.mesh import Mesh import numpy as np diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index 72501ea..ab845d0 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,6 +1,6 @@ import unittest -import model.mesh_struct.mesh as mesh -from model.mesh_struct.mesh_elements import Dart, Node, Face +import mesh_model.mesh_struct.mesh as mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face import numpy.testing from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_mesh_analysis.py b/test_modules/test_mesh_analysis.py index a28321a..5710b67 100644 --- a/test_modules/test_mesh_analysis.py +++ b/test_modules/test_mesh_analysis.py @@ -1,8 +1,8 @@ import unittest -from model.mesh_struct.mesh import Mesh -from model.mesh_struct.mesh_elements import Dart -import model.mesh_analysis as Mesh_analysis +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart +import mesh_model.mesh_analysis as Mesh_analysis from actions.triangular_actions import split_edge_ids diff --git a/test_modules/test_mesh_structure.py b/test_modules/test_mesh_structure.py index 6828fe0..5a48163 100644 --- a/test_modules/test_mesh_structure.py +++ b/test_modules/test_mesh_structure.py @@ -1,5 +1,5 @@ import unittest -import model.mesh_struct.mesh as mesh +import mesh_model.mesh_struct.mesh as mesh import numpy.testing diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index 9c87657..ebe3cf7 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -1,6 +1,6 @@ import unittest -from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle -from model.mesh_struct.mesh import Mesh +from mesh_model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh, mesh_shuffle +from mesh_model.mesh_struct.mesh import Mesh from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_reader.py b/test_modules/test_reader.py index d9a2184..0302829 100644 --- a/test_modules/test_reader.py +++ b/test_modules/test_reader.py @@ -1,6 +1,6 @@ import unittest -from model.reader import read_medit -from model.reader import read_gmsh +from mesh_model.reader import read_medit +from mesh_model.reader import read_gmsh import os TESTFILE_FOLDER = os.path.join(os.path.dirname(__file__), '../mesh_files/') diff --git a/train.py b/train.py index c1e619f..c6a8c73 100644 --- a/train.py +++ b/train.py @@ -1,4 +1,4 @@ -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM import torch from environment.trimesh_env import TriMesh @@ -30,12 +30,12 @@ def train(): # critic = Critic(30, lr=0.0001) # policy = NNPolicy(env, 30, 64,5, 0.9, lr=0.0001) - model = PPO(env, lr, gamma, nb_iterations=8, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) + model = PPO(env, lr, gamma, nb_iterations=7, nb_episodes_per_iteration=100, nb_epochs=2, batch_size=8) actor, rewards, wins, steps = model.train() if rewards is not None: plot_training_results(rewards, wins, steps) - torch.save(actor.state_dict(), 'policy saved/actor_network.pth') + torch.save(actor.state_dict(), 'policy_saved/actor_network.pth') avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 60) if rewards is not None: diff --git a/user_game.py b/user_game.py index 558e628..aea31bb 100644 --- a/user_game.py +++ b/user_game.py @@ -1,6 +1,6 @@ from view.window import Game -import model.random_trimesh as TM +import mesh_model.random_trimesh as TM from mesh_display import MeshDisplay From 85d7c3d1d37af4182178b2c8bc9753af57a49ff9 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 11:07:12 +0100 Subject: [PATCH 18/22] Fixed some Codacy issues --- environment/trimesh_env.py | 4 +--- exploit.py | 4 +--- main.py | 8 +++----- mesh_model/mesh_analysis.py | 6 +++--- model_RL/evaluate_model.py | 3 +-- test_modules/test_actions.py | 2 +- test_modules/test_random_trimesh.py | 4 ++-- 7 files changed, 12 insertions(+), 19 deletions(-) diff --git a/environment/trimesh_env.py b/environment/trimesh_env.py index 6475f98..ea8167f 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,7 +1,7 @@ from typing import Any import math import numpy as np -from mesh_model.mesh_analysis import global_score, isValidAction, find_template_opposite_node +from mesh_model.mesh_analysis import global_score, find_template_opposite_node from mesh_model.mesh_struct.mesh_elements import Dart from mesh_model.mesh_struct.mesh import Mesh from actions.triangular_actions import flip_edge, split_edge, collapse_edge @@ -83,11 +83,9 @@ def get_x_global_4(env, state: Mesh): template = get_template_2(mesh) darts_to_delete = [] darts_id = [] - all_action_type = 3 for i, d_info in enumerate(mesh.active_darts()): d_id = d_info[0] - d = Dart(mesh, d_id) if d_info[2] == -1: #test the validity of all action type darts_to_delete.append(i) else: diff --git a/exploit.py b/exploit.py index fa8cdf8..208c8f6 100644 --- a/exploit.py +++ b/exploit.py @@ -3,13 +3,11 @@ from environment.trimesh_env import TriMesh from model_RL.utilities.actor_critic_networks import Actor -from plots.create_plots import plot_training_results, plot_test_results +from plots.create_plots import plot_test_results from plots.mesh_plotter import plot_dataset from model_RL.evaluate_model import testPolicy -from model_RL.PPO_model import PPO - LOCAL_MESH_FEAT = 0 diff --git a/main.py b/main.py index 077c326..21646c9 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,10 @@ import sys from user_game import user_game -from train import train +#from train import train from exploit import exploit -from mesh_model.reader import read_gmsh -from view.window import Game -from mesh_display import MeshDisplay -from mesh_model.random_trimesh import random_mesh +#from mesh_model.reader import read_gmsh +#from mesh_display import MeshDisplay # Press the green button in the gutter to run the script. diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index 34e67a0..73fc21c 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -75,7 +75,7 @@ def get_angle(d1: Dart, d2: Dart, n: Node) -> float: cos_theta = np.clip(cos_theta, -1, 1) angle = np.arccos(cos_theta) if np.isnan(angle): - raise(ValueError("Angle error")) + raise ValueError("Angle error") return degrees(angle) @@ -345,7 +345,7 @@ def isSplitOk(d: Dart) -> bool: def isCollapseOk(d: Dart) -> bool: mesh = d.mesh - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + _, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) d112 = d11.get_beta(2) d12 = d1.get_beta(2) @@ -417,7 +417,7 @@ def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float :return: True if valid, False otherwise """ for f in faces: - d, d1, d11, A, B, C = f.get_surrounding() + _, _, _, A, B, C = f.get_surrounding() if A.id == n_id: vect_AB = (B.x() - new_x, B.y() - new_y) vect_AC = (C.x() - new_x, C.y() - new_y) diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 21542a8..a433b16 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,7 +1,7 @@ from numpy import ndarray from environment.trimesh_env import TriMesh -from mesh_model.mesh_analysis import global_score, get_boundary_darts +from mesh_model.mesh_analysis import global_score from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy @@ -39,7 +39,6 @@ def testPolicy( if action is None: env.terminal = True break - boundary_darts = get_boundary_darts(env.mesh) env.step(action) ep_rewards += env.reward ep_length += 1 diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index ab845d0..a922ba0 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,6 +1,6 @@ import unittest import mesh_model.mesh_struct.mesh as mesh -from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face +from mesh_model.mesh_struct.mesh_elements import Dart, Node import numpy.testing from plots.mesh_plotter import plot_mesh diff --git a/test_modules/test_random_trimesh.py b/test_modules/test_random_trimesh.py index ebe3cf7..a529f79 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -16,7 +16,7 @@ def test_regular_trimesh(self): self.assertEqual(m.nb_nodes(), 60) def test_random_trimesh(self): - for i in range(10): + for _ in range(10): m = random_mesh(30) self.assertIsInstance(m, Mesh) @@ -30,7 +30,7 @@ def test_random_flip_mesh(self): def test_mesh_suffle(self): m = regular_mesh(15) - mesh = mesh_shuffle(m) + mesh = mesh_shuffle(m, 15) plot_mesh(mesh) From 77242242a828fa8a2e86615c67908d4dc417d60c Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 13:38:01 +0100 Subject: [PATCH 19/22] Fixed Codacy complexity issue --- actions/triangular_actions.py | 44 +++++++---------------------------- main.py | 5 ++-- mesh_model/mesh_analysis.py | 22 ++++++++++++++++-- test_modules/test_actions.py | 1 - 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 6bff48f..a357131 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts +from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts, isSplitOk def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -17,9 +17,6 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - test_degree(n3) - test_degree(n4) - f1 = d.get_face() f2 = d2.get_face() @@ -54,16 +51,11 @@ def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found: + if not found or not isFlipOk(d): return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - if not test_degree(n3): - return False - elif not test_degree(n4): - return False - # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) @@ -98,10 +90,6 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - elif not test_degree(n1): - return False - elif not test_boundary(n1, n2): - return False d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) @@ -149,7 +137,6 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: ds = (ds2.get_beta(1)).get_beta(1) ds2 = ds.get_beta(2) - #update beta2 relations if d112 is not None: d112.set_beta(2, d12) @@ -164,23 +151,7 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: #delete n2 node mesh.del_node(n2) - #check for duplicate darts - if not check_double(mesh): - raise ValueError("double error") - if not check_beta2_relation(mesh): - raise ValueError("error beta2") - return True - -def test_degree(n: Node) -> bool: - """ - Verify that the degree of a vertex is lower than 10 - :param n: a Node - :return: True if the degree is lower than 10, False otherwise - """ - if degree(n) > 10: - return False - else: - return True + return mesh_check(mesh) def test_boundary(n1: Node, n2: Node) -> bool: @@ -203,9 +174,9 @@ def check_beta2_relation(mesh: Mesh) -> bool: d = dart_info[0] d2 = dart_info[2] if d2 >= 0 and mesh.dart_info[d2, 0] < 0: - return False + raise ValueError("error beta2") elif d2 >= 0 and mesh.dart_info[d2, 2] != d: - return False + raise ValueError("error beta2") return True @@ -231,7 +202,10 @@ def check_double(mesh: Mesh) -> bool: ns2 = ds2.get_node().id if n1 == ns1 and n2 == ns2: - return False + raise ValueError("double error") elif n2 == ns1 and n1 == ns2: return False return True + +def mesh_check(mesh: Mesh) -> bool: + return check_double(mesh) and check_beta2_relation(mesh) \ No newline at end of file diff --git a/main.py b/main.py index 21646c9..ad9cd7b 100644 --- a/main.py +++ b/main.py @@ -14,9 +14,10 @@ user_game(int(sys.argv[1])) else: exploit() - """ + +""" cmap = read_gmsh("mesh_files/irr_losange.msh") mesh_disp = MeshDisplay(cmap) g = Game(cmap, mesh_disp) g.run() - """ +""" \ No newline at end of file diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index 73fc21c..ebba650 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -278,7 +278,10 @@ def isFlipOk(d: Dart) -> bool: if d.get_beta(2) is None: return False else: - d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) + + if not test_degree(A) or not test_degree(B): + return False # Check angle at d limits to avoid edge reversal angle_B = get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), D.x(), D.y()) @@ -310,6 +313,9 @@ def isSplitOk(d: Dart) -> bool: else: d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + if not test_degree(A) or not test_degree(B): + return False + newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 #Check if new triangle will be valid @@ -345,7 +351,7 @@ def isSplitOk(d: Dart) -> bool: def isCollapseOk(d: Dart) -> bool: mesh = d.mesh - _, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) d112 = d11.get_beta(2) d12 = d1.get_beta(2) @@ -359,6 +365,8 @@ def isCollapseOk(d: Dart) -> bool: return False elif on_boundary(n1) or on_boundary(n2): return False + elif not test_degree(n1): + return False else: # search for all adjacent faces to n1 and n2 if d12 is None and d2112 is None: @@ -478,3 +486,13 @@ def angle_from_sides(a, b, c): raise ValueError("Math domain error : cos>1.01") return acos(cosA) +def test_degree(n: Node) -> bool: + """ + Verify that the degree of a vertex is lower than 10 + :param n: a Node + :return: True if the degree is lower than 10, False otherwise + """ + if degree(n) > 10: + return False + else: + return True \ No newline at end of file diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index a922ba0..28e70ca 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,7 +1,6 @@ import unittest import mesh_model.mesh_struct.mesh as mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -import numpy.testing from plots.mesh_plotter import plot_mesh from actions.triangular_actions import split_edge, flip_edge, collapse_edge From cf29ca72dd00536785aad833172a128324265ca6 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 13:46:19 +0100 Subject: [PATCH 20/22] Fixed Codacy issues --- actions/triangular_actions.py | 6 +++--- main.py | 11 +++++------ mesh_model/mesh_analysis.py | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index a357131..1e2c7b4 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -2,7 +2,7 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Dart, Node -from mesh_model.mesh_analysis import degree, isFlipOk, isCollapseOk, adjacent_darts, isSplitOk +from mesh_model.mesh_analysis import isFlipOk, isCollapseOk, adjacent_darts, isSplitOk def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -51,10 +51,10 @@ def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: found, d = mesh.find_inner_edge(n1, n2) - if not found or not isFlipOk(d): + if not found or not isSplitOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d2, d1, _, d21, _, n1, n2, n3, n4 = mesh.active_triangles(d) # create a new node in the middle of [n1, n2] N5 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) diff --git a/main.py b/main.py index ad9cd7b..8176b0e 100644 --- a/main.py +++ b/main.py @@ -15,9 +15,8 @@ else: exploit() -""" - cmap = read_gmsh("mesh_files/irr_losange.msh") - mesh_disp = MeshDisplay(cmap) - g = Game(cmap, mesh_disp) - g.run() -""" \ No newline at end of file + + #cmap = read_gmsh("mesh_files/irr_losange.msh") + #mesh_disp = MeshDisplay(cmap) + #g = Game(cmap, mesh_disp) + #g.run() diff --git a/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py index ebba650..dfb8a21 100644 --- a/mesh_model/mesh_analysis.py +++ b/mesh_model/mesh_analysis.py @@ -311,7 +311,7 @@ def isSplitOk(d: Dart) -> bool: if d.get_beta(2) is None: return False else: - d2, d1, d11, d21, d211, A, B, C, D = mesh.active_triangles(d) + _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) if not test_degree(A) or not test_degree(B): return False From 879f60f522f0eb28a72f04d34523c87a59fbda6b Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 16:26:21 +0100 Subject: [PATCH 21/22] Fixed Codacy issues --- actions/triangular_actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 1e2c7b4..d69ab87 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -91,8 +91,8 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - + d2, d1, dbeta1, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d11 = dbeta1 #T1 d212 = d21.get_beta(2) From 35f51aec516a3d73b2d8d1b86a62354c38c6cb67 Mon Sep 17 00:00:00 2001 From: ropercha Date: Wed, 27 Nov 2024 16:33:13 +0100 Subject: [PATCH 22/22] Fix Codacy issues --- actions/triangular_actions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index d69ab87..3df7fe3 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -91,19 +91,12 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if not found or not isCollapseOk(d): return False - d2, d1, dbeta1, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) - d11 = dbeta1 - #T1 - d212 = d21.get_beta(2) + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) - #T2 - d2112 = d211.get_beta(2) - - #T3 - d12 = d1.get_beta(2) - - #T4 - d112 = d11.get_beta(2) + d212 = d21.get_beta(2) #T1 + d2112 = d211.get_beta(2) #T2 + d12 = d1.get_beta(2) #T3 + d112 = d11.get_beta(2) #T4 #Delete the darts around selected dart mesh.del_adj_triangles(d)