diff --git a/.github/workflows/model-view-ci.yml b/.github/workflows/model-view-ci.yml index 9041aa0..1af7a75 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: @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest numpy coverage pygame + pip install flake8 pytest numpy coverage pygame matplotlib if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/actions/triangular_actions.py b/actions/triangular_actions.py index 1c458e7..a05831c 100644 --- a/actions/triangular_actions.py +++ b/actions/triangular_actions.py @@ -1,8 +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 +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_analysis import isFlipOk, isCollapseOk, adjacent_darts, isSplitOk def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: @@ -15,10 +15,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) - - test_degree(n3) - test_degree(n4) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) f1 = d.get_face() f2 = d2.get_face() @@ -54,12 +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: + if not found or not isSplitOk(d): return False - d2, d1, d11, d21, d211, n1, n2, n3, n4 = active_triangles(mesh, d) - test_degree(n3) - test_degree(n4) + 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) @@ -86,33 +81,110 @@ 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 - """ - 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 - - -def test_degree(n: Node) -> bool: +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 + + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) + + 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) + + #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) """ - 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 + 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) """ - if degree(n) > 10: - return False - else: - return True \ No newline at end of file + #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 mesh_check(mesh) + + +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: + raise ValueError("error beta2") + elif d2 >= 0 and mesh.dart_info[d2, 2] != d: + raise ValueError("error beta2") + 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: + 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/environment/trimesh_env.py b/environment/trimesh_env.py index 412ae2d..ea8167f 100644 --- a/environment/trimesh_env.py +++ b/environment/trimesh_env.py @@ -1,27 +1,29 @@ 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 model.random_trimesh import random_flip_mesh +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 +from mesh_model.random_trimesh import random_flip_mesh, random_mesh # possible actions FLIP = 0 +SPLIT = 1 +COLLAPSE = 2 GLOBAL = 0 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.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 @@ -30,30 +32,34 @@ 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] + 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]]]: + def get_x(self, s: Mesh, a: int): """ Get the feature vector of the state-action pair :param s: the state @@ -66,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 @@ -74,11 +80,31 @@ 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 = [] + + for i, d_info in enumerate(mesh.active_darts()): + d_id = d_info[0] + 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.dart_info) + size = len(mesh.active_darts()) template = np.zeros((size, 6)) - for d_info in mesh.dart_info: + for i, d_info in enumerate(mesh.active_darts()): d = Dart(mesh, d_info[0]) A = d.get_node() @@ -87,35 +113,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/exploit.py b/exploit.py new file mode 100644 index 0000000..208c8f6 --- /dev/null +++ b/exploit.py @@ -0,0 +1,31 @@ +import mesh_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_test_results +from plots.mesh_plotter import plot_dataset + +from model_RL.evaluate_model import testPolicy + +LOCAL_MESH_FEAT = 0 + + +def exploit(): + mesh_size = 12 + feature = LOCAL_MESH_FEAT + + dataset = [TM.random_mesh(30) for _ in range(9)] + plot_dataset(dataset) + + 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.pth')) + + 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) + plot_dataset(final_meshes) diff --git a/main.py b/main.py index 21b2812..8176b0e 100644 --- a/main.py +++ b/main.py @@ -1,7 +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 mesh_display import MeshDisplay # Press the green button in the gutter to run the script. @@ -10,4 +13,10 @@ 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) + #g = Game(cmap, mesh_disp) + #g.run() diff --git a/mesh_display.py b/mesh_display.py index 1e09b67..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: @@ -9,22 +9,23 @@ 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 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): """ - 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.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,5 @@ 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) + return [nodes_score, mesh_score, ideal_mesh_score] 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/mesh_model/mesh_analysis.py b/mesh_model/mesh_analysis.py new file mode 100644 index 0000000..d7bc4f8 --- /dev/null +++ b/mesh_model/mesh_analysis.py @@ -0,0 +1,501 @@ +from math import sqrt, degrees, radians, cos, sin, acos +import numpy as np + +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) + 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) + else: + nodes_score.append(0) + return nodes_score, mesh_score, mesh_ideal_score + + +def score_calculation(n: Node) -> int: + """ + Function to calculate the irregularity of a node in the mesh. + :param n: a node in the mesh. + :return: the irregularity of the node + """ + adjacency = degree(n) + if on_boundary(n): + angle = get_boundary_angle(n) + ideal_adjacency = max(round(angle/60)+1, 2) + else: + ideal_adjacency = 360/60 + + return ideal_adjacency-adjacency + + +def get_angle(d1: Dart, d2: Dart, n: Node) -> float: + """ + Function to calculate the angle of the boundary at the node n. + The angle is named ABC and node n is at point A. + :param d1: the first boundary dart. + :param d2: the second boundary dart. + :param n: the boundary node + :return: the angle (degrees) + """ + if d1.get_node() == n: + A = n + B = d1.get_beta(1).get_node() + C = d2.get_node() + + else: + A = n + B = d2.get_beta(1).get_node() + C = d1.get_node() + if d2.get_node() != A: + raise ValueError("Angle error") + + vect_AB = (B.x() - A.x(), B.y() - A.y()) + 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) + 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) + + +def get_boundary_angle(n: Node) -> float: + """ + Calculate the boundary angle of a node in the mesh. + :param n: a boundary node + :return: the boundary angle (degrees) + """ + adj_darts_list = adjacent_darts(n) + boundary_darts = [] + for d in adj_darts_list: + d_twin = d.get_beta(2) + if d_twin is None: + boundary_darts.append(d) + if len(boundary_darts) > 7: + raise ValueError("Boundary error") + angle = get_angle(boundary_darts[0], boundary_darts[1], n) + return angle + + +def on_boundary(n: Node) -> bool: + """ + Test if the node n is on boundary. + :param n: a node in the mesh. + :return: True if the node n is on boundary, False otherwise. + """ + adj_darts_list = adjacent_darts(n) + for d in adj_darts_list: + d_twin = d.get_beta(2) + if d_twin is None: + return True + return False + + +def adjacent_darts(n: Node) -> list[Dart]: + """ + Function that retrieve the adjacent darts of node n. + :param n: a node in the mesh. + :return: the list of adjacent darts + """ + adj_darts = [] + 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) + if d_nfrom == n and d not in adj_darts: + adj_darts.append(d) + if d_nto.get_node() == n and d not in adj_darts: + adj_darts.append(d) + return adj_darts + + +def degree(n: Node) -> int: + """ + Function to calculate the degree of a node in the mesh. + :param n: a node in the mesh. + :return: the degree of the node + """ + adj_darts_list = adjacent_darts(n) + adjacency = 0 + b = on_boundary(n) + boundary_darts = [] + for d in adj_darts_list: + d_twin = d.get_beta(2) + if d_twin is None and b: + adjacency += 1 + boundary_darts.append(d) + else: + adjacency += 0.5 + if adjacency != int(adjacency): + raise ValueError("Adjacency error") + return adjacency + + +def get_boundary_darts(m: Mesh) -> list[Dart]: + """ + Find all boundary darts + :param m: a mesh + :return: a list of all boundary darts + """ + boundary_darts = [] + for d_info in m.active_darts(): + d = Dart(m, d_info[0]) + d_twin = d.get_beta(2) + if d_twin is None: + boundary_darts.append(d) + return boundary_darts + + +def find_opposite_node(d: Dart) -> (int, int): + """ + Find the coordinates of the vertex opposite in the adjacent triangle + :param d: a dart + :return: (X Coordinate, Y Coordinate) + """ + A = d.get_node() + d1 = d.get_beta(1) + B = d1.get_node() + + vect_AB = (B.x() - A.x(), B.y() - A.y()) + + angle_rot = radians(300) + x_AC = round(vect_AB[0] * cos(angle_rot) - vect_AB[1] * sin(angle_rot), 2) + y_AC = round(vect_AB[1] * cos(angle_rot) + vect_AB[0] * sin(angle_rot), 2) + + x_C = A.x() + x_AC + y_C = A.y() + y_AC + + return x_C, y_C + + +def find_template_opposite_node(d: Dart) -> int: + """ + Find the the vertex opposite in the adjacent triangle + :param d: a dart + :return: the node found + """ + + d2 = d.get_beta(2) + if d2 is not None: + d21 = d2.get_beta(1) + d211 = d21.get_beta(1) + node_opposite = d211.get_node() + return node_opposite.id + else: + return None + + +def node_in_mesh(mesh: Mesh, x: float, y: float) -> (bool, int): + """ + Search if the node of coordinate (x, y) is inside the mesh. + :param mesh: the mesh to work with + :param x: X coordinate + :param y: Y coordinate + :return: a boolean indicating if the node is inside the mesh and the id of the node if it is. + """ + n_id = 0 + for n in mesh.nodes: + 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: 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: + return isFlipOk(d) + elif action == split: + return isSplitOk(d) + elif action == collapse: + return isCollapseOk(d) + elif action == test_all: + return isFlipOk(d) and isCollapseOk(d) and isSplitOk(d) + else: + 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 + + +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: + _, _, _, _, _, 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()) + 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 isSplitOk(d: Dart) -> bool: + mesh = d.mesh + + if d.get_beta(2) is None: + return False + else: + _, _, _, _, _, 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 + + # 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 + if d.get_beta(2) is None: + return False + else: + _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + 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 + 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: + 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 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: a mesh + :param faces: adjacents faces to node of id n_id + :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: + _, _, _, 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) + 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("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 # One face is not correctly oriented or is flat + elif not valid_triangle(vect_AB, vect_AC, vect_BC): + return False + return True + + +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) + target_mesh_size = 1 + + L_max = max(dist_AB, dist_AC, dist_BC) + + if target_mesh_size/1.5*sqrt(2) < L_max < target_mesh_size*1.5*sqrt(2): + pass + else: + return False + + # Calcul des angles avec le théorème du cosinus + 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: + return False + return True + + +def angle_from_sides(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 + 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) + +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 + +""" +def get_boundary_nodes(m: Mesh) -> list[Node]: + # + Find all boundary nodes + :param m: a mesh + :return: a list of all boundary nodes + # + boundary_nodes = [] + 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 +""" \ No newline at end of file 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 63% rename from model/mesh_struct/mesh.py rename to mesh_model/mesh_struct/mesh.py index b54a444..1921811 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 @@ -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 = 0 + self.first_free_node = 0 + self.first_free_face = 0 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,16 +65,35 @@ 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) :param ni: a node """ ni.set_x(sys.float_info.max) + self.del_node(ni) def add_triangle(self, n1: Node, n2: Node, n3: Node) -> Face: """ @@ -96,14 +118,49 @@ 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 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: """ Add a quad defined by nodes of indices n1, n2, n3 and n4. @@ -141,7 +198,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 +207,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 +225,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: @@ -206,10 +263,29 @@ 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 """ - 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 +294,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 +302,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/mesh_model/mesh_struct/mesh_elements.py similarity index 92% rename from model/mesh_struct/mesh_elements.py rename to mesh_model/mesh_struct/mesh_elements.py index 4b02e71..970a3e0 100644 --- a/model/mesh_struct/mesh_elements.py +++ b/mesh_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: """ @@ -217,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/mesh_model/random_trimesh.py similarity index 65% rename from model/random_trimesh.py rename to mesh_model/random_trimesh.py index aaddac6..2e1b367 100644 --- a/model/random_trimesh.py +++ b/mesh_model/random_trimesh.py @@ -1,10 +1,10 @@ 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 -from actions.triangular_actions import flip_edge_ids, split_edge_ids +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 def regular_mesh(num_nodes_max: int) -> Mesh: @@ -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_flip(mesh) + mesh_shuffle(mesh, num_nodes_max) return mesh @@ -85,24 +86,36 @@ 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. + 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_flip = len(mesh.dart_info) - nb_action =nb_flip * 2 - nb_nodes = len(mesh.nodes) - for i in range(nb_action): - i1 = np.random.randint(nb_nodes) - i2 = np.random.randint(nb_nodes) - if i1 != i2 and i%2 == 0: - flip_edge_ids(mesh, i1, i2) - elif i1 !=i2 : - split_edge_ids(mesh, i1, i2) + nb_action_max = int(num_nodes) + nb_action = 0 + active_darts_list = mesh.active_darts() + i = 0 + 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 == 0 and isValidAction(mesh, d_id, action_type): + flip_edge_ids(mesh, i1.id, i2.id) + nb_action += 1 + 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 == 2 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 - diff --git a/model/reader.py b/mesh_model/reader.py similarity index 96% rename from model/reader.py rename to mesh_model/reader.py index c9326dd..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: @@ -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/model/mesh_analysis.py b/model/mesh_analysis.py deleted file mode 100644 index 2120603..0000000 --- a/model/mesh_analysis.py +++ /dev/null @@ -1,269 +0,0 @@ -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 import Mesh - - -def global_score(m: Mesh) -> (int, int): - """ - 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 = [] - 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) - return nodes_score, mesh_score, mesh_ideal_score - - -def score_calculation(n: Node) -> int: - """ - Function to calculate the irregularity of a node in the mesh. - :param n: a node in the mesh. - :return: the irregularity of the node - """ - adjacency = degree(n) - if on_boundary(n): - angle = get_boundary_angle(n) - ideal_adjacency = max(round(angle/60)+1, 2) - else: - ideal_adjacency = 360/60 - - return ideal_adjacency-adjacency - - -def get_angle(d1: Dart, d2: Dart, n: Node) -> float: - """ - Function to calculate the angle of the boundary at the node n. - The angle is named ABC and node n is at point A. - :param d1: the first boundary dart. - :param d2: the second boundary dart. - :param n: the boundary node - :return: the angle (degrees) - """ - if d1.get_node() == n: - A = n - B = d1.get_beta(1).get_node() - C = d2.get_node() - - else: - A = n - B = d2.get_beta(1).get_node() - C = d1.get_node() - if d2.get_node() != A: - raise ValueError("Angle error") - - vect_AB = (B.x() - A.x(), B.y() - A.y()) - 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)) - return degrees(angle) - - -def get_boundary_angle(n: Node) -> float: - """ - Calculate the boundary angle of a node in the mesh. - :param n: a boundary node - :return: the boundary angle (degrees) - """ - adj_darts_list = adjacent_darts(n) - boundary_darts = [] - for d in adj_darts_list: - d_twin = d.get_beta(2) - if d_twin is None: - boundary_darts.append(d) - if len(boundary_darts) > 3: - raise ValueError("Boundary error") - angle = get_angle(boundary_darts[0], boundary_darts[1], n) - return angle - - -def on_boundary(n: Node) -> bool: - """ - Test if the node n is on boundary. - :param n: a node in the mesh. - :return: True if the node n is on boundary, False otherwise. - """ - adj_darts_list = adjacent_darts(n) - for d in adj_darts_list: - d_twin = d.get_beta(2) - if d_twin is None: - return True - return False - - -def adjacent_darts(n: Node) -> list[Dart]: - """ - Function that retrieve the adjacent darts of node n. - :param n: a node in the mesh. - :return: the list of adjacent darts - """ - adj_darts = [] - for d_info in n.mesh.dart_info: - d = Dart(n.mesh, d_info[0]) - d_nfrom = d.get_node() - d_nto = d.get_beta(1) - if d_nfrom == n and d not in adj_darts: - adj_darts.append(d) - if d_nto.get_node() == n and d not in adj_darts: - adj_darts.append(d) - return adj_darts - - -def degree(n: Node) -> int: - """ - Function to calculate the degree of a node in the mesh. - :param n: a node in the mesh. - :return: the degree of the node - """ - adj_darts_list = adjacent_darts(n) - adjacency = 0 - b = on_boundary(n) - boundary_darts = [] - for d in adj_darts_list: - d_twin = d.get_beta(2) - if d_twin is None and b: - adjacency += 1 - boundary_darts.append(d) - else: - adjacency += 0.5 - if adjacency != int(adjacency): - raise ValueError("Adjacency error") - return adjacency - - -def get_boundary_darts(m: Mesh) -> list[Dart]: - """ - Find all boundary darts - :param m: a mesh - :return: a list of all boundary darts - """ - boundary_darts = [] - for d_info in m.dart_info: - d = Dart(m, d_info[0]) - d_twin = d.get_beta(2) - if d_twin is None : - boundary_darts.append(d) - return boundary_darts - - -def get_boundary_nodes(m: Mesh) -> list[Node]: - """ - Find all boundary nodes - :param m: a mesh - :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) - return boundary_nodes - - -def find_opposite_node(d: Dart) -> (int, int): - """ - Find the coordinates of the vertex opposite in the adjacent triangle - :param d: a dart - :return: (X Coordinate, Y Coordinate) - """ - A = d.get_node() - d1 = d.get_beta(1) - B = d1.get_node() - - vect_AB = (B.x() - A.x(), B.y() - A.y()) - - angle_rot = radians(300) - x_AC = round(vect_AB[0] * cos(angle_rot) - vect_AB[1] * sin(angle_rot), 2) - y_AC = round(vect_AB[1] * cos(angle_rot) + vect_AB[0] * sin(angle_rot), 2) - - x_C = A.x() + x_AC - y_C = A.y() + y_AC - - return x_C, y_C - -def find_template_opposite_node(d: Dart) -> (int): - """ - Find the the vertex opposite in the adjacent triangle - :param d: a dart - :return: the node found - """ - - d2 = d.get_beta(2) - if d2 is not None: - d21 = d2.get_beta(1) - d211 = d21.get_beta(1) - node_opposite = d211.get_node() - return node_opposite.id - else: - return None - - -def node_in_mesh(mesh: Mesh, x: float, y: float) -> (bool, int): - """ - Search if the node of coordinate (x, y) is inside the mesh. - :param mesh: the mesh to work with - :param x: X coordinate - :param y: Y coordinate - :return: a boolean indicating if the node is inside the mesh and the id of the node if it is. - """ - 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 - return False, None - - -def isValidAction(mesh, dart_id: int) -> bool: - d = Dart(mesh, dart_id) - boundary_darts = get_boundary_darts(mesh) - if d in boundary_darts or not isFlipOk(d): - return False - else: - return True - -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)) - - rad = acos(cos_ABC) - deg = degrees(rad) - return deg - - -def isFlipOk(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 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 d1ae1ce..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 @@ -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 @@ -68,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 = [] @@ -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..a433b16 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 -from model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis import global_score +from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy from tqdm import tqdm @@ -34,8 +34,11 @@ 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 env.step(action) ep_rewards += env.reward ep_length += 1 @@ -57,7 +60,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..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): @@ -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/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 6bad764..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 @@ -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..a995250 --- /dev/null +++ b/test_modules/test_actions.py @@ -0,0 +1,111 @@ +import unittest +import mesh_model.mesh_struct.mesh as mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.random_trimesh import regular_mesh +from actions.triangular_actions import split_edge, flip_edge, collapse_edge +from plots.mesh_plotter import plot_mesh + + +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) + plot_mesh(cmap) + n00 = Node(cmap, 0) + n11 = Node(cmap, 2) + split_edge(cmap, n00, n11) + plot_mesh(cmap) + n5 = Node(cmap, 4) + collapse_edge(cmap, n00, n5) + d1_to_test = Dart(cmap, 7) + d2_to_test = Dart(cmap, 0) + self.assertEqual(collapse_edge(cmap, n00, n5), False) + # Test possible collapse + cmap = regular_mesh(16) + d = Dart(cmap, 0) + n0 = d.get_node() + n1 = d.get_beta(1).get_node() + self.assertEqual(collapse_edge(cmap, n0, n1), True) + + 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) + n7 = Node(cmap, 7) + n8 = Node(cmap, 8) + collapse_edge(cmap, n7, n8) + collapse_edge(cmap, n5, n7) + + + diff --git a/test_modules/test_mesh_analysis.py b/test_modules/test_mesh_analysis.py index 3ff7580..48fc908 100644 --- a/test_modules/test_mesh_analysis.py +++ b/test_modules/test_mesh_analysis.py @@ -1,10 +1,10 @@ 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 - +from plots.mesh_plotter import plot_mesh class TestMeshAnalysis(unittest.TestCase): @@ -38,9 +38,9 @@ def test_split_score(self): faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] cmap = Mesh(nodes, faces) split_edge_ids(cmap, 0, 2) - split_edge_ids(cmap, 1, 2) + split_edge_ids(cmap, 1, 2) # split impossible nodes_score, mesh_score, mesh_ideal_score = Mesh_analysis.global_score(cmap) - self.assertEqual((5, 1), (mesh_score, mesh_ideal_score)) + self.assertEqual((3, 1), (mesh_score, mesh_ideal_score)) def test_find_template_opposite_node_not_found(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [2.0, 0.0]] @@ -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]] @@ -69,6 +70,26 @@ def test_isFlipOk(self): dart_to_test = Dart(cmap, 2) self.assertTrue(Mesh_analysis.isFlipOk(dart_to_test)) + def test_isSplitOk(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) + plot_mesh(cmap) + dart_to_test = Dart(cmap, 0) + self.assertFalse(Mesh_analysis.isSplitOk(dart_to_test)) + dart_to_test = Dart(cmap, 2) + self.assertTrue(Mesh_analysis.isSplitOk(dart_to_test)) + + def test_isCollapseOk(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) + plot_mesh(cmap) + dart_to_test = Dart(cmap, 0) + self.assertFalse(Mesh_analysis.isCollapseOk(dart_to_test)) + dart_to_test = Dart(cmap, 2) + self.assertFalse(Mesh_analysis.isCollapseOk(dart_to_test)) + if __name__ == '__main__': unittest.main() diff --git a/test_modules/test_mesh_structure.py b/test_modules/test_mesh_structure.py index 2d49484..bf5c231 100644 --- a/test_modules/test_mesh_structure.py +++ b/test_modules/test_mesh_structure.py @@ -1,9 +1,7 @@ import unittest -import model.mesh_struct.mesh as mesh +import mesh_model.mesh_struct.mesh as mesh import numpy.testing -from actions.triangular_actions import split_edge, flip_edge - class TestMeshStructure(unittest.TestCase): @@ -17,7 +15,6 @@ def test_nodes(self): self.assertEqual(0, cmap.nb_nodes()) n = cmap.add_node(1.1, 2.3) - self.assertEqual(1, cmap.nb_nodes()) self.assertEqual(1.1, n.x()) self.assertEqual(2.3, n.y()) n.set_x(3) @@ -29,6 +26,7 @@ def test_nodes(self): self.assertEqual(6, n.y()) n2 = cmap.add_node(1, 23) n3 = cmap.add_node(3, 1) + cmap.add_triangle(n, n2, n3) self.assertEqual(3, cmap.nb_nodes()) cmap.del_vertex(n2) self.assertEqual(2, cmap.nb_nodes()) @@ -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..6209045 100644 --- a/test_modules/test_random_trimesh.py +++ b/test_modules/test_random_trimesh.py @@ -1,6 +1,8 @@ import unittest -from model.random_trimesh import regular_mesh, random_mesh, random_flip_mesh -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 class TestRandomTrimesh(unittest.TestCase): @@ -14,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 _ in range(10): + m = random_mesh(30) + self.assertIsInstance(m, Mesh) def test_random_flip_mesh(self): m = random_flip_mesh(44) @@ -29,6 +28,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(15) + mesh = mesh_shuffle(m, 15) + #plot_mesh(mesh) + if __name__ == '__main__': diff --git a/test_modules/test_reader.py b/test_modules/test_reader.py index d9a2184..1455363 100644 --- a/test_modules/test_reader.py +++ b/test_modules/test_reader.py @@ -1,8 +1,9 @@ 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/') class TestReader(unittest.TestCase): @@ -10,7 +11,7 @@ class TestReader(unittest.TestCase): def test_read_medit(self): filename = os.path.join(TESTFILE_FOLDER, 'circle_coarse.mesh') m = read_medit(filename) - self.assertEqual(m.nb_nodes(), 99) + self.assertEqual(m.nb_nodes(), 98) self.assertEqual(m.nb_faces(), 164) def test_read_gmsh_tri(self): diff --git a/train.py b/train.py index ae7c0ae..c6a8c73 100644 --- a/train.py +++ b/train.py @@ -1,5 +1,5 @@ -import model.random_trimesh as TM - +import mesh_model.random_trimesh as TM +import torch from environment.trimesh_env import TriMesh from plots.create_plots import plot_training_results, plot_test_results @@ -15,27 +15,29 @@ def train(): - mesh_size = 12 + mesh_size = 30 lr = 0.0001 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(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=2, nb_episodes_per_iteration=100, nb_epochs=1, 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) - 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, 60) 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) diff --git a/user_game.py b/user_game.py index 470b51d..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 @@ -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..46e95ac 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: @@ -110,10 +110,10 @@ def clear(self): self.edges = [] def update(self, vertices, edges, scores): - for idx, n in enumerate(vertices): - nodes_scores= scores[0] - n_value = nodes_scores[idx] - self.create_vertex(idx, n[0], n[1], n_value) + for n in vertices: + nodes_scores = scores[0] + 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]) @@ -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!!")