diff --git a/.github/workflows/model-view-ci.yml b/.github/workflows/model-view-ci.yml index 1af7a75..9084ab5 100644 --- a/.github/workflows/model-view-ci.yml +++ b/.github/workflows/model-view-ci.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest numpy coverage pygame matplotlib + pip install flake8 pytest numpy coverage pygame matplotlib scipy shapely if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/README.md b/README.md index 93c430d..ddf01b9 100644 --- a/README.md +++ b/README.md @@ -34,25 +34,27 @@ For training on triangular meshes, you can use an agent with all three actions: ##### 1. Using `tune/model_RL/PPO_model` - Configure the model and environment parameters in: - `tune/training/train.py` + `tune/training/config/trimesh_config_PPO_perso.yaml` - Then run the following command from the `tune/` directory: ```bash - python main.py + python -m training.train_trimesh.py ``` - - + ##### 2. Using PPO from Stable Baselines 3 (SB3) - Configure the model and environment parameters in: - - `tune/environment/environment_config.json` - - `tune/model_RL/parameters/PPO_config.json` + `tune/training/config/trimesh_config_PPO_SB3.yaml` -- Then run the training script in pycharm `tune/training/train_trimesh_SB3.py` +- Then run the following command from the `tune/` directory: + ```bash + python -m training.train_trimesh_SB3.py + ``` ###### Flip-Only Training (SB3 PPO) To train an agent using only the flip action with SB3 PPO, run the training script in pycharm `tune/training/train_trimesh_flip_SB3.py` +> โ— This environment may be deprecated. --- @@ -65,21 +67,26 @@ For training on quadrangular meshes, you can use an agent with all four actions: #### ๐Ÿš€ Starting Training -##### 1. Configure the model and environment parameters in : - - `tune/environment/environment_config.json` - - `tune/model_RL/parameters/PPO_config.json` - -##### 2. Using `tune/model_RL/PPO_model_pers` -Run the following command from the `tune/` directory: +##### 1. Using `tune/model_RL/PPO_model` + +- Configure the model and environment parameters in: + `tune/training/config/quadmesh_config_PPO_perso.yaml` + +- Then run the following command from the `tune/` directory: ```bash - python -m training.train_quadmesh + python -m training.train_quadmesh.py ``` + +##### 2. Using PPO from Stable Baselines 3 (SB3) -##### 3. Using PPO from Stable Baselines 3 (SB3) -Run the following command from the `tune/` directory: +- Configure the model and environment parameters in: + `tune/training/config/quadmesh_config_PPO_SB3.yaml` + +- Then run the following command from the `tune/` directory: ```bash - python -m training.train_quadmesh_SB3 + python -m training.train_quadmesh_SB3.py ``` + #### ๐Ÿงช Testing a Saved SB3 Policy After training, the model is saved as a `.zip` file in the `tune/training/policy_saved/` directory. To evaluate the policy, follow these steps in `tune/training/exploit_SB3_policy.py` : diff --git a/docs/actions.md b/docs/actions.md index 2b00115..3350838 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -18,7 +18,7 @@ However, in some configurations, flip action can lead to problematic configurati * **triangular quad:** This configuration should be avoided, as it results in the creation of a degenerate (flat) triangle. As illustrated in the figures below, flipping dart 0 leads to a flattened face between nodes 2, 0, and 7. - * + @@ -51,6 +51,54 @@ However, in some configurations, collapse action can also lead to problematic co * **Adjacency too high :** When nodes A already has an adjacency higher than 10, collapse is not possible. * **Configuration who tends to imply edge reversal**: To detect these situations, we look a the type of darts in the surrounding. When there are some darts with concave surrounding, collapse action can lead to edge reversal. - - \ No newline at end of file + + +--- + +# Quadrangular Actions + +## Flip + + + +The **flip** action can be decomposed as follows: + +* Check whether the dart `d` exists. +* Update the **beta1** relations. +* Update the links from nodes `n1` and `n2`, if they were previously linked to the flipped dart. +* Update the links from faces `F1` and `F2`, if they were previously linked to the flipped dart. +* Update the node links for `d` and `d2`. +* Update the face links depending on the orientation (darts which change of face): + * If **counterclockwise**: update links for `d1` and `d21`. + * If **clockwise**: update links for `d2111` and `d111`. +* Update the scores of the nodes. + +## Split + + + +The **split** action can be decomposed as follows: + +* Check whether the dart `d` exists. +* Create a new node **n10** in the middle of [n1,n2] +* Update node links of `d` and `d21`, they are now linked to `n10` +* Create a new face with yellow darts +* Update the **beta2** relations (`d1112`, `d111`,`d212`, `d21`). +* Update the scores of the nodes. + +## Collapse + + + +The **collapse** action can be decomposed as follows: + +* Check whether the dart `d` exists. +* Save the score of node `n1` for later computation. +* If `n3` is not on the boundary, move its coordinates to the midpoint between `n3` and `n1`. +* Check if nodes `n2` and `n4` are linked to the dart being deleted. If so, reassign their links to existing darts (node `n3` will be checked later). +* Delete the face associated with `d`, along with its related darts. +* Reassign all darts previously linked to `n1` so they now point to `n3`. +* Link the dart associated with `n3` to an existing node. +* Update the **beta2** relations (`d2`/`d12`, `d112`/`d1112`). +* Recalculate the scores of the affected nodes. diff --git a/docs/img/actions/collapse_quad.png b/docs/img/actions/collapse_quad.png new file mode 100644 index 0000000..6909cf6 Binary files /dev/null and b/docs/img/actions/collapse_quad.png differ diff --git a/docs/img/actions/flip_quad.png b/docs/img/actions/flip_quad.png new file mode 100644 index 0000000..936ee2e Binary files /dev/null and b/docs/img/actions/flip_quad.png differ diff --git a/docs/img/actions/split_quad.png b/docs/img/actions/split_quad.png new file mode 100644 index 0000000..2279194 Binary files /dev/null and b/docs/img/actions/split_quad.png differ diff --git a/docs/tests/actions.md b/docs/tests/actions.md new file mode 100644 index 0000000..04d36bf --- /dev/null +++ b/docs/tests/actions.md @@ -0,0 +1,38 @@ +# Actions tests + +## Triangular actions + +### Flip + +We want to ensure that **flip** operations are performed correctly. +Consider the following mesh: + + + +If we flip the dart between nodes **n0** and **n3**, we should obtain the following mesh: + + + +### Split + +We want to ensure that **split** operations are performed correctly. +Consider the following mesh: + + + +If we split the dart between nodes **n0** and **n3**, we add a node **n5** on the middle of the edge and two faces are created. +We should obtain the following mesh: + + + +### Collapse + +We want to ensure that **split** operations are performed correctly. +Consider the following mesh: + + + +Here we can't collapse the dart between nodes **n5** and **n2** because n2 is on boundary. However we can collapse the edge between **n4** and **n5**. +We should obtain the following mesh: + + \ No newline at end of file diff --git a/docs/tests/img/tri_collapse_after.png b/docs/tests/img/tri_collapse_after.png new file mode 100644 index 0000000..9b0ca5c Binary files /dev/null and b/docs/tests/img/tri_collapse_after.png differ diff --git a/docs/tests/img/tri_collapse_before.png b/docs/tests/img/tri_collapse_before.png new file mode 100644 index 0000000..48f3289 Binary files /dev/null and b/docs/tests/img/tri_collapse_before.png differ diff --git a/docs/tests/img/tri_flip_after.png b/docs/tests/img/tri_flip_after.png new file mode 100644 index 0000000..33cec2e Binary files /dev/null and b/docs/tests/img/tri_flip_after.png differ diff --git a/docs/tests/img/tri_flip_before.png b/docs/tests/img/tri_flip_before.png new file mode 100644 index 0000000..8ae9e65 Binary files /dev/null and b/docs/tests/img/tri_flip_before.png differ diff --git a/docs/tests/img/tri_split_after.png b/docs/tests/img/tri_split_after.png new file mode 100644 index 0000000..df8768a Binary files /dev/null and b/docs/tests/img/tri_split_after.png differ diff --git a/docs/tests/img/tri_split_before.png b/docs/tests/img/tri_split_before.png new file mode 100644 index 0000000..8ae9e65 Binary files /dev/null and b/docs/tests/img/tri_split_before.png differ diff --git a/environment/actions/quadrangular_actions.py b/environment/actions/quadrangular_actions.py index d1716bf..14efd5a 100644 --- a/environment/actions/quadrangular_actions.py +++ b/environment/actions/quadrangular_actions.py @@ -1,26 +1,37 @@ from __future__ import annotations +from copy import deepcopy + +from mesh_model.mesh_analysis.global_mesh_analysis import NodeAnalysis from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Node -from mesh_model.mesh_analysis.global_mesh_analysis import adjacent_darts, degree, mesh_check, on_boundary -from mesh_model.mesh_analysis.quadmesh_analysis import isFlipCCWOk, isFlipCWOk, isCollapseOk, isSplitOk, isCleanupOk +from mesh_model.reader import read_gmsh +from view.mesh_plotter.mesh_plots import plot_mesh +""" +Quadrangular actions performed on meshes. -def flip_edge_cntcw_ids(mesh: Mesh, id1: int, id2: int) -> True: - return flip_edge_cntcw(mesh, Node(mesh, id1), Node(mesh, id2)) +Each function returns three Booleans: + * action_validity: If the action has been performed. + * topo: if the action is topologically valid + * geo: if the action is geometrically valid +""" -def flip_edge_cntcw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): - found, d = mesh.find_inner_edge(n1, n2) +def flip_edge_cntcw_ids(mesh_analysis, id1: int, id2: int) -> (True, True, True): + return flip_edge_cntcw(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) + +def flip_edge_cntcw(mesh_analysis, n1: Node, n2: Node) -> (True, True, True): + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) if found: - topo, geo = isFlipCCWOk(d) + topo, geo = mesh_analysis.isFlipCCWOk(d) if not geo or not topo: return False, topo, geo else: return False, False, False - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh_analysis.mesh.active_quadrangles(d) f1 = d.get_face() f2 = d2.get_face() @@ -33,7 +44,7 @@ def flip_edge_cntcw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): d2111.set_beta(1, d1) d1.set_beta(1, d2) - + #Update nodes links if n1.get_dart().id == d.id: n1.set_dart(d21) if n2.get_dart().id == d2.id: @@ -41,7 +52,6 @@ def flip_edge_cntcw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): if f1.get_dart().id == d1.id: f1.set_dart(d) - if f2.get_dart().id == d21.id: f2.set_dart(d2) @@ -50,23 +60,28 @@ def flip_edge_cntcw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): d21.set_face(f1) d1.set_face(f2) - return True, topo, geo + # update nodes scores + n1.set_score(n1.get_score() + 1) + n2.set_score(n2.get_score() + 1) + n3.set_score(n3.get_score() - 1) + n5.set_score(n5.get_score() - 1) -def flip_edge_cw_ids(mesh: Mesh, id1: int, id2: int) -> True: - return flip_edge_cw(mesh, Node(mesh, id1), Node(mesh, id2)) + return True, topo, geo +def flip_edge_cw_ids(mesh_analysis, id1: int, id2: int) -> (True, True, True): + return flip_edge_cw(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) -def flip_edge_cw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): - found, d = mesh.find_inner_edge(n1, n2) +def flip_edge_cw(mesh_analysis, n1: Node, n2: Node) -> (True, True, True): + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) if found: - topo, geo = isFlipCWOk(d) + topo, geo = mesh_analysis.isFlipCWOk(d) if not geo or not topo: return False, topo, geo else: return False, False, False - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh_analysis.mesh.active_quadrangles(d) f1 = d.get_face() f2 = d2.get_face() @@ -97,51 +112,64 @@ def flip_edge_cw(mesh: Mesh, n1: Node, n2: Node) -> (True, True, True): d2111.set_face(f1) d111.set_face(f2) - return True, topo, geo - + # update nodes scores + n1.set_score(n1.get_score() + 1) + n2.set_score(n2.get_score() + 1) + n4.set_score(n3.get_score() - 1) + n6.set_score(n5.get_score() - 1) -def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return split_edge(mesh, Node(mesh, id1), Node(mesh, id2)) + return True, topo, geo +def split_edge_ids(mesh_analysis, id1: int, id2: int) -> (True, True, True): + return split_edge(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) -def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: - found, d = mesh.find_inner_edge(n1, n2) +def split_edge(mesh_analysis, n1: Node, n2: Node) -> (True, True, True): + mesh_before = deepcopy(mesh_analysis.mesh) + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) if found: - topo, geo = isSplitOk(d) + topo, geo = mesh_analysis.isSplitOk(d) if not geo or not topo: return False, topo, geo else: return False, True, False - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh_analysis.mesh.active_quadrangles(d) d1112 = d111.get_beta(2) d212 = d21.get_beta(2) # create a new node in the middle of [n1, n2] - N10 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) + N10 = mesh_analysis.mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) # modify existing triangles d.set_node(N10) d21.set_node(N10) # create a new quadrangle - f5 = mesh.add_quad(n1, n5, N10, n4) + f5 = mesh_analysis.mesh.add_quad(n1, n5, N10, n4) # update beta2 relations - mesh.set_face_beta2(f5,[d111,d1112,d21,d212]) + mesh_analysis.mesh.set_face_beta2(f5,[d111,d1112,d21,d212]) + + # update nodes scores + n1.set_score(n1.get_score() + 1) + n3.set_score(n3.get_score() - 1) + n4.set_score(n4.get_score() - 1) + N10.set_score(1) # new nodes have an adjacency of 3, wich means a score of 1 + N10.set_ideal_adjacency(4) # the inner vertices of quadrangular meshes have an ideal adjacency of 4 return True, topo, geo -def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return collapse_edge(mesh, Node(mesh, id1), Node(mesh, id2)) +def collapse_edge_ids(mesh_analysis, id1: int, id2: int) -> (True, True, True): + return collapse_edge(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) -def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: +def collapse_edge(mesh_analysis, n1: Node, n2: Node) -> (True, True, True): + mesh = mesh_analysis.mesh found, d = mesh.find_inner_edge(n1, n2) if found: - topo, geo = isCollapseOk(d) + topo, geo = mesh_analysis.isCollapseOk(d) if not geo or not topo: return False, topo, geo else: @@ -149,27 +177,50 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + n1_score = n1.get_score() + d1112 = d111.get_beta(2) d12 = d1.get_beta(2) d112 = d11.get_beta(2) + n3_analysis = NodeAnalysis(n3) # Move n3 node in the middle of [n3, n1] - if not on_boundary(n3): + if not n3_analysis.on_boundary(): n3.set_xy((n3.x() + n1.x()) / 2, (n1.y() + n3.y()) / 2) - #Delete the face F5 + # Check if nodes n2 and n4 are not linked to deleted dart (n3 will be checked after) + if n2.get_dart() == d1: + if mesh.is_dart_active(d2): + n2.set_dart(d2) + else: + n2.set_dart(d12.get_beta(1)) + if n4.get_dart() == d111: + if mesh.is_dart_active(d112): + n4.set_dart(d112) + else: + n4.set_dart(d1112.get_beta(1)) + + # Delete the face F5 f5 = d.get_face() - mesh.del_quad(d, d1, d11, d111, f5) + mesh_analysis.mesh.del_quad(d, d1, d11, d111, f5) n_from = n1 n_to = n3 - adj_darts = adjacent_darts(n_from) + + n_from_analysis = NodeAnalysis(n_from) + adj_darts = n_from_analysis.adjacent_darts() for d in adj_darts: if d.get_node() == n_from: d.set_node(n_to) - mesh.del_node(n_from) + # Change n3 dart association + for d in adj_darts: + if d.get_node() == n_to: + n_to.set_dart(d) + break + + mesh_analysis.mesh.del_node(n_from) #Update beta2 relations if d2 is not None: @@ -184,22 +235,27 @@ def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if d112 is not None: d112.set_beta(2, d1112) - return mesh_check(mesh), topo, geo + # update nodes scores + n2.set_score(n1.get_score() + 1) + n4.set_score(n2.get_score() + 1) + n3.set_score(n3.get_score() + n1_score - 2) + return mesh_analysis.mesh_check(), topo, geo -def cleanup_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return cleanup_edge(mesh, Node(mesh, id1), Node(mesh, id2)) -def cleanup_edge(mesh: Mesh, n1: Node, n2: Node) -> True: - found, d = mesh.find_inner_edge(n1, n2) +def cleanup_edge_ids(mesh_analysis, id1: int, id2: int) -> (True, True, True): + return cleanup_edge(mesh_analysis.mesh, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) + +def cleanup_edge(mesh_analysis, n1: Node, n2: Node) -> (True, True, True): + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) if found: - topo, geo = isCleanupOk(d) + topo, geo = mesh_analysis.isCleanupOk(d) if not geo or not topo: return False, topo, geo else: return False, False, False - parallel_darts = mesh.find_parallel_darts(d) + parallel_darts = mesh_analysis.mesh.find_parallel_darts(d) last_dart = parallel_darts[-1] ld1 = last_dart.get_beta(1) @@ -207,8 +263,9 @@ def cleanup_edge(mesh: Mesh, n1: Node, n2: Node) -> True: ld111 = ld11.get_beta(1) last_node = ld111.get_node() node_to = ld11.get_node() - adj_darts = adjacent_darts(last_node) - mesh.del_node(last_node) + na_last_node = NodeAnalysis(last_node) + adj_darts = na_last_node.adjacent_darts() + mesh_analysis.mesh.del_node(last_node) for da in adj_darts: if da.get_node() == last_node: @@ -231,15 +288,15 @@ def cleanup_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if d12 is not None: d12.set_beta(2, d1112) - mesh.del_quad(d, d1, d11, d111, f) - - adj_darts = adjacent_darts(n_from) + mesh_analysis.mesh.del_quad(d, d1, d11, d111, f) + na_nfrom = NodeAnalysis(n_from) + adj_darts = na_nfrom.adjacent_darts() for d in adj_darts: if d.get_node() == n_from: d.set_node(n_to) - mesh.del_node(n_from) + mesh_analysis.mesh.del_node(n_from) - return mesh_check(mesh), topo, geo + return mesh_analysis.mesh_check(), topo, geo diff --git a/environment/actions/smoothing.py b/environment/actions/smoothing.py index 9b5f431..3c1dc42 100644 --- a/environment/actions/smoothing.py +++ b/environment/actions/smoothing.py @@ -2,7 +2,9 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Node -from mesh_model.mesh_analysis.global_mesh_analysis import adjacent_darts, on_boundary +from mesh_model.mesh_analysis.global_mesh_analysis import NodeAnalysis +from view.mesh_plotter.mesh_plots import plot_mesh + def smoothing_mean(mesh: Mesh) -> True: for i in range (20): @@ -10,8 +12,9 @@ def smoothing_mean(mesh: Mesh) -> True: for i, n_info in enumerate (mesh.nodes, start=0): if n_info[2] >=0: node_to_smooth = Node(mesh, i) - if not on_boundary(node_to_smooth): - list_darts = adjacent_darts(node_to_smooth) + na = NodeAnalysis(node_to_smooth) + if not na.on_boundary(): + list_darts = na.adjacent_darts() sum_x = 0.0 sum_y = 0.0 nb_nodes = 0.0 @@ -21,4 +24,7 @@ def smoothing_mean(mesh: Mesh) -> True: sum_x += n.x() sum_y += n.y() nb_nodes += 1 + if nb_nodes == 0: + plot_mesh(mesh) + raise ValueError("Isolated vertex ") node_to_smooth.set_xy(sum_x/nb_nodes, sum_y/nb_nodes) \ No newline at end of file diff --git a/environment/actions/triangular_actions.py b/environment/actions/triangular_actions.py index bb7a357..a7fb79e 100644 --- a/environment/actions/triangular_actions.py +++ b/environment/actions/triangular_actions.py @@ -1,25 +1,44 @@ from __future__ import annotations -from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_struct.mesh_elements import Node -from mesh_model.mesh_analysis.global_mesh_analysis import mesh_check -from mesh_model.mesh_analysis.trimesh_analysis import isFlipOk, isCollapseOk, isSplitOk +import copy +import numpy as np +from mesh_model.mesh_analysis.global_mesh_analysis import NodeAnalysis +from mesh_model.mesh_struct.mesh_elements import Node, Dart +from view.mesh_plotter.mesh_plots import plot_mesh +""" +Triangular actions performed on meshes. +Each function returns three Booleans: + * action_validity: If the action has been performed. + * topo: if the action is topologically valid + * geo: if the action is geometrically valid +""" -def flip_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return flip_edge(mesh, Node(mesh, id1), Node(mesh, id2)) +def flip_edge_ids(mesh_analysis, id1: int, id2: int) -> True: + return flip_edge(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) -def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: - found, d = mesh.find_inner_edge(n1, n2) +def flip_edge(mesh_analysis, n1: Node, n2: Node) -> True: + """ + When a dart is flipped, beta2 relationships are not affected, only beta1. + :param mesh_analysis: mesh analysis object from the mesh to modify + :param n1: first node of the edge to be flipped + :param n2: second node of the edge to be flipped + :return: valid_action : True if valid, false otherwise ; topo: true if topologically valid, false otherwise ; geo : true if geometrically valid, false otherwise + """ + valid_action = True + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) + mesh_before = copy.deepcopy(mesh_analysis.mesh) + if found: - topo, geo = isFlipOk(d) + topo, geo = mesh_analysis.isFlipOk(d) if not geo or not topo: return False, topo, geo else: - return False, False, False + return False, False, True # the geometrical criteria is True because if the dart is not found, it means it's a boundary dart and a topological criteria - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh_analysis.mesh.active_triangles(d) f1 = d.get_face() f2 = d2.get_face() @@ -38,7 +57,6 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: if f1.get_dart().id == d11.id: f1.set_dart(d) - if f2.get_dart().id == d211.id: f2.set_dart(d2) @@ -46,27 +64,44 @@ def flip_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d2.set_node(n4) d211.set_face(f1) d11.set_face(f2) - return True, topo, geo + #Update dart quality and nodes scores + d.set_quality(mesh_analysis.get_dart_geometric_quality(d)) # updates d and d2 + d1.set_quality(mesh_analysis.get_dart_geometric_quality(d1)) # updates d1 and d12 + d11.set_quality(mesh_analysis.get_dart_geometric_quality(d11)) # updates d11 and d112 + d21.set_quality(mesh_analysis.get_dart_geometric_quality(d21)) # updates d21 and d212 + d211.set_quality(mesh_analysis.get_dart_geometric_quality(d211)) # updates d211 and d2112 -def split_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return split_edge(mesh, Node(mesh, id1), Node(mesh, id2)) + n1.set_score(n1.get_score() + 1) # an edge is removed from vertex n1 + n2.set_score(n2.get_score() + 1) # an edge is removed from vertex n2 + n3.set_score(n3.get_score() - 1) # an edge is added to vertex n3 + n4.set_score(n4.get_score() - 1) # an edge is added to vertex n4 + after_check = check_mesh(mesh_analysis, mesh_before) + if not after_check: + raise ValueError("Some checks are missing") -def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: - found, d = mesh.find_inner_edge(n1, n2) + return valid_action, topo, geo + +def split_edge_ids(mesh_analysis, id1: int, id2: int) -> True: + return split_edge(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) + + +def split_edge(mesh_analysis, n1: Node, n2: Node) -> True: + found, d = mesh_analysis.mesh.find_inner_edge(n1, n2) + mesh_before = copy.deepcopy(mesh_analysis.mesh) if found: - topo, geo = isSplitOk(d) + topo, geo = mesh_analysis.isSplitOk(d) if not geo or not topo: return False, topo, geo else: - return False, True, False + return False, False, True # the geometrical criteria is True because if the dart is not found, it means it's a boundary dart - d2, d1, _, d21, _, n1, n2, n3, n4 = mesh.active_triangles(d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh_analysis.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) + # Create a new node in the middle of [n1, n2] + N5 = mesh_analysis.mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) # modify existing triangles d1.set_node(N5) @@ -75,86 +110,292 @@ def split_edge(mesh: Mesh, n1: Node, n2: Node) -> True: d2.set_beta(1, d21) # create 2 new triangles - F3 = mesh.add_triangle(n2, n3, N5) - F4 = mesh.add_triangle(N5, n1, n4) + F3 = mesh_analysis.mesh.add_triangle(n2, n3, N5) + F4 = mesh_analysis.mesh.add_triangle(N5, n1, n4) # update beta2 relations - mesh.set_face_beta2(F3, [d1, d2]) - d2b2 = d2.get_beta(2) - d2b21 = d2b2.get_beta(1) - mesh.set_beta2(d2b21) - mesh.set_face_beta2(F4, [d, d21]) - db2 = d.get_beta(2) - db21 = db2.get_beta(1) - mesh.set_beta2(db21) + mesh_analysis.mesh.set_face_beta2(F3, [d1, d2, d1.get_beta(2)]) + mesh_analysis.mesh.set_face_beta2(F4, [d, d21, d21.get_beta(2)]) + + #update nodes scores + n3.set_score(n3.get_score() - 1) + n4.set_score(n4.get_score() - 1) + N5.set_score(2) # new nodes have an adjacency of 4, wich means a score of 2 + N5.set_ideal_adjacency(6) # the inner vertices of triangular meshes have an ideal adjacency of 6 + + #update darts quality + d.set_quality(mesh_analysis.get_dart_geometric_quality(d)) # d and d twin update + d1.set_quality(mesh_analysis.get_dart_geometric_quality(d1)) # d1 and d12 update + d11.set_quality(mesh_analysis.get_dart_geometric_quality(d11)) # d11 and d112 update + d2.set_quality(mesh_analysis.get_dart_geometric_quality(d2)) # d2 and d22 update (d and d2 are no longer twin darts) + d21.set_quality(mesh_analysis.get_dart_geometric_quality(d21)) # d21 and d212 update + d211.set_quality(mesh_analysis.get_dart_geometric_quality(d211)) # d211 and d2112 update + + #f3 face + d22f3 = d2.get_beta(2) + d221f3 = d22f3.get_beta(1) + d221f3.set_quality(mesh_analysis.get_dart_geometric_quality(d221f3)) + + #f4 face + d2f4 = d.get_beta(2) + d21f4 = d2f4.get_beta(1) + d21f4.set_quality(mesh_analysis.get_dart_geometric_quality(d21f4)) + + after_check = check_mesh(mesh_analysis, mesh_before) + if not after_check: + raise ValueError("Some checks are missing") return True, topo, geo -def collapse_edge_ids(mesh: Mesh, id1: int, id2: int) -> True: - return collapse_edge(mesh, Node(mesh, id1), Node(mesh, id2)) +def collapse_edge_ids(mesh_analysis, id1: int, id2: int) -> True: + return collapse_edge(mesh_analysis, Node(mesh_analysis.mesh, id1), Node(mesh_analysis.mesh, id2)) -def collapse_edge(mesh: Mesh, n1: Node, n2: Node) -> True: +def collapse_edge(mesh_analysis, n1: Node, n2: Node) -> True: + mesh = mesh_analysis.mesh + mesh_before = copy.deepcopy(mesh) found, d = mesh.find_inner_edge(n1, n2) + if found: - topo, geo = isCollapseOk(d) + topo, geo = mesh_analysis.isCollapseOk(d) if not geo or not topo: return False, topo, geo else: - return False, False, False + return False, False, True # the geometrical criteria is True because if the dart is not found, it means it's a boundary dart + + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + + d212 = d21.get_beta(2) # T1 + d2112 = d211.get_beta(2) # T2 + d12 = d1.get_beta(2) # T3 + if not mesh.is_dart_active(d12): + print("error") + d112 = d11.get_beta(2) # T4 + # Delete the darts around selected dart + mesh_analysis.mesh.del_adj_triangles(d) + + #Check if nodes n3 and n4 are not linked to deleted dart + + if n3.get_dart().id == d11.id: + if mesh.is_dart_active(d12): + n3.set_dart(d12) + else: + n3.set_dart(d112.get_beta(1)) + if n4.get_dart().id == d211.id: + if mesh.is_dart_active(d212): + n4.set_dart(d212) + else: + n4.set_dart(d2112.get_beta(1)) + if n1.get_dart().id == d.id or n1.get_dart().id == d21.id: + if mesh.is_dart_active(d112): + n1.set_dart(d112) + else: + n1.set_dart(d2112) + + #update beta2 relations + if mesh.is_dart_active(d112): + d112.set_beta(2, d12) + if mesh.is_dart_active(d12): + d12.set_beta(2, d112) - _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) + if mesh.is_dart_active(d212): + d212.set_beta(2, d2112) + if mesh.is_dart_active(d2112): + d2112.set_beta(2, d212) - d212 = d21.get_beta(2) #T1 - d2112 = d211.get_beta(2) #T2 - d12 = d1.get_beta(2) #T3 - d112 = d11.get_beta(2) #T4 + n2_score = n2.get_score() + n2x = n2.x() + n2y =n2.y() + mid = np.array([(n1.x() + n2x) / 2, (n1.y() + n2y) / 2]) + near_n1 = np.array([(n1.x() * 0.9 + n2x * 0.1), (n1.y() * 0.9 + n2y * 0.1)]) + near_n2 = np.array([(n1.x() * 0.1 + n2x * 0.9), (n1.y() * 0.1 + n2y * 0.9)]) + #delete n2 node + mesh_analysis.mesh.del_node(n2) - #Delete the darts around selected dart - mesh.del_adj_triangles(d) + # update nodes scores + n1.set_score(n1.get_score() + n2_score - 2) # node n1 becomes the union of nodes n1 and n2 + n3.set_score(n3.get_score() + 1) # an edge is removed from vertex n3 + n4.set_score(n4.get_score() + 1) # an edge is removed from vertex n4 - #Move n1 node in the middle of [n1, n2] - n1.set_xy((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) + # d12.set_quality(mesh_analysis.get_dart_geometric_quality(d12)) + # d212.set_quality(mesh_analysis.get_dart_geometric_quality(d212, mesh_before)) - #Update node relations - if d12 is not None: + # Update node relations + i = 0 + if mesh.is_dart_active(d12): d121 = d12.get_beta(1) d121.set_node(n1) ds = d121 + ds1 = ds.get_beta(1) while ds is not None and ds != d2112: + i += 1 d2s = ds.get_beta(2) - if d2s is None: + if not mesh.is_dart_active(d2s): ds = d2112 - while ds is not None: + while mesh.is_dart_active(ds): + i += 1 ds.set_node(n1) ds1 = ds.get_beta(1) ds11 = ds1.get_beta(1) ds = ds11.get_beta(2) + if i > 30: + i = 0 + raise ValueError("Potential infinite loop in action collapse") 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: - 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) + ds1 = ds.get_beta(1) + if i > 30: + i = 0 + raise ValueError("Potential infinite loop in action collapse") + + found, new_x, new_y = mesh_analysis.find_star_vertex(n1) + if found: + n1.set_xy(new_x, new_y) + else: + # plot_mesh(mesh_before) + # plot_mesh(mesh_analysis.mesh) + mesh_analysis.find_star_vertex(n1, plot=True) + raise ValueError("No star vertex found") + + # if mesh_analysis.is_star_vertex(n1, mid): + # n1.set_xy(mid[0], mid[1]) + # elif mesh_analysis.is_star_vertex(n1, near_n1): + # n1.set_xy(near_n1[0], near_n1[1]) + # elif mesh_analysis.is_star_vertex(n1, near_n2): + # n1.set_xy(near_n2[0], near_n2[1]) + # elif mesh_analysis.is_star_vertex(n1, np.array([n1.x(), n1.y()])): + # pass + # elif mesh_analysis.is_star_vertex(n1, np.array([n2x, n2y])): + # n1.set_xy(n2x, n2y) + # else: + # plot_mesh(mesh_before) + # plot_mesh(mesh_analysis.mesh) + # mesh_analysis.is_star_vertex(n1, np.array([n1.x(), n1.y()]), True) + # raise ValueError("No star vertex found") + + # Update dart quality + n1_analysis = NodeAnalysis(n1) + adj_darts = n1_analysis.adjacent_darts() + d_updated = [] + for _d in adj_darts: + _d2 = _d.get_beta(2) + _d1 = _d.get_beta(1) + _d12 = _d1.get_beta(2) + _d11 = _d1.get_beta(1) + _d112 = _d11.get_beta(2) + # if d2, d12 or d112 is None, dart quality is -1 and was not modified by collapse action + if _d2 is not None and _d2.id not in d_updated: + _d.set_quality(mesh_analysis.get_dart_geometric_quality(_d, mesh_before)) + d_updated.append(_d.id) + if _d12 is not None and _d12.id not in d_updated: + _d1.set_quality(mesh_analysis.get_dart_geometric_quality(_d1, mesh_before)) + d_updated.append(_d1.id) + if _d112 is not None and _d112.id not in d_updated: + _d11.set_quality(mesh_analysis.get_dart_geometric_quality(_d11)) + d_updated.append(_d11.id) + + after_check = check_mesh(mesh_analysis, mesh_before) + if not after_check: + # mesh_analysis.is_star_vertex(n1, np.array([n1.x(), n1.y()]), True) + raise ValueError("Some checks are missing") + return True, topo, geo - return mesh_check(mesh), topo, geo +def check_mesh(mesh_analysis, m=None) -> bool: + for dart_info in mesh_analysis.mesh.active_darts(): + #Check beta2 relation + d = dart_info[0] + d2 = dart_info[2] + # if associated twin dart no longer exist + if d2 >= 0 and mesh_analysis.mesh.dart_info[d2, 0] < 0: + return False + # if beta2 relation is not symetrical + elif d2 >= 0 and mesh_analysis.mesh.dart_info[d2, 2] != d: + return False + + # null dart + elif d2>=0 and mesh_analysis.mesh.dart_info[d2, 3] == mesh_analysis.mesh.dart_info[d, 3]: + return False + #if adjacent face is the same + elif d2>=0 and mesh_analysis.mesh.dart_info[d2, 4] == mesh_analysis.mesh.dart_info[d, 4]: + return False + + + d1 = mesh_analysis.mesh.dart_info[d,1] + d11 = mesh_analysis.mesh.dart_info[d1,1] + + #Check beta1 + if mesh_analysis.mesh.dart_info[d11,1]!=d : + return False + #check if the quality is the same for twin darts + if d2>=0 and mesh_analysis.mesh.dart_info[d2,5] != mesh_analysis.mesh.dart_info[d, 5]: + plot_mesh(mesh_analysis.mesh) + return False + d = Dart(mesh_analysis.mesh, d) + if d2 >= 0 : + d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh_analysis.mesh.active_triangles(d) + if len(set([n1.id, n2.id, n3.id, n4.id])) < 4 and d.get_quality() != 3: # not flat faces + plot_mesh(m) + plot_mesh(mesh_analysis.mesh) + return False + if dart_info[5] != mesh_analysis.get_dart_geometric_quality(d): + plot_mesh(m) + plot_mesh(mesh_analysis.mesh) + return False + if dart_info[5] not in [-1,0,1,2]: + plot_mesh(m) + plot_mesh(mesh_analysis.mesh) + return False + return True + +# +# def check_mesh_debug(mesh_analysis, mesh_before=None)->True: +# for dart_info in mesh_analysis.mesh.active_darts(): +# #Check beta2 relation +# d = dart_info[0] +# d2 = dart_info[2] +# # if associated twin dart no longer exist +# if d2 >= 0 and mesh_analysis.mesh.dart_info[d2, 0] < 0: +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("error beta2") +# # if beta2 relation is not symetrical +# elif d2 >= 0 and mesh_analysis.mesh.dart_info[d2, 2] != d: +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("error beta2") +# # null dart +# elif d2>=0 and mesh_analysis.mesh.dart_info[d2, 3] == mesh_analysis.mesh.dart_info[d, 3]: +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("same node for twin darts") +# #if adjacent face is the same +# elif d2>=0 and mesh_analysis.mesh.dart_info[d2, 4] == mesh_analysis.mesh.dart_info[d, 4]: +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("same adjacent face") +# +# +# d1 = mesh_analysis.mesh.dart_info[d,1] +# d11 = mesh_analysis.mesh.dart_info[d1,1] +# +# #Check beta1 +# if mesh_analysis.mesh.dart_info[d11,1]!=d : +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("error beta1") +# +# if d2 >= 0 : +# d = Dart(mesh_analysis.mesh, d) +# d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh_analysis.mesh.active_triangles(d) +# +# if len(set([n1.id, n2.id, n3.id, n4.id])) < 4: +# plot_mesh(mesh_analysis.mesh) +# if mesh_before is not None: +# plot_mesh(mesh_before) +# raise ValueError("same traingle for two faces") \ No newline at end of file diff --git a/environment/gymnasium_envs/quadmesh_env/envs/mesh_conv.py b/environment/gymnasium_envs/quadmesh_env/envs/mesh_conv.py index d6f4130..961d49a 100644 --- a/environment/gymnasium_envs/quadmesh_env/envs/mesh_conv.py +++ b/environment/gymnasium_envs/quadmesh_env/envs/mesh_conv.py @@ -1,23 +1,23 @@ import numpy as np -from mesh_model.mesh_analysis.quadmesh_analysis import isValidAction +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis from mesh_model.mesh_struct.mesh_elements import Dart from mesh_model.mesh_struct.mesh import Mesh -def get_x(state: Mesh, n_darts_selected: int, deep :int, degree: bool, restricted:bool, nodes_scores: list[int], nodes_adjacency: list[int]): - mesh = state +def get_x(m_analysis, n_darts_selected: int, deep :int, degree: bool, restricted:bool, nodes_scores: list[int], nodes_adjacency: list[int]): + mesh = m_analysis.mesh if degree: deep = int(deep / 2) - template, darts_id = get_template_boundary(mesh, deep, nodes_scores, nodes_adjacency) + template, darts_id = get_template_boundary(m_analysis, deep) else: - template, darts_id = get_template(mesh, deep, nodes_scores) + template, darts_id = get_template(m_analysis, deep) if restricted: darts_to_delete = [] darts_id = [] for i, d_info in enumerate(mesh.active_darts()): d_id = d_info[0] - if d_info[2] == -1 or not isValidAction(mesh, d_info[0], 4)[0]: # test the validity of all action type + if d_info[2] == -1 or not m_analysis.isValidAction(d_info[0], 4)[0]: # test the validity of all action type darts_to_delete.append(i) else: darts_id.append(d_id) @@ -31,17 +31,17 @@ def get_x(state: Mesh, n_darts_selected: int, deep :int, degree: bool, restricte return X, np.array(valid_dart_ids) -def get_template(mesh: Mesh, deep: int, nodes_scores): - size = len(mesh.dart_info) +def get_template(m_analysis, deep: int): + size = len(m_analysis.mesh.dart_info) template = np.zeros((size, deep), dtype=np.int64) dart_ids = [] n_darts = 0 - for d_info in mesh.active_darts(): + for d_info in m_analysis.mesh.active_darts(): n_darts += 1 d_id = d_info[0] dart_ids.append(d_id) - d = Dart(mesh, d_id) + d = Dart(m_analysis.mesh, d_id) A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() @@ -51,10 +51,10 @@ def get_template(mesh: Mesh, deep: int, nodes_scores): D = d111.get_node() # Template niveau 1 - template[n_darts - 1, 0] = nodes_scores[A.id] - template[n_darts - 1, 1] = nodes_scores[B.id] - template[n_darts - 1, 2] = nodes_scores[C.id] - template[n_darts - 1, 3] = nodes_scores[D.id] + template[n_darts - 1, 0] = A.get_score() + template[n_darts - 1, 1] = B.get_score() + template[n_darts - 1, 2] = C.get_score() + template[n_darts - 1, 3] = D.get_score() E = [A,B,C,D] @@ -76,9 +76,9 @@ def get_template(mesh: Mesh, deep: int, nodes_scores): F.append(df111) N1, N2 = df11.get_node(), df111.get_node() E.append(N1) - template[n_darts-1, len(E)-1] = nodes_scores[N1.id] + template[n_darts-1, len(E)-1] = N1.get_score() E.append(N2) - template[n_darts - 1, len(E)-1] = nodes_scores[N2.id] + template[n_darts - 1, len(E)-1] = N2.get_score() else: E.extend([None,None]) #template[n_darts - 1, len(E) - 1] = -500 # dummy vertices are assigned to -500 @@ -88,17 +88,18 @@ def get_template(mesh: Mesh, deep: int, nodes_scores): return template, dart_ids -def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): - size = len(mesh.dart_info) + +def get_template_boundary(m_analysis, deep: int): + size = len(m_analysis.mesh.dart_info) template = np.zeros((size, deep*2), dtype=np.int64) dart_ids = [] n_darts = 0 - for d_info in mesh.active_darts(): + for d_info in m_analysis.mesh.active_darts(): n_darts += 1 d_id = d_info[0] dart_ids.append(d_id) - d = Dart(mesh, d_id) + d = Dart(m_analysis.mesh, d_id) A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() @@ -108,14 +109,14 @@ def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): D = d111.get_node() # Template niveau 1 - template[n_darts - 1, 0] = nodes_scores[A.id] - template[n_darts - 1, deep] = nodes_adjacency[A.id] - template[n_darts - 1, 1] = nodes_scores[B.id] - template[n_darts - 1, deep+1] = nodes_adjacency[B.id] - template[n_darts - 1, 2] = nodes_scores[C.id] - template[n_darts - 1, deep+2] = nodes_adjacency[C.id] - template[n_darts - 1, 3] = nodes_scores[D.id] - template[n_darts - 1, deep + 3] = nodes_adjacency[D.id] + template[n_darts - 1, 0] = A.get_score() + template[n_darts - 1, deep] = 1 + template[n_darts - 1, 1] = B.get_score() + template[n_darts - 1, deep+1] = 1 + template[n_darts - 1, 2] = C.get_score() + template[n_darts - 1, deep+2] = 1 + template[n_darts - 1, 3] = D.get_score() + template[n_darts - 1, deep + 3] = 1 E = [A, B, C, D] deep_captured = len(E) @@ -136,11 +137,11 @@ def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): F.append(df111) N1, N2 = df11.get_node(), df111.get_node() E.append(N1) - template[n_darts-1, len(E)-1] = nodes_scores[N1.id] - template[n_darts-1, deep + len(E)-1] = nodes_adjacency[N1.id] + template[n_darts-1, len(E)-1] = N1.get_score() + template[n_darts-1, deep + len(E)-1] = 1 E.append(N2) - template[n_darts - 1, len(E)-1] = nodes_scores[N2.id] - template[n_darts - 1, deep + len(E)-1] = nodes_adjacency[N2.id] + template[n_darts - 1, len(E)-1] = N2.get_score() + template[n_darts - 1, deep + len(E)-1] = 1 else: E.extend([None,None]) #template[n_darts - 1, len(E) - 1] = -500 # dummy vertices are assigned to -500 @@ -149,17 +150,18 @@ def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): template = template[:n_darts, :] return template, dart_ids -def get_template_boundary(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): - size = len(mesh.dart_info) + +def get_template_deg(m_analysis, deep: int, nodes_scores, nodes_adjacency): + size = len(m_analysis.mesh.dart_info) template = np.zeros((size, deep*2), dtype=np.int64) dart_ids = [] n_darts = 0 - for d_info in mesh.active_darts(): + for d_info in m_analysis.mesh.active_darts(): n_darts += 1 d_id = d_info[0] dart_ids.append(d_id) - d = Dart(mesh, d_id) + d = Dart(m_analysis.mesh, d_id) A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() @@ -169,14 +171,14 @@ def get_template_boundary(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): D = d111.get_node() # Template niveau 1 - template[n_darts - 1, 0] = nodes_scores[A.id] - template[n_darts - 1, deep] = 1 - template[n_darts - 1, 1] = nodes_scores[B.id] - template[n_darts - 1, deep+1] = 1 - template[n_darts - 1, 2] = nodes_scores[C.id] - template[n_darts - 1, deep+2] = 1 - template[n_darts - 1, 3] = nodes_scores[D.id] - template[n_darts - 1, deep + 3] = 1 + template[n_darts - 1, 0] = A.get_score() + template[n_darts - 1, deep] = nodes_adjacency[A.id] + template[n_darts - 1, 1] = B.get_score() + template[n_darts - 1, deep+1] = nodes_adjacency[B.id] + template[n_darts - 1, 2] = C.get_score() + template[n_darts - 1, deep+2] = nodes_adjacency[C.id] + template[n_darts - 1, 3] = D.get_score() + template[n_darts - 1, deep + 3] = nodes_adjacency[D.id] E = [A, B, C, D] deep_captured = len(E) @@ -197,11 +199,11 @@ def get_template_boundary(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): F.append(df111) N1, N2 = df11.get_node(), df111.get_node() E.append(N1) - template[n_darts-1, len(E)-1] = nodes_scores[N1.id] - template[n_darts-1, deep + len(E)-1] = 1 + template[n_darts-1, len(E)-1] = N1.get_score() + template[n_darts-1, deep + len(E)-1] = nodes_adjacency[N1.id] E.append(N2) - template[n_darts - 1, len(E)-1] = nodes_scores[N2.id] - template[n_darts - 1, deep + len(E)-1] = 1 + template[n_darts - 1, len(E)-1] = N2.get_score() + template[n_darts - 1, deep + len(E)-1] = nodes_adjacency[N2.id] else: E.extend([None,None]) #template[n_darts - 1, len(E) - 1] = -500 # dummy vertices are assigned to -500 diff --git a/environment/gymnasium_envs/quadmesh_env/envs/quadmesh.py b/environment/gymnasium_envs/quadmesh_env/envs/quadmesh.py index 3be7753..2937045 100644 --- a/environment/gymnasium_envs/quadmesh_env/envs/quadmesh.py +++ b/environment/gymnasium_envs/quadmesh_env/envs/quadmesh.py @@ -1,4 +1,3 @@ - import copy import pygame import imageio @@ -13,8 +12,7 @@ from mesh_model.random_quadmesh import random_mesh from mesh_model.mesh_struct.mesh_elements import Dart -from mesh_model.mesh_analysis.global_mesh_analysis import global_score -from mesh_model.mesh_analysis.quadmesh_analysis import isTruncated +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis from environment.gymnasium_envs.quadmesh_env.envs.mesh_conv import get_x from environment.actions.quadrangular_actions import flip_edge_cntcw, flip_edge_cw, split_edge, collapse_edge, cleanup_edge from environment.observation_register import ObservationRegistry @@ -70,10 +68,10 @@ def __init__( else : self.config = {"mesh": None} self.mesh = random_mesh() - + self.mesh_analysis = QuadMeshOldAnalysis(self.mesh) #self.mesh_size = len(self.mesh.nodes) #self.nb_darts = len(self.mesh.dart_info) - self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = global_score(self.mesh) + self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = self.mesh_analysis.global_score() self._ideal_rewards = (self._mesh_score - self._ideal_score)*10 #arbitrary factor of 10 for rewards self.next_mesh_score = 0 self.n_darts_selected = n_darts_selected @@ -115,8 +113,8 @@ def __init__( # Observation and action spaces self.observation_space = gym.spaces.Box( - low=-6, # nodes min degree : -6 - high=2, # nodes max degree : 2 + low=-8, # nodes min degree : -6 + high=4, # nodes max degree : 2 shape=(self.n_darts_selected, deep), dtype=np.int64 ) @@ -125,8 +123,6 @@ def __init__( # We have 4 actions, flip clockwise, flip counterclockwise, split, collapse self.action_space = gym.spaces.MultiDiscrete([4, self.n_darts_selected]) - - def reset(self, seed=None, options=None): # We need the following line to seed self.np_random super().reset(seed=seed) @@ -136,8 +132,10 @@ def reset(self, seed=None, options=None): self.mesh = copy.deepcopy(self.config["mesh"]) else: self.mesh = random_mesh() + + self.mesh_analysis = QuadMeshOldAnalysis(self.mesh) #self.nb_darts = len(self.mesh.dart_info) - self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = global_score(self.mesh) + self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = self.mesh_analysis.global_score() self._ideal_rewards = (self._mesh_score - self._ideal_score) * 10 self.nb_invalid_actions = 0 self.close() @@ -161,9 +159,8 @@ def reset(self, seed=None, options=None): return self.observation, info - def _get_obs(self): - irregularities, darts_list = get_x(self.mesh, self.n_darts_selected, self.deep, self.degree_observation, self.restricted, self._nodes_scores, self._nodes_adjacency) + irregularities, darts_list = get_x(self.mesh_analysis, self.n_darts_selected, self.deep, self.degree_observation, self.restricted, self._nodes_scores, self._nodes_adjacency) self.darts_selected = darts_list return irregularities @@ -187,6 +184,7 @@ def _get_info(self, terminated, valid_act, action, mesh_reward): "invalid_collapse": 1.0 if action[0]==Actions.COLLAPSE.value and not valid_action else 0.0, "invalid_cleanup": 1.0 if action[0]==Actions.CLEANUP.value and not valid_action else 0.0, "mesh" : self.mesh, + "mesh_analysis" : self.mesh_analysis, "darts_selected" : self.darts_selected, "observation_registry" : self.observation_registry if self.observation_count else None, } @@ -209,19 +207,19 @@ def step(self, action: np.ndarray): valid_action, valid_topo, valid_geo = False, False, False if action[0] == Actions.FLIP_CW.value: self.actions_info["n_flip_cw"] += 1 - valid_action, valid_topo, valid_geo = flip_edge_cw(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = flip_edge_cw(self.mesh_analysis, n1, n2) elif action[0] == Actions.FLIP_CNTCW.value: self.actions_info["n_flip_cntcw"] += 1 - valid_action, valid_topo, valid_geo = flip_edge_cntcw(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = flip_edge_cntcw(self.mesh_analysis, n1, n2) elif action[0] == Actions.SPLIT.value: self.actions_info["n_split"] += 1 - valid_action, valid_topo, valid_geo = split_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = split_edge(self.mesh_analysis, n1, n2) elif action[0] == Actions.COLLAPSE.value: self.actions_info["n_collapse"] += 1 - valid_action, valid_topo, valid_geo = collapse_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = collapse_edge(self.mesh_analysis, n1, n2) elif action[0] == Actions.CLEANUP.value: self.actions_info["n_cleanup"] += 1 - valid_action, valid_topo, valid_geo = cleanup_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = cleanup_edge(self.mesh_analysis, n1, n2) else: raise ValueError("Action not defined") @@ -230,7 +228,7 @@ def step(self, action: np.ndarray): if valid_action: # An episode is done if the actual score is the same as the ideal - next_nodes_score, self.next_mesh_score, _, next_nodes_adjacency = global_score(self.mesh) + next_nodes_score, self.next_mesh_score, _, next_nodes_adjacency = self.mesh_analysis.global_score() terminated = np.array_equal(self._ideal_score, self.next_mesh_score) if terminated: mesh_reward = (self._mesh_score - self.next_mesh_score)*10 @@ -254,7 +252,7 @@ def step(self, action: np.ndarray): else: raise ValueError("Invalid action") if self.nb_invalid_actions > 10 : - truncated = isTruncated(self.mesh, self.darts_selected) + truncated = self.mesh_analysis.isTruncated(self.darts_selected) else: truncated = False valid_act = valid_action, valid_topo, valid_geo @@ -271,7 +269,6 @@ def step(self, action: np.ndarray): return self.observation, reward, terminated, truncated, info - def _render_frame(self): if self.render_mode == "human" and self.window is None: pygame.init() diff --git a/environment/gymnasium_envs/quadmesh_env/wrappers/__init__.py b/environment/gymnasium_envs/quadmesh_env/wrappers/__init__.py index c1fc21e..1a77df7 100644 --- a/environment/gymnasium_envs/quadmesh_env/wrappers/__init__.py +++ b/environment/gymnasium_envs/quadmesh_env/wrappers/__init__.py @@ -1,4 +1,4 @@ -from trimesh_env.wrappers.clip_reward import ClipReward -from trimesh_env.wrappers.discrete_actions import DiscreteActions -from trimesh_env.wrappers.reacher_weighted_reward import ReacherRewardWrapper -from trimesh_env.wrappers.relative_position import RelativePosition +from environment.gymnasium_envs.quadmesh_env.wrappers.clip_reward import ClipReward +from environment.gymnasium_envs.quadmesh_env.wrappers.discrete_actions import DiscreteActions +from environment.gymnasium_envs.quadmesh_env.wrappers.reacher_weighted_reward import ReacherRewardWrapper +from environment.gymnasium_envs.quadmesh_env.wrappers.relative_position import RelativePosition diff --git a/environment/gymnasium_envs/trimesh_full_env/__init__.py b/environment/gymnasium_envs/trimesh_full_env/__init__.py index 063182b..76685b6 100644 --- a/environment/gymnasium_envs/trimesh_full_env/__init__.py +++ b/environment/gymnasium_envs/trimesh_full_env/__init__.py @@ -5,5 +5,5 @@ id="Trimesh-v0", entry_point="environment.gymnasium_envs.trimesh_full_env.envs:TriMeshEnvFull", max_episode_steps=100, - kwargs={"mesh": None, "mesh_size": 30, "n_darts_selected": 20, "deep": 6, "with_degree_obs": True, "action_restriction": False}, + kwargs={"mesh": None, "mesh_size": 7, "n_darts_selected": 7, "deep": 6, "with_quality_obs": True, "action_restriction": False}, ) diff --git a/environment/gymnasium_envs/trimesh_full_env/envs/mesh_conv.py b/environment/gymnasium_envs/trimesh_full_env/envs/mesh_conv.py index e6cc4e4..27616dd 100644 --- a/environment/gymnasium_envs/trimesh_full_env/envs/mesh_conv.py +++ b/environment/gymnasium_envs/trimesh_full_env/envs/mesh_conv.py @@ -1,22 +1,21 @@ import numpy as np -from mesh_model.mesh_analysis.trimesh_analysis import isValidAction, find_template_opposite_node -from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.mesh_struct.mesh_elements import Dart, Node from mesh_model.mesh_struct.mesh import Mesh -def get_x(state: Mesh, n_darts_selected: int, deep :int, degree: bool, restricted:bool, nodes_scores: list[int], nodes_adjacency: list[int]): - mesh = state - if degree: - template, darts_id = get_template_deg(mesh, deep, nodes_scores, nodes_adjacency) +def get_x(m_analysis, n_darts_selected: int, deep :int, quality: bool, restricted:bool, nodes_scores: list[int], nodes_adjacency: list[int]): + mesh = m_analysis.mesh + if quality: + template, darts_id = get_template_with_quality(m_analysis, deep) else: - template, darts_id = get_template(mesh, deep, nodes_scores) + template, darts_id = get_template(m_analysis, deep, nodes_scores) if restricted: darts_to_delete = [] darts_id = [] for i, d_info in enumerate(mesh.active_darts()): d_id = d_info[0] - if d_info[2] == -1 or not isValidAction(mesh, d_info[0], 4)[0]: # test the validity of all action type + if d_info[2] == -1 or not m_analysis.isValidAction(d_info[0], 4)[0]: # test the validity of all action type darts_to_delete.append(i) else: darts_id.append(d_id) @@ -30,17 +29,17 @@ def get_x(state: Mesh, n_darts_selected: int, deep :int, degree: bool, restricte return X, np.array(valid_dart_ids) -def get_template(mesh: Mesh, deep: int, nodes_scores): - size = len(mesh.dart_info) +def get_template(m_analysis, deep: int, nodes_scores): + size = len(m_analysis.mesh.dart_info) template = np.zeros((size, deep), dtype=np.int64) dart_ids = [] n_darts = 0 - for d_info in mesh.active_darts(): + for d_info in m_analysis.mesh.active_darts(): n_darts += 1 d_id = d_info[0] dart_ids.append(d_id) - d = Dart(mesh, d_id) + d = Dart(m_analysis.mesh, d_id) A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() @@ -48,68 +47,81 @@ def get_template(mesh: Mesh, deep: int, nodes_scores): C = d11.get_node() # Template niveau 1 - template[n_darts - 1, 0] = nodes_scores[C.id] - template[n_darts - 1, 1] = nodes_scores[A.id] - template[n_darts - 1, 2] = nodes_scores[B.id] + template[n_darts - 1, 0] = C.get_score() + template[n_darts - 1, 1] = A.get_score() + template[n_darts - 1, 2] = B.get_score() if deep>3: # template niveau 2 deep = 6 - n_id = find_template_opposite_node(d) + n_id = m_analysis.find_template_opposite_node(d) if n_id is not None: - template[n_darts - 1, 3] = nodes_scores[n_id] - n_id = find_template_opposite_node(d1) + template[n_darts - 1, 3] = n_id.get_score() + n_id = m_analysis.find_template_opposite_node(d1) if n_id is not None: - template[n_darts - 1, 4] = nodes_scores[n_id] - n_id = find_template_opposite_node(d11) + template[n_darts - 1, 4] = n_id.get_score() + n_id = m_analysis.find_template_opposite_node(d11) if n_id is not None: - template[n_darts - 1, 5] = nodes_scores[n_id] + template[n_darts - 1, 5] = n_id.get_score() if deep>6: # template niveau 3 - deep = 12 - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = m_analysis.mesh.active_triangles(d) #Triangle F2 - n_id = find_template_opposite_node(d21) + n_id = m_analysis.find_template_opposite_node(d21) if n_id is not None: - template[n_darts - 1, 6] = nodes_scores[n_id] - n_id = find_template_opposite_node(d211) + template[n_darts - 1, 6] = n_id.get_score() + n_id = m_analysis.find_template_opposite_node(d211) if n_id is not None: - template[n_darts - 1, 7] = nodes_scores[n_id] + template[n_darts - 1, 7] = n_id.get_score() # Triangle T3 d12 = d1.get_beta(2) d121 = d12.get_beta(1) d1211 = d121.get_beta(1) - n_id = find_template_opposite_node(d121) + n_id = m_analysis.find_template_opposite_node(d121) if n_id is not None: - template[n_darts - 1, 8] = nodes_scores[n_id] - n_id = find_template_opposite_node(d1211) + template[n_darts - 1, 8] = n_id.get_score() + n_id = m_analysis.find_template_opposite_node(d1211) if n_id is not None: - template[n_darts - 1, 9] = nodes_scores[n_id] + template[n_darts - 1, 9] = n_id.get_score() # Triangle T4 d112 = d11.get_beta(2) d1121 = d112.get_beta(1) d11211 = d1121.get_beta(1) - n_id = find_template_opposite_node(d1121) + n_id = m_analysis.find_template_opposite_node(d1121) if n_id is not None: - template[n_darts - 1, 10] = nodes_scores[n_id] - n_id = find_template_opposite_node(d11211) + template[n_darts - 1, 10] = n_id.get_score() + n_id = m_analysis.find_template_opposite_node(d11211) if n_id is not None: - template[n_darts - 1, 11] = nodes_scores[n_id] + template[n_darts - 1, 11] = n_id.get_score() template = template[:n_darts, :] return template, dart_ids -def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): - size = len(mesh.dart_info) +def get_template_with_quality(m_analysis, deep: int): + """ + Create a template matrix representing the entire mesh, composed of: + + * Node scores: i.e. the difference between ideal adjacency and actual adjacency. + * Dart surrounding quality: a measure of the geometric quality around each dart. + + Each column in the matrix corresponds to the local surrounding of a dart, + including the scores of its surrounding nodes and its associated quality. + + :param m_analysis: mesh to analyze + :param deep: observation deep (how many nodes observed on each dart surrounding) + :return: template matrix + """ + size = len(m_analysis.mesh.dart_info) template = np.zeros((size, deep*2), dtype=np.int64) dart_ids = [] n_darts = 0 - for d_info in mesh.active_darts(): + for d_info in m_analysis.mesh.active_darts(): n_darts += 1 d_id = d_info[0] dart_ids.append(d_id) - d = Dart(mesh, d_id) + d = Dart(m_analysis.mesh, d_id) A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() @@ -117,67 +129,76 @@ def get_template_deg(mesh: Mesh, deep: int, nodes_scores, nodes_adjacency): C = d11.get_node() # Template niveau 1 - template[n_darts - 1, 0] = nodes_scores[C.id] - template[n_darts - 1, deep] = nodes_adjacency[C.id] - template[n_darts - 1, 1] = nodes_scores[A.id] - template[n_darts - 1, deep+1] = nodes_adjacency[A.id] - template[n_darts - 1, 2] = nodes_scores[B.id] - template[n_darts - 1, deep+2] = nodes_adjacency[B.id] + template[n_darts - 1, 0] = C.get_score() + template[n_darts - 1, deep] = d.get_quality() + template[n_darts - 1, 1] = A.get_score() + template[n_darts - 1, deep+1] = d1.get_quality() + template[n_darts - 1, 2] = B.get_score() + template[n_darts - 1, deep+2] = d11.get_quality() if deep>3: # template niveau 2 - n_id = find_template_opposite_node(d) + n_id = m_analysis.find_template_opposite_node(d) if n_id is not None: - template[n_darts - 1, 3] = nodes_scores[n_id] - template[n_darts - 1, deep+3] = nodes_adjacency[n_id] - n_id = find_template_opposite_node(d1) + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+3] = d.get_quality() #quality around dart d is equivalent to quality around dart d2 + n_id = m_analysis.find_template_opposite_node(d1) if n_id is not None: - template[n_darts - 1, 4] = nodes_scores[n_id] - template[n_darts - 1, deep+4] = nodes_adjacency[n_id] - n_id = find_template_opposite_node(d11) + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+4] = d1.get_quality() + n_id = m_analysis.find_template_opposite_node(d11) if n_id is not None: - template[n_darts - 1, 5] = nodes_scores[n_id] - template[n_darts - 1, deep+5] = nodes_adjacency[n_id] + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+5] = d11.get_quality() if deep>6: # template niveau 3 - deep = 12 if d.get_beta(2) is not None: - d2, d1, d11, d21, d211, n1, n2, n3, n4 = mesh.active_triangles(d) + d2, d1, d11, d21, d211, n1, n2, n3, n4 = m_analysis.mesh.active_triangles(d) #Triangle F2 - n_id = find_template_opposite_node(d21) + n_id = m_analysis.find_template_opposite_node(d21) if n_id is not None: - template[n_darts - 1, 6] = nodes_scores[n_id] - template[n_darts - 1, deep+6] = nodes_adjacency[n_id] - n_id = find_template_opposite_node(d211) + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+6] = d21.get_quality() + n_id = m_analysis.find_template_opposite_node(d211) if n_id is not None: - template[n_darts - 1, 7] = nodes_scores[n_id] - template[n_darts - 1, deep+7] = nodes_adjacency[n_id] + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+7] = d211.get_quality() # Triangle T3 d12 = d1.get_beta(2) if d12 is not None: d121 = d12.get_beta(1) d1211 = d121.get_beta(1) - n_id = find_template_opposite_node(d121) + n_id = m_analysis.find_template_opposite_node(d121) if n_id is not None: - template[n_darts - 1, 8] = nodes_scores[n_id] - template[n_darts - 1, deep+8] = nodes_adjacency[n_id] - n_id = find_template_opposite_node(d1211) + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+8] = d121.get_quality() + n_id = m_analysis.find_template_opposite_node(d1211) if n_id is not None: - template[n_darts - 1, 9] = nodes_scores[n_id] - template[n_darts - 1, deep+9] = nodes_adjacency[n_id] + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+9] = d1211.get_quality() # Triangle T4 d112 = d11.get_beta(2) if d112 is not None: d1121 = d112.get_beta(1) d11211 = d1121.get_beta(1) - n_id = find_template_opposite_node(d1121) + n_id = m_analysis.find_template_opposite_node(d1121) if n_id is not None: - template[n_darts - 1, 10] = nodes_scores[n_id] - template[n_darts - 1, deep+10] = nodes_adjacency[n_id] - n_id = find_template_opposite_node(d11211) + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+10] = d1121.get_quality() + n_id = m_analysis.find_template_opposite_node(d11211) if n_id is not None: - template[n_darts - 1, 11] = nodes_scores[n_id] - template[n_darts - 1, deep+11] = nodes_adjacency[n_id] + n = Node(m_analysis.mesh, n_id) + template[n_darts - 1, 3] = n.get_score() + template[n_darts - 1, deep+11] = d11211.get_quality() template = template[:n_darts, :] return template, dart_ids \ No newline at end of file diff --git a/environment/gymnasium_envs/trimesh_full_env/envs/trimesh.py b/environment/gymnasium_envs/trimesh_full_env/envs/trimesh.py index 6658c69..b097e05 100644 --- a/environment/gymnasium_envs/trimesh_full_env/envs/trimesh.py +++ b/environment/gymnasium_envs/trimesh_full_env/envs/trimesh.py @@ -1,19 +1,31 @@ -from enum import Enum -import gymnasium as gym -from gymnasium import spaces +import copy import pygame +import imageio +import sys +import os + import numpy as np +import gymnasium as gym + +from copy import deepcopy +from enum import Enum + +from gymnasium import spaces +from pygame.locals import * + from mesh_model.random_trimesh import random_mesh from mesh_model.mesh_struct.mesh_elements import Dart -from mesh_model.mesh_analysis.global_mesh_analysis import global_score -from mesh_model.mesh_analysis.trimesh_analysis import isTruncated +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis, TriMeshOldAnalysis from environment.gymnasium_envs.trimesh_full_env.envs.mesh_conv import get_x -from environment.actions.triangular_actions import flip_edge, split_edge, collapse_edge - -from view.window import Game +from environment.actions.triangular_actions import flip_edge, split_edge, collapse_edge, check_mesh +from view.mesh_plotter.mesh_plots import plot_mesh +from view.window import window_data, graph from mesh_display import MeshDisplay +#from view.window import Game +#from mesh_display import MeshDisplay + class Actions(Enum): FLIP = 0 @@ -24,27 +36,59 @@ class Actions(Enum): class TriMeshEnvFull(gym.Env): metadata = {"render_modes": ["human", "rgb_array"], "render_fps": 60} - def __init__(self, mesh=None, mesh_size=16, n_darts_selected=20, deep=6, with_degree_obs=True, action_restriction=False, render_mode=None): - self.mesh = mesh if mesh is not None else random_mesh(mesh_size) - self.mesh_size = len(self.mesh.nodes) - self.nb_darts = len(self.mesh.dart_info) - self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = global_score(self.mesh) + def __init__( + self, + mesh=None, + mesh_size=9, + max_episode_steps=20, + n_darts_selected=7, + deep=6, + with_quality_obs=False, + action_restriction=False, + render_mode=None, + analysis_type = "quality" + ) -> None: + + assert render_mode is None or render_mode in self.metadata["render_modes"] + self.render_mode = render_mode + + # If a mesh has been entered, it is used, otherwise a random mesh is generated. + if mesh is not None: + self.config = {"mesh": mesh} + self.mesh = copy.deepcopy(mesh) + self.mesh_size = 0 + else: + self.config = {"mesh": None} + self.mesh_size = mesh_size + self.mesh = random_mesh(mesh_size) + self.analysis_type = analysis_type + self.m_analysis = TriMeshQualityAnalysis(self.mesh) if self.analysis_type == "quality" else TriMeshOldAnalysis(self.mesh) + self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = self.m_analysis.global_score() self._ideal_rewards = (self._mesh_score - self._ideal_score)*10 self.next_mesh_score = 0 self.deep = deep self.n_darts_selected = n_darts_selected self.restricted = action_restriction - self.degree_observation = with_degree_obs + self.quality_observation = with_quality_obs self.window_size = 512 # The size of the PyGame window self.g = None self.nb_invalid_actions = 0 self.darts_selected = [] # darts id observed - deep = self.deep*2 if self.degree_observation else deep + self.episode_count = 0 + self.ep_len = 0 + self.darts_selected = [] + self.max_steps = max_episode_steps + + self.actions_info = { + "n_flip": 0, + "n_split": 0, + "n_collapse": 0, + } self.observation_space = spaces.Box( low=-15, # nodes min degree : 15 high=15, # nodes max degree : 15 - shape=(self.n_darts_selected, self.deep * 2 if self.degree_observation else self.deep), + shape=(self.n_darts_selected, self.deep * 2 if self.quality_observation else self.deep), dtype=np.int64 ) @@ -53,9 +97,6 @@ def __init__(self, mesh=None, mesh_size=16, n_darts_selected=20, deep=6, with_de # We have 3 action, flip, split, collapse self.action_space = spaces.MultiDiscrete([3, self.n_darts_selected]) - assert render_mode is None or render_mode in self.metadata["render_modes"] - self.render_mode = render_mode - """ If human-rendering is used, `self.window` will be a reference to the window that we draw to. `self.clock` will be a clock that is used @@ -63,37 +104,63 @@ def __init__(self, mesh=None, mesh_size=16, n_darts_selected=20, deep=6, with_de human-mode. They will remain `None` until human-mode is used for the first time. """ - self.window = None - self.clock = None + # Render + if self.render_mode == "human": + self.mesh_disp = MeshDisplay(self.mesh) + self.graph = graph.Graph(self.mesh_disp.get_nodes_coordinates(), self.mesh_disp.get_edges(), + self.mesh_disp.get_scores()) + self.win_data = window_data() + self.window_size = 512 # The size of the PyGame window + self.window = None + self.clock = None + + self.recording = False + self.frames = [] + self._render_frame() def reset(self, seed=None, options=None): # We need the following line to seed self.np_random super().reset(seed=seed) if options is not None: self.mesh = options['mesh'] + elif self.config["mesh"] is not None: + self.mesh = copy.deepcopy(self.config["mesh"]) else: self.mesh = random_mesh(self.mesh_size) - self.nb_darts = len(self.mesh.dart_info) - self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = global_score(self.mesh) + + self.m_analysis = TriMeshQualityAnalysis(self.mesh) if self.analysis_type=="quality" else TriMeshOldAnalysis(self.mesh) + self._nodes_scores, self._mesh_score, self._ideal_score, self._nodes_adjacency = self.m_analysis.global_score() self._ideal_rewards = (self._mesh_score - self._ideal_score) * 10 self.nb_invalid_actions = 0 + self.ep_len = 0 self.close() self.observation = self._get_obs() info = self._get_info(terminated=False,valid_act=(None,None,None), action=(None,None), mesh_reward=None) + self.actions_info = { + "n_flip": 0, + "n_split": 0, + "n_collapse": 0, + } + if self.render_mode == "human": + self.recording = True self._render_frame() + else: + self.recording = False return self.observation, info def _get_obs(self): - irregularities, darts_list = get_x(self.mesh, self.n_darts_selected, self.deep, self.degree_observation, self.restricted, self._nodes_scores, self._nodes_adjacency) + irregularities, darts_list = get_x(self.m_analysis, self.n_darts_selected, self.deep, self.quality_observation, self.restricted, self._nodes_scores, self._nodes_adjacency) self.darts_selected = darts_list return irregularities def _get_info(self, terminated, valid_act, action, mesh_reward): valid_action, valid_topo, valid_geo = valid_act + if self._mesh_score - self._ideal_score <0: + raise ValueError("score impossible") return { "distance": self._mesh_score - self._ideal_score, "mesh_reward" : mesh_reward, @@ -108,7 +175,9 @@ def _get_info(self, terminated, valid_act, action, mesh_reward): "invalid_flip": 1.0 if action[0]==Actions.FLIP.value and not valid_action else 0.0, "invalid_split": 1.0 if action[0]==Actions.SPLIT.value and not valid_action else 0.0, "invalid_collapse": 1.0 if action[0]==Actions.COLLAPSE.value and not valid_action else 0.0, - "mesh" : self.mesh, + "mesh" : self.m_analysis.mesh, + "mesh_analysis": self.m_analysis, + "darts_selected": self.darts_selected, } def _action_to_dart_id(self, action: np.ndarray) -> int: @@ -120,45 +189,50 @@ def _action_to_dart_id(self, action: np.ndarray) -> int: return self.darts_selected[int(action[1])] def step(self, action: np.ndarray): + self.ep_len+=1 dart_id = self._action_to_dart_id(action) d = Dart(self.mesh, dart_id) d1 = d.get_beta(1) n1 = d.get_node() n2 = d1.get_node() valid_action, valid_topo, valid_geo = False, False, False - + # before_mesh = deepcopy(self.mesh) if action[0] == Actions.FLIP.value: - valid_action, valid_topo, valid_geo = flip_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = flip_edge(self.m_analysis, n1, n2) + self.actions_info["n_flip"] += 1 elif action[0] == Actions.SPLIT.value: - valid_action, valid_topo, valid_geo = split_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = split_edge(self.m_analysis, n1, n2) + self.actions_info["n_split"] += 1 elif action[0] == Actions.COLLAPSE.value: - valid_action, valid_topo, valid_geo = collapse_edge(self.mesh, n1, n2) + valid_action, valid_topo, valid_geo = collapse_edge(self.m_analysis, n1, n2) + self.actions_info["n_collapse"] += 1 else: raise ValueError("Action not defined") if valid_action: + next_nodes_score, self.next_mesh_score, _, next_nodes_adjacency = self.m_analysis.global_score() # An episode is done if the actual score is the same as the ideal - next_nodes_score, self.next_mesh_score, _, next_nodes_adjacency = global_score(self.mesh) terminated = np.array_equal(self._ideal_score, self.next_mesh_score) mesh_reward = (self._mesh_score - self.next_mesh_score)*10 + # if mesh_reward == 10: # it should be impossible to improve only one irregularity + # b_mesh_analysis = TriMeshTopoAnalysis(before_mesh) + # plot_mesh(before_mesh) + # plot_mesh(self.mesh) + # bool1 = check_mesh(b_mesh_analysis) + # bool2 = check_mesh(self.m_analysis) reward = mesh_reward self._nodes_scores, self._mesh_score, self._nodes_adjacency = next_nodes_score, self.next_mesh_score, next_nodes_adjacency self.observation = self._get_obs() self.nb_invalid_actions = 0 - elif not valid_topo: + elif not valid_topo or not valid_geo: reward = -10 mesh_reward = 0 terminated = False self.nb_invalid_actions += 1 - elif not valid_geo: - mesh_reward = 0 - terminated = False - reward = 0 - self.nb_invalid_actions += 1 else: raise ValueError("Invalid action") if self.nb_invalid_actions > 10 : - truncated = isTruncated(self.mesh, self.darts_selected) + truncated = self.m_analysis.isTruncated(self.darts_selected) else: truncated = False valid_act = valid_action, valid_topo, valid_geo @@ -166,30 +240,91 @@ def step(self, action: np.ndarray): if self.render_mode == "human": self._render_frame() + #Saving episode rendering as gif + if terminated or self.ep_len>= self.max_steps: + if self.recording and self.frames: + base_path = f"training/episode_recording/del/episode_star_{self.episode_count}" + filename = base_path + ".gif" + index = 1 + while os.path.exists(filename): + filename = f"{base_path}_{index}.gif" + index += 1 - return self.observation, reward, terminated, truncated, info + imageio.mimsave(filename, self.frames, fps=1) + print("Image recorded") + self.episode_count +=1 - def render(self): - if self.render_mode == "human": - return self._render_frame() + return self.observation, reward, terminated, truncated, info def _render_frame(self): - if self.g is None and self.render_mode == "human": - mesh_disp = MeshDisplay(self.mesh) - self.g = Game(self.mesh, mesh_disp) - self.window = True - self.g.control_events() - self.g.window.fill((255, 255, 255)) - self.g.draw() - pygame.display.flip() - if self.clock is None and self.render_mode == "human": + if self.render_mode == "human" and self.window is None: + pygame.init() + pygame.display.init() + self.window = pygame.display.set_mode(self.win_data.size, self.win_data.options) + pygame.display.set_caption('QuadMesh') + self.window.fill((255, 255, 255)) + self.font = pygame.font.SysFont(None, self.win_data.font_size) self.clock = pygame.time.Clock() - if self.render_mode == "human": - self.g.mesh_disp = MeshDisplay(self.mesh) - self.g.control_events() + self.clock.tick(60) + self.win_data.scene_xmin, self.win_data.scene_ymin, self.win_data.scene_xmax, self.win_data.scene_ymax = self.graph.bounding_box() + self.win_data.scene_center = pygame.math.Vector2((self.win_data.scene_xmax + self.win_data.scene_xmin) / 2.0, + (self.win_data.scene_ymax + self.win_data.scene_ymin) / 2.0) + + pygame.event.pump() + self.window.fill((255, 255, 255)) # white + for event in pygame.event.get(): + if event.type == QUIT: + pygame.quit() + sys.exit() + + if event.type == VIDEORESIZE or event.type == VIDEOEXPOSE: # handles window minimising/maximising + x, y = self.window.get_size() + text_margin = 200 + self.win_data.center.x = (x - text_margin) / 2 + self.win_data.center.y = y / 2 + ratio = float(x - text_margin) / float(self.win_data.scene_xmax - self.win_data.scene_xmin) + ratio_y = float(y) / float(self.win_data.scene_ymax - self.win_data.scene_ymin) + if ratio_y < ratio: + ratio = ratio_y + + self.win_data.node_size = max(ratio / 100, 10) + self.win_data.stretch = 0.75 * ratio + + self.window.fill((255, 255, 255)) + pygame.display.flip() + + self.graph.clear() + self.mesh_disp = MeshDisplay(self.mesh) + self.graph.update(self.mesh_disp.get_nodes_coordinates(), self.mesh_disp.get_edges(), + self.mesh_disp.get_scores()) + + #Draw mesh + for e in self.graph.edges: + e.draw(self.window, self.win_data) + for n in self.graph.vertices: + n.draw(self.window, self.font, self.win_data) + + #Print action type + if hasattr(self, 'actions_info'): + x = self.window.get_width() - 150 + y_start = 100 + line_spacing = 25 + + for i, (action_name, count) in enumerate(self.actions_info.items()): + text = f"{action_name}: {count}" + text_surface = self.font.render(text, True, (0, 0, 0)) + self.window.blit(text_surface, (x, y_start + i * line_spacing)) + + self.clock.tick(60) + pygame.time.delay(1200) + pygame.display.flip() + if self.recording: + pixels = pygame.surfarray.array3d(pygame.display.get_surface()) + frame = pixels.transpose([1,0,2]) + self.frames.append(frame) def close(self): - if self.window is not None: + if self.render_mode=="human" and self.window is not None: pygame.display.quit() pygame.quit() - self.g = None + self.window = None diff --git a/environment/environment_config.json b/environment/old_files/environment_config.json similarity index 51% rename from environment/environment_config.json rename to environment/old_files/environment_config.json index 8ee4646..db6e82a 100644 --- a/environment/environment_config.json +++ b/environment/old_files/environment_config.json @@ -1,9 +1,9 @@ { - "env_name": "Quadmesh-v0", - "mesh_size": 16, + "env_name": "Trimesh-v0", + "mesh_size": 10, "max_episode_steps": 20, - "n_darts_selected": 10, - "deep": 8, + "n_darts_selected": 11, + "deep": 6, "action_restriction": false, "with_degree_observation": false } \ No newline at end of file diff --git a/environment/eval_environment_config.json b/environment/old_files/eval_environment_config.json similarity index 100% rename from environment/eval_environment_config.json rename to environment/old_files/eval_environment_config.json diff --git a/environment/trimesh_env.py b/environment/old_files/trimesh_env.py similarity index 100% rename from environment/trimesh_env.py rename to environment/old_files/trimesh_env.py diff --git a/mesh_display.py b/mesh_display.py index 0d21e38..6b816df 100644 --- a/mesh_display.py +++ b/mesh_display.py @@ -1,5 +1,5 @@ from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_analysis.global_mesh_analysis import global_score +from mesh_model.mesh_analysis.global_mesh_analysis import GlobalMeshAnalysis class MeshDisplay: @@ -35,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) """ - nodes_score, mesh_score, ideal_mesh_score, adjacency = global_score(self.mesh) + ma = GlobalMeshAnalysis(self.mesh) + nodes_score, mesh_score, ideal_mesh_score, adjacency = ma.global_score() return [nodes_score, mesh_score, ideal_mesh_score] diff --git a/mesh_files/tri-delaunay.msh b/mesh_files/tri-delaunay.msh new file mode 100644 index 0000000..9497d75 --- /dev/null +++ b/mesh_files/tri-delaunay.msh @@ -0,0 +1,125 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +4 4 1 0 +2 3 0 0 0 +3 1 2 0 0 +4 1 -2 0 0 +6 -1 0 0 0 +1 -1 0 0 1 2 0 0 2 3 -6 +2 -1 -2 0 1 0 0 0 2 6 -4 +3 1 -2 0 3 0 0 0 2 4 -2 +4 1 0 0 3 2 0 0 2 2 -3 +1 -1 -2 0 3 2 0 0 4 1 2 3 4 +$EndEntities +$Nodes +9 21 1 21 +0 2 0 1 +1 +3 0 0 +0 3 0 1 +2 +1 2 0 +0 4 0 1 +3 +1 -2 0 +0 6 0 1 +4 +-1 0 0 +1 1 0 2 +5 +6 +0.3333333333351847 1.333333333335185 0 +-0.3333333333310193 0.6666666666689807 0 +1 2 0 2 +7 +8 +-0.3333333333350901 -0.6666666666649099 0 +0.3333333333315289 -1.333333333331529 0 +1 3 0 2 +9 +10 +1.666666666666666 -1.333333333333334 0 +2.333333333332406 -0.6666666666675944 0 +1 4 0 2 +11 +12 +2.333333333336016 0.6666666666639838 0 +1.666666666669397 1.333333333330603 0 +2 1 0 9 +13 +14 +15 +16 +17 +18 +19 +20 +21 +1.000000000000195 -2.053357484044227e-13 0 +1.833333333333501 -4.514097429197325e-13 0 +0.1666666666669092 5.088429677603714e-13 0 +1.000000000000573 0.8333333333331049 0 +0.9999999999997741 -0.8333333333337288 0 +1.65476190476127 -0.6547619047618515 0 +0.3452380952395821 0.6547619047612705 0 +0.3452380952388717 -0.6547619047606879 0 +1.654761904761082 0.6547619047601926 0 +$EndNodes +$Elements +9 44 1 44 +0 2 15 1 +1 1 +0 3 15 1 +2 2 +0 4 15 1 +3 3 +0 6 15 1 +4 4 +1 1 1 3 +5 2 5 +6 5 6 +7 6 4 +1 2 1 3 +8 4 7 +9 7 8 +10 8 3 +1 3 1 3 +11 3 9 +12 9 10 +13 10 1 +1 4 1 3 +14 1 11 +15 11 12 +16 12 2 +2 1 2 28 +17 1 11 14 +18 10 1 14 +19 6 4 15 +20 12 2 16 +21 2 5 16 +22 4 7 15 +23 3 9 17 +24 8 3 17 +25 14 13 18 +26 15 13 19 +27 13 16 19 +28 13 17 18 +29 13 15 20 +30 13 14 21 +31 16 13 21 +32 17 13 20 +33 11 12 21 +34 7 8 20 +35 5 6 19 +36 9 10 18 +37 14 11 21 +38 16 5 19 +39 15 7 20 +40 10 14 18 +41 17 9 18 +42 6 15 19 +43 12 16 21 +44 8 17 20 +$EndElements diff --git a/mesh_files/tri-easy.msh b/mesh_files/tri-easy.msh new file mode 100644 index 0000000..04b605a --- /dev/null +++ b/mesh_files/tri-easy.msh @@ -0,0 +1,109 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +4 4 1 0 +2 2 0 0 0 +3 1 2 0 0 +4 1 -2 0 0 +6 0 0 0 0 +1 0 0 0 1 2 0 0 2 3 -6 +2 0 -2 0 1 0 0 0 2 6 -4 +3 1 -2 0 2 0 0 0 2 4 -2 +4 1 0 0 2 2 0 0 2 2 -3 +1 0 -2 0 2 2 0 0 4 1 2 3 4 +$EndEntities +$Nodes +9 17 1 17 +0 2 0 1 +1 +2 0 0 +0 3 0 1 +2 +1 2 0 +0 4 0 1 +3 +1 -2 0 +0 6 0 1 +4 +0 0 0 +1 1 0 2 +5 +6 +0.6666666666675913 1.333333333335183 0 +0.3333333333347209 0.6666666666694419 0 +1 2 0 2 +7 +8 +0.3333333333325024 -0.6666666666650047 0 +0.6666666666657883 -1.333333333331577 0 +1 3 0 2 +9 +10 +1.333333333332593 -1.333333333334814 0 +1.666666666665557 -0.6666666666688863 0 +1 4 0 2 +11 +12 +1.666666666667332 0.6666666666653366 0 +1.333333333334036 1.333333333331929 0 +2 1 0 5 +13 +14 +15 +16 +17 +0.9999999999993636 7.09969592564692e-13 0 +0.9166666666670574 0.694444444445225 0 +1.083333333332518 -0.6944444444445459 0 +1.499999999999302 -0.1666666666677049 0 +0.4999999999993019 0.1666666666687502 0 +$EndNodes +$Elements +9 36 1 36 +0 2 15 1 +1 1 +0 3 15 1 +2 2 +0 4 15 1 +3 3 +0 6 15 1 +4 4 +1 1 1 3 +5 2 5 +6 5 6 +7 6 4 +1 2 1 3 +8 4 7 +9 7 8 +10 8 3 +1 3 1 3 +11 3 9 +12 9 10 +13 10 1 +1 4 1 3 +14 1 11 +15 11 12 +16 12 2 +2 1 2 20 +17 13 7 15 +18 13 11 14 +19 7 13 17 +20 11 13 16 +21 5 12 2 +22 9 8 3 +23 4 7 17 +24 1 11 16 +25 7 8 15 +26 11 12 14 +27 9 10 15 +28 5 6 14 +29 8 9 15 +30 12 5 14 +31 13 15 16 +32 13 14 17 +33 14 6 17 +34 15 10 16 +35 6 4 17 +36 10 1 16 +$EndElements diff --git a/mesh_files/tri-medium.msh b/mesh_files/tri-medium.msh new file mode 100644 index 0000000..bbf4f17 --- /dev/null +++ b/mesh_files/tri-medium.msh @@ -0,0 +1,93 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +4 4 1 0 +1 0 0 0 0 +2 1 0 0 0 +3 1 1 0 0 +4 0 1 0 0 +1 -9.999999994736442e-08 -1e-07 -1e-07 1.0000001 1e-07 1e-07 0 2 1 -2 +2 0.9999999000000001 -9.999999994736442e-08 -1e-07 1.0000001 1.0000001 1e-07 0 2 2 -3 +3 -9.999999994736442e-08 0.9999999000000001 -1e-07 1.0000001 1.0000001 1e-07 0 2 3 -4 +4 -1e-07 -9.999999994736442e-08 -1e-07 1e-07 1.0000001 1e-07 0 2 4 -1 +1 -9.999999994736442e-08 -9.999999994736442e-08 -1e-07 1.0000001 1.0000001 1e-07 0 4 1 2 3 4 +$EndEntities +$Nodes +9 13 1 13 +0 1 0 1 +1 +0 0 0 +0 2 0 1 +2 +1 0 0 +0 3 0 1 +3 +1 1 0 +0 4 0 1 +4 +0 1 0 +1 1 0 1 +5 +0.4999999999999988 0 0 +1 2 0 1 +6 +1 0.4999999999999988 0 +1 3 0 1 +7 +0.5000000000000011 1 0 +1 4 0 1 +8 +0 0.5000000000000011 0 +2 1 0 5 +9 +10 +11 +12 +13 +0.5 0.5 0 +0.7499999999999996 0.2499999999999996 0 +0.2499999999999996 0.2500000000000004 0 +0.7500000000000002 0.7499999999999997 0 +0.2500000000000003 0.7500000000000002 0 +$EndNodes +$Elements +9 28 1 28 +0 1 15 1 +1 1 +0 2 15 1 +2 2 +0 3 15 1 +3 3 +0 4 15 1 +4 4 +1 1 1 2 +5 1 5 +6 5 2 +1 2 1 2 +7 2 6 +8 6 3 +1 3 1 2 +9 3 7 +10 7 4 +1 4 1 2 +11 4 8 +12 8 1 +2 1 2 16 +13 2 10 5 +14 1 11 8 +15 3 12 6 +16 4 13 7 +17 5 10 9 +18 9 11 5 +19 9 10 6 +20 6 12 9 +21 9 12 7 +22 7 13 9 +23 8 11 9 +24 9 13 8 +25 5 11 1 +26 6 10 2 +27 7 12 3 +28 8 13 4 +$EndElements diff --git a/mesh_files/tri-star.msh b/mesh_files/tri-star.msh new file mode 100644 index 0000000..4c4791e --- /dev/null +++ b/mesh_files/tri-star.msh @@ -0,0 +1,73 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +4 4 1 0 +2 3 0 0 0 +3 1 2 0 0 +4 1 -2 0 0 +6 -1 0 0 0 +1 -1 0 0 1 2 0 0 2 3 -6 +2 -1 -2 0 1 0 0 0 2 6 -4 +3 1 -2 0 3 0 0 0 2 4 -2 +4 1 0 0 3 2 0 0 2 2 -3 +1 -1 -2 0 3 2 0 0 4 1 2 3 4 +$EndEntities +$Nodes +9 8 1 8 +0 2 0 1 +1 +3 0 0 +0 3 0 1 +2 +1 2 0 +0 4 0 1 +3 +1 -2 0 +0 6 0 1 +4 +-1 0 0 +1 1 0 0 +1 2 0 0 +1 3 0 0 +1 4 0 0 +2 1 0 4 +5 +6 +7 +8 +1.275322183424842 -0.2731007972534724 0 +1.421496441969191 0.5796966623384481 0 +0.5822083392261085 0.3098632504963102 0 +0.4643826306627374 -0.4908093866892904 0 +$EndNodes +$Elements +9 18 1 18 +0 2 15 1 +1 1 +0 3 15 1 +2 2 +0 4 15 1 +3 3 +0 6 15 1 +4 4 +1 1 1 1 +5 2 4 +1 2 1 1 +6 4 3 +1 3 1 1 +7 3 1 +1 4 1 1 +8 1 2 +2 1 2 10 +9 7 5 6 +10 2 4 7 +11 2 7 6 +12 8 4 3 +13 7 4 8 +14 5 8 3 +15 7 8 5 +16 1 2 6 +17 1 6 5 +18 1 5 3 +$EndElements diff --git a/mesh_model/mesh_analysis/global_mesh_analysis.py b/mesh_model/mesh_analysis/global_mesh_analysis.py index 3553c6d..ab3eea6 100644 --- a/mesh_model/mesh_analysis/global_mesh_analysis.py +++ b/mesh_model/mesh_analysis/global_mesh_analysis.py @@ -1,301 +1,297 @@ -from math import sqrt, degrees, acos import numpy as np +from math import sqrt, degrees, acos, atan2 +from abc import ABC, abstractmethod from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face from mesh_model.mesh_struct.mesh import Mesh from view.mesh_plotter.mesh_plots import plot_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: 4 return: a list of the nodes score, the current mesh score and the ideal mesh score, and the adjacency - """ - mesh_ideal_score = 0 - mesh_score = 0 - nodes_score = [] - nodes_adjacency = [] - for i in range(len(m.nodes)): - if m.nodes[i, 2] >= 0: - n_id = i - node = Node(m, n_id) - n_score, adjacency= score_calculation(node) - nodes_score.append(n_score) - nodes_adjacency.append(adjacency) - mesh_ideal_score += n_score - mesh_score += abs(n_score) - else: - nodes_score.append(0) - nodes_adjacency.append(6) - return nodes_score, mesh_score, mesh_ideal_score, nodes_adjacency - +class NodeAnalysis: + def __init__(self, n: Node): + self.n = n + + def score_calculation(self) -> 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 + :raises ValueError: if the node is associated to no dart + """ + + d = self.n.get_dart() + if d.mesh.dart_info[d.id,0] < 0: + raise ValueError("No existing dart") + + adjacency = self.degree() + ideal_adjacency =self.n.get_ideal_adjacency() + + return ideal_adjacency - adjacency + + def get_angle(self, d1: Dart, d2: Dart) -> float: + """ + Function to calculate the angle of the boundary at the node n. + The angle is named ABC and node self.n is at point A. + :param d1: the first boundary dart. + :param d2: the second boundary dart. + :return: the angle (degrees) + """ + if d1.get_node() == self.n: + A = self.n + B = d1.get_beta(1).get_node() + C = d2.get_node() -def score_calculation(n: Node) -> (int, 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) - # Check if the mesh is triangular or quad - d = n.get_dart() - if d.id <0: - raise ValueError("No existing dart") - d1 = d.get_beta(1) - d11 = d1.get_beta(1) - d111 = d11.get_beta(1) - triangular = (d111.id == d.id) - if on_boundary(n): - angle = get_boundary_angle(n) - if triangular: - ideal_adjacency = max(round(angle/60)+1, 2) else: - ideal_adjacency = max(round(angle/90)+1, 2) - elif triangular: - ideal_adjacency = 360/60 - else: - ideal_adjacency = 360/90 + A = self.n + B = d2.get_beta(1).get_node() + C = d1.get_node() + if d2.get_node() != A: + raise ValueError("Angle error") - return ideal_adjacency-adjacency, adjacency + vect_AB = (B.x() - A.x(), B.y() - A.y()) + vect_AC = (C.x() - A.x(), C.y() - A.y()) -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") + dot = vect_AB[0] * vect_AC[0] + vect_AB[1] * vect_AC[1] - 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): - plot_mesh(n.mesh) - raise ValueError("Angle error") - return degrees(angle) - -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 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 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 + # cross product + cross = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] + angle_rad = atan2(cross, dot) + angle = degrees(angle_rad) % 360 + if np.isnan(angle): + raise ValueError("Angle error") + return angle + + def get_boundary_angle(self) -> float: + """ + Calculate the boundary angle of a node in the mesh. + :return: the boundary angle (degrees) + """ + adj_darts_list = self.adjacent_darts() + 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) > 2 : # or len(boundary_darts) < 2 + #plot_mesh(self.n.mesh) + #raise ValueError("Boundary error") + angle = self.get_angle(boundary_darts[0], boundary_darts[1]) + return angle + + def on_boundary(self) -> bool: + """ + Test if the node self.n is on boundary. + :return: True if the node n is on boundary, False otherwise. + """ + adj_darts_list = self.adjacent_darts() + 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 adjacent_faces_id(n: Node) -> list[int]: - adj_darts = adjacent_darts(n) - adj_faces = [] - for d in adj_darts: - f = d.get_face() - if f.id not in adj_faces: - adj_faces.append(f.id) - return adj_faces - - -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) + def adjacent_darts(self) -> list[Dart]: + """ + Function that retrieve the adjacent darts of node n. + :return: the list of adjacent darts + """ + adj_darts = [] + m = self.n.mesh + for d_info in m.active_darts(): + d = Dart(m, d_info[0]) + d_nfrom = d.get_node() + d_nto = d.get_beta(1) + if d_nfrom == self.n and d not in adj_darts: + adj_darts.append(d) + if d_nto.get_node() == self.n and d not in adj_darts: + adj_darts.append(d) + return adj_darts + + def adjacent_faces_id(self) -> list[int]: + adj_darts = self.adjacent_darts() + adj_faces = [] + for d in adj_darts: + f = d.get_face() + if f.id not in adj_faces: + adj_faces.append(f.id) + return adj_faces + + def degree(self) -> int: + """ + Function to calculate the degree of a node in the mesh. + :return: the degree of the node + """ + adj_darts_list = self.adjacent_darts() + adjacency = 0 + b = self.on_boundary() + 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 test_degree(self) -> bool: + """ + Verify that the degree of a vertex is lower than 10 + :return: True if the degree is lower than 10, False otherwise + """ + if self.degree() >= 10: + return False 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 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 + return True -def test_degree(n: Node) -> bool: +class GlobalMeshAnalysis(ABC): """ - 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 + The base of mesh analysis + :param mesh: A mesh to analise """ - if degree(n) >= 10: - return False - else: + def __init__(self, mesh: Mesh) -> None: + self.mesh = mesh + + def set_adjacency(self): + pass + + def set_scores(self): + pass + + def set_geometric_quality(self): + pass + + def get_dart_geometric_quality(self, d: Dart) -> int: + pass + + def global_score(self): + """ + Calculate the overall mesh score. The mesh cannot achieve a better score than the ideal one. + And the current score is the mesh score. + :return: 4 return: a list of the nodes score, the current mesh score and the ideal mesh score, and the adjacency + """ + mesh_ideal_score = 0 + mesh_score = 0 + nodes_score = [] + nodes_adjacency = [] + for i in range(len(self.mesh.nodes)): + if self.mesh.nodes[i, 2] >= 0: + n_id = i + node = Node(self.mesh, n_id) + n_a = NodeAnalysis(node) + n_score = n_a.score_calculation() + nodes_score.append(n_score) + nodes_adjacency.append(n_a.degree()) + mesh_ideal_score += n_score + mesh_score += abs(n_score) + else: + nodes_score.append(0) + nodes_adjacency.append(6) + return nodes_score, mesh_score, mesh_ideal_score, nodes_adjacency + + def get_boundary_darts(self) -> list[Dart]: + """ + Find all boundary darts + :return: a list of all boundary darts + """ + boundary_darts = [] + for d_info in self.mesh.active_darts(): + d = Dart(self.mesh, d_info[0]) + d_twin = d.get_beta(2) + if d_twin is None: + boundary_darts.append(d) + return boundary_darts + + def node_in_mesh(self, x: float, y: float) -> (bool, int): + """ + Search if the node of coordinate (x, y) is inside the mesh. + :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 self.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 check_beta2_relation(self) -> bool: + for dart_info in self.mesh.active_darts(): + d = dart_info[0] + d2 = dart_info[2] + if d2 >= 0 and self.mesh.dart_info[d2, 0] < 0: + raise ValueError("error beta2") + elif d2 >= 0 and self.mesh.dart_info[d2, 2] != d: + raise ValueError("error beta2") return True + def check_double(self) -> bool: + for dart_info in self.mesh.active_darts(): + d = Dart(self.mesh, dart_info[0]) + d2 = Dart(self.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 self.mesh.active_darts(): + ds = Dart(self.mesh, dart_info2[0]) + ds2 = Dart(self.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: + plot_mesh(self.mesh) + raise ValueError("double error") + elif n2 == ns1 and n1 == ns2: + plot_mesh(self.mesh) + raise ValueError("double error") + 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: - 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: - plot_mesh(mesh) - raise ValueError("double error") - elif n2 == ns1 and n1 == ns2: - plot_mesh(mesh) - raise ValueError("double error") - return True - - -def mesh_check(mesh: Mesh) -> bool: - return check_double(mesh) and check_beta2_relation(mesh) - - -""" -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 + def mesh_check(self) -> bool: + return self.check_double() and self.check_beta2_relation() + + def angle_from_sides(self, 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 get_angle_by_coord(self, 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 cross_product(self, vect_AB, vect_AC): + """ Return the cross product between AB et AC. + 0 means A, B and C are coolinear + > 0 mean A, B and C are "sens des aiguilles d'une montre" + < 0 sens inverse + """ + val = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] + return val + + def signe(self, a: int): + pass \ No newline at end of file diff --git a/mesh_model/mesh_analysis/old_files/old_mesh_analysis.py b/mesh_model/mesh_analysis/old_files/old_mesh_analysis.py new file mode 100644 index 0000000..a152d4b --- /dev/null +++ b/mesh_model/mesh_analysis/old_files/old_mesh_analysis.py @@ -0,0 +1,273 @@ +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshAnalysis +from mesh_model.mesh_analysis.global_mesh_analysis import NodeAnalysis +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart, Face +from math import sqrt, degrees + +FLIP = 0 +SPLIT = 1 +COLLAPSE = 2 +TEST_ALL = 3 +ONE_VALID = 4 + + +class TriMeshOldAnalysis(TriMeshAnalysis): + """ + Triangular mesh analysis with geometrical criteria + """ + + def __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + + def isValidAction(self, dart_id: int, action: int) -> (bool, bool): + d = Dart(self.mesh, dart_id) + boundary_darts = self.get_boundary_darts() + if d in boundary_darts: + return False, True + elif action == FLIP: + return self.isFlipOk(d) + elif action == SPLIT: + return self.isSplitOk(d) + elif action == COLLAPSE: + return self.isCollapseOk(d) + elif action == TEST_ALL: + topo, geo = self.isFlipOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isSplitOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isCollapseOk(d) + if not (topo and geo): + return False, False + elif topo and geo: + return True, True + elif action == ONE_VALID: + topo_flip, geo_flip = self.isFlipOk(d) + if (topo_flip and geo_flip): + return True, True + topo_split, geo_split = self.isSplitOk(d) + if (topo_split and geo_split): + return True, True + topo_collapse, geo_collapse = self.isCollapseOk(d) + if (topo_collapse and geo_collapse): + return True, True + return False, False + else: + raise ValueError("No valid action") + + def isFlipOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + # if d is on boundary, flip is not possible + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, _, _, _, _, A, B, C, D = self.mesh.active_triangles(d) + + nA_analysis = NodeAnalysis(A) + nB_analysis = NodeAnalysis(B) + + if not nA_analysis.test_degree() or not nB_analysis.test_degree(): + topo = False + return topo, geo + + # Check angle at d limits to avoid edge reversal + angle_B = self.get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + self.get_angle_by_coord(A.x(), + A.y(), + B.x(), + B.y(), + D.x(), + D.y()) + angle_A = self.get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + self.get_angle_by_coord(B.x(), + B.y(), + A.x(), + A.y(), + D.x(), + D.y()) + if angle_B >= 180 or angle_A >= 180: + topo = False + return topo, geo + + # 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 self.valid_triangle(vect_AC, vect_AD, vect_DC) or not self.valid_triangle(vect_BC, vect_BD, vect_DC): + geo = False + return topo, geo + + return topo, geo + + def isSplitOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, _, _, _, _, A, B, C, D = self.mesh.active_triangles(d) + + nC_analysis = NodeAnalysis(C) + nD_analysis = NodeAnalysis(D) + + if not nC_analysis.test_degree() or not nD_analysis.test_degree(): + topo = False + return topo, geo + + 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 self.valid_triangle(vect_AE, vect_AC, vect_EC): + geo = False + return topo, geo + + # Triangle ADE + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not self.valid_triangle(vect_AD, vect_AE, vect_ED): + geo = False + return topo, geo + + # 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 self.valid_triangle(vect_BC, vect_BE, vect_EC): + geo = False + return topo, geo + + # Triangle BDE + vect_BD = (D.x() - B.x(), D.y() - B.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not self.valid_triangle(vect_BD, vect_BE, vect_ED): + geo = False + return topo, geo + + return topo, geo + + def isCollapseOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, d1, d11, d21, d211, n1, n2, _, _ = self.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 + + n1_analysis = NodeAnalysis(n1) + n2_analysis = NodeAnalysis(n2) + + if d112 is None or d12 is None or d2112 is None or d212 is None: + topo = False + return topo, geo + elif n1_analysis.on_boundary() or n2_analysis.on_boundary(): + topo = False + return topo, geo + elif not n1_analysis.test_degree(): + topo = False + return topo, geo + else: + # search for all adjacent faces to n1 and n2 + if d12 is None and d2112 is None: + adj_faces_n1 = self.get_adjacent_faces(n1, d212, d112) + return topo, self.valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) + elif d212 is None and d112 is None: + adj_faces_n2 = self.get_adjacent_faces(n2, d12, d2112) + return topo, self.valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y) + else: + adj_faces_n1 = self.get_adjacent_faces(n1, d212, d112) + adj_faces_n2 = self.get_adjacent_faces(n2, d12, d2112) + if not self.valid_faces_changes(adj_faces_n1, n1.id, newNode_x, + newNode_y) or not self.valid_faces_changes(adj_faces_n2, n2.id, + newNode_x, newNode_y): + geo = False + return topo, geo + else: + return topo, geo + + def valid_faces_changes(self, 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 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_triangle() + 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 self.valid_triangle(vect_AB, vect_AC, vect_BC): + return False + return True + + def valid_triangle(self, 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 / 2 * sqrt(2) < L_max and L_max < target_mesh_size * 3 * sqrt(2): # 0.35 bool: + for d_id in darts_list: + if self.isValidAction(d_id, 4)[0]: # if on action is valid, it means it's valid topologically and geometrically, so no need to verify the two + return False + return True \ No newline at end of file diff --git a/mesh_model/mesh_analysis/quadmesh_analysis.py b/mesh_model/mesh_analysis/quadmesh_analysis.py index 08df7e7..1eef54a 100644 --- a/mesh_model/mesh_analysis/quadmesh_analysis.py +++ b/mesh_model/mesh_analysis/quadmesh_analysis.py @@ -1,8 +1,13 @@ +import math + import numpy as np +import matplotlib.pyplot as plt +from shapely.geometry import Polygon, Point, LineString +from shapely import affinity from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_analysis.global_mesh_analysis import test_degree, on_boundary, adjacent_faces_id, degree +from mesh_model.mesh_analysis.global_mesh_analysis import GlobalMeshAnalysis, NodeAnalysis FLIP_CW = 0 # flip clockwise FLIP_CCW = 1 # flip counterclockwise @@ -12,271 +17,312 @@ TEST_ALL = 5 # test if all actions are valid ONE_VALID = 6 # test if at least one action is valid - -def isValidAction(mesh: Mesh, dart_id: int, action: int) -> (bool, bool): +class QuadMeshAnalysis(GlobalMeshAnalysis): """ - Test if an action is valid. You can select the ype of action between {flip clockwise, flip counterclockwise, split, collapse, cleanup, all action, one action no matter wich one}. :param mesh: - :param mesh: a mesh - :param dart_id: a dart on which to test the action - :param action: an action type - :return: + The base of quadrangular mesh analysis """ - d = Dart(mesh, dart_id) - if d.get_beta(2) is None: - return False, True - elif action == FLIP_CW: - return isFlipCWOk(d) - elif action == FLIP_CCW: - return isFlipCCWOk(d) - elif action == SPLIT: - return isSplitOk(d) - elif action == COLLAPSE: - return isCollapseOk(d) - elif action == CLEANUP: - return isCleanupOk(d) - elif action == TEST_ALL: - topo, geo = isFlipCCWOk(d) - if not (topo and geo): - return False, False - topo, geo = isFlipCWOk(d) - if not (topo and geo): - return False, False - topo, geo = isSplitOk(d) - if not (topo and geo): - return False, False - topo, geo = isCollapseOk(d) - if not (topo and geo): + def __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + #If initial scores and adjacency have not already been set + if self.mesh.nodes[0,4] == -99: + self.set_adjacency() + self.set_scores() + #self.set_geometric_quality() + + def isValidAction(self, dart_id: int, action: int) -> (bool,bool): + pass + + def set_adjacency(self) -> None: + i = 0 + for n_info in self.mesh.nodes: + if n_info[2] >= 0: + n = Node(self.mesh, i) + na = NodeAnalysis(n) + if na.on_boundary(): + angle = na.get_boundary_angle() + ideal_adj = max(round(angle / 90) + 1, 2) + n.set_ideal_adjacency(ideal_adj) + else: + n.set_ideal_adjacency(4) + i += 1 + + def set_scores(self) -> None: + i = 0 + for n_info in self.mesh.nodes: + if n_info[2] >= 0: + n = Node(self.mesh, i) + na = NodeAnalysis(n) + s = na.score_calculation() + n.set_score(s) + i += 1 + +class QuadMeshOldAnalysis(QuadMeshAnalysis): + """ + Quadmesh old analysis + """ + + def __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + + def isValidAction(self, dart_id: int, action: int) -> (bool, bool): + """ + Test if an action is valid. You can select the ype of action between {flip clockwise, flip counterclockwise, split, collapse, cleanup, all action, one action no matter wich one}. + :param dart_id: a dart on which to test the action + :param action: an action type + :return: + """ + d = Dart(self.mesh, dart_id) + if d.get_beta(2) is None: + return False, True + elif action == FLIP_CW: + return self.isFlipCWOk(d) + elif action == FLIP_CCW: + return self.isFlipCCWOk(d) + elif action == SPLIT: + return self.isSplitOk(d) + elif action == COLLAPSE: + return self.isCollapseOk(d) + elif action == CLEANUP: + return self.isCleanupOk(d) + elif action == TEST_ALL: + topo, geo = self.isFlipCCWOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isFlipCWOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isSplitOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isCollapseOk(d) + if not (topo and geo): + return False, False + elif topo and geo: + return True, True + elif action == ONE_VALID: + topo_flip, geo_flip = self.isFlipCCWOk(d) + if (topo_flip and geo_flip): + return True, True + topo_flip, geo_flip = self.isFlipCWOk(d) + if (topo_flip and geo_flip): + return True, True + topo_split, geo_split = self.isSplitOk(d) + if (topo_split and geo_split): + return True, True + topo_collapse, geo_collapse = self.isCollapseOk(d) + if (topo_collapse and geo_collapse): + return True, True return False, False - elif topo and geo: - return True, True - elif action == ONE_VALID: - topo_flip, geo_flip = isFlipCCWOk(d) - if (topo_flip and geo_flip): - return True, True - topo_flip, geo_flip = isFlipCWOk(d) - if (topo_flip and geo_flip): - return True, True - topo_split, geo_split = isSplitOk(d) - if (topo_split and geo_split): - return True, True - topo_collapse, geo_collapse = isCollapseOk(d) - if (topo_collapse and geo_collapse): - return True, True - return False, False - else: - raise ValueError("No valid action") - - -def isFlipCCWOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - - # if d is on boundary, flip is not possible - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + else: + raise ValueError("No valid action") - # if degree will not too high - if not test_degree(n5) or not test_degree(n3): - topo = False - return topo, geo - # if two faces share two edges - if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): - topo = False - return topo, geo + def isFlipCCWOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + + # if d is on boundary, flip is not possible + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = self.mesh.active_quadrangles(d) - # check validity of the two modified quads - geo = isValidQuad(n5, n6, n2, n3) and isValidQuad(n1, n5, n3, n4) + n5_analysis = NodeAnalysis(n5) + n3_analysis = NodeAnalysis(n3) + # if degree will not too high + if not n5_analysis.test_degree() or not n3_analysis.test_degree(): + topo = False + return topo, geo - return topo, geo + # if two faces share two edges + if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): + topo = False + return topo, geo -def isFlipCWOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - # if d is on boundary, flip is not possible - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) - # if degree are - if not test_degree(n4) or not test_degree(n6): - topo = False - return topo, geo + # check validity of the two modified quads + geo = self.isValidQuad(n5, n6, n2, n3) and self.isValidQuad(n1, n5, n3, n4) - if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): - topo = False return topo, geo - geo = isValidQuad(n4, n6, n2, n3) and isValidQuad(n1, n5, n6, n4) - return topo, geo + def isFlipCWOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + # if d is on boundary, flip is not possible + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = self.mesh.active_quadrangles(d) + n4_analysis = NodeAnalysis(n4) + n6_analysis = NodeAnalysis(n6) + if not n4_analysis.test_degree() or not n6_analysis.test_degree(): + topo = False + return topo, geo -def isSplitOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): + topo = False + return topo, geo + geo = self.isValidQuad(n4, n6, n2, n3) and self.isValidQuad(n1, n5, n6, n4) - if not test_degree(n4) or not test_degree(n2): - topo = False return topo, geo - if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): - topo = False - return topo, geo - n10 = mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) - geo = isValidQuad(n4, n1, n5, n10) and isValidQuad(n4, n10, n2, n3) and isValidQuad(n10, n5, n6, n2) - mesh.del_node(n10) - return topo, geo + def isSplitOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = self.mesh.active_quadrangles(d) + n4_analysis = NodeAnalysis(n4) + n2_analysis = NodeAnalysis(n2) + if not n4_analysis.test_degree() or not n2_analysis.test_degree(): + topo = False + return topo, geo -def isCollapseOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = mesh.active_quadrangles(d) + if d211.get_node() == d111.get_node() or d11.get_node() == d2111.get_node(): + topo = False + return topo, geo - if on_boundary(n1): # on_boundary(n3) or - topo = False + n10 = self.mesh.add_node((n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2) + geo = self.isValidQuad(n4, n1, n5, n10) and self.isValidQuad(n4, n10, n2, n3) and self.isValidQuad(n10, n5, n6, n2) + self.mesh.del_node(n10) return topo, geo - if (degree(n3)+degree(n1)-2) > 10: - topo = False - return topo, geo - adjacent_faces_lst = [] - f1 = d2.get_face() - adjacent_faces_lst.append(f1.id) - d12 = d1.get_beta(2) - if d12 is not None: - f2 = d12.get_face() - adjacent_faces_lst.append(f2.id) - d112 = d11.get_beta(2) - if d112 is not None: - f3 = d112.get_face() - adjacent_faces_lst.append(f3.id) - d1112 = d111.get_beta(2) - if d1112 is not None: - f4 = d1112.get_face() - adjacent_faces_lst.append(f4.id) - - # Check that there are no adjacent faces in common - if len(adjacent_faces_lst) != len(set(adjacent_faces_lst)): - topo = False - return topo, geo + def isCollapseOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + d2, d1, d11, d111, d21, d211, d2111, n1, n2, n3, n4, n5, n6 = self.mesh.active_quadrangles(d) - adj_faces = adjacent_faces_id(n3) - adj_faces.extend(adjacent_faces_id(n1)) - - #If the opposite vertex is on the edge, it is not moved - - if on_boundary(n3): - n10 = mesh.add_node( n3.x(), n3.y()) - else: - n10 = mesh.add_node((n1.x() + n3.x()) / 2, (n1.y() + n3.y()) / 2) - - for f_id in adj_faces: - if f_id != (d.get_face()).id: - f = Face(mesh, f_id) - df = f.get_dart() - df1 = df.get_beta(1) - df11 = df1.get_beta(1) - df111 = df11.get_beta(1) - A = df.get_node() - B = df1.get_node() - C = df11.get_node() - D = df111.get_node() - if A==n1 or A==n3: - A=n10 - elif B==n1 or B==n3: - B=n10 - elif C==n1 or C==n3: - C=n10 - elif D==n1 or D==n3: - D=n10 - - if not isValidQuad(A, B, C, D): - geo = False - mesh.del_node(n10) - return topo, geo + n1_analysis = NodeAnalysis(n1) + n3_analysis = NodeAnalysis(n3) - mesh.del_node(n10) - return topo, geo + if n1_analysis.on_boundary(): + topo = False + return topo, geo + if (n3_analysis.degree() +n1_analysis.degree()-2) > 10: + topo = False + return topo, geo -def isCleanupOk(d: Dart) -> (bool, bool): - topo = True - geo = True - if d.get_beta(2) is None: - topo = False - mesh = d.mesh - parallel_darts = mesh.find_parallel_darts(d) - for d in parallel_darts: - d111 = ((d.get_beta(1)).get_beta(1)).get_beta(1) - if d111.get_beta(2) is None: + adjacent_faces_lst = [] + f1 = d2.get_face() + adjacent_faces_lst.append(f1.id) + d12 = d1.get_beta(2) + if d12 is not None: + f2 = d12.get_face() + adjacent_faces_lst.append(f2.id) + d112 = d11.get_beta(2) + if d112 is not None: + f3 = d112.get_face() + adjacent_faces_lst.append(f3.id) + d1112 = d111.get_beta(2) + if d1112 is not None: + f4 = d1112.get_face() + adjacent_faces_lst.append(f4.id) + + # Check that there are no adjacent faces in common + if len(adjacent_faces_lst) != len(set(adjacent_faces_lst)): topo = False return topo, geo - return topo, geo + adj_faces = n3_analysis.adjacent_faces_id() + adj_faces.extend(n1_analysis.adjacent_faces_id()) + + #If the opposite vertex is on the edge, it is not moved + + if n3_analysis.on_boundary(): + n10 = self.mesh.add_node( n3.x(), n3.y()) + else: + n10 = self.mesh.add_node((n1.x() + n3.x()) / 2, (n1.y() + n3.y()) / 2) + + for f_id in adj_faces: + if f_id != (d.get_face()).id: + f = Face(self.mesh, f_id) + df = f.get_dart() + df1 = df.get_beta(1) + df11 = df1.get_beta(1) + df111 = df11.get_beta(1) + A = df.get_node() + B = df1.get_node() + C = df11.get_node() + D = df111.get_node() + if A==n1 or A==n3: + A=n10 + elif B==n1 or B==n3: + B=n10 + elif C==n1 or C==n3: + C=n10 + elif D==n1 or D==n3: + D=n10 + + if not self.isValidQuad(A, B, C, D): + geo = False + self.mesh.del_node(n10) + return topo, geo + + self.mesh.del_node(n10) + return topo, geo -def isTruncated(m: Mesh, darts_list)-> bool: - for d_id in darts_list: - if isValidAction(m, d_id, 4)[0]: - return False - return True -def cross_product(vect_AB, vect_AC): - """ Return the cross product between AB et AC. - 0 means A, B and C are coolinear - > 0 mean A, B and C are "sens des aiguilles d'une montre" - < 0 sens inverse - """ - val = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] - return val - -def signe(a: int): - if a<=0: - return 0 - else: - return 1 - -def isValidQuad(A: Node, B: Node, C: Node, D: Node): - u1 = np.array([B.x() - A.x(), B.y() - A.y()]) # vect(AB) - u2 = np.array([C.x() - B.x(), C.y() - B.y()]) # vect(BC) - u3 = np.array([D.x() - C.x(), D.y() - C.y()]) # vect(CD) - u4 = np.array([A.x() - D.x(), A.y() - D.y()]) # vect(DA) - - # Checking for near-zero vectors (close to (0,0)) - if (np.linalg.norm(u1) < 1e-5 or - np.linalg.norm(u2) < 1e-5 or - np.linalg.norm(u3) < 1e-5 or - np.linalg.norm(u4) < 1e-5): - return False # Quad invalid because one side is almost zero - - cp_A = cross_product(-1*u4, u1) - cp_B = cross_product(-1*u1, u2) - cp_C = cross_product(-1*u2, u3) - cp_D = cross_product(-1*u3, u4) - - zero_count = sum(-1e-5=2: - return False - elif 0<= signe(cp_A)+signe(cp_B)+signe(cp_C)+signe(cp_D) <2 : + def isCleanupOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + mesh = d.mesh + parallel_darts = mesh.find_parallel_darts(d) + for d in parallel_darts: + d111 = ((d.get_beta(1)).get_beta(1)).get_beta(1) + if d111.get_beta(2) is None: + topo = False + return topo, geo + return topo, geo + + + def isTruncated(self, darts_list)-> bool: + for d_id in darts_list: + if self.isValidAction(d_id, 4)[0]: + return False return True - else: - return False + + def isValidQuad(self, A: Node, B: Node, C: Node, D: Node): + u1 = np.array([B.x() - A.x(), B.y() - A.y()]) # vect(AB) + u2 = np.array([C.x() - B.x(), C.y() - B.y()]) # vect(BC) + u3 = np.array([D.x() - C.x(), D.y() - C.y()]) # vect(CD) + u4 = np.array([A.x() - D.x(), A.y() - D.y()]) # vect(DA) + + # Checking for near-zero vectors (close to (0,0)) + if (np.linalg.norm(u1) < 1e-5 or + np.linalg.norm(u2) < 1e-5 or + np.linalg.norm(u3) < 1e-5 or + np.linalg.norm(u4) < 1e-5): + return False # Quad invalid because one side is almost zero + + cp_A = self.cross_product(-1*u4, u1) + cp_B = self.cross_product(-1*u1, u2) + cp_C = self.cross_product(-1*u2, u3) + cp_D = self.cross_product(-1*u3, u4) + + zero_count = sum(-1e-5=2: + return False + elif 0<= self.signe(cp_A)+ self.signe(cp_B)+ self.signe(cp_C)+ self.signe(cp_D) <2 : + return True + else: + return False + + def signe(self, a: int): + if a <= 0: # Before it was 1e-8 + return 0 + else: + return 1 \ No newline at end of file diff --git a/mesh_model/mesh_analysis/trimesh_analysis.py b/mesh_model/mesh_analysis/trimesh_analysis.py index 6729e9a..5354035 100644 --- a/mesh_model/mesh_analysis/trimesh_analysis.py +++ b/mesh_model/mesh_analysis/trimesh_analysis.py @@ -1,321 +1,1005 @@ -from math import sqrt, degrees, radians, cos, sin +import math +from math import radians, cos, sin +from scipy.spatial import ConvexHull, Delaunay +from shapely import affinity + +from shapely.geometry import Polygon, Point, LineString + +import numpy as np +import matplotlib.pyplot as plt + +from mesh_model.mesh_analysis.global_mesh_analysis import GlobalMeshAnalysis, NodeAnalysis from mesh_model.mesh_struct.mesh_elements import Dart, Node, Face from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_analysis.global_mesh_analysis import get_boundary_darts, test_degree, on_boundary, get_angle_by_coord, angle_from_sides, degree, get_boundary_angle - - -def isValidAction(mesh: Mesh, dart_id: int, action: int) -> (bool, bool): - flip = 0 - split = 1 - collapse = 2 - test_all = 3 - one_valid = 4 - d = Dart(mesh, dart_id) - boundary_darts = get_boundary_darts(mesh) - if d in boundary_darts: - return False, True - elif action == flip: - return isFlipOk(d) - elif action == split: - return isSplitOk(d) - elif action == collapse: - return isCollapseOk(d) - elif action == test_all: - topo, geo = isFlipOk(d) - if not (topo and geo): - return False, False - topo, geo = isSplitOk(d) - if not (topo and geo): - return False, False - topo, geo = isCollapseOk(d) - if not (topo and geo): - return False, False - elif topo and geo: - return True, True - elif action == one_valid: - topo_flip, geo_flip = isFlipOk(d) - if (topo_flip and geo_flip): - return True, True - topo_split, geo_split = isSplitOk(d) - if (topo_split and geo_split): - return True, True - topo_collapse, geo_collapse = isCollapseOk(d) - if (topo_collapse and geo_collapse): - return True, True - return False, False - else: - raise ValueError("No valid action") - - -def isFlipOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - #if d is on boundary, flip is not possible - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) +from view.mesh_plotter.mesh_plots import plot_mesh - if not test_degree(A) or not test_degree(B): - topo = False - return topo, geo +FLIP = 0 +SPLIT = 1 +COLLAPSE = 2 +TEST_ALL = 3 +ONE_VALID = 4 - # 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: - topo = False - return topo, geo +class TriMeshAnalysis(GlobalMeshAnalysis): + """ + The base of triangular mesh analysis + """ + def __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + #If initial scores and adjacency have not already been set + if self.mesh.dart_info[0,5] == -99: + self.set_adjacency() + self.set_scores() + self.set_geometric_quality() + + def isValidAction(self, dart_id: int, action: int) -> (bool,bool): + pass - #Check if new triangle will be valid + def set_adjacency(self) -> None: + i = 0 + for n_info in self.mesh.nodes: + if n_info[2] >= 0: + n = Node(self.mesh, i) + na = NodeAnalysis(n) + if na.on_boundary(): + angle = na.get_boundary_angle() + ideal_adj = max(round(angle / 60) + 1, 2) + n.set_ideal_adjacency(ideal_adj) + else: + n.set_ideal_adjacency(6) + i += 1 + + def set_scores(self) -> None: + i = 0 + for n_info in self.mesh.nodes: + if n_info[2] >= 0: + n = Node(self.mesh, i) + na = NodeAnalysis(n) + s = na.score_calculation() + n.set_score(s) + i += 1 + + def set_geometric_quality(self) -> None: + for d_info in self.mesh.active_darts(): + d_id = d_info[0] + d = Dart(self.mesh, d_id) + d.set_quality(self.get_dart_geometric_quality(d)) + + def get_dart_geometric_quality(self, d: Dart, m=None) -> int: + """ + Calculate the geometric quality of the surrounding of a dart and his twin dart. + * quality = -1: boundary dart + * quality = 0: convex quad + * quality = 1: concave quad + * quality = 2: triangular quad + * quality = 3: crossed quad + * quality = 4: half flat concave quad + * quality = 6: flat quad + * quality = 5: one adjacent triangular face is flat + + :param d: dart + :return: geometric quality + """ + if d.get_beta(2) is None: + return -1 # boundary dart + + d2, d1, d11, d21, d211, A, B, C, D = self.mesh.active_triangles(d) + + u1 = np.array([B.x() - A.x(), B.y() - A.y()]) # d vector (unused) + u2 = np.array([C.x() - B.x(), C.y() - B.y()]) # vect(BC) + u3 = np.array([A.x() - C.x(), A.y() - C.y()]) # vect(CA) + u4 = np.array([D.x() - A.x(), D.y() - A.y()]) # vect(AD) + u5 = np.array([B.x() - D.x(), B.y() - D.y()]) # vect(DB) + + # Checking for near-zero vectors (close to (0,0)) + if (np.linalg.norm(u2) < 1e-8 or + np.linalg.norm(u3) < 1e-8 or + np.linalg.norm(u4) < 1e-8 or + np.linalg.norm(u5) < 1e-8): + plot_mesh(self.mesh) + #raise ValueError("near zero vector") # Quad invalid because one side is almost zero + + # Calculate cross product at each node + cp_A = self.cross_product(-1 * u3, u4) + cp_D = self.cross_product(-1 * u4, u5) + cp_B = self.cross_product(-1 * u5, u2) + cp_C = self.cross_product(-1 * u2, u3) + + # Counts how many angles are oriented clockwise. If the angle is clockwise oriented, signe(cp) return 1 + sum_cp = self.signe(cp_A) + self.signe(cp_B) + self.signe(cp_C) + self.signe(cp_D) + + + zero_count = sum(-1e-8 < cp < 1e-8 for cp in [cp_A, cp_B, cp_C, cp_D]) + + if zero_count == 0: # not angles of 180ยฐ or 360ยฐ / no coolinear vectors + if sum_cp == 0: + return 0 #convexe + elif sum_cp == 1: + return 1 # concave + elif sum_cp == 2: + return 3 # crossed + elif zero_count == 1: + if sum_cp == 0: + return 2 # triangular quad + elif sum_cp == 1: + print(cp_A, cp_B, cp_C, cp_D) + return 4 # half_face_flat + elif sum_cp == 2: + return 5 # half face flat + elif zero_count >= 2: + return 6 # full flat face + else: + raise ValueError("Quality configuration doesn't exist") - #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()) + def find_star_vertex(self, n:Node, plot=False)-> (bool, float, float): + # Retrieve all neighboring vertices in order + d = n.get_dart() + d2 = d.get_beta(2) + n_start = d2.get_node() # First neighbour to retrieve - #Triangle CBD - vect_BC = (C.x() - B.x(), C.y() - B.y()) - vect_BD = (D.x() - B.x(), D.y() - B.y()) + adj_nodes = [n_start] + nodes_coord = [[n_start.x(), n_start.y()]] - if not valid_triangle(vect_AC, vect_AD, vect_DC) or not valid_triangle(vect_BC, vect_BD, vect_DC): - geo = False - return topo, geo + d = d2.get_beta(1) + d2 = d.get_beta(2) + n_neighbour = d2.get_node() - return topo, geo + # As long as we haven't returned to the first neighbor, we keep searching. + # This works because the collapse action is restricted to inner darts that are not connected to a boundary node. + # Therefore, we are guaranteed to find the first vertex by following the beta1 and beta2 relations. + + while n_neighbour != n_start: + adj_nodes.append(n_neighbour) + nodes_coord.append([n_neighbour.x(), n_neighbour.y()]) + d = d2.get_beta(1) + d2 = d.get_beta(2) + n_neighbour = d2.get_node() + + nodes_coord = np.array(nodes_coord) + + # Create a Polygon with shapely package + poly = Polygon(nodes_coord) + + # If polygon is convexe + if poly.is_valid and poly.is_simple and poly.convex_hull.equals(poly): + centroid = poly.centroid + return True, centroid.x, centroid.y + + #Else if polygon is concav, we're looking if there is a "star area" + p_before = None + p_first = None + star_poly = poly + for p in poly.exterior.coords[:-1]: + if p_before is not None: + # one side of the polygon + seg = LineString([p, p_before]) + + # we create a big quad box + box = Polygon([(-1e5, 0), (1e5, 0), (1e5, 1e5), (-1e5, 1e5)]) + + # segment angle + dx = seg.coords[1][0] - seg.coords[0][0] + dy = seg.coords[1][1] - seg.coords[0][1] + angle = math.degrees(math.atan2(dy, dx)) + + #moving box + box_rot = affinity.rotate(box, angle, origin=(0, 0)) + origin_pt = Point(seg.coords[0]) + box_final = affinity.translate(box_rot, origin_pt.x, origin_pt.y) + + star_poly = star_poly.intersection(box_final) + if star_poly.is_empty: + plt.figure(figsize=(6, 6)) + # Polygone + x, y = poly.exterior.xy + plt.fill(x, y, alpha=0.3, edgecolor='blue', + label='Polygon formed par by neighbours vertices') + + # Voisins + plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + + plt.legend() + plt.gca().set_aspect('equal') + plt.show() + return False, 0, 0 + elif p_before is None: + p_first = p + p_before = p + + #We do the same for the last segment + seg = LineString([p_first, p_before]) + box = Polygon([(-1e5, 0), (1e5, 0), (1e5, 1e5), (-1e5, 1e5)]) + dx = seg.coords[1][0] - seg.coords[0][0] + dy = seg.coords[1][1] - seg.coords[0][1] + angle = math.degrees(math.atan2(dy, dx)) + box_rot = affinity.rotate(box, angle, origin=(0, 0)) + origin_pt = Point(seg.coords[0]) + box_final = affinity.translate(box_rot, origin_pt.x, origin_pt.y) + + star_poly = star_poly.intersection(box_final) + if star_poly.is_empty: + plt.figure(figsize=(6, 6)) + # Polygone + x, y = poly.exterior.xy + plt.fill(x, y, alpha=0.3, edgecolor='blue', + label='Polygon formed par by neighbours vertices') + + # Voisins + plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + + plt.legend() + plt.gca().set_aspect('equal') + plt.show() + return False, 0, 0 + + if plot: + plt.figure(figsize=(6, 6)) + # Polygone + x, y = poly.exterior.xy + plt.fill(x, y, alpha=0.3, edgecolor='blue', + label='Polygon formed par by neighbours vertices') + + # Voisins + plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + + # Polygone + x_s, y_s = star_poly.exterior.xy + plt.fill(x_s, y_s, alpha=0.3, facecolor='lightcoral', + label='Star area') + + plt.legend() + plt.gca().set_aspect('equal') + plt.show() + + centroid = star_poly.centroid + return True, centroid.x ,centroid.y + + def get_adjacent_faces(self, 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 find_opposite_node(self, 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(self, d: Dart) -> int: + """ + Find 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 signe(self, a: int): + if a <= 1e-8: # We compare to 1e-8 and not 0; otherwise, the triangular configuration may not be detected. + return 0 + else: + return 1 -def isSplitOk(d: Dart) -> (bool, bool): - mesh = d.mesh - topo = True - geo = True - if d.get_beta(2) is None: - topo = False - return topo, geo - else: - _, _, _, _, _, A, B, C, D = mesh.active_triangles(d) +class TriMeshQualityAnalysis(TriMeshAnalysis): + """ + The base of triangular mesh analysis + """ + def __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + + def isValidAction(self, dart_id: int, action: int) -> (bool,bool): + d = Dart(self.mesh, dart_id) + #boundary_darts = self.get_boundary_darts() + + geo = True # The geometric validity is automatically set to True, it is not tested here + + if d.get_beta(2) is None: #if d in boundary_darts: + return False, geo + elif action == FLIP: + return self.isFlipOk(d) + elif action == SPLIT: + return self.isSplitOk(d) + elif action == COLLAPSE: + return self.isCollapseOk(d) + elif action == TEST_ALL: + topo, geo = self.isFlipOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isSplitOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isCollapseOk(d) + if not (topo and geo): + return False, False + elif topo and geo: + return True, True + elif action == ONE_VALID: + topo_flip, geo_flip = self.isFlipOk(d) + if (topo_flip and geo_flip): + return True, True + topo_split, geo_split = self.isSplitOk(d) + if (topo_split and geo_split): + return True, True + topo_collapse, geo_collapse = self.isCollapseOk(d) + if (topo_collapse and geo_collapse): + return True, True + return False, False + else: + raise ValueError("No valid action") - if not test_degree(C) or not test_degree(D): - topo = False - return topo, geo + def isFlipOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True - newNode_x, newNode_y = (A.x() + B.x()) / 2, (A.y() + B.y()) / 2 + #if d is on boundary, flip is not possible + if d.get_beta(2) is None: + topo = False + return topo, geo - #Check if new triangle will be valid + d2, d1, d11, d21, d211, A, B, C, D = self.mesh.active_triangles(d) - # 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): - geo = False - return topo, geo + nA_analysis = NodeAnalysis(A) + nB_analysis = NodeAnalysis(B) - # 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): - geo = False - return topo, geo + if not nA_analysis.test_degree() or not nB_analysis.test_degree(): + topo = 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): - geo = False - return topo, geo + #Edge reversal not allowed + if d.get_quality() != 0: + geo = 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): - geo = False return topo, geo - return topo, geo + def isSplitOk(self, d: Dart) -> (bool,bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + _, _, _, _, _, A, B, C, D = self.mesh.active_triangles(d) + nC_analysis = NodeAnalysis(C) + nD_analysis = NodeAnalysis(D) + if not nC_analysis.test_degree() or not nD_analysis.test_degree(): + topo = False -def isCollapseOk(d: Dart) -> (bool, bool): + if d.get_quality() not in [0,1,2]: # if the face around is crossed or flat, or half flat + geo = False - mesh = d.mesh - topo = True - geo = True - if d.get_beta(2) is None: - topo = False return topo, geo - else: - _, d1, d11, d21, d211, n1, n2, _, _ = mesh.active_triangles(d) - d112 = d11.get_beta(2) - d12 = d1.get_beta(2) + def isCollapseOk(self, d: Dart) -> (bool,bool): - d212 = d21.get_beta(2) - d2112 = d211.get_beta(2) + topo = True + geo = True - newNode_x, newNode_y = (n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2 + if d.get_beta(2) is None: + topo = False + return topo, geo - if d112 is None or d12 is None or d2112 is None or d212 is None: - topo = False - return topo, geo - elif on_boundary(n1) or on_boundary(n2): - topo = False - return topo, geo - elif not test_degree(n1): - topo = False + _, d1, d11, d21, d211, n1, n2, _, _ = self.mesh.active_triangles(d) + + d112 = d11.get_beta(2) + d12 = d1.get_beta(2) + + d212 = d21.get_beta(2) + d2112 = d211.get_beta(2) + + n1_analysis = NodeAnalysis(n1) + n2_analysis = NodeAnalysis(n2) + + if n1_analysis.on_boundary() or n2_analysis.on_boundary(): + topo = False + elif not n1_analysis.test_degree(): + topo = False + elif d112 is None or d12 is None or d2112 is None or d212 is None: + topo = False + + if d.get_quality() not in [0,1,2]: # if the face around is crossed or flat, or half flat + geo = False + elif d1.get_quality() == 1 or d11.get_quality() == 1 or d21.get_quality() == 1 or d211.get_quality() == 1: + geo = False + # elif d.get_quality() == 1: + # n = d.get_node() + # na = NodeAnalysis(n) + # adj_darts = na.adjacent_darts() + # for d in adj_darts: + # if d.get_quality() == 1: + # geo = False return topo, geo - 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 topo, 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 topo, 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): - geo = False - return topo, geo - else: - return topo, geo - - -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 isTruncated(self, darts_list) -> bool: + for d_id in darts_list: + if self.isValidAction( d_id, 4)[0]: + return False + return True -def valid_faces_changes(faces: list[Face], n_id: int, new_x: float, new_y: float) -> bool: + +class TriMeshOldAnalysis(TriMeshAnalysis): """ - 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 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 + Triangular mesh analysis with geometrical criteria """ - for f in faces: - _, _, _, A, B, C = f.get_surrounding_triangle() - 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 __init__(self, mesh: Mesh): + super().__init__(mesh=mesh) + + def isValidAction(self, dart_id: int, action: int) -> (bool, bool): + d = Dart(self.mesh, dart_id) + boundary_darts = self.get_boundary_darts() + if d in boundary_darts: + return False, True + elif action == FLIP: + return self.isFlipOk(d) + elif action == SPLIT: + return self.isSplitOk(d) + elif action == COLLAPSE: + return self.isCollapseOk(d) + elif action == TEST_ALL: + topo, geo = self.isFlipOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isSplitOk(d) + if not (topo and geo): + return False, False + topo, geo = self.isCollapseOk(d) + if not (topo and geo): + return False, False + elif topo and geo: + return True, True + elif action == ONE_VALID: + topo_flip, geo_flip = self.isFlipOk(d) + if (topo_flip and geo_flip): + return True, True + topo_split, geo_split = self.isSplitOk(d) + if (topo_split and geo_split): + return True, True + topo_collapse, geo_collapse = self.isCollapseOk(d) + if (topo_collapse and geo_collapse): + return True, True + return False, False + else: + raise ValueError("No valid action") + + def isFlipOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + # if d is on boundary, flip is not possible + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, _, _, _, _, A, B, C, D = self.mesh.active_triangles(d) + + nA_analysis = NodeAnalysis(A) + nB_analysis = NodeAnalysis(B) + + if not nA_analysis.test_degree() or not nB_analysis.test_degree(): + topo = False + return topo, geo + + # Check angle at d limits to avoid edge reversal + angle_B = self.get_angle_by_coord(A.x(), A.y(), B.x(), B.y(), C.x(), C.y()) + self.get_angle_by_coord(A.x(), + A.y(), + B.x(), + B.y(), + D.x(), + D.y()) + angle_A = self.get_angle_by_coord(B.x(), B.y(), A.x(), A.y(), C.x(), C.y()) + self.get_angle_by_coord(B.x(), + B.y(), + A.x(), + A.y(), + D.x(), + D.y()) + if angle_B >= 180 or angle_A >= 180: + topo = False + return topo, geo + + # 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 self.valid_triangle(vect_AC, vect_AD, vect_DC) or not self.valid_triangle(vect_BC, vect_BD, vect_DC): + geo = False + return topo, geo -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 + return topo, geo - L_max = max(dist_AB, dist_AC, dist_BC) + def isSplitOk(self, d: Dart) -> (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, _, _, _, _, A, B, C, D = self.mesh.active_triangles(d) + + nC_analysis = NodeAnalysis(C) + nD_analysis = NodeAnalysis(D) + + if not nC_analysis.test_degree() or not nD_analysis.test_degree(): + topo = False + return topo, geo + + 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 self.valid_triangle(vect_AE, vect_AC, vect_EC): + geo = False + return topo, geo + + # Triangle ADE + vect_AD = (D.x() - A.x(), D.y() - A.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not self.valid_triangle(vect_AD, vect_AE, vect_ED): + geo = False + return topo, geo + + # 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 self.valid_triangle(vect_BC, vect_BE, vect_EC): + geo = False + return topo, geo + + # Triangle BDE + vect_BD = (D.x() - B.x(), D.y() - B.y()) + vect_ED = (D.x() - newNode_x, D.y() - newNode_y) + if not self.valid_triangle(vect_BD, vect_BE, vect_ED): + geo = False + return topo, geo - if target_mesh_size/2*sqrt(2) < L_max and L_max < target_mesh_size*3*sqrt(2): # 0.35 (bool, bool): + topo = True + geo = True + if d.get_beta(2) is None: + topo = False + return topo, geo + else: + _, d1, d11, d21, d211, n1, n2, _, _ = self.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 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() + newNode_x, newNode_y = (n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2 - vect_AB = (B.x() - A.x(), B.y() - A.y()) + n1_analysis = NodeAnalysis(n1) + n2_analysis = NodeAnalysis(n2) - 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) + if d112 is None or d12 is None or d2112 is None or d212 is None: + topo = False + return topo, geo + elif n1_analysis.on_boundary() or n2_analysis.on_boundary(): + topo = False + return topo, geo + elif not n1_analysis.test_degree(): + topo = False + return topo, geo + else: + # search for all adjacent faces to n1 and n2 + if d12 is None and d2112 is None: + adj_faces_n1 = self.get_adjacent_faces(n1, d212, d112) + return topo, self.valid_faces_changes(adj_faces_n1, n1.id, newNode_x, newNode_y) + elif d212 is None and d112 is None: + adj_faces_n2 = self.get_adjacent_faces(n2, d12, d2112) + return topo, self.valid_faces_changes(adj_faces_n2, n2.id, newNode_x, newNode_y) + else: + adj_faces_n1 = self.get_adjacent_faces(n1, d212, d112) + adj_faces_n2 = self.get_adjacent_faces(n2, d12, d2112) + if not self.valid_faces_changes(adj_faces_n1, n1.id, newNode_x, + newNode_y) or not self.valid_faces_changes(adj_faces_n2, n2.id, + newNode_x, newNode_y): + geo = False + return topo, geo + else: + return topo, geo + + def valid_faces_changes(self, 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 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_triangle() + 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 - x_C = A.x() + x_AC - y_C = A.y() + y_AC + cross_product = vect_AB[0] * vect_AC[1] - vect_AB[1] * vect_AC[0] - return x_C, y_C + if cross_product <= 0: + return False # One face is not correctly oriented or is flat + elif not self.valid_triangle(vect_AB, vect_AC, vect_BC): + return False + return True + def valid_triangle(self, vect_AB, vect_AC, vect_BC) -> bool: + dist_AB = math.sqrt(vect_AB[0] ** 2 + vect_AB[1] ** 2) + dist_AC = math.sqrt(vect_AC[0] ** 2 + vect_AC[1] ** 2) + dist_BC = math.sqrt(vect_BC[0] ** 2 + vect_BC[1] ** 2) + target_mesh_size = 1 -def find_template_opposite_node(d: Dart) -> int: - """ - Find the vertex opposite in the adjacent triangle - :param d: a dart - :return: the node found - """ + L_max = max(dist_AB, dist_AC, dist_BC) - 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 + if target_mesh_size / 2 * math.sqrt(2) < L_max and L_max < target_mesh_size * 3 * math.sqrt(2): # 0.35 bool: - for d_id in darts_list: - if isValidAction(m, d_id, 4)[0]: + # 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 \ No newline at end of file + return True + + def isTruncated(self, darts_list) -> bool: + for d_id in darts_list: + if self.isValidAction(d_id, 4)[0]: # if on action is valid, it means it's valid topologically and geometrically, so no need to verify the two + return False + return True + + + # def is_star_vertex(self, n1:Node, new_coordinates, plot=False): + # #plot_mesh(self.mesh) + # + # # Retrieve all neighboring vertices in order + # d = n1.get_dart() + # d2 = d.get_beta(2) + # n_start = d2.get_node() #First neighbour to retrieve + # + # adj_nodes = [n_start] + # nodes_coord = [[n_start.x(), n_start.y()]] + # + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # + # # As long as we haven't returned to the first neighbor, we keep searching. + # # This works because the collapse action is restricted to inner darts that are not connected to a boundary node. + # # Therefore, we are guaranteed to find the first vertex by following the beta1 and beta2 relations. + # + # while n != n_start: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # + # nodes_coord = np.array(nodes_coord) + # + # # Create a Polygon with shapely package + # poly = Polygon(nodes_coord) + # # Create the point for wich we want to check star property + # point_v = Point(new_coordinates) + # + # if plot : + # plt.figure(figsize=(6, 6)) + # # Polygone + # x, y = poly.exterior.xy + # plt.fill(x, y, alpha=0.3, edgecolor='red', facecolor='lightcoral', + # label='Polygon formed par by neighbours vertices') + # + # # Voisins + # plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + # + # # Sommet testรฉ + # plt.scatter(new_coordinates[0], new_coordinates[1], color='green', s=100, zorder=5, label='Vertex to test') + # + # plt.legend() + # plt.gca().set_aspect('equal') + # plt.show() + # + # # If polygon is convexe + # if poly.is_valid and poly.is_simple and poly.convex_hull.equals(poly): + # return True + # p_before = None + # # Si concave : vรฉrifier visibilitรฉ + # for p in poly.exterior.coords[:-1]: + # full_seg = LineString([new_coordinates, p]) + # new_seg_end = full_seg.interpolate(full_seg.length - 1e-5) + # seg = LineString([new_coordinates, new_seg_end]) + # if not poly.contains_properly(seg): + # return False + # elif seg.crosses(poly.boundary): + # return False + # elif seg.touches(poly.boundary): + # return False + # elif p_before is not None: # test coolinearity of two vectors + # v1 = new_coordinates[0]-p[0], new_coordinates[1]-p[1] + # v2 = new_coordinates[0]-p_before[0], new_coordinates[1]-p_before[1] + # + # det = v1[0] * v2[1] - v1[1] * v2[0] + # if -1e-5 < det < 1e-5: + # return False + # p_before = p + # return Truedef is_star_vertex2(self, n1:Node, n2:Node, v): + #plot_mesh(self.mesh) + # + # adj_nodes = [] + # nodes_coord = [] + # d = n1.get_dart() + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n2: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # d = d.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n1: + # if n not in adj_nodes: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # d = d.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n2: + # if n not in adj_nodes: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # + # nodes_coord = np.array(nodes_coord) + # + # # Crรฉer le polygone + # poly = Polygon(nodes_coord) + # point_v = Point(v) + # + # # Vรฉrifier si polygone est convexe + # if poly.is_valid and poly.is_simple and poly.convex_hull.equals(poly): + # return True + # + # # Si concave : vรฉrifier visibilitรฉ + # for p in poly.exterior.coords[:-1]: + # seg = LineString([v, p]) + # if not poly.contains(seg): + # # plt.figure(figsize=(6, 6)) + # # # Polygone + # # x, y = poly.exterior.xy + # # plt.fill(x, y, alpha=0.3, edgecolor='red', facecolor='lightcoral', + # # label='Polygon formed par by neighbours vertices') + # # + # # # Voisins + # # plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + # # + # # # Sommet testรฉ + # # plt.scatter(v[0], v[1], color='green', s=100, zorder=5, label='Vertex to test') + # # + # # plt.legend() + # # plt.gca().set_aspect('equal') + # # plt.show() + # return False + # return True + + # def is_star_vertex2(self, n1:Node, n2:Node, v): + # #plot_mesh(self.mesh) + # + # adj_nodes = [] + # nodes_coord = [] + # d = n1.get_dart() + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n2: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # d = d.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n1: + # if n not in adj_nodes: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # d = d.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # while n != n2: + # if n not in adj_nodes: + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # d = d2.get_beta(1) + # d2 = d.get_beta(2) + # n = d2.get_node() + # + # nodes_coord = np.array(nodes_coord) + # + # # Crรฉer le polygone + # poly = Polygon(nodes_coord) + # point_v = Point(v) + # + # # Vรฉrifier si polygone est convexe + # if poly.is_valid and poly.is_simple and poly.convex_hull.equals(poly): + # return True + # + # # Si concave : vรฉrifier visibilitรฉ + # for p in poly.exterior.coords[:-1]: + # seg = LineString([v, p]) + # if not poly.contains(seg): + # # plt.figure(figsize=(6, 6)) + # # # Polygone + # # x, y = poly.exterior.xy + # # plt.fill(x, y, alpha=0.3, edgecolor='red', facecolor='lightcoral', + # # label='Polygon formed par by neighbours vertices') + # # + # # # Voisins + # # plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Neighbours') + # # + # # # Sommet testรฉ + # # plt.scatter(v[0], v[1], color='green', s=100, zorder=5, label='Vertex to test') + # # + # # plt.legend() + # # plt.gca().set_aspect('equal') + # # plt.show() + # return False + # return True + + # def find_star_vertex2(self, n1:Node, n2:Node) -> (float, float): + # adj_nodes = [] + # nodes_coord = [] + # for d_info in self.mesh.active_darts(): + # if d_info[3] == n1.id or d_info[3] == n2.id: + # d2 = Dart(self.mesh, d_info[2]) + # if d2 is not None: + # n = d2.get_node() + # adj_nodes.append(n) + # nodes_coord.append([n.x(), n.y()]) + # else: + # raise ValueError("Collapse action may not be done near boundary") + # nodes_coord = np.array(nodes_coord) + # + # # Ordonner les voisins autour de v + # vectors = nodes_coord - v + # angles = np.arctan2(vectors[:, 1], vectors[:, 0]) + # order = np.argsort(angles) + # neighbors_ordered = nodes_coord[order] + # + # hull = ConvexHull(nodes_coord) + # delaunay = Delaunay(nodes_coord) + # plt.plot(nodes_coord[:, 0], nodes_coord[:, 1], 'o') + # + # for simplex in hull.simplices: + # plt.plot(nodes_coord[simplex, 0], nodes_coord[simplex, 1], 'k-') + # plt.plot(nodes_coord[hull.vertices, 0], nodes_coord[hull.vertices, 1], 'r--', lw=2) + # plt.plot(nodes_coord[hull.vertices[0], 0], nodes_coord[hull.vertices[0], 1], 'ro') + # plt.show() + # + # _ = scipy.spatial.delaunay_plot_2d(delaunay) + # plt.show() + # + # mid = np.array([(n1.x() + n2.x()) / 2, (n1.y() + n2.y()) / 2]) + # is_star_vertex = delaunay.find_simplex(mid) >=0 + # + # # Calcul des angles des voisins autour de v + # vectors = nodes_coord - mid + # angles = np.arctan2(vectors[:, 1], vectors[:, 0]) + # order = np.argsort(angles) + # neighbors_ordered = nodes_coord[order] + # + # # Construire le polygone + # poly = Polygon(neighbors_ordered) + # + # # Vรฉrifier si v est ร  l'intรฉrieur ou sur la frontiรจre + # point_v = Point(mid) + # is_star = poly.contains(point_v) or poly.touches(point_v) + # + # plt.figure(figsize=(6, 6)) + # # Polygone + # x, y = poly.exterior.xy + # plt.fill(x, y, alpha=0.3, edgecolor='red', facecolor='lightcoral', label='Polygone formรฉ par les voisins') + # + # # Voisins + # plt.scatter(nodes_coord[:, 0], nodes_coord[:, 1], color='blue', zorder=5, label='Voisins') + # + # # Sommet testรฉ + # plt.scatter(mid[0], mid[1], color='green', s=100, zorder=5, label='Sommet testรฉ') + # + # plt.legend() + # plt.gca().set_aspect('equal') + # plt.title(f"Le sommet est-il รฉtoilรฉ ? {is_star}") + # plt.show() + # + # if is_star: + # return mid + # elif poly.contains(Point(n1.x(), n1.y())) or poly.touches(Point(n1.x(), n1.y())): + # return n1.x(), n1.y() + # elif poly.contains(Point(n2.x(), n2.y())) or poly.touches(Point(n2.x(), n2.y())): + # return n2.x(), n2.y() + # else: + # plot_mesh(self.mesh) + # raise ValueError("No star vertex was found") diff --git a/mesh_model/mesh_struct/mesh.py b/mesh_model/mesh_struct/mesh.py index 01b8bb4..86da604 100644 --- a/mesh_model/mesh_struct/mesh.py +++ b/mesh_model/mesh_struct/mesh.py @@ -13,13 +13,14 @@ class Mesh: def __init__(self, nodes=[], faces=[]): """ - Vertices are stored in a numpy array containing coordinates (x,y, dart id) + Vertices are stored in a numpy array containing coordinates (x,y, dart id, ideal adjacency, vertex score) Faces are stored in a numpy array of simple (dart ids) - Darts are stored in a numpy array, where each dart is a 5-tuple (dart id, beta_1, beta_2, vertex_id, face_id) + Darts are stored in a numpy array, where each dart is a 5-tuple (dart id, beta_1, beta_2, vertex_id, face_id, geo_quality) + Ideal adjacency, vertex scores and geometric quality are not defined here. You must use Mesh analysis class to define them """ - self.nodes = numpy.empty((0, 3)) + self.nodes = numpy.empty((0, 5), dtype=float) self.faces = numpy.empty(0, dtype=int) - self.dart_info = numpy.empty((0, 5), dtype=int) + self.dart_info = numpy.empty((0, 6), dtype=int) self.first_free_dart = 0 self.first_free_node = 0 self.first_free_face = 0 @@ -61,19 +62,20 @@ def nb_faces(self) -> int: def add_node(self, x: float, y: float) -> Node: """ Add a vertex in the mesh, this node is not connected to a dart here + The ideal adjacency and vertex score are not defined here :param x: X coordinate :param y: Y coordinate :return: the created node """ if len(self.nodes) <= self.first_free_node: - self.nodes = numpy.append(self.nodes, [[x, y, -1]], axis=0) + self.nodes = numpy.append(self.nodes, [[x, y, -1, -1, -99]], 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] + self.nodes[n_id] = [x, y, -1, -1, -99] else: print(n_id) print(type(n_id)) @@ -272,17 +274,18 @@ def set_face_beta2(self, f: Face, darts: list[Dart]) -> None: df_current = df_current.get_beta(1) end = (df_current.id == f.get_dart().id) - def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1) -> Dart: + def add_dart(self, a1: int = -1, a2: int = -1, v: int = -1, f: int = -1, q: int = -99) -> Dart: """ This function add a dart in the mesh. It must not be used directly :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 + :param q: geometric quality around the dart :return: the created 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.dart_info = numpy.append(self.dart_info, [[len(self.dart_info), a1, a2, v, f, q]], axis=0) self.first_free_dart += 1 return Dart(self, len(self.dart_info) - 1) elif len(self.dart_info) > self.first_free_dart: @@ -308,7 +311,16 @@ def del_dart(self, d: Dart): """ self.dart_info[d.id][0] = -self.first_free_dart - 1 self.first_free_dart = d.id + d.active = False + def is_dart_active(self, d: Dart) -> bool: + """Check if the dart has been deleted""" + if d is None: + return False + elif self.dart_info[d.id,0] < 0: + return False + elif self.dart_info[d.id,0] >=0 : + return True def set_beta2(self, dart: Dart) -> None: """ @@ -405,6 +417,8 @@ def find_parallel_darts(self, d: Dart) -> list[Dart]: return parallel_darts + + def inverseQuad(A: Node, B: Node, C: Node, D: Node): u1 = numpy.array([B.x() - A.x(), B.y() - A.y()]) # vect(AB) u2 = numpy.array([C.x() - B.x(), C.y() - B.y()]) # vect(BC) diff --git a/mesh_model/mesh_struct/mesh_elements.py b/mesh_model/mesh_struct/mesh_elements.py index ac68310..a1b12e2 100644 --- a/mesh_model/mesh_struct/mesh_elements.py +++ b/mesh_model/mesh_struct/mesh_elements.py @@ -44,6 +44,9 @@ def get_beta(self, i: int) -> Dart: raise ValueError("Wrong alpha dimension") if self.mesh.dart_info[self.id, i] == -1: return None + d2_id = self.mesh.dart_info[self.id, i] + if self.mesh.dart_info[d2_id, 0] <0 : + raise ValueError("Dart deleted") return Dart(self.mesh, self.mesh.dart_info[self.id, i]) @@ -96,6 +99,36 @@ def set_face(self, face: Face) -> None: """ self.mesh.dart_info[self.id, 4] = face.id + def get_quality(self) -> int: + """ + Get the geometric quality around a given dart. + + :return: the geometric quality around the dart. + :raises ValueError: if there is no quality dimension + """ + dart_quality = self.mesh.dart_info[self.id, 5] + if dart_quality == -99: + raise ValueError("No quality dimension") + return dart_quality + + def set_quality(self, quality: int) -> None: + """ + Set the geometric quality around a given dart. Automatically set the same quality for the twin dart + The quality is a parameter used to determine whether applying an operation to the dart would flip a face. + + * For triangular meshes: + The dart's surrounding quality is determined by analyzing the quadrilateral formed by the two adjacent triangles. + The configuration is classified as convex, crossed, or concave. + + * For quadrilateral meshes: + The dart's surrounding quality is determined based on whether the associated node forms a "star-shaped" (รฉtoilรฉ) configuration. + :param quality: calculated quality + """ + + d2_id = self.mesh.dart_info[self.id, 2] + self.mesh.dart_info[self.id, 5] = quality + if d2_id >=0: # inner dart + self.mesh.dart_info[d2_id, 5] = quality class Node: _mesh_type: type = None @@ -175,6 +208,42 @@ def set_xy(self, x: float, y: float) -> None: self.set_x(x) self.set_y(y) + def set_ideal_adjacency(self, i: int) -> None: + """ + Set the ideal adjacency of this node. + :param i: calculated ideal adjacency + """ + self.mesh.nodes[self.id,3] = i + + def get_ideal_adjacency(self) -> int: + """ + Get the ideal adjacency of this node. + :return: ideal adjacency + :raises ValueError: if there is no ideal adjacency + """ + ideal_adjacency = self.mesh.nodes[self.id,3] + if ideal_adjacency == -1: + raise ValueError("No ideal adjacency") + return ideal_adjacency + + def set_score(self, s: int) -> None: + """ + Set the score of a node. + :param s: calculated score + """ + self.mesh.nodes[self.id,4] = s + + def get_score(self) -> int: + """ + Get the score of this node. + :return: score + :raises ValueError: if there is no score defined + """ + score = self.mesh.nodes[self.id,4] + if score == -99: + raise ValueError("No score") + return score + class Face: _mesh_type: type = None diff --git a/mesh_model/random_quadmesh.py b/mesh_model/random_quadmesh.py index 66a39cf..12fbbb0 100644 --- a/mesh_model/random_quadmesh.py +++ b/mesh_model/random_quadmesh.py @@ -4,7 +4,7 @@ from mesh_model.mesh_struct.mesh_elements import Dart from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_analysis.quadmesh_analysis import isValidAction +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis from environment.actions.quadrangular_actions import flip_edge_cntcw_ids, split_edge_ids, collapse_edge_ids from mesh_model.reader import read_gmsh @@ -32,6 +32,7 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: nb_action_max = int(num_nodes) nb_action = 0 active_darts_list = mesh.active_darts() + m_analysis = QuadMeshOldAnalysis(mesh) i = 0 while i < nb_action_max: action_type = np.random.randint(0, 3) @@ -41,17 +42,17 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: i1 = dart.get_node() i2 = (dart.get_beta(1)).get_node() #plot_mesh(mesh) - if action_type == 0 and isValidAction(mesh, d_id, action_type)[0]: - flip_edge_cntcw_ids(mesh, i1.id, i2.id) + if action_type == 0 and m_analysis.isValidAction(d_id, action_type)[0]: + flip_edge_cntcw_ids(m_analysis, i1.id, i2.id) nb_action += 1 - elif action_type == 1: # and isValidAction(mesh, d_id, action_type)[0] - split_edge_ids(mesh, i1.id, i2.id) + elif action_type == 1 and m_analysis.isValidAction(d_id, action_type)[0]: + split_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 - elif action_type == 2 and isValidAction(mesh, d_id, action_type)[0]: - collapse_edge_ids(mesh, i1.id, i2.id) + elif action_type == 2 and m_analysis.isValidAction(d_id, action_type)[0]: + collapse_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 - elif action_type == 3 and isValidAction(mesh, d_id, action_type)[0]: - collapse_edge_ids(mesh, i1.id, i2.id) + elif action_type == 3 and m_analysis.isValidAction(d_id, action_type)[0]: + collapse_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 i += 1 active_darts_list = mesh.active_darts() diff --git a/mesh_model/random_trimesh.py b/mesh_model/random_trimesh.py index db17a34..86a3834 100644 --- a/mesh_model/random_trimesh.py +++ b/mesh_model/random_trimesh.py @@ -1,10 +1,10 @@ from __future__ import annotations import numpy as np +from mesh_model.mesh_analysis.global_mesh_analysis import NodeAnalysis from mesh_model.mesh_struct.mesh_elements import Dart, Node from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_analysis.trimesh_analysis import find_opposite_node, isValidAction -from mesh_model.mesh_analysis.global_mesh_analysis import node_in_mesh +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis from environment.actions.triangular_actions import flip_edge_ids, split_edge_ids, collapse_edge_ids @@ -18,6 +18,7 @@ def regular_mesh(num_nodes_max: int) -> Mesh: nodes = [[0.0, 0.0], [1.0, 0.0], [0.5, 0.87]] faces = [[0, 1, 2]] mesh = Mesh(nodes, faces) + m_analysis = TriMeshQualityAnalysis(mesh) num_nodes = 3 dart_id = 0 @@ -28,10 +29,10 @@ def regular_mesh(num_nodes_max: int) -> Mesh: A = d.get_node() d1 = d.get_beta(1) B = d1.get_node() - x_C, y_C = find_opposite_node(d) + x_C, y_C = m_analysis.find_opposite_node(d) # Search if the node C already exist in the actual mesh - found, n_id = node_in_mesh(mesh, x_C, y_C) + found, n_id = m_analysis.node_in_mesh( x_C, y_C) if found and d.get_beta(2) is None: C = Node(mesh, n_id) @@ -46,6 +47,9 @@ def regular_mesh(num_nodes_max: int) -> Mesh: dart_id += 1 mesh.set_twin_pointers() + m_analysis.set_adjacency() + m_analysis.set_scores() + m_analysis.set_geometric_quality() return mesh @@ -80,11 +84,12 @@ def mesh_shuffle_flip(mesh: Mesh) -> Mesh: """ nb_flip = len(mesh.dart_info) nb_nodes = len(mesh.nodes) + m_analysis = TriMeshQualityAnalysis(mesh) for i in range(nb_flip): i1 = np.random.randint(nb_nodes) i2 = np.random.randint(nb_nodes) if i1 != i2: - flip_edge_ids(mesh, i1, i2) + flip_edge_ids(m_analysis, i1, i2) return mesh def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: @@ -94,25 +99,26 @@ def mesh_shuffle(mesh: Mesh, num_nodes) -> Mesh: :param num_nodes: number nodes of the mesh :return: a mesh with randomly flipped darts. """ - nb_action_max = int(num_nodes) + nb_action_max = 3 nb_action = 0 active_darts_list = mesh.active_darts() + m_analysis = TriMeshQualityAnalysis(mesh) i = 0 - while i < nb_action_max: + while nb_action < nb_action_max: 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)[0]: - flip_edge_ids(mesh, i1.id, i2.id) + if action_type == 0 and m_analysis.isValidAction(d_id, action_type)[0]: + flip_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 - elif action_type == 1 and isValidAction(mesh, d_id, action_type)[0]: - split_edge_ids(mesh, i1.id, i2.id) + elif action_type == 1 and m_analysis.isValidAction(d_id, action_type)[0]: + split_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 - elif action_type == 2 and isValidAction(mesh, d_id, action_type)[0]: - collapse_edge_ids(mesh, i1.id, i2.id) + elif action_type == 2 and m_analysis.isValidAction(d_id, action_type)[0]: + collapse_edge_ids(m_analysis, i1.id, i2.id) nb_action += 1 i += 1 active_darts_list = mesh.active_darts() diff --git a/model_RL/PPO_model_pers.py b/model_RL/PPO_model_pers.py index f2b860b..f390cb9 100644 --- a/model_RL/PPO_model_pers.py +++ b/model_RL/PPO_model_pers.py @@ -1,4 +1,3 @@ -from mesh_model.mesh_analysis.global_mesh_analysis import global_score import copy import random from tqdm import tqdm @@ -7,7 +6,6 @@ import torch.nn as nn from torch.optim import Adam from torch.distributions import Categorical -from mesh_model.mesh_analysis.quadmesh_analysis import isValidAction class NaNExceptionActor(Exception): @@ -19,16 +17,17 @@ class NaNExceptionCritic(Exception): class Actor(nn.Module): - def __init__(self, env, input_dim, output_dim, lr=0.0001, eps=0): + def __init__(self, env, input_dim, n_actions, n_darts_observed, lr=0.0001, eps=0): super(Actor, self).__init__() self.fc1 = nn.Linear(input_dim, 64) self.fc2 = nn.Linear(64, 64) - self.fc3 = nn.Linear(64, output_dim) + self.fc3 = nn.Linear(64, n_actions*n_darts_observed) self.softmax = nn.Softmax(dim=-1) self.gamma = 0.9 self.optimizer = Adam(self.parameters(), lr=lr, weight_decay=0.01) self.env = env self.eps = eps + self.n_actions = n_actions def reset(self, env=None): self.fc1.reset_parameters() @@ -37,6 +36,7 @@ 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, observation, info): + ma = info["mesh_analysis"] if np.random.rand() < self.eps: action = self.env.sample() # random choice of an action dart_id = self.env.darts_selected[action[1]] @@ -44,7 +44,7 @@ def select_action(self, observation, info): total_actions_possible = np.prod(self.env.action_space.nvec) prob = 1/total_actions_possible i = 0 - while not isValidAction(self.env.mesh, dart_id, action_type): + while not ma.isValidAction(dart_id, action_type): if i > 15: return None, None action = self.env.sample() @@ -58,11 +58,11 @@ def select_action(self, observation, info): action = dist.sample() action = action.tolist() prob = pmf[action] - action_dart = int(action/4) - action_type = action % 4 + action_dart = int(action/self.n_actions) + action_type = action % self.n_actions dart_id = info["darts_selected"][action_dart] i = 0 - while not isValidAction(info["mesh"], dart_id, action_type): + while not ma.isValidAction(dart_id, action_type): if i > 15: return None, None pmf = self.forward(obs) @@ -70,8 +70,8 @@ def select_action(self, observation, info): action = dist.sample() action = action.tolist() prob = pmf[action] - action_dart = int(action/4) - action_type = action % 4 + action_dart = int(action/self.n_actions) + action_type = action % self.n_actions dart_id = info["darts_selected"][action_dart] i += 1 action_list = [action, dart_id, action_type] @@ -137,10 +137,11 @@ def learn(self, critic_loss): class PPO: - def __init__(self, env, obs_size, max_steps, lr, gamma, nb_iterations, nb_episodes_per_iteration, nb_epochs, batch_size): + def __init__(self, env, obs_size, n_actions, n_darts_observed, max_steps, lr, gamma, nb_iterations, nb_episodes_per_iteration, nb_epochs, batch_size): self.env = env self.max_steps = max_steps - self.actor = Actor(self.env, obs_size, 4*10, lr=lr) + self.n_actions =n_actions + self.actor = Actor(self.env, obs_size, n_actions, n_darts_observed, lr=lr) self.critic = Critic(obs_size, lr=lr) self.lr = lr self.gamma = gamma @@ -166,7 +167,7 @@ def train(self, dataset): critic_loss = [] actor_loss = [] self.critic.optimizer.zero_grad() - for _, (s, o, a, r, G, old_prob, next_o, done) in enumerate(batch, 1): + for _, (ma, o, a, r, G, old_prob, next_o, done) in enumerate(batch, 1): o = torch.tensor(o.flatten(), dtype=torch.float32) next_o = torch.tensor(next_o.flatten(), dtype=torch.float32) value = self.critic(o) @@ -174,7 +175,7 @@ def train(self, dataset): log_prob = torch.log(pmf[a[0]]) next_value = torch.tensor(0.0, dtype=torch.float32) if done else self.critic(next_o) delta = r + 0.9 * next_value - value - _, st, ideal_s, _ = global_score(s) # Comparaison ร  l'รฉtat s et pas s+1 ? + _, st, ideal_s, _ = ma.global_score() # Comparaison ร  l'รฉtat s et pas s+1 ? if st == ideal_s: continue advantage = 1 if done else G / (st - ideal_s) @@ -226,13 +227,13 @@ def learn(self, writer): done = False step = 0 while step < self.max_steps: - state = copy.deepcopy(info["mesh"]) + ma = copy.deepcopy(info["mesh_analysis"]) obs = next_obs action, prob = self.actor.select_action(obs, info) if action is None: wins.append(0) break - gym_action = [action[2],int(action[0]/4)] + gym_action = [action[2],int(action[0]/self.n_actions)] next_obs, reward, terminated, truncated, info = self.env.step(gym_action) ep_reward += reward ep_mesh_reward += info["mesh_reward"] @@ -241,13 +242,13 @@ def learn(self, writer): if terminated: if truncated: wins.append(0) - trajectory.append((state, obs, action, reward, G, prob, next_obs, done)) + trajectory.append((ma, obs, action, reward, G, prob, next_obs, done)) else: wins.append(1) done = True - trajectory.append((state, obs, action, reward, G, prob, next_obs, done)) + trajectory.append((ma, obs, action, reward, G, prob, next_obs, done)) break - trajectory.append((state, obs, action, reward, G, prob, next_obs, done)) + trajectory.append((ma, obs, action, reward, G, prob, next_obs, done)) step += 1 if len(trajectory) != 0: rewards.append(ep_reward) @@ -275,4 +276,4 @@ def learn(self, writer): print("NaN Exception on Critic Network") return None, None, None, None - return self.actor, rewards, wins, len_ep, info["observation_registry"] + return self.actor, rewards, wins, len_ep, None diff --git a/model_RL/evaluate_model.py b/model_RL/evaluate_model.py index 1f002ed..f6984be 100644 --- a/model_RL/evaluate_model.py +++ b/model_RL/evaluate_model.py @@ -1,19 +1,19 @@ -from numpy import ndarray - -from environment.trimesh_env import TriMesh -from mesh_model.mesh_analysis.global_mesh_analysis import global_score -from mesh_model.mesh_struct.mesh import Mesh import numpy as np import copy + from tqdm import tqdm +from environment.old_files.trimesh_env import TriMesh +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshOldAnalysis +from mesh_model.mesh_struct.mesh import Mesh + def testPolicy( policy, n_eval_episodes: int, dataset: list[Mesh], max_steps: int -) -> tuple[ndarray, ndarray, ndarray, list[Mesh]]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[Mesh]]: """ Tests policy on each mesh of a dataset with n_eval_episodes. :param policy: the policy to test @@ -60,7 +60,9 @@ def isBetterPolicy(actual_best_policy, policy_to_test): def isBetterMesh(best_mesh, actual_mesh): - if best_mesh is None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: + ma1 = TriMeshOldAnalysis(best_mesh) + ma2 = TriMeshOldAnalysis(actual_mesh) + if best_mesh is None or ma1.global_score()[1] > ma2.global_score()[1]: return True else: return False diff --git a/model_RL/AC_model.py b/model_RL/old_files/AC_model.py similarity index 96% rename from model_RL/AC_model.py rename to model_RL/old_files/AC_model.py index f48582f..615dd18 100644 --- a/model_RL/AC_model.py +++ b/model_RL/old_files/AC_model.py @@ -1,6 +1,6 @@ import torch from tqdm import tqdm -from model_RL.utilities.actor_critic_networks import NaNExceptionActor, NaNExceptionCritic, Actor, Critic +from model_RL.old_files.utilities import NaNExceptionActor, NaNExceptionCritic, Actor, Critic class AC: diff --git a/model_RL/PPO_model.py b/model_RL/old_files/PPO_model.py similarity index 98% rename from model_RL/PPO_model.py rename to model_RL/old_files/PPO_model.py index a6dc568..ecbe9dd 100644 --- a/model_RL/PPO_model.py +++ b/model_RL/old_files/PPO_model.py @@ -2,7 +2,7 @@ Old version of PPO for triangular environement """ -from model_RL.utilities.actor_critic_networks import NaNExceptionActor, NaNExceptionCritic, Actor, Critic +from model_RL.old_files.utilities import NaNExceptionActor, NaNExceptionCritic, Actor, Critic from mesh_model.mesh_analysis.global_mesh_analysis import global_score import copy import torch diff --git a/model_RL/SAC_model.py b/model_RL/old_files/SAC_model.py similarity index 97% rename from model_RL/SAC_model.py rename to model_RL/old_files/SAC_model.py index eff6636..62212f4 100644 --- a/model_RL/SAC_model.py +++ b/model_RL/old_files/SAC_model.py @@ -1,4 +1,4 @@ -from model_RL.utilities.actor_critic_networks import NaNExceptionActor, NaNExceptionCritic, Actor, Critic +from model_RL.old_files.utilities import NaNExceptionActor, NaNExceptionCritic, Actor, Critic import torch import random diff --git a/model_RL/parameters/ppo_config.json b/model_RL/old_files/ppo_config.json similarity index 59% rename from model_RL/parameters/ppo_config.json rename to model_RL/old_files/ppo_config.json index 1b844e5..299000f 100644 --- a/model_RL/parameters/ppo_config.json +++ b/model_RL/old_files/ppo_config.json @@ -5,7 +5,7 @@ "batch_size": 64, "learning_rate": 0.0001, "gamma": 0.9, - "verbose": 1, - "tensorboard_log": "training/results/quad/", - "total_timesteps": 80000 + "verbose": 2, + "tensorboard_log": "training/results/tri/", + "total_timesteps": 20000 } diff --git a/model_RL/utilities/actor_critic_networks.py b/model_RL/old_files/utilities/actor_critic_networks.py similarity index 100% rename from model_RL/utilities/actor_critic_networks.py rename to model_RL/old_files/utilities/actor_critic_networks.py diff --git a/model_RL/utilities/nnPolicy.py b/model_RL/old_files/utilities/nnPolicy.py similarity index 100% rename from model_RL/utilities/nnPolicy.py rename to model_RL/old_files/utilities/nnPolicy.py diff --git a/model_RL/utilities/__init__.py b/model_RL/utilities/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test_modules/test_actions.py b/test_modules/test_actions.py index b2fcad6..c21de43 100644 --- a/test_modules/test_actions.py +++ b/test_modules/test_actions.py @@ -1,5 +1,7 @@ import unittest +import scipy import mesh_model.mesh_struct.mesh as mesh +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis from mesh_model.mesh_struct.mesh_elements import Dart, Node from mesh_model.random_trimesh import regular_mesh from environment.actions.triangular_actions import split_edge, flip_edge, collapse_edge @@ -31,7 +33,10 @@ def test_flip(self): d1.set_beta(2, d2) d2.set_beta(2, d1) - flip_edge(cmap, n00, n11) + m_analysis = TriMeshQualityAnalysis(cmap) + done = flip_edge(m_analysis, n00, n11) + + self.assertTrue(done[0]) self.assertEqual(2, cmap.nb_faces()) self.assertEqual(4, cmap.nb_nodes()) @@ -45,69 +50,78 @@ def test_split(self): t1 = cmap.add_triangle(n00, n10, n11) t2 = cmap.add_triangle(n00, n11, n01) - split_edge(cmap, n00, n11) + cmap.set_twin_pointers() + m_analysis = TriMeshQualityAnalysis(cmap) + + done = split_edge(m_analysis, n00, n11) + n_new = Node(cmap, 4) + + self.assertTrue(done[0]) d1 = t1.get_dart() - # d1 goes from n00 to n10 - self.assertEqual(d1.get_node(), n00) + # d1 goes from nnew to n10 + self.assertEqual(d1.get_node(), n_new) 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) + self.assertEqual(4, cmap.nb_faces()) + self.assertEqual(5, cmap.nb_nodes()) 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) + m_analysis = TriMeshQualityAnalysis(cmap) + + split_edge(m_analysis, n00, n11) n5 = Node(cmap, 4) - valid, _, _ = collapse_edge(cmap, n00, n5) - d1_to_test = Dart(cmap, 7) - d2_to_test = Dart(cmap, 0) + split_edge(m_analysis, n11, n5) + n6 = Node(cmap, 5) + plot_mesh(cmap) + #Collapse not possible + valid, _, _ = collapse_edge(m_analysis, n11, n6) self.assertEqual(valid, False) - # Test possible collapse - cmap = regular_mesh(16) - d = Dart(cmap, 0) - n0 = d.get_node() - n1 = d.get_beta(1).get_node() - valid, _, _ = collapse_edge(cmap, n0, n1) + + # Collapse possible + valid, _, _ = collapse_edge(m_analysis, n6, n5) self.assertEqual(valid, True) + 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) + m_analysis = TriMeshQualityAnalysis(cmap) + n0 = Node(cmap, 0) n1 = Node(cmap, 1) n2 = Node(cmap, 2) n3 = Node(cmap, 3) - split_edge(cmap, n0, n2) + split_edge(m_analysis, n0, n2) n4 = Node(cmap, 4) - collapse_edge(cmap, n0, n4) - split_edge(cmap, n0, n2) + collapse_edge(m_analysis, n0, n4) + split_edge(m_analysis, 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) + collapse_edge(m_analysis, n0, n5) + split_edge(m_analysis, n4, n2) + collapse_edge(m_analysis, n4, n5) + collapse_edge(m_analysis, n2, n4) + split_edge(m_analysis, n0, n2) + split_edge(m_analysis, n0, n4) + split_edge(m_analysis, n4, n3) + split_edge(m_analysis, n4, n1) + split_edge(m_analysis, n5, n1) n7 = Node(cmap, 7) n8 = Node(cmap, 8) - collapse_edge(cmap, n7, n8) - collapse_edge(cmap, n5, n7) - + collapse_edge(m_analysis, n7, n8) + collapse_edge(m_analysis, n5, n7) +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test_modules/test_actions_quad.py b/test_modules/test_actions_quad.py index fdc1903..24469d6 100644 --- a/test_modules/test_actions_quad.py +++ b/test_modules/test_actions_quad.py @@ -1,9 +1,9 @@ import unittest import os import mesh_model.mesh_struct.mesh as mesh -from mesh_model.mesh_analysis.global_mesh_analysis import global_score from mesh_model.mesh_struct.mesh_elements import Dart, Node from mesh_model.random_quadmesh import random_mesh +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis from environment.actions.quadrangular_actions import flip_edge_cntcw, flip_edge_cw, split_edge, collapse_edge, cleanup_edge from view.mesh_plotter.mesh_plots import plot_mesh from mesh_model.reader import read_gmsh @@ -24,6 +24,7 @@ def test_flip(self): q1 = cmap.add_quad(n11, n10, n20, n21) q2 = cmap.add_quad(n10, n11, n01, n00) cmap.set_twin_pointers() + ma = QuadMeshOldAnalysis(cmap) plot_mesh(cmap) d0 = q1.get_dart() @@ -34,11 +35,12 @@ def test_flip(self): self.assertEqual(d2.get_node(), n10) - self.assertEqual(flip_edge_cntcw(cmap, n11, n10), (True,True,True)) + self.assertEqual(flip_edge_cntcw(ma, n11, n10), (True,True,True)) self.assertEqual(2, cmap.nb_faces()) self.assertEqual(6, cmap.nb_nodes()) plot_mesh(cmap) - self.assertFalse(flip_edge_cntcw(cmap, n11, n10)[0]) + self.assertFalse(flip_edge_cntcw(ma, n11, n10)[0]) + self.assertEqual(flip_edge_cw(ma, n01, n20), (True, True, True)) def test_split(self): cmap = mesh.Mesh() @@ -57,14 +59,15 @@ def test_split(self): q3 = cmap.add_quad(n11, n21, n22, n12) q4 = cmap.add_quad(n01, n11, n12, n02) cmap.set_twin_pointers() + ma = QuadMeshOldAnalysis(cmap) plot_mesh(cmap) found, d = cmap.find_inner_edge(n11, n21) self.assertTrue(found) - self.assertEqual(split_edge(cmap, n11, n21), (True,True,True)) + self.assertEqual(split_edge(ma, n11, n21), (True,True,True)) self.assertEqual(10, cmap.nb_nodes()) self.assertEqual(5, cmap.nb_faces()) plot_mesh(cmap) - self.assertFalse(split_edge(cmap, n20, n21)[0]) + self.assertFalse(split_edge(ma, n20, n21)[0]) def test_collapse(self): cmap = mesh.Mesh() @@ -88,12 +91,13 @@ def test_collapse(self): plot_mesh(cmap) found, d = cmap.find_inner_edge(n151, n12) self.assertTrue(found) + ma = QuadMeshOldAnalysis(cmap) plot_mesh(cmap) - self.assertEqual(collapse_edge(cmap, n151, n12), (True, True, True)) + self.assertEqual(collapse_edge(ma, n151, n12), (True, True, True)) self.assertEqual(9, cmap.nb_nodes()) self.assertEqual(4, cmap.nb_faces()) plot_mesh(cmap) - self.assertFalse(split_edge(cmap, n20, n21)[0]) + self.assertFalse(split_edge(ma, n20, n21)[0]) def test_cleanup(self): cmap = mesh.Mesh() @@ -114,10 +118,11 @@ def test_cleanup(self): q4 = cmap.add_quad(n01, n051, n12, n02) q5 = cmap.add_quad(n051, n10, n151, n12) cmap.set_twin_pointers() + ma = QuadMeshOldAnalysis(cmap) plot_mesh(cmap) found, d = cmap.find_inner_edge(n151, n12) self.assertTrue(found) - self.assertEqual(cleanup_edge(cmap, n151, n21), (True, True, True)) + self.assertEqual(cleanup_edge(ma, n151, n21), (True, True, True)) self.assertEqual(7, cmap.nb_nodes()) self.assertEqual(3, cmap.nb_faces()) plot_mesh(cmap) @@ -126,16 +131,17 @@ def test_cleanup(self): def test_actions(self): filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') cmap = read_gmsh(filename) + ma = QuadMeshOldAnalysis(cmap) plot_mesh(cmap) d = Dart(cmap, 14) n1= d.get_node() n2 = (d.get_beta(1)).get_node() - self.assertEqual(collapse_edge(cmap, n1, n2), (True,True,True)) + self.assertEqual(collapse_edge(ma, n1, n2), (True,True,True)) plot_mesh(cmap) d = Dart(cmap, 32) n1 = d.get_node() n2 = (d.get_beta(1)).get_node() - self.assertEqual(flip_edge_cntcw(cmap, n1, n2), (True,True,True)) + self.assertEqual(flip_edge_cntcw(ma, n1, n2), (True,True,True)) plot_mesh(cmap) @@ -151,100 +157,103 @@ 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) + ma = QuadMeshOldAnalysis(cmap) n0 = Node(cmap, 0) n1 = Node(cmap, 1) n2 = Node(cmap, 2) n3 = Node(cmap, 3) - split_edge(cmap, n0, n2) + split_edge(ma, n0, n2) n4 = Node(cmap, 4) - collapse_edge(cmap, n0, n4) - split_edge(cmap, n0, n2) + collapse_edge(ma, n0, n4) + split_edge(ma, 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) + collapse_edge(ma, n0, n5) + split_edge(ma, n4, n2) + collapse_edge(ma, n4, n5) + collapse_edge(ma, n2, n4) + split_edge(ma, n0, n2) + split_edge(ma, n0, n4) + split_edge(ma, n4, n3) + split_edge(ma, n4, n1) + split_edge(ma, n5, n1) n7 = Node(cmap, 7) n8 = Node(cmap, 8) - collapse_edge(cmap, n7, n8) - collapse_edge(cmap, n5, n7) + collapse_edge(ma, n7, n8) + collapse_edge(ma, n5, n7) def test_simple_mesh(self): filename = os.path.join(TESTFILE_FOLDER, 'simple_quad.msh') cmap = read_gmsh(filename) + ma = QuadMeshOldAnalysis(cmap) self.assertEqual(6, cmap.nb_faces()) self.assertEqual(11, cmap.nb_nodes()) #Collapse node 10 (0.67,0.67) from edge 10-6 - collapse_edge(cmap, Node(cmap, 10), Node(cmap, 6)) + collapse_edge(ma, Node(cmap, 10), Node(cmap, 6)) self.assertEqual(5, cmap.nb_faces()) self.assertEqual(10, cmap.nb_nodes()) self.assertTrue(Node(cmap,10).get_dart().id < 0) plot_mesh(cmap) #Flip edge 5-3 self.assertTrue(cmap.find_inner_edge(Node(cmap, 5), Node(cmap, 3))[0]) - flip_edge_cntcw(cmap, Node(cmap, 5), Node(cmap, 3)) + flip_edge_cntcw(ma, Node(cmap, 5), Node(cmap, 3)) self.assertFalse(cmap.find_inner_edge(Node(cmap, 5), Node(cmap, 3))[0]) plot_mesh(cmap) #Collapse node 9 (0.33, 0.33) from edge 9-4 - collapse_edge(cmap, Node(cmap, 9), Node(cmap, 4)) + collapse_edge(ma, Node(cmap, 9), Node(cmap, 4)) self.assertEqual(4, cmap.nb_faces()) self.assertEqual(9, cmap.nb_nodes()) self.assertTrue(Node(cmap, 9).get_dart().id < 0) plot_mesh(cmap) #Flip edge 7-1 self.assertTrue(cmap.find_inner_edge(Node(cmap, 7), Node(cmap, 1))[0]) - flip_edge_cntcw(cmap, Node(cmap, 7), Node(cmap, 1)) + flip_edge_cntcw(ma, Node(cmap, 7), Node(cmap, 1)) self.assertFalse(cmap.find_inner_edge(Node(cmap, 7), Node(cmap, 1))[0]) plot_mesh(cmap) #Flip edge 3-8 self.assertTrue(cmap.find_inner_edge(Node(cmap, 3), Node(cmap, 8))[0]) - flip_edge_cntcw(cmap, Node(cmap, 3), Node(cmap, 8)) + flip_edge_cntcw(ma, Node(cmap, 3), Node(cmap, 8)) self.assertFalse(cmap.find_inner_edge(Node(cmap, 3), Node(cmap, 8))[0]) plot_mesh(cmap) #Flip edge 1-8 self.assertTrue(cmap.find_inner_edge(Node(cmap, 1), Node(cmap, 8))[0]) - flip_edge_cntcw(cmap, Node(cmap, 1), Node(cmap, 8)) + flip_edge_cntcw(ma, Node(cmap, 1), Node(cmap, 8)) self.assertFalse(cmap.find_inner_edge(Node(cmap, 1), Node(cmap, 8))[0]) plot_mesh(cmap) #Split edge 2-7 and create new node n9 at coordinate (0.5, 0.75) - split_edge(cmap, Node(cmap, 2), Node(cmap, 7)) + split_edge(ma, Node(cmap, 2), Node(cmap, 7)) self.assertEqual(5, cmap.nb_faces()) self.assertEqual(10, cmap.nb_nodes()) self.assertTrue(Node(cmap, 9).get_dart().id > 0) plot_mesh(cmap) # Split edge 0-5 and create new node n10 at coordinate (0.5, 0.25) - split_edge(cmap, Node(cmap, 0), Node(cmap, 5)) + split_edge(ma, Node(cmap, 0), Node(cmap, 5)) self.assertEqual(6, cmap.nb_faces()) self.assertEqual(11, cmap.nb_nodes()) self.assertTrue(Node(cmap, 10).get_dart().id > 0) plot_mesh(cmap) # Flip edge 0-8 self.assertTrue(cmap.find_inner_edge(Node(cmap, 0), Node(cmap, 8))[0]) - flip_edge_cw(cmap, Node(cmap, 0), Node(cmap, 8)) + flip_edge_cw(ma, Node(cmap, 0), Node(cmap, 8)) self.assertFalse(cmap.find_inner_edge(Node(cmap, 0), Node(cmap, 8))[0]) plot_mesh(cmap) # Collapse node 8 (0.5, 0.5) from edge 8-10 - collapse_edge(cmap, Node(cmap, 8), Node(cmap, 10)) + collapse_edge(ma, Node(cmap, 8), Node(cmap, 10)) self.assertEqual(5, cmap.nb_faces()) self.assertEqual(10, cmap.nb_nodes()) self.assertTrue(Node(cmap, 8).get_dart().id < 0) # Collapse node 10 (0.5, 0.25) from edge 10-5 - collapse_edge(cmap, Node(cmap, 10), Node(cmap, 5)) + collapse_edge(ma, Node(cmap, 10), Node(cmap, 5)) self.assertEqual(4, cmap.nb_faces()) self.assertEqual(9, cmap.nb_nodes()) self.assertTrue(Node(cmap, 10).get_dart().id < 0) plot_mesh(cmap) - self.assertEqual(global_score(cmap)[1], 0) - + self.assertEqual(ma.global_score()[1], 0) +if __name__ == '__main__': + unittest.main() diff --git a/test_modules/test_global_mesh_analysis.py b/test_modules/test_global_mesh_analysis.py new file mode 100644 index 0000000..58ef84a --- /dev/null +++ b/test_modules/test_global_mesh_analysis.py @@ -0,0 +1,33 @@ +import math +import unittest +import os + +from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis, TriMeshOldAnalysis +from mesh_model.reader import read_gmsh +from view.mesh_plotter.mesh_plots import plot_mesh + +TESTFILE_FOLDER = os.path.join(os.path.dirname(__file__), '../mesh_files/') + +class TestGlobalMeshAnalysis(unittest.TestCase): + + def test_angle_by_coord(self): + filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') + cmap = read_gmsh(filename) + m_analysis = TriMeshOldAnalysis(cmap) + self.assertEqual(m_analysis.get_angle_by_coord(1,0,0,0,0,1), 90) + self.assertEqual(m_analysis.get_angle_by_coord(-1,0,0,0,1,0), 180) + + def test_angle_from_sides(self): + filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') + cmap = read_gmsh(filename) + m_analysis = TriMeshOldAnalysis(cmap) + self.assertAlmostEquals(math.degrees(m_analysis.angle_from_sides(1,1,1)), 60) + self.assertAlmostEquals(math.degrees(m_analysis.angle_from_sides(0.00000001, 1, 1)), 0) + self.assertAlmostEquals(math.degrees(m_analysis.angle_from_sides(1, 0.5, 0.5)), 180) + + with self.assertRaises(ValueError): + m_analysis.angle_from_sides(1.1, 0.5, 0.5) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test_modules/test_quadrangular_mesh_analysis.py b/test_modules/test_quadrangular_mesh_analysis.py index aea10de..ff4a41b 100644 --- a/test_modules/test_quadrangular_mesh_analysis.py +++ b/test_modules/test_quadrangular_mesh_analysis.py @@ -3,77 +3,89 @@ from mesh_model.mesh_struct.mesh import Mesh from mesh_model.mesh_struct.mesh_elements import Dart -import mesh_model.mesh_analysis.global_mesh_analysis as GMA -import mesh_model.mesh_analysis.quadmesh_analysis as QMA -from environment.actions.triangular_actions import split_edge_ids +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis +from environment.actions.quadrangular_actions import split_edge_ids, flip_edge_cw_ids from view.mesh_plotter.mesh_plots import plot_mesh from mesh_model.reader import read_gmsh TESTFILE_FOLDER = os.path.join(os.path.dirname(__file__), '../mesh_files/') -class TestMeshAnalysis(unittest.TestCase): +class TestMeshOldAnalysis(unittest.TestCase): def test_mesh_regular_score(self): nodes = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [0.0, 1.0], [1.0, 1.0], [2.0, 1.0], [0.0, 2.0], [1.0, 2.0], [2.0, 2.0], [3.0, 3.0]] faces = [[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]] cmap = Mesh(nodes,faces) - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + qma = QuadMeshOldAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = qma.global_score() self.assertEqual((0,0), (mesh_score, mesh_ideal_score) ) def test_mesh_with_irregularities(self): filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') cmap = read_gmsh(filename) - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + qma = QuadMeshOldAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = qma.global_score() self.assertIsNot((0, 0), (mesh_score,mesh_ideal_score) ) def test_is_valid_action(self): filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') cmap = read_gmsh(filename) + qma = QuadMeshOldAnalysis(cmap) #Boundary dart - self.assertEqual(QMA.isValidAction(cmap, 20, 0), (False, True)) + self.assertEqual(qma.isValidAction(20, 0), (False, True)) - # Flip test - self.assertEqual(QMA.isValidAction(cmap, 3, 0), (True, True)) - self.assertEqual(QMA.isValidAction(cmap, 27, 0), (False, True)) + # Flip Clockwise test + self.assertEqual(qma.isValidAction(3, 0), (True, True)) + self.assertEqual(qma.isValidAction(27, 0), (False, True)) + + # Flip Counterclockwise test + self.assertEqual(qma.isValidAction(3, 1), (True, True)) + self.assertEqual(qma.isValidAction(27, 1), (False, True)) #Split test - self.assertEqual(QMA.isValidAction(cmap, 0, 1), (True, True)) - self.assertEqual(QMA.isValidAction(cmap, 27, 1), (False, True)) + self.assertEqual(qma.isValidAction(0, 2), (True, True)) + self.assertEqual(qma.isValidAction(27, 2), (False, True)) #Collapse test - self.assertEqual(QMA.isValidAction(cmap, 0, 2), (True, True)) + self.assertEqual(qma.isValidAction(0, 3), (True, True)) plot_mesh(cmap) - self.assertEqual(QMA.isValidAction(cmap, 27, 2), (False, True)) + self.assertEqual(qma.isValidAction(27, 3), (False, True)) + + #Cleanup test action id = 4 #All action test - self.assertEqual(QMA.isValidAction(cmap, 27, 3), (False, True)) - self.assertEqual(QMA.isValidAction(cmap, 9, 3), (True, True)) + self.assertEqual(qma.isValidAction(27, 5), (False, True)) + flip_edge_cw_ids(qma,13,37) + self.assertEqual(qma.isValidAction(66, 5), (False, False)) + self.assertEqual(qma.isValidAction(9, 5), (True, True)) #One action test - self.assertEqual(QMA.isValidAction(cmap, 0, 4), (True, True)) - self.assertEqual(QMA.isValidAction(cmap, 9, 4), (True, True)) - self.assertEqual(QMA.isValidAction(cmap, 27, 4), (False, True)) + self.assertEqual(qma.isValidAction(0, 6), (True, True)) + self.assertEqual(qma.isValidAction(9, 6), (True, True)) + self.assertEqual(qma.isValidAction(27, 6), (False, True)) #Invalid action with self.assertRaises(ValueError): - QMA.isValidAction(cmap, 0, 7) + qma.isValidAction(0, 7) def test_isTruncated(self): filename = os.path.join(TESTFILE_FOLDER, 't1_quad.msh') cmap = read_gmsh(filename) + qma = QuadMeshOldAnalysis(cmap) darts_list = [] for d_info in cmap.active_darts(): darts_list.append(d_info[0]) - self.assertFalse(QMA.isTruncated(cmap, darts_list)) + self.assertFalse(qma.isTruncated(darts_list)) nodes = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]] faces = [[0, 1, 3, 2]] cmap = Mesh(nodes, faces) + qma = QuadMeshOldAnalysis(cmap) darts_list = [] for d_info in cmap.active_darts(): darts_list.append(d_info[0]) - self.assertTrue(QMA.isTruncated(cmap, darts_list)) + self.assertTrue(qma.isTruncated(darts_list)) if __name__ == '__main__': unittest.main() diff --git a/test_modules/test_triangular_mesh_analysis.py b/test_modules/test_triangular_mesh_analysis.py index 8eeff64..8dd1fc3 100644 --- a/test_modules/test_triangular_mesh_analysis.py +++ b/test_modules/test_triangular_mesh_analysis.py @@ -1,20 +1,19 @@ import unittest from mesh_model.mesh_struct.mesh import Mesh -from mesh_model.mesh_struct.mesh_elements import Dart -import mesh_model.mesh_analysis.global_mesh_analysis as GMA -import mesh_model.mesh_analysis.trimesh_analysis as TMA -import mesh_model.mesh_analysis.quadmesh_analysis as QMA +from mesh_model.mesh_struct.mesh_elements import Dart, Node +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis, TriMeshOldAnalysis from environment.actions.triangular_actions import split_edge_ids from view.mesh_plotter.mesh_plots import plot_mesh -class TestMeshAnalysis(unittest.TestCase): +class TestMeshQualityAnalysis(unittest.TestCase): def test_mesh_regular_score(self): nodes = [[0.0, 0.0], [1.0, 0.0], [0.5, 1], [-0.5, 1.0], [0.0, 2.0], [-1.0,0.0],[-0.5,-1.0],[0.0,-2.0], [0.5,-1.0]] faces = [[0, 1, 2], [0, 2, 3], [3, 2, 4], [3, 5, 0], [0, 5, 6], [0, 6, 8], [6, 7, 8], [0, 8, 1]] cmap = Mesh(nodes,faces) - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = m_analysis.global_score() self.assertEqual((0,0), (mesh_score, mesh_ideal_score) ) def test_mesh_with_irregularities(self): @@ -25,34 +24,38 @@ def test_mesh_with_irregularities(self): [3, 4, 5], [3, 5, 6], [3, 6, 7], [7, 6, 8], [7, 8, 9], [7, 9, 10], [10, 9, 11], [10, 11, 12], [14, 12, 13], [14, 13, 15], [1, 14, 15], [1, 15, 16], [1, 16, 17], [1, 17, 2], [2, 17, 18], [2, 18, 4]] cmap = Mesh(nodes, faces) - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = m_analysis.global_score() self.assertEqual((6, -2), (mesh_score,mesh_ideal_score) ) def test_mesh_bad_score(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) - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = m_analysis.global_score() self.assertEqual((3, 1), (mesh_score, mesh_ideal_score)) def test_split_score(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) - split_edge_ids(cmap, 0, 2) - split_edge_ids(cmap, 1, 2) # split impossible - nodes_score, mesh_score, mesh_ideal_score, adjacency = GMA.global_score(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + split_edge_ids(m_analysis, 0, 2) + split_edge_ids(m_analysis, 1, 4) # split impossible + nodes_score, mesh_score, mesh_ideal_score, adjacency = m_analysis.global_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]] faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] cmap = Mesh(nodes, faces) + m_analysis = TriMeshQualityAnalysis(cmap) dart_to_test = Dart(cmap, 0) - node = TMA.find_template_opposite_node(dart_to_test) + node = m_analysis.find_template_opposite_node(dart_to_test) self.assertEqual(node, None) dart_to_test = Dart(cmap, 2) - node = TMA.find_template_opposite_node(dart_to_test) + node = m_analysis.find_template_opposite_node(dart_to_test) self.assertEqual(node, 3) def test_is_valid_action(self): @@ -63,102 +66,320 @@ def test_is_valid_action(self): [3, 4, 5], [3, 5, 6], [3, 6, 7], [7, 6, 8], [7, 8, 9], [7, 9, 10], [10, 9, 11], [10, 11, 12], [14, 12, 13], [14, 13, 15], [1, 14, 15], [1, 15, 16], [1, 16, 17], [1, 17, 2], [2, 17, 18], [2, 18, 4]] cmap = Mesh(nodes, faces) - split_edge_ids(cmap, 0, 1) + m_analysis = TriMeshQualityAnalysis(cmap) + split_edge_ids(m_analysis, 0, 1) #Boundary dart - self.assertEqual(TMA.isValidAction(cmap, 25, 0), (False, True)) + self.assertEqual(m_analysis.isValidAction(25, 0), (False, True)) # Flip test - self.assertEqual(TMA.isValidAction(cmap, 3, 0), (True, True)) - self.assertEqual(TMA.isValidAction(cmap, 0, 0), (False, True)) + self.assertEqual(m_analysis.isValidAction(3, 0), (True, True)) + self.assertEqual(m_analysis.isValidAction(0, 0), (True, False)) #Split test - self.assertEqual(TMA.isValidAction(cmap, 0, 1), (True, True)) - split_edge_ids(cmap, 1, 19) - split_edge_ids(cmap, 1, 20) - self.assertEqual(TMA.isValidAction(cmap, 20, 1), (True, False)) - split_edge_ids(cmap, 0, 19) - split_edge_ids(cmap, 0, 22) - split_edge_ids(cmap, 0, 23) - self.assertEqual(TMA.isValidAction(cmap, 20, 1), (False, True)) + self.assertEqual(m_analysis.isValidAction(0, 1), (True, True)) + split_edge_ids(m_analysis, 1, 19) + split_edge_ids(m_analysis, 1, 20) + plot_mesh(m_analysis.mesh) + self.assertEqual(m_analysis.isValidAction(20, 1), (True, True)) + split_edge_ids(m_analysis, 0, 19) + split_edge_ids(m_analysis, 0, 22) + split_edge_ids(m_analysis, 0, 23) + plot_mesh(m_analysis.mesh) + self.assertEqual(m_analysis.isValidAction(20, 1), (False, True)) #node n2 and n14 degree are >= 10 #Collapse test - self.assertEqual(TMA.isValidAction(cmap, 20, 2), (True, True)) + self.assertEqual(m_analysis.isValidAction(20, 2), (True, True)) + self.assertEqual(m_analysis.isValidAction(2, 2), (False, False)) + + #All action test + self.assertEqual(m_analysis.isValidAction(2, 3), (False, False)) + self.assertEqual(m_analysis.isValidAction(26, 3), (False, False)) + self.assertEqual(m_analysis.isValidAction(9, 3), (True, True)) + + #One action test + self.assertEqual(m_analysis.isValidAction(0, 4), (True, True)) + self.assertEqual(m_analysis.isValidAction(9, 4), (True, True)) + self.assertEqual(m_analysis.isValidAction(46, 4), (False, True)) + + 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]] + faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] + cmap = Mesh(nodes, faces) + m_analysis = TriMeshQualityAnalysis(cmap) + dart_to_test = Dart(cmap, 0) + self.assertFalse(m_analysis.isFlipOk(dart_to_test)[0]) + dart_to_test = Dart(cmap, 2) + self.assertTrue(m_analysis.isFlipOk(dart_to_test)[0]) + + 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) + m_analysis = TriMeshQualityAnalysis(cmap) + dart_to_test = Dart(cmap, 0) + self.assertEqual(m_analysis.isSplitOk(dart_to_test), (False, True)) + dart_to_test = Dart(cmap, 2) + self.assertEqual(m_analysis.isSplitOk(dart_to_test), (True, True)) + + 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) + m_analysis = TriMeshQualityAnalysis(cmap) + dart_to_test = Dart(cmap, 0) + self.assertFalse(m_analysis.isCollapseOk(dart_to_test)[0]) + dart_to_test = Dart(cmap, 2) + self.assertFalse(m_analysis.isCollapseOk(dart_to_test)[0]) + + split_edge_ids(m_analysis, 0, 2) + split_edge_ids(m_analysis, 0, 5) + dart_to_test = Dart(cmap, 12) + self.assertTrue(m_analysis.isCollapseOk(dart_to_test)[0]) + + def test_isTruncated(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]] + faces = [[0, 1, 2]] + cmap = Mesh(nodes, faces) + m_analysis = TriMeshQualityAnalysis(cmap) + darts_list = [] + for d_info in cmap.active_darts(): + darts_list.append(d_info[0]) + self.assertTrue(m_analysis.isTruncated(darts_list)) + 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) + m_analysis = TriMeshQualityAnalysis(cmap) + darts_list = [] + for d_info in cmap.active_darts(): + darts_list.append(d_info[0]) + self.assertFalse(m_analysis.isTruncated(darts_list)) + + def test_get_geometric_quality(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], [0, 1, 4], [0, 4, 1]] + cmap = Mesh(nodes, faces) plot_mesh(cmap) - self.assertEqual(TMA.isValidAction(cmap, 2, 2), (False, True)) + m_analysis = TriMeshQualityAnalysis(cmap) + + # Half flat + d_to_test = Dart(cmap, 0) + self.assertEqual(m_analysis.get_dart_geometric_quality(d_to_test), 4) + + # Full flat + d_to_test = Dart(cmap, 11) + self.assertEqual(m_analysis.get_dart_geometric_quality(d_to_test), 6) + + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] + faces = [[0, 1, 2], [2, 1, 3]] + cmap = Mesh(nodes, faces) + plot_mesh(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + + # Crossed + d_to_test = Dart(cmap, 1) + self.assertEqual(m_analysis.get_dart_geometric_quality(d_to_test), 3) + + def test_find_star_vertex(self): + # Polygon with ker + nodes = [[0.0, 0.0], [1.0, 1.0], [1.0, -1.0], [0.0, -2.0], [-1.0, 0.0], [-0.5, 0.5], [-0.25, -0.25]] + faces = [[0, 2, 1], [0, 3, 2], [0, 4, 3], [0, 5, 4], [0, 1, 5]] + cmap = Mesh(nodes, faces) + plot_mesh(cmap) + m_analysis = TriMeshQualityAnalysis(cmap) + n_to_test = Node(cmap, 0) + self.assertTrue(m_analysis.find_star_vertex(n_to_test)[0]) + + +class TestMeshOldAnalysis(unittest.TestCase): + + def test_mesh_regular_score(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [0.5, 1], [-0.5, 1.0], [0.0, 2.0], [-1.0,0.0],[-0.5,-1.0],[0.0,-2.0], [0.5,-1.0]] + faces = [[0, 1, 2], [0, 2, 3], [3, 2, 4], [3, 5, 0], [0, 5, 6], [0, 6, 8], [6, 7, 8], [0, 8, 1]] + cmap = Mesh(nodes,faces) + tma = TriMeshOldAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = tma.global_score() + self.assertEqual((0,0), (mesh_score, mesh_ideal_score) ) + + def test_mesh_with_irregularities(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [0.5, 1], [-0.5, 1.0], [0.0, 2.0], [-1.0, 2.0], [-2.0, 1.0], [-1.0, 0.0], + [-2.0, 0.0], [-2.0, -1.0], [-0.5, -1.0], [-1.0, -2.0], [0.0, -2.0], [1.0, -2.0], + [0.5, -1.0], [2.0, -1.0], [2.0, 0.0], [2.0, 1.0], [1.0, 2.0]] + faces = [[0, 1, 2], [0, 2, 3], [3, 2, 4], [7, 0, 3], [7, 10, 0], [10, 14, 0], [0, 14, 1], [10, 12, 14], + [3, 4, 5], [3, 5, 6], [3, 6, 7], [7, 6, 8], [7, 8, 9], [7, 9, 10], [10, 9, 11], [10, 11, 12], + [14, 12, 13], [14, 13, 15], [1, 14, 15], [1, 15, 16], [1, 16, 17], [1, 17, 2], [2, 17, 18], [2, 18, 4]] + cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = tma.global_score() + self.assertEqual((6, -2), (mesh_score,mesh_ideal_score) ) + + def test_mesh_bad_score(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) + tma = TriMeshOldAnalysis(cmap) + nodes_score, mesh_score, mesh_ideal_score, adjacency = tma.global_score() + self.assertEqual((3, 1), (mesh_score, mesh_ideal_score)) + + def test_split_score(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) + tma = TriMeshOldAnalysis(cmap) + split_edge_ids(tma, 0, 2) + split_edge_ids(tma, 1, 2) # split impossible + nodes_score, mesh_score, mesh_ideal_score, adjacency = tma.global_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]] + faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] + cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) + dart_to_test = Dart(cmap, 0) + node = tma.find_template_opposite_node(dart_to_test) + self.assertEqual(node, None) + dart_to_test = Dart(cmap, 2) + node = tma.find_template_opposite_node(dart_to_test) + self.assertEqual(node, 3) + + def test_is_valid_action(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [0.5, 1], [-0.5, 1.0], [0.0, 2.0], [-1.0, 2.0], [-2.0, 1.0], [-1.0, 0.0], + [-2.0, 0.0], [-2.0, -1.0], [-0.5, -1.0], [-1.0, -2.0], [0.0, -2.0], [1.0, -2.0], + [0.5, -1.0], [2.0, -1.0], [2.0, 0.0], [2.0, 1.0], [1.0, 2.0]] + faces = [[0, 1, 2], [0, 2, 3], [3, 2, 4], [7, 0, 3], [7, 10, 0], [10, 14, 0], [0, 14, 1], [10, 12, 14], + [3, 4, 5], [3, 5, 6], [3, 6, 7], [7, 6, 8], [7, 8, 9], [7, 9, 10], [10, 9, 11], [10, 11, 12], + [14, 12, 13], [14, 13, 15], [1, 14, 15], [1, 15, 16], [1, 16, 17], [1, 17, 2], [2, 17, 18], [2, 18, 4]] + cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) + split_edge_ids(tma, 0, 1) + + #Boundary dart + self.assertEqual(tma.isValidAction( 25, 0), (False, True)) + + # Flip test + self.assertEqual(tma.isValidAction( 3, 0), (True, True)) + self.assertEqual(tma.isValidAction(0, 0), (False, True)) + + #Split test + self.assertEqual(tma.isValidAction(0, 1), (True, True)) + split_edge_ids(tma, 1, 19) + split_edge_ids(tma, 1, 20) + self.assertEqual(tma.isValidAction(20, 1), (True, False)) + split_edge_ids(tma, 0, 19) + split_edge_ids(tma, 0, 22) + split_edge_ids(tma, 0, 23) + self.assertEqual(tma.isValidAction(20, 1), (False, True)) + + #Collapse test + self.assertEqual(tma.isValidAction(20, 2), (True, True)) + plot_mesh(cmap) + self.assertEqual(tma.isValidAction(2, 2), (False, True)) #All action test - self.assertEqual(TMA.isValidAction(cmap, 2, 3), (False, False)) - self.assertEqual(TMA.isValidAction(cmap, 26, 3), (False, False)) - self.assertEqual(TMA.isValidAction(cmap, 9, 3), (True, True)) + self.assertEqual(tma.isValidAction(2, 3), (False, False)) + self.assertEqual(tma.isValidAction(26, 3), (False, False)) + self.assertEqual(tma.isValidAction(9, 3), (True, True)) #One action test - self.assertEqual(TMA.isValidAction(cmap, 0, 4), (True, True)) - self.assertEqual(TMA.isValidAction(cmap, 9, 4), (True, True)) - self.assertEqual(TMA.isValidAction(cmap, 94, 4), (False, False)) + self.assertEqual(tma.isValidAction(0, 4), (True, True)) + self.assertEqual(tma.isValidAction(9, 4), (True, True)) + self.assertEqual(tma.isValidAction(94, 4), (False, 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]] faces = [[0, 1, 2], [0, 2, 3], [1, 4, 2]] cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) plot_mesh(cmap) dart_to_test = Dart(cmap, 0) - self.assertFalse(TMA.isFlipOk(dart_to_test)[0]) + self.assertFalse(tma.isFlipOk(dart_to_test)[0]) dart_to_test = Dart(cmap, 2) - self.assertTrue(TMA.isFlipOk(dart_to_test)) + self.assertTrue(tma.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) + tma = TriMeshOldAnalysis(cmap) plot_mesh(cmap) dart_to_test = Dart(cmap, 0) - self.assertEqual(TMA.isSplitOk(dart_to_test), (False, True)) + self.assertEqual(tma.isSplitOk(dart_to_test), (False, True)) dart_to_test = Dart(cmap, 2) - self.assertEqual(TMA.isSplitOk(dart_to_test), (True, True)) + self.assertEqual(tma.isSplitOk(dart_to_test), (True, True)) 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) + tma = TriMeshOldAnalysis(cmap) plot_mesh(cmap) dart_to_test = Dart(cmap, 0) - self.assertFalse(TMA.isCollapseOk(dart_to_test)[0]) + self.assertFalse(tma.isCollapseOk(dart_to_test)[0]) dart_to_test = Dart(cmap, 2) - self.assertFalse(TMA.isCollapseOk(dart_to_test)[0]) + self.assertFalse(tma.isCollapseOk(dart_to_test)[0]) + split_edge_ids(tma,2,1) + split_edge_ids(tma, 1, 5) + dart_to_test = Dart(cmap, 17) + self.assertTrue(tma.isCollapseOk(dart_to_test)[0]) def test_valid_triangle(self): + nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]] + faces = [[0, 1, 2]] + cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) + # test Lmax invalid vect_AB = (5.0, 0.0) vect_AC = (2.5, 5.0) vect_BC = (-2.5, 5.0) - self.assertFalse(TMA.valid_triangle(vect_AB, vect_AC, vect_BC)) + self.assertFalse(tma.valid_triangle(vect_AB, vect_AC, vect_BC)) # test invalid angles vect_AB = (3.0, 0.0) vect_AC = (1.5, 0.05) vect_BC = (-1.5, 0.05) - self.assertFalse(TMA.valid_triangle(vect_AB, vect_AC, vect_BC)) + self.assertFalse(tma.valid_triangle(vect_AB, vect_AC, vect_BC)) # test valid triangle vect_AB = (3.0, 0.0) vect_AC = (1.5, 3.0) vect_BC = (-1.5, 3.0) - self.assertTrue(TMA.valid_triangle(vect_AB, vect_AC, vect_BC)) + self.assertTrue(tma.valid_triangle(vect_AB, vect_AC, vect_BC)) def test_isTruncated(self): nodes = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]] faces = [[0, 1, 2]] cmap = Mesh(nodes, faces) + tma = TriMeshOldAnalysis(cmap) darts_list = [] for d_info in cmap.active_darts(): darts_list.append(d_info[0]) - self.assertTrue(TMA.isTruncated(cmap, darts_list)) + self.assertTrue(tma.isTruncated(darts_list)) 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) + tma = TriMeshOldAnalysis(cmap) darts_list = [] for d_info in cmap.active_darts(): darts_list.append(d_info[0]) - self.assertFalse(TMA.isTruncated(cmap, darts_list)) + self.assertFalse(tma.isTruncated(darts_list)) if __name__ == '__main__': unittest.main() + + + # def test_valid_triangle(self): + # # test Lmax invalid + # vect_AB = (5.0, 0.0) + # vect_AC = (2.5, 5.0) + # vect_BC = (-2.5, 5.0) + # self.assertFalse(m_analysis.valid_triangle(vect_AB, vect_AC, vect_BC)) + # # test invalid angles + # vect_AB = (3.0, 0.0) + # vect_AC = (1.5, 0.05) + # vect_BC = (-1.5, 0.05) + # self.assertFalse(m_analysis.valid_triangle(vect_AB, vect_AC, vect_BC)) + # # test valid triangle + # vect_AB = (3.0, 0.0) + # vect_AC = (1.5, 3.0) + # vect_BC = (-1.5, 3.0) + # self.assertTrue(m_analysis.valid_triangle(vect_AB, vect_AC, vect_BC)) diff --git a/training/config_PPO_SB3.yaml b/training/config/quadmesh_config_PPO_SB3.yaml similarity index 76% rename from training/config_PPO_SB3.yaml rename to training/config/quadmesh_config_PPO_SB3.yaml index 254798b..9a1d4d4 100644 --- a/training/config_PPO_SB3.yaml +++ b/training/config/quadmesh_config_PPO_SB3.yaml @@ -1,5 +1,5 @@ project_name : "Quadmesh" -experiment_name : "test" +experiment_name : "TEST" description : "" total_timesteps : 10000 @@ -7,7 +7,9 @@ total_timesteps : 10000 paths: log_dir : "training/results/quad-sb3/" policy_saving_dir : "training/policy_saved/quad-sb3/" - wandb_model_saving_dir : "training/wandb_models/" + wandb_model_saving_dir : "training/wandb_models/quad-sb3/" + episode_recording_dir: "training/results/quad-sb3/episode_recording/" + observation_counts_dir: "training/results/quad-sb3/observation_counts/" dataset: evaluation_mesh_file_path : "mesh_files/simple_quad.msh" @@ -27,6 +29,7 @@ env: reward_function : 0 # 0 if basics, 1 if penalize, 2 otherwise render_mode : null obs_count : true + analysis_type : "old" ppo: policy : MlpPolicy @@ -46,9 +49,9 @@ eval: n_darts_selected: 10 deep: 12 obs_size: 120 - render_mode: "human" + render_mode: null action_restriction: false - with_degree_observation: false + with_quality_observation: false metrics: diff --git a/training/config_PPO_perso.yaml b/training/config/quadmesh_config_PPO_perso.yaml similarity index 68% rename from training/config_PPO_perso.yaml rename to training/config/quadmesh_config_PPO_perso.yaml index 5e946dc..3b374ed 100644 --- a/training/config_PPO_perso.yaml +++ b/training/config/quadmesh_config_PPO_perso.yaml @@ -1,11 +1,13 @@ project_name : "Quadmesh" -experiment_name : "random_basic_quad_test_config-TEST" +experiment_name : "TEST" description : "" paths: log_dir : "training/results/quad-perso/" policy_saving_dir : "training/policy_saved/quad-perso/" - wandb_model_saving_dir : "training/wandb_models/" + wandb_model_saving_dir : "training/wandb_models/quad-perso/" + episode_recording_dir: "training/results/quad-perso/episode_recording/" + observation_counts_dir: "training/results/quad-perso/observation_counts/" dataset: evaluation_mesh_file_path : "mesh_files/simple_quad.msh" @@ -24,10 +26,12 @@ env: reward_function : 0 # 0 if basics, 1 if penalize, 2 otherwise render_mode: null obs_count: true + analysis_type : "old" ppo: - n_iterations : 5 - n_episodes_per_iteration : 50 + n_actions : 4 + n_iterations : 2 + n_episodes_per_iteration : 20 n_epochs : 5 batch_size: 64 learning_rate : 0.0001 diff --git a/training/exploit.py b/training/exploit.py deleted file mode 100644 index 8f02119..0000000 --- a/training/exploit.py +++ /dev/null @@ -1,31 +0,0 @@ -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 view.mesh_plotter.create_plots import plot_test_results -from view.mesh_plotter.mesh_plots 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/training/exploit_PPO_perso.py b/training/exploit_PPO_perso.py index 7355785..275bee7 100644 --- a/training/exploit_PPO_perso.py +++ b/training/exploit_PPO_perso.py @@ -1,36 +1,42 @@ -from numpy import ndarray - -import gymnasium as gym import json import torch +import copy +import numpy as np +import gymnasium as gym +import yaml + +from tqdm import tqdm +from numpy import ndarray from torch.distributions import Categorical -from model_RL.PPO_model_pers import Actor -from mesh_model.mesh_analysis.global_mesh_analysis import global_score +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshOldAnalysis, TriMeshQualityAnalysis +from mesh_model.mesh_struct.mesh_elements import Dart from mesh_model.mesh_struct.mesh import Mesh from mesh_model.reader import read_gmsh +from model_RL.PPO_model_pers import Actor + from view.mesh_plotter.create_plots import plot_test_results from view.mesh_plotter.mesh_plots import plot_dataset from environment.actions.smoothing import smoothing_mean -import mesh_model.random_quadmesh as QM + from environment.gymnasium_envs.quadmesh_env.envs.quadmesh import QuadMeshEnv -import numpy as np -import copy -from tqdm import tqdm +from environment.gymnasium_envs.trimesh_full_env.envs.trimesh import TriMeshEnvFull +import mesh_model.random_quadmesh as QM def testPolicy( actor, n_eval_episodes: int, - env_config, + config, dataset: list[Mesh] ) -> tuple[ndarray, ndarray, ndarray, ndarray, list[Mesh]]: """ Tests policy on each mesh of a dataset with n_eval_episodes. - :param policy: the policy to test + :param actor: the policy to test :param n_eval_episodes: number of evaluation episodes on each mesh + :param config: configuration :param dataset: list of mesh objects - :param max_steps: max steps to evaluate :return: average length of evaluation episodes, number of wins,average reward per mesh, dataset with the modified meshes """ print('Testing policy') @@ -42,14 +48,15 @@ def testPolicy( for i, mesh in tqdm(enumerate(dataset, 1)): best_mesh = mesh env = gym.make( - env_config["env_name"], - max_episode_steps=30, - mesh = mesh, - n_darts_selected=env_config["n_darts_selected"], - deep= env_config["deep"], - action_restriction=env_config["action_restriction"], - with_degree_obs=env_config["with_degree_observation"], - render_mode="human" + config["env"]["env_id"], + max_episode_steps=config["env"]["max_episode_steps"], + mesh=mesh, + #mesh_size = 30, + n_darts_selected=config["env"]["n_darts_selected"], + deep=config["env"]["deep"], + action_restriction=config["env"]["action_restriction"], + with_quality_obs=config["env"]["with_quality_observation"], + render_mode=config["env"]["render_mode"], ) for _ in range(n_eval_episodes): terminated = False @@ -63,8 +70,8 @@ def testPolicy( dist = Categorical(pmf) action = dist.sample() action = action.tolist() - action_dart = int(action / 4) - action_type = action % 4 + action_dart = int(action / config["ppo"]["n_actions"]) + action_type = action % config["ppo"]["n_actions"] gymnasium_action = [action_type, action_dart] if action is None: env.terminal = True @@ -74,7 +81,7 @@ def testPolicy( ep_length += 1 if terminated: nb_wins[i-1] += 1 - if isBetterMesh(best_mesh, info['mesh']): + if isBetterMesh(best_mesh, info['mesh'], config["env"]["analysis_type"]): best_mesh = copy.deepcopy(info['mesh']) avg_length[i-1] += ep_length avg_mesh_rewards[i-1] += ep_mesh_rewards @@ -90,8 +97,27 @@ def isBetterPolicy(actual_best_policy, policy_to_test): if actual_best_policy is None: return True -def isBetterMesh(best_mesh, actual_mesh): - if best_mesh is None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: +def isBetterMesh(best_mesh, actual_mesh, analysis_type): + tri = False + for d_info in actual_mesh.dart_info: + if d_info[0]>=0: + d = Dart(actual_mesh, d_info[0]) + if d == ((d.get_beta(1)).get_beta(1)).get_beta(1): + tri = True + else: + tri = False + break + if tri: + if analysis_type == "old": + ma_best_mesh = TriMeshOldAnalysis(best_mesh) + ma_actual_mesh = TriMeshOldAnalysis(actual_mesh) + else: + ma_best_mesh = TriMeshQualityAnalysis(best_mesh) + ma_actual_mesh = TriMeshQualityAnalysis(actual_mesh) + else: + ma_best_mesh = QuadMeshOldAnalysis(best_mesh) + ma_actual_mesh = QuadMeshOldAnalysis(actual_mesh) + if best_mesh is None or ma_best_mesh.global_score()[1] > ma_actual_mesh.global_score()[1]: return True else: return False @@ -99,28 +125,29 @@ def isBetterMesh(best_mesh, actual_mesh): if __name__ == '__main__': - #Create a dataset of 9 meshes - mesh = read_gmsh("../mesh_files/medium_quad.msh") + mesh = read_gmsh("../mesh_files/t1_tri.msh") dataset = [mesh for _ in range(9)] - with open("../environment/environment_config.json", "r") as f: - env_config = json.load(f) + with open("../training/config/trimesh_config_PPO_perso.yaml", "r") as f: + config = yaml.safe_load(f) plot_dataset(dataset) env = gym.make( - env_config["env_name"], + config["env"]["env_id"], + max_episode_steps=config["env"]["max_episode_steps"], mesh=mesh, - max_episode_steps=env_config["max_episode_steps"], - n_darts_selected=env_config["n_darts_selected"], - deep=env_config["deep"], - action_restriction=env_config["action_restriction"], - with_degree_obs=env_config["with_degree_observation"] + # mesh_size = 30, + n_darts_selected=config["env"]["n_darts_selected"], + deep=config["env"]["deep"], + action_restriction=config["env"]["action_restriction"], + with_quality_obs=config["env"]["with_quality_observation"], + render_mode=config["env"]["render_mode"], ) #Load the model - actor = Actor(env, 10*8, 4*10, lr=0.0001) - actor.load_state_dict(torch.load('policy_saved/quad-perso/medium_quad_perso-2.pth')) - avg_steps, avg_wins, avg_rewards, normalized_return, final_meshes = testPolicy(actor, 15, env_config, dataset) + actor = Actor(env, config["env"]["obs_size"], config["ppo"]["n_actions"], n_darts_observed=config["env"]["n_darts_selected"], lr=0.0001) + actor.load_state_dict(torch.load('policy_saved/tri-perso/TEST-Exploit.pth')) + avg_steps, avg_wins, avg_rewards, normalized_return, final_meshes = testPolicy(actor, 15, config, dataset) plot_test_results(avg_rewards, avg_wins, avg_steps, normalized_return) plot_dataset(final_meshes) diff --git a/training/exploit_SB3_policy.py b/training/exploit_SB3_policy.py index 132f234..15ebcf2 100644 --- a/training/exploit_SB3_policy.py +++ b/training/exploit_SB3_policy.py @@ -1,5 +1,5 @@ import copy -import json +import yaml import gymnasium as gym import numpy as np @@ -8,15 +8,23 @@ from numpy import ndarray from stable_baselines3 import PPO -from mesh_model.mesh_analysis.global_mesh_analysis import global_score +from environment.actions.triangular_actions import flip_edge +from mesh_model.mesh_analysis.global_mesh_analysis import GlobalMeshAnalysis +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshOldAnalysis +from mesh_model.mesh_analysis.quadmesh_analysis import QuadMeshOldAnalysis +from mesh_model.mesh_analysis.trimesh_analysis import TriMeshQualityAnalysis from mesh_model.mesh_struct.mesh import Mesh +from mesh_model.mesh_struct.mesh_elements import Dart +from mesh_model.random_trimesh import random_mesh, regular_mesh +from environment.actions.triangular_actions import flip_edge_ids, split_edge_ids from mesh_model.reader import read_gmsh from view.mesh_plotter.create_plots import plot_test_results -from view.mesh_plotter.mesh_plots import plot_dataset +from view.mesh_plotter.mesh_plots import plot_dataset, plot_mesh from environment.actions.smoothing import smoothing_mean -import mesh_model.random_quadmesh as QM -from environment.gymnasium_envs.quadmesh_env.envs.quadmesh import QuadMeshEnv +from environment.gymnasium_envs import trimesh_full_env + + def testPolicy( model, @@ -42,12 +50,13 @@ def testPolicy( best_mesh = mesh env = gym.make( config["eval"]["eval_env_id"], - max_episode_steps=20, + max_episode_steps=config["eval"]["max_episode_steps"], mesh = mesh, + #mesh_size = 30, n_darts_selected=config["eval"]["n_darts_selected"], deep= config["eval"]["deep"], action_restriction=config["eval"]["action_restriction"], - with_degree_obs=config["eval"]["with_degree_observation"], + with_degree_obs=config["eval"]["with_quality_observation"], render_mode = config["eval"]["render_mode"], ) for _ in range(n_eval_episodes): @@ -66,7 +75,7 @@ def testPolicy( ep_length += 1 if terminated: nb_wins[i-1] += 1 - if isBetterMesh(best_mesh, info['mesh']): + if isBetterMesh(best_mesh, info['mesh'], config["env"]["analysis_type"]): best_mesh = copy.deepcopy(info['mesh']) avg_length[i-1] += ep_length avg_mesh_rewards[i-1] += ep_mesh_rewards @@ -82,25 +91,50 @@ def isBetterPolicy(actual_best_policy, policy_to_test): if actual_best_policy is None: return True -def isBetterMesh(best_mesh, actual_mesh): - if best_mesh is None or global_score(best_mesh)[1] > global_score(actual_mesh)[1]: +def isBetterMesh(best_mesh, actual_mesh, analysis_type): + tri = False + for d_info in actual_mesh.dart_info: + if d_info[0]>=0: + d = Dart(actual_mesh, d_info[0]) + if d == ((d.get_beta(1)).get_beta(1)).get_beta(1): + tri = True + else: + tri = False + break + if tri: + if analysis_type == "old": + ma_best_mesh = TriMeshOldAnalysis(best_mesh) + ma_actual_mesh = TriMeshOldAnalysis(actual_mesh) + else: + ma_best_mesh = TriMeshQualityAnalysis(best_mesh) + ma_actual_mesh = TriMeshQualityAnalysis(actual_mesh) + else: + ma_best_mesh = QuadMeshOldAnalysis(best_mesh) + ma_actual_mesh = QuadMeshOldAnalysis(actual_mesh) + if best_mesh is None or ma_best_mesh.global_score()[1] > ma_actual_mesh.global_score()[1]: return True else: return False if __name__ == '__main__': - #Create a dataset of 9 meshes - mesh = read_gmsh("../mesh_files/medium_quad.msh") - dataset = [mesh for _ in range(9)] - with open("../environment/environment_config.json", "r") as f: - env_config = json.load(f) + mesh = read_gmsh("../mesh_files/tri-star.msh") + # ma = TriMeshQualityAnalysis(mesh) + # split_edge_ids(ma, 5, 2) + # split_edge_ids(ma, 2, 10) + # flip_edge_ids(ma, 3,7) + # #plot_mesh(mesh) + + dataset = [mesh for _ in range(1)] + # PARAMETERS CONFIGURATION + with open("../training/config/trimesh_config_PPO_SB3.yaml", "r") as f: + config = yaml.safe_load(f) plot_dataset(dataset) #Load the model - model = PPO.load("policy_saved/quad/4-actions-quad-simple-PPO43.zip") - avg_steps, avg_wins, avg_rewards, avg_normalized_return, final_meshes = testPolicy(model, 1, env_config, dataset) + model = PPO.load("policy_saved/tri-sb3/PPO_SB3_tri-delaunay-v0.zip") + avg_steps, avg_wins, avg_rewards, avg_normalized_return, final_meshes = testPolicy(model, 1, config, dataset) plot_test_results(avg_rewards, avg_wins, avg_steps, avg_normalized_return) plot_dataset(final_meshes) diff --git a/training/exploit_trimesh.py b/training/exploit_trimesh.py index b8c323f..4cd2c80 100644 --- a/training/exploit_trimesh.py +++ b/training/exploit_trimesh.py @@ -2,8 +2,8 @@ import torch import json import gymnasium as gym -from environment.trimesh_env import TriMesh -from model_RL.utilities.actor_critic_networks import Actor +from environment.old_files.trimesh_env import TriMesh +from model_RL.old_files.utilities import Actor from mesh_model.reader import read_gmsh from view.mesh_plotter.create_plots import plot_test_results from view.mesh_plotter.mesh_plots import plot_dataset @@ -39,7 +39,7 @@ def exploit(): #Create a dataset of 9 meshes dataset = [mesh for _ in range(9)] - with open("../environment/environment_config.json", "r") as f: + with open("../environment/old_files/environment_config.json", "r") as f: env_config = json.load(f) plot_dataset(dataset) diff --git a/training/results/trimesh_results/FLIP/PPO_10/events.out.tfevents.1735828642.UN00315924-UNAL.dam.intra.cea.fr.292560.0 b/training/results/0-trimesh_results/FLIP/PPO_10/events.out.tfevents.1735828642.UN00315924-UNAL.dam.intra.cea.fr.292560.0 similarity index 100% rename from training/results/trimesh_results/FLIP/PPO_10/events.out.tfevents.1735828642.UN00315924-UNAL.dam.intra.cea.fr.292560.0 rename to training/results/0-trimesh_results/FLIP/PPO_10/events.out.tfevents.1735828642.UN00315924-UNAL.dam.intra.cea.fr.292560.0 diff --git a/training/results/trimesh_results/FLIP/PPO_9/events.out.tfevents.1735828526.UN00315924-UNAL.dam.intra.cea.fr.291743.0 b/training/results/0-trimesh_results/FLIP/PPO_9/events.out.tfevents.1735828526.UN00315924-UNAL.dam.intra.cea.fr.291743.0 similarity index 100% rename from training/results/trimesh_results/FLIP/PPO_9/events.out.tfevents.1735828526.UN00315924-UNAL.dam.intra.cea.fr.291743.0 rename to training/results/0-trimesh_results/FLIP/PPO_9/events.out.tfevents.1735828526.UN00315924-UNAL.dam.intra.cea.fr.291743.0 diff --git a/training/results/trimesh_results/e1/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 b/training/results/0-trimesh_results/e1/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 rename to training/results/0-trimesh_results/e1/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 diff --git a/training/results/trimesh_results/e1/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 b/training/results/0-trimesh_results/e1/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 rename to training/results/0-trimesh_results/e1/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 diff --git a/training/results/trimesh_results/e1/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 b/training/results/0-trimesh_results/e1/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 rename to training/results/0-trimesh_results/e1/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 diff --git a/training/results/trimesh_results/e1/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 b/training/results/0-trimesh_results/e1/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 rename to training/results/0-trimesh_results/e1/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 diff --git a/training/results/trimesh_results/e1/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 b/training/results/0-trimesh_results/e1/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 rename to training/results/0-trimesh_results/e1/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 diff --git a/training/results/trimesh_results/e1/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/e1/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/e1/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/e1/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/e2/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 b/training/results/0-trimesh_results/e2/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 rename to training/results/0-trimesh_results/e2/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 diff --git a/training/results/trimesh_results/e2/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 b/training/results/0-trimesh_results/e2/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 rename to training/results/0-trimesh_results/e2/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 diff --git a/training/results/trimesh_results/e2/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 b/training/results/0-trimesh_results/e2/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 rename to training/results/0-trimesh_results/e2/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 diff --git a/training/results/trimesh_results/e2/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/e2/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/e2/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/e2/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 b/training/results/0-trimesh_results/e2/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 rename to training/results/0-trimesh_results/e2/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 diff --git a/training/results/trimesh_results/e2/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 b/training/results/0-trimesh_results/e2/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 rename to training/results/0-trimesh_results/e2/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 diff --git a/training/results/trimesh_results/e2/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 b/training/results/0-trimesh_results/e2/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 rename to training/results/0-trimesh_results/e2/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 diff --git a/training/results/trimesh_results/e2/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 b/training/results/0-trimesh_results/e2/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 rename to training/results/0-trimesh_results/e2/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 diff --git a/training/results/trimesh_results/e2/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 b/training/results/0-trimesh_results/e2/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 similarity index 100% rename from training/results/trimesh_results/e2/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 rename to training/results/0-trimesh_results/e2/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 diff --git a/training/results/trimesh_results/e3/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 b/training/results/0-trimesh_results/e3/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 rename to training/results/0-trimesh_results/e3/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 diff --git a/training/results/trimesh_results/e3/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 b/training/results/0-trimesh_results/e3/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 rename to training/results/0-trimesh_results/e3/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 diff --git a/training/results/trimesh_results/e3/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 b/training/results/0-trimesh_results/e3/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 rename to training/results/0-trimesh_results/e3/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 diff --git a/training/results/trimesh_results/e3/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 b/training/results/0-trimesh_results/e3/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 rename to training/results/0-trimesh_results/e3/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 diff --git a/training/results/trimesh_results/e3/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 b/training/results/0-trimesh_results/e3/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 rename to training/results/0-trimesh_results/e3/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 diff --git a/training/results/trimesh_results/e3/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 b/training/results/0-trimesh_results/e3/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 rename to training/results/0-trimesh_results/e3/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 diff --git a/training/results/trimesh_results/e3/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 b/training/results/0-trimesh_results/e3/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 rename to training/results/0-trimesh_results/e3/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 diff --git a/training/results/trimesh_results/e3/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/e3/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/e3/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/e3/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/e4/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 b/training/results/0-trimesh_results/e4/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 similarity index 100% rename from training/results/trimesh_results/e4/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 rename to training/results/0-trimesh_results/e4/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 diff --git a/training/results/trimesh_results/e4/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 b/training/results/0-trimesh_results/e4/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 similarity index 100% rename from training/results/trimesh_results/e4/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 rename to training/results/0-trimesh_results/e4/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 diff --git a/training/results/trimesh_results/e4/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 b/training/results/0-trimesh_results/e4/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 similarity index 100% rename from training/results/trimesh_results/e4/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 rename to training/results/0-trimesh_results/e4/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 diff --git a/training/results/trimesh_results/e4/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 b/training/results/0-trimesh_results/e4/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 similarity index 100% rename from training/results/trimesh_results/e4/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 rename to training/results/0-trimesh_results/e4/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 diff --git a/training/results/trimesh_results/e5/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 b/training/results/0-trimesh_results/e5/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 rename to training/results/0-trimesh_results/e5/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 diff --git a/training/results/trimesh_results/e5/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 b/training/results/0-trimesh_results/e5/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 rename to training/results/0-trimesh_results/e5/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 diff --git a/training/results/trimesh_results/e5/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 b/training/results/0-trimesh_results/e5/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 rename to training/results/0-trimesh_results/e5/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 diff --git a/training/results/trimesh_results/e5/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 b/training/results/0-trimesh_results/e5/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 rename to training/results/0-trimesh_results/e5/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 diff --git a/training/results/trimesh_results/e5/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 b/training/results/0-trimesh_results/e5/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 rename to training/results/0-trimesh_results/e5/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 diff --git a/training/results/trimesh_results/e5/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 b/training/results/0-trimesh_results/e5/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 similarity index 100% rename from training/results/trimesh_results/e5/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 rename to training/results/0-trimesh_results/e5/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 diff --git a/training/results/trimesh_results/e6/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 b/training/results/0-trimesh_results/e6/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 similarity index 100% rename from training/results/trimesh_results/e6/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 rename to training/results/0-trimesh_results/e6/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 diff --git a/training/results/trimesh_results/e6/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 b/training/results/0-trimesh_results/e6/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 similarity index 100% rename from training/results/trimesh_results/e6/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 rename to training/results/0-trimesh_results/e6/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 diff --git a/training/results/trimesh_results/e6/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 b/training/results/0-trimesh_results/e6/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 similarity index 100% rename from training/results/trimesh_results/e6/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 rename to training/results/0-trimesh_results/e6/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 diff --git a/training/results/trimesh_results/e6/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 b/training/results/0-trimesh_results/e6/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 similarity index 100% rename from training/results/trimesh_results/e6/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 rename to training/results/0-trimesh_results/e6/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 diff --git a/training/results/trimesh_results/efull/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 b/training/results/0-trimesh_results/efull/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 rename to training/results/0-trimesh_results/efull/PPO_1/events.out.tfevents.1735818941.UN00315924-UNAL.dam.intra.cea.fr.253347.0 diff --git a/training/results/trimesh_results/efull/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 b/training/results/0-trimesh_results/efull/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 rename to training/results/0-trimesh_results/efull/PPO_10-v0/events.out.tfevents.1734446254.UN00315924-UNAL.dam.intra.cea.fr.817111.0 diff --git a/training/results/trimesh_results/efull/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 b/training/results/0-trimesh_results/efull/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 rename to training/results/0-trimesh_results/efull/PPO_10-v1/events.out.tfevents.1734553188.UN00315924-UNAL.dam.intra.cea.fr.173728.0 diff --git a/training/results/trimesh_results/efull/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 b/training/results/0-trimesh_results/efull/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 rename to training/results/0-trimesh_results/efull/PPO_20-v0/events.out.tfevents.1734432461.UN00315924-UNAL.dam.intra.cea.fr.760588.0 diff --git a/training/results/trimesh_results/efull/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/efull/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/efull/PPO_20-v1/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/efull/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 b/training/results/0-trimesh_results/efull/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 rename to training/results/0-trimesh_results/efull/PPO_30-v/events.out.tfevents.1734555115.UN00315924-UNAL.dam.intra.cea.fr.181453.0 diff --git a/training/results/trimesh_results/efull/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 b/training/results/0-trimesh_results/efull/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 rename to training/results/0-trimesh_results/efull/PPO_30-v0/events.out.tfevents.1734435261.UN00315924-UNAL.dam.intra.cea.fr.775239.0 diff --git a/training/results/trimesh_results/efull/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 b/training/results/0-trimesh_results/efull/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 rename to training/results/0-trimesh_results/efull/PPO_30-v1/events.out.tfevents.1734559178.UN00315924-UNAL.dam.intra.cea.fr.195914.0 diff --git a/training/results/trimesh_results/efull/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 b/training/results/0-trimesh_results/efull/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 rename to training/results/0-trimesh_results/efull/PPO_4/events.out.tfevents.1735770299.UN00315924-UNAL.dam.intra.cea.fr.132277.0 diff --git a/training/results/trimesh_results/efull/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 b/training/results/0-trimesh_results/efull/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 rename to training/results/0-trimesh_results/efull/PPO_5-v0/events.out.tfevents.1734432449.UN00315924-UNAL.dam.intra.cea.fr.760459.0 diff --git a/training/results/trimesh_results/efull/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 b/training/results/0-trimesh_results/efull/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 rename to training/results/0-trimesh_results/efull/PPO_5-v1/events.out.tfevents.1734552568.UN00315924-UNAL.dam.intra.cea.fr.170760.0 diff --git a/training/results/trimesh_results/efull/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 b/training/results/0-trimesh_results/efull/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 rename to training/results/0-trimesh_results/efull/PPO_8_deep3-v1/events.out.tfevents.1734611428.UN00315924-UNAL.dam.intra.cea.fr.278749.0 diff --git a/training/results/trimesh_results/efull/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 b/training/results/0-trimesh_results/efull/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 rename to training/results/0-trimesh_results/efull/PPO_deep12-v0/events.out.tfevents.1734598832.UN00315924-UNAL.dam.intra.cea.fr.221925.0 diff --git a/training/results/trimesh_results/efull/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 b/training/results/0-trimesh_results/efull/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 rename to training/results/0-trimesh_results/efull/PPO_deep12-v1/events.out.tfevents.1734606176.UN00315924-UNAL.dam.intra.cea.fr.253849.0 diff --git a/training/results/trimesh_results/efull/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 b/training/results/0-trimesh_results/efull/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 rename to training/results/0-trimesh_results/efull/PPO_deep3-v0/events.out.tfevents.1734559585.UN00315924-UNAL.dam.intra.cea.fr.197662.0 diff --git a/training/results/trimesh_results/efull/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 b/training/results/0-trimesh_results/efull/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 rename to training/results/0-trimesh_results/efull/PPO_deep3-v1/events.out.tfevents.1734606200.UN00315924-UNAL.dam.intra.cea.fr.254037.0 diff --git a/training/results/trimesh_results/efull/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 b/training/results/0-trimesh_results/efull/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 rename to training/results/0-trimesh_results/efull/PPO_deep3-v2/events.out.tfevents.1734618832.UN00315924-UNAL.dam.intra.cea.fr.310315.0 diff --git a/training/results/trimesh_results/efull/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 b/training/results/0-trimesh_results/efull/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 rename to training/results/0-trimesh_results/efull/PPO_deep6-v0/events.out.tfevents.1734598818.UN00315924-UNAL.dam.intra.cea.fr.221824.0 diff --git a/training/results/trimesh_results/efull/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/efull/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/efull/PPO_deep6-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/efull/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 b/training/results/0-trimesh_results/efull/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 rename to training/results/0-trimesh_results/efull/PPO_l30-v0/events.out.tfevents.1735812367.UN00315924-UNAL.dam.intra.cea.fr.228874.0 diff --git a/training/results/trimesh_results/efull/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 b/training/results/0-trimesh_results/efull/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 rename to training/results/0-trimesh_results/efull/PPO_l50-v0/events.out.tfevents.1735812393.UN00315924-UNAL.dam.intra.cea.fr.229064.0 diff --git a/training/results/trimesh_results/efull/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 b/training/results/0-trimesh_results/efull/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 rename to training/results/0-trimesh_results/efull/PPO_l50-v1/events.out.tfevents.1735818955.UN00315924-UNAL.dam.intra.cea.fr.253446.0 diff --git a/training/results/trimesh_results/efull/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 b/training/results/0-trimesh_results/efull/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 rename to training/results/0-trimesh_results/efull/PPO_m16-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 diff --git a/training/results/trimesh_results/efull/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 b/training/results/0-trimesh_results/efull/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 rename to training/results/0-trimesh_results/efull/PPO_m16-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 diff --git a/training/results/trimesh_results/efull/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 b/training/results/0-trimesh_results/efull/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 rename to training/results/0-trimesh_results/efull/PPO_m25-v0/events.out.tfevents.1735744674.UN00315924-UNAL.dam.intra.cea.fr.63182.0 diff --git a/training/results/trimesh_results/efull/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 b/training/results/0-trimesh_results/efull/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 rename to training/results/0-trimesh_results/efull/PPO_m25-v1/events.out.tfevents.1735761690.UN00315924-UNAL.dam.intra.cea.fr.109198.0 diff --git a/training/results/trimesh_results/efull/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 b/training/results/0-trimesh_results/efull/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 rename to training/results/0-trimesh_results/efull/PPO_m40-v0/events.out.tfevents.1735744703.UN00315924-UNAL.dam.intra.cea.fr.63292.0 diff --git a/training/results/trimesh_results/efull/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 b/training/results/0-trimesh_results/efull/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 rename to training/results/0-trimesh_results/efull/PPO_no_deg-v0/events.out.tfevents.1735664190.UN00315924-UNAL.dam.intra.cea.fr.16322.0 diff --git a/training/results/trimesh_results/efull/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 b/training/results/0-trimesh_results/efull/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 rename to training/results/0-trimesh_results/efull/PPO_no_deg-v1/events.out.tfevents.1735739679.UN00315924-UNAL.dam.intra.cea.fr.49745.0 diff --git a/training/results/trimesh_results/efull/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 b/training/results/0-trimesh_results/efull/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 rename to training/results/0-trimesh_results/efull/PPO_restricted-v0/events.out.tfevents.1734431836.UN00315924-UNAL.dam.intra.cea.fr.757470.0 diff --git a/training/results/trimesh_results/efull/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 b/training/results/0-trimesh_results/efull/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 rename to training/results/0-trimesh_results/efull/PPO_restricted-v1/events.out.tfevents.1734514338.UN00315924-UNAL.dam.intra.cea.fr.70489.0 diff --git a/training/results/trimesh_results/efull/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 b/training/results/0-trimesh_results/efull/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 rename to training/results/0-trimesh_results/efull/PPO_restricted-v2/events.out.tfevents.1734517568.UN00315924-UNAL.dam.intra.cea.fr.83005.0 diff --git a/training/results/trimesh_results/efull/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 b/training/results/0-trimesh_results/efull/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 rename to training/results/0-trimesh_results/efull/PPO_unrestricted-v0/events.out.tfevents.1734431823.UN00315924-UNAL.dam.intra.cea.fr.757302.0 diff --git a/training/results/trimesh_results/efull/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 b/training/results/0-trimesh_results/efull/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 rename to training/results/0-trimesh_results/efull/PPO_unrestricted-v1/events.out.tfevents.1734513709.UN00315924-UNAL.dam.intra.cea.fr.67221.0 diff --git a/training/results/trimesh_results/efull/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 b/training/results/0-trimesh_results/efull/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 rename to training/results/0-trimesh_results/efull/PPO_unrestricted-v2/events.out.tfevents.1734516617.UN00315924-UNAL.dam.intra.cea.fr.79025.0 diff --git a/training/results/trimesh_results/efull/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 b/training/results/0-trimesh_results/efull/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 rename to training/results/0-trimesh_results/efull/PPO_with_deg-v0/events.out.tfevents.1735663450.UN00315924-UNAL.dam.intra.cea.fr.13339.0 diff --git a/training/results/trimesh_results/efull/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 b/training/results/0-trimesh_results/efull/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 similarity index 100% rename from training/results/trimesh_results/efull/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 rename to training/results/0-trimesh_results/efull/PPO_with_deg-v1/events.out.tfevents.1735670821.UN00315924-UNAL.dam.intra.cea.fr.37218.0 diff --git a/training/results/trimesh_results/final/PPO_1/events.out.tfevents.1735901763.UN00315924-UNAL.dam.intra.cea.fr.608011.0 b/training/results/0-trimesh_results/final/PPO_1/events.out.tfevents.1735901763.UN00315924-UNAL.dam.intra.cea.fr.608011.0 similarity index 100% rename from training/results/trimesh_results/final/PPO_1/events.out.tfevents.1735901763.UN00315924-UNAL.dam.intra.cea.fr.608011.0 rename to training/results/0-trimesh_results/final/PPO_1/events.out.tfevents.1735901763.UN00315924-UNAL.dam.intra.cea.fr.608011.0 diff --git a/training/results/trimesh_results/final/PPO_2/events.out.tfevents.1735901862.UN00315924-UNAL.dam.intra.cea.fr.608834.0 b/training/results/0-trimesh_results/final/PPO_2/events.out.tfevents.1735901862.UN00315924-UNAL.dam.intra.cea.fr.608834.0 similarity index 100% rename from training/results/trimesh_results/final/PPO_2/events.out.tfevents.1735901862.UN00315924-UNAL.dam.intra.cea.fr.608834.0 rename to training/results/0-trimesh_results/final/PPO_2/events.out.tfevents.1735901862.UN00315924-UNAL.dam.intra.cea.fr.608834.0 diff --git a/training/results/trimesh_results/final/PPO_3/events.out.tfevents.1738140564.UN00315924-UNAL.dam.intra.cea.fr.233861.0 b/training/results/0-trimesh_results/final/PPO_3/events.out.tfevents.1738140564.UN00315924-UNAL.dam.intra.cea.fr.233861.0 similarity index 100% rename from training/results/trimesh_results/final/PPO_3/events.out.tfevents.1738140564.UN00315924-UNAL.dam.intra.cea.fr.233861.0 rename to training/results/0-trimesh_results/final/PPO_3/events.out.tfevents.1738140564.UN00315924-UNAL.dam.intra.cea.fr.233861.0 diff --git a/training/results/trimesh_results/final/PPO_4/events.out.tfevents.1738157639.UN00315924-UNAL.dam.intra.cea.fr.304319.0 b/training/results/0-trimesh_results/final/PPO_4/events.out.tfevents.1738157639.UN00315924-UNAL.dam.intra.cea.fr.304319.0 similarity index 100% rename from training/results/trimesh_results/final/PPO_4/events.out.tfevents.1738157639.UN00315924-UNAL.dam.intra.cea.fr.304319.0 rename to training/results/0-trimesh_results/final/PPO_4/events.out.tfevents.1738157639.UN00315924-UNAL.dam.intra.cea.fr.304319.0 diff --git a/training/results/trimesh_results/test/PPO_1/events.out.tfevents.1736411485.UN00315924-UNAL.dam.intra.cea.fr.1486540.0 b/training/results/0-trimesh_results/test/PPO_1/events.out.tfevents.1736411485.UN00315924-UNAL.dam.intra.cea.fr.1486540.0 similarity index 100% rename from training/results/trimesh_results/test/PPO_1/events.out.tfevents.1736411485.UN00315924-UNAL.dam.intra.cea.fr.1486540.0 rename to training/results/0-trimesh_results/test/PPO_1/events.out.tfevents.1736411485.UN00315924-UNAL.dam.intra.cea.fr.1486540.0 diff --git a/training/results/files_csv/curve_smoothing.py b/training/results/files_csv/curve_smoothing.py index 28c9e8e..02913f5 100644 --- a/training/results/files_csv/curve_smoothing.py +++ b/training/results/files_csv/curve_smoothing.py @@ -1,9 +1,9 @@ import pandas as pd -df = pd.read_csv("PPO_22.csv") +df = pd.read_csv("medium_quad_perso.csv") if 'Value' in df.columns: alpha =0.1 df["Smoothed"]=df["Value"].ewm(alpha=alpha, adjust=False).mean() - df.to_csv("PPO_SB3_fixed_quad.csv",index=False) \ No newline at end of file + df.to_csv("medium_quad_perso.csv",index=False) \ No newline at end of file diff --git a/training/train_quadmesh.py b/training/train_quadmesh.py index 6e76a28..deb6915 100644 --- a/training/train_quadmesh.py +++ b/training/train_quadmesh.py @@ -1,30 +1,23 @@ from __future__ import annotations -import wandb -import time -import yaml -import os import random import torch +import yaml +import os +import time +import wandb -import numpy as np -import gymnasium as gym import matplotlib.pyplot as plt +import gymnasium as gym +import numpy as np from torch.utils.tensorboard import SummaryWriter #Internal import from environment.gymnasium_envs import quadmesh_env - -from mesh_model.random_quadmesh import random_mesh from mesh_model.reader import read_gmsh from model_RL.PPO_model_pers import PPO -from model_RL.evaluate_model import testPolicy - -from view.mesh_plotter.create_plots import plot_training_results, plot_test_results -from view.mesh_plotter.mesh_plots import plot_dataset - def log_init(log_writer, config): @@ -61,9 +54,11 @@ def log_end(log_writer, config, obs_registry): if __name__ == '__main__': # PARAMETERS CONFIGURATION - with open("training/config_PPO_perso.yaml", "r") as f: + with open("training/config/quadmesh_config_PPO_perso.yaml", "r") as f: config = yaml.safe_load(f) + experiment_name = config["experiment_name"] + # Create log dir log_dir = config["paths"]["log_dir"] os.makedirs(log_dir, exist_ok=True) @@ -100,6 +95,8 @@ def log_end(log_writer, config, obs_registry): model = PPO( env=env, obs_size= config["env"]["obs_size"], + n_actions=config["ppo"]["n_actions"], + n_darts_observed=config["env"]["n_darts_selected"], max_steps=config["env"]["max_episode_steps"], lr=config["ppo"]["learning_rate"], gamma=config["ppo"]["gamma"], diff --git a/training/train_quadmesh_SB3.py b/training/train_quadmesh_SB3.py index e7d6dae..0c0ed66 100644 --- a/training/train_quadmesh_SB3.py +++ b/training/train_quadmesh_SB3.py @@ -8,8 +8,8 @@ import wandb import matplotlib.pyplot as plt -import numpy as np import gymnasium as gym +import numpy as np from stable_baselines3 import PPO @@ -231,7 +231,7 @@ def _on_training_end(self) -> None: if __name__ == '__main__': # PARAMETERS CONFIGURATION - with open("training/config_PPO_SB3.yaml", "r") as f: + with open("training/config/quadmesh_config_PPO_SB3.yaml", "r") as f: config = yaml.safe_load(f) experiment_name = config["experiment_name"] diff --git a/training/train_trimesh-flip_SB3.py b/training/train_trimesh-flip_SB3.py index 82f6cb7..6de2032 100644 --- a/training/train_trimesh-flip_SB3.py +++ b/training/train_trimesh-flip_SB3.py @@ -94,9 +94,9 @@ def _on_training_end(self) -> None: self.logger.dump(step=0) -with open("../model_RL/parameters/ppo_config.json", "r") as f: +with open("../model_RL/old_files/ppo_config.json", "r") as f: ppo_config = json.load(f) -with open("../environment/environment_config.json", "r") as f: +with open("../environment/old_files/environment_config.json", "r") as f: env_config = json.load(f) # Create log dir diff --git a/training/train_trimesh.py b/training/train_trimesh.py index 0d67e81..0516259 100644 --- a/training/train_trimesh.py +++ b/training/train_trimesh.py @@ -1,43 +1,100 @@ -import mesh_model.random_trimesh as TM +from __future__ import annotations + +import random import torch -from environment.trimesh_env import TriMesh +import os +import time +import yaml +import wandb + +import gymnasium as gym +import numpy as np + +from torch.utils.tensorboard import SummaryWriter +from stable_baselines3.common.env_checker import check_env + +#Internal import +from environment.gymnasium_envs import trimesh_full_env +from model_RL.PPO_model_pers import PPO + + +def log_init(log_writer, config): + log_writer.add_text("Description", config["description"]) + log_writer.add_hparams(hparam_dict=config["env"], metric_dict=config["metrics"], run_name=config["experiment_name"]) + + +if __name__ == '__main__': + + # PARAMETERS CONFIGURATION + with open("training/config/trimesh_config_PPO_perso.yaml", "r") as f: + config = yaml.safe_load(f) -from view.mesh_plotter.create_plots import plot_training_results, plot_test_results -from view.mesh_plotter.mesh_plots import plot_dataset + experiment_name = config["experiment_name"] -from model_RL.evaluate_model import testPolicy + # Create log dir + log_dir = config["paths"]["log_dir"] + os.makedirs(log_dir, exist_ok=True) -from model_RL.PPO_model import PPO -#from model_RL.SAC_model import SAC -#from model_RL.AC_model import AC + wandb.tensorboard.patch(root_logdir=log_dir) + wandb.init( + project="Trimesh-learning", + name=experiment_name, + config=config, + sync_tensorboard=True, + save_code=True + ) -LOCAL_MESH_FEAT = 0 + # SEEDING + seed = config["seed"] + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.backends.cudnn.deterministic = True + # Create the environment + env = gym.make( + config["env"]["env_id"], + # mesh=read_gmsh(config["dataset"]["evaluation_mesh_file_path"]), + mesh_size=config["env"]["mesh_size"], + max_episode_steps=config["env"]["max_episode_steps"], + n_darts_selected=config["env"]["n_darts_selected"], + deep=config["env"]["deep"], + action_restriction=config["env"]["action_restriction"], + with_quality_obs=config["env"]["with_quality_observation"], + render_mode=config["env"]["render_mode"], + analysis_type=config["env"]["analysis_type"], + ) -def train(): - mesh_size = 30 - lr = 0.0001 - gamma = 0.9 - feature = LOCAL_MESH_FEAT + check_env(env, warn=True) - dataset = [TM.random_mesh(30) for _ in range(9)] - plot_dataset(dataset) + model = PPO( + env=env, + obs_size= config["env"]["obs_size"], + n_actions=config["ppo"]["n_actions"], + n_darts_observed=config["env"]["n_darts_selected"], + max_steps=config["env"]["max_episode_steps"], + lr=config["ppo"]["learning_rate"], + gamma=config["ppo"]["gamma"], + nb_iterations=config["ppo"]["n_iterations"], + nb_episodes_per_iteration=config["ppo"]["n_episodes_per_iteration"], + nb_epochs=config["ppo"]["n_epochs"], + batch_size=config["ppo"]["batch_size"], + ) - env = TriMesh(None, mesh_size, max_steps=80, feat=feature) + writer = SummaryWriter(log_dir + config["experiment_name"]) + log_init(writer, config) - # 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) + # LEARNING + start_time = time.perf_counter() + print("-----------Starting learning-----------") - model = PPO(env, lr, gamma, nb_iterations=3, nb_episodes_per_iteration=10, nb_epochs=2, batch_size=8) - actor, rewards, wins, steps = model.train() - if rewards is not None: - plot_training_results(rewards, wins, steps) + actor, rewards, wins, steps, obs_registry = model.learn(writer) - # torch.save(actor.state_dict(), 'policy_saved/actor_network.pth') - avg_steps, avg_wins, avg_rewards, final_meshes = testPolicy(actor, 5, dataset, 60) + end_time = time.perf_counter() + print("-----------Learning ended------------") + print(f"Temps d'apprentissage : {end_time - start_time:.4} s") - if rewards is not None: - plot_test_results(avg_rewards, avg_wins, avg_steps, avg_rewards) - plot_dataset(final_meshes) + # SAVING POLICY + torch.save(actor.state_dict(), config["paths"]["policy_saving_dir"]+config["experiment_name"]+".pth") + writer.close() + wandb.finish() \ No newline at end of file diff --git a/training/train_trimesh_SB3.py b/training/train_trimesh_SB3.py index 1dc3bf0..b836c07 100644 --- a/training/train_trimesh_SB3.py +++ b/training/train_trimesh_SB3.py @@ -1,18 +1,75 @@ from __future__ import annotations +import random +import torch import os -import json +import time +import yaml +import wandb -import mesh_model.random_trimesh as TM -from view.mesh_plotter.mesh_plots import dataset_plt -from training.exploit_SB3_policy import testPolicy +import gymnasium as gym +import numpy as np + +from copy import deepcopy from stable_baselines3 import PPO from stable_baselines3.common.env_checker import check_env from stable_baselines3.common.callbacks import BaseCallback -from stable_baselines3.common.logger import Figure +from stable_baselines3.common.logger import Figure, HParam + +from wandb.integration.sb3 import WandbCallback + +from environment.actions.smoothing import smoothing_mean +from mesh_model.reader import read_gmsh +from view.mesh_plotter.mesh_plots import dataset_plt, plot_mesh +from training.exploit_SB3_policy import testPolicy + from environment.gymnasium_envs import trimesh_full_env -import gymnasium as gym +import mesh_model.random_trimesh as TM + + +class HParamCallback(BaseCallback): + """ + Saves the hyperparameters and metrics at the start of the training, and logs them to TensorBoard. + """ + + def _on_training_start(self) -> None: + hparam_dict = { + "algorithm": self.model.__class__.__name__, + "experiment": experiment_name, + "description": config["description"], + "learning rate": self.model.learning_rate, + "gamma": self.model.gamma, + "batch_size": config["ppo"]["batch_size"], + "epochs": config["ppo"]["n_epochs"], + "clip_range": config["ppo"]["clip_range"], + "training_meshes": config["dataset"]["training_mesh_file_path"], + "evaluation_meshes": config["dataset"]["evaluation_mesh_file_path"], + "max_steps": config["env"]["max_episode_steps"], + "max_timesteps": config["total_timesteps"], + "deep": config["env"]["deep"], + "with_quality": config["env"]["with_quality_observation"], + "nb_darts_selected": config["env"]["n_darts_selected"], + "reward_mode": config["env"]["reward_function"], + + + } + # define the metrics that will appear in the `HPARAMS` Tensorboard tab by referencing their tag + # Tensorbaord will find & display metrics from the `SCALARS` tab + metric_dict = { + "normalized_return": 0, + "rollout/ep_len_mean": 0.0, + "rollout/ep_rew_mean": 0.0 + } + self.logger.record( + "hparams", + HParam(hparam_dict, metric_dict), + exclude=("stdout", "log", "json", "csv"), + ) + + def _on_step(self) -> bool: + return True + class TensorboardCallback(BaseCallback): """ @@ -40,14 +97,6 @@ def __init__(self, model, verbose=0): self.final_distance = 0 self.normalized_return = 0 - def _on_training_start(self) -> None: - """ - Record PPO parameters and environment configuration at the training start. - """ - self.logger.record("parameters/ppo", f"
{json.dumps(ppo_config, indent=4)}
") - self.logger.record("parameters/env", f"
{json.dumps(env_config, indent=4)}
") - self.logger.dump(step=0) - def _on_step(self) -> bool: """ Record different learning variables to monitor @@ -65,6 +114,7 @@ def _on_step(self) -> bool: self.actions_info["nb_invalid_split"] += self.locals["infos"][0].get("invalid_split", 0.0) self.actions_info["nb_invalid_collapse"] += self.locals["infos"][0].get("invalid_collapse", 0.0) + self.final_distance = self.locals["infos"][0].get("distance", 0.0) self.mesh_reward += self.locals["infos"][0].get("mesh_reward", 0.0) # When the episode is over @@ -73,20 +123,26 @@ def _on_step(self) -> bool: mesh_ideal_reward = self.locals["infos"][0].get("mesh_ideal_rewards", 0.0) # maximum achievable reward if mesh_ideal_reward > 0: self.normalized_return = self.mesh_reward/ mesh_ideal_reward + if self.normalized_return > 1: + plot_mesh(self.locals["infos"][0].get("mesh")) + smoothed_m = deepcopy(self.locals["infos"][0].get("mesh")) + smoothing_mean(smoothed_m) + plot_mesh(smoothed_m) + raise ValueError("normalized return above 1 imposssible") else: self.normalized_return = 0 - self.final_distance = self.locals["infos"][0].get("distance", 0.0) + self.logger.record("final_distance", self.final_distance) - self.logger.record("valid_actions", self.actions_info["episode_valid_actions"]*100/self.current_episode_length if self.current_episode_length > 0 else 0) + self.logger.record("valid_actions (%)", self.actions_info["episode_valid_actions"]*100/self.current_episode_length if self.current_episode_length > 0 else 0) self.logger.record("n_invalid_topo", self.actions_info["episode_invalid_topo"]) self.logger.record("n_invalid_geo", self.actions_info["episode_invalid_geo"]) self.logger.record("nb_flip", self.actions_info["nb_flip"]) self.logger.record("nb_split", self.actions_info["nb_split"]) self.logger.record("nb_collapse", self.actions_info["nb_collapse"]) - self.logger.record("invalid_flip", self.actions_info["nb_invalid_flip"]*100/self.actions_info["nb_flip"] if self.actions_info["nb_flip"] > 0 else 0) - self.logger.record("invalid_split", self.actions_info["nb_invalid_split"]*100/self.actions_info["nb_split"] if self.actions_info["nb_split"] > 0 else 0) - self.logger.record("invalid_collapse", self.actions_info["nb_invalid_collapse"]*100/self.actions_info["nb_collapse"]if self.actions_info["nb_collapse"] > 0 else 0) + self.logger.record("invalid_flip (%)", self.actions_info["nb_invalid_flip"]*100/self.actions_info["nb_flip"] if self.actions_info["nb_flip"] > 0 else 0) + self.logger.record("invalid_split (%)", self.actions_info["nb_invalid_split"]*100/self.actions_info["nb_split"] if self.actions_info["nb_split"] > 0 else 0) + self.logger.record("invalid_collapse (%)", self.actions_info["nb_invalid_collapse"]*100/self.actions_info["nb_collapse"]if self.actions_info["nb_collapse"] > 0 else 0) self.logger.record("episode_mesh_reward", self.mesh_reward) self.logger.record("episode_reward", self.current_episode_reward) @@ -111,50 +167,82 @@ def _on_training_end(self) -> None: """ Records policy evaluation results : before and after dataset images """ - dataset = [TM.random_mesh(30) for _ in range(9)] # dataset of 9 meshes of size 30 + print("-------- Testing Policy ---------") + dataset = [TM.random_mesh(30) for _ in range(4)] # dataset of 9 meshes of size 30 before = dataset_plt(dataset) # plot the datasat as image - length, wins, rewards, normalized_return, final_meshes = testPolicy(self.model, 10, env_config, dataset) # test model policy on the dataset + length, wins, rewards, normalized_return, final_meshes = testPolicy(self.model, 5, config, dataset) # test model policy on the dataset after = dataset_plt(final_meshes) self.logger.record("figures/before", Figure(before, close=True), exclude=("stdout", "log")) self.logger.record("figures/after", Figure(after, close=True), exclude=("stdout", "log")) self.logger.dump(step=0) - -with open("../model_RL/parameters/ppo_config.json", "r") as f: - ppo_config = json.load(f) -with open("../environment/environment_config.json", "r") as f: - env_config = json.load(f) - -# Create log dir -log_dir = ppo_config["tensorboard_log"] -os.makedirs(log_dir, exist_ok=True) - -# Create the environment -env = gym.make( - env_config["env_name"], - mesh_size=env_config["mesh_size"], - max_episode_steps=env_config["max_episode_steps"], - n_darts_selected=env_config["n_darts_selected"], - deep= env_config["deep"], - action_restriction=env_config["action_restriction"], - with_degree_obs=env_config["with_degree_observation"] -) - -check_env(env, warn=True) - -model = PPO( - policy=ppo_config["policy"], - env=env, - n_steps=ppo_config["n_steps"], - n_epochs=ppo_config["n_epochs"], - batch_size=ppo_config["batch_size"], - learning_rate=ppo_config["learning_rate"], - gamma=ppo_config["gamma"], - verbose=ppo_config["verbose"], - tensorboard_log=log_dir -) - -print("-----------Starting learning-----------") -model.learn(total_timesteps=ppo_config["total_timesteps"], callback=TensorboardCallback(model)) -print("-----------Learning ended------------") -model.save("policy_saved/test/test-PPO-4") \ No newline at end of file +if __name__ == '__main__': + + # PARAMETERS CONFIGURATION + with open("training/config/trimesh_config_PPO_SB3.yaml", "r") as f: + config = yaml.safe_load(f) + + experiment_name = config["experiment_name"] + + # SEEDING + seed = config["seed"] + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.backends.cudnn.deterministic = True + + # # WANDB + # run = wandb.init( + # project="Trimesh-learning", + # name=experiment_name, + # sync_tensorboard=True, # auto-upload sb3's tensorboard metrics + # save_code=True, # optional + # ) + # Create tensorboard log dir + log_dir = config["paths"]["log_dir"] + os.makedirs(log_dir, exist_ok=True) + + #training_mesh = read_gmsh(config["dataset"]["training_mesh_file_path"]) + # Create the environment + env = gym.make( + config["env"]["env_id"], + #mesh=training_mesh, + mesh_size = config["env"]["mesh_size"], + max_episode_steps=config["env"]["max_episode_steps"], + n_darts_selected=config["env"]["n_darts_selected"], + deep=config["env"]["deep"], + action_restriction=config["env"]["action_restriction"], + with_quality_obs=config["env"]["with_quality_observation"], + render_mode=config["env"]["render_mode"], + analysis_type=config["env"]["analysis_type"], + ) + + check_env(env, warn=True) + + model = PPO( + policy=config["ppo"]["policy"], + env=env, + n_steps=config["ppo"]["n_steps"], + n_epochs=config["ppo"]["n_epochs"], + batch_size=config["ppo"]["batch_size"], + learning_rate=config["ppo"]["learning_rate"], + gamma=config["ppo"]["gamma"], + verbose=1, + tensorboard_log=log_dir + ) + + start_time = time.perf_counter() + print("-----------Starting learning-----------") + model.learn( + total_timesteps=config["total_timesteps"], + tb_log_name=config["experiment_name"], + callback=[HParamCallback(), + + TensorboardCallback(model)], + progress_bar=True + ) + end_time = time.perf_counter() + print("-----------Learning ended------------") + print(f"Temps d'apprentissage : {end_time - start_time:.4} s") + model.save(config["paths"]["policy_saving_dir"] + config["experiment_name"]) + # run.finish() \ No newline at end of file diff --git a/view/graph.py b/view/graph.py index 46e95ac..343d7e4 100644 --- a/view/graph.py +++ b/view/graph.py @@ -1,6 +1,7 @@ import pygame import sys from pygame import math +import warnings edge_color_normal = pygame.Color(30, 30, 30) # Dark Grey vertex_color_normal = pygame.Color(255, 0, 0) # Red @@ -123,11 +124,14 @@ 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, n2 = None, None for v in self.vertices: if v.idx == i1: n1 = v elif v.idx == i2: n2 = v + if n1 is None or n2 is None: + warnings.warn("try to create an edge between nodes not found") self.add_edge(Edge(n1, n2)) return len(self.edges) - 1 diff --git a/view/mesh_plotter/mesh_plots.py b/view/mesh_plotter/mesh_plots.py index 5a79718..584e345 100644 --- a/view/mesh_plotter/mesh_plots.py +++ b/view/mesh_plotter/mesh_plots.py @@ -11,7 +11,8 @@ def plot_mesh(mesh: Mesh) -> None: Plot a mesh using matplotlib :param mesh: a Mesh """ - _, _ = plt.subplots() + fig, ax = plt.subplots(figsize=(15, 15)) + subplot_mesh(mesh) plt.show(block=True) @@ -43,8 +44,37 @@ def subplot_mesh(mesh: Mesh) -> None: n1 = d1.get_node() n2 = d2.get_node() n3 = d3.get_node() + + # Nodes coordinates + p1 = np.array([n1.x(), n1.y()]) + p2 = np.array([n2.x(), n2.y()]) + p3 = np.array([n3.x(), n3.y()]) + polygon = np.array([(n1.x(), n1.y()), (n2.x(), n2.y()), (n3.x(), n3.y()), (n1.x(), n1.y())]) plt.plot(polygon[:, 0], polygon[:, 1], 'k-') + + # Plot darts ID + mid1 = (p1 + p2) / 2 + mid2 = (p2 + p3) / 2 + mid3 = (p3 + p1) / 2 + + centroid = (p1 + p2 + p3) / 3 + + pos1 = mid1 +0.2* (centroid - mid1) + pos2 = mid2 +0.2* (centroid - mid2) + pos3 = mid3 +0.2* (centroid - mid3) + + plt.text(*pos1, f"{d1.id}", color='blue', fontsize=10, ha='center', va='center') + plt.text(*pos2, f"{d2.id}", color='blue', fontsize=10, ha='center', va='center') + plt.text(*pos3, f"{d3.id}", color='blue', fontsize=10, ha='center', va='center') + + # Plot nodes ID + n_id =0 + for n_info in mesh.nodes: + if n_info[2] >=0: + plt.text(n_info[0] + 0.03, n_info[1] - 0.02, f"{n_id}", fontsize=12, color='red', ha='right', va='top') + n_id+=1 + elif quad: for dart_id in faces: d1 = Dart(mesh, dart_id)