diff --git a/src/ada/api/containers.py b/src/ada/api/containers.py index fe9f63451..127f62bbd 100644 --- a/src/ada/api/containers.py +++ b/src/ada/api/containers.py @@ -1048,13 +1048,14 @@ def insert_node(n, i): def remove(self, nodes: Union[Node, Iterable[Node]]): """Remove node(s) from the nodes container""" nodes = list(nodes) if isinstance(nodes, Iterable) else [nodes] - for node in nodes: - if node in self._nodes: - logger.debug(f"Removing {node}") - self._nodes.pop(self._nodes.index(node)) - self.renumber() + ids = [node.id for node in nodes] + for node_id in ids: + if node_id in self._idmap.keys(): + self._idmap.pop(node_id) else: - logger.error(f"'{node}' not found in node-container.") + logger.error(f"'{node_id}' not found in node-container.") + self._nodes = list(self._idmap.values()) + self.renumber() def remove_standalones(self) -> None: """Remove nodes that are without any usage references""" diff --git a/src/ada/api/spatial/part.py b/src/ada/api/spatial/part.py index e71f31a84..9e158cd0d 100644 --- a/src/ada/api/spatial/part.py +++ b/src/ada/api/spatial/part.py @@ -720,6 +720,7 @@ def to_fem_obj( experimental_pl_splitting=True, name=None, debug_mode=False, + merge_coincident_nodes=True, ) -> FEM: from ada import Beam, Plate, Shape from ada.fem.elements import Mass @@ -769,6 +770,12 @@ def to_fem_obj( n = fem.nodes.add(Node(cog_absolute)) fem.add_mass(Mass(f"{mass_shape.name}_mass", [n], mass_shape.mass)) + if merge_coincident_nodes: + n_before = len(fem.nodes) + fem.nodes.remove_standalones() + n_after = len(fem.nodes) + logger.info(f"Removed {n_before - n_after} standalone nodes") + return fem def to_gltf(self, gltf_file: str | pathlib.Path, **kwargs): diff --git a/src/ada/fem/containers.py b/src/ada/fem/containers.py index 04bda694c..812ffd5b3 100644 --- a/src/ada/fem/containers.py +++ b/src/ada/fem/containers.py @@ -613,7 +613,7 @@ def remove(self, fs_in: Union[List[FemSection], FemSection]): class FemSets: - def __init__(self, sets: List[FemSet] = None, parent: FEM = None): + def __init__(self, sets: list[FemSet] = None, parent: FEM = None): self._fem_obj = parent self._sets = sorted(sets, key=attrgetter("type", "name")) if sets is not None else [] # Merge same name sets diff --git a/src/ada/fem/formats/sesam/read/read_sets.py b/src/ada/fem/formats/sesam/read/read_sets.py index a402cbf89..6ab5649ee 100644 --- a/src/ada/fem/formats/sesam/read/read_sets.py +++ b/src/ada/fem/formats/sesam/read/read_sets.py @@ -16,7 +16,7 @@ def get_sets(bulk_str: str, parent: "FEM") -> FemSets: set_reader = SetReader(bulk_str, parent) - return FemSets(list(set_reader.run()), parent=parent) + return FemSets(set_reader.run(), parent=parent) @dataclass @@ -26,7 +26,7 @@ class SetReader: _set_type_map: dict = field(default_factory=dict) - def run(self) -> Iterable[FemSet]: + def iter_sets(self) -> Iterable[FemSet]: set_map = dict() set_groups = (self.get_setmap(m, self.parent) for m in cards.re_setmembs.finditer(self.bulk_str)) @@ -45,7 +45,11 @@ def run(self) -> Iterable[FemSet]: for fs in self.get_femsets(m, set_map, self.parent): yield fs - def get_setmap(self, m, parent): + def run(self) -> list[FemSet]: + return list(self.iter_sets()) + + @staticmethod + def get_setmap(m, parent): d = m.groupdict() set_type = "nset" if str_to_int(d["istype"]) == 1 else "elset" mem_list = d["members"].split() diff --git a/src/ada/fem/formats/sesam/write/write_sets.py b/src/ada/fem/formats/sesam/write/write_sets.py new file mode 100644 index 000000000..1caea7ca9 --- /dev/null +++ b/src/ada/fem/formats/sesam/write/write_sets.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from os import write +from typing import TYPE_CHECKING + +from ada.fem.formats.sesam.write.write_utils import write_ff + +if TYPE_CHECKING: + from ada import FEM + + +def sets_str(fem: FEM) -> str: + out_str = "" + + for i, fs in enumerate(fem.sets.sets): + out_str += write_ff("TDSETNAM", [(4, i, 100 + len(fs.name), 0), (fs.name,)]) + nfield = len(fs.members) + 5 + mem_ids = [mem.id for mem in fs.members] + if fs.type == "elset": + istype = 2 + else: + istype = 1 + + start = 0 + card_idx = 0 + if nfield > 1024: + num_cards = int(nfield / 1019) + for card_idx in range(1, num_cards + 1): + mem_ids_local = mem_ids[start : card_idx * 1019] + remainder_mem_ids = [] + for k in range(3, len(mem_ids_local), 4): + remainder_mem_ids.append(mem_ids_local[k : k + 4]) + out_str += write_ff( + "GSETMEMB", [(1024, i, card_idx, istype), (0, *mem_ids_local[:3]), *remainder_mem_ids] + ) + start = card_idx * 1019 + + card_idx += 1 + length = nfield - start + if length < 4: + out_str += write_ff("GSETMEMB", [(length, i, card_idx, istype), (0, *mem_ids[start : start + length])]) + else: + mem_ids_local = mem_ids[start : start + length - 5] + remainder_mem_ids = [] + for k in range(3, len(mem_ids_local), 4): + remainder_mem_ids.append(mem_ids_local[k : k + 4]) + out_str += write_ff( + "GSETMEMB", [(len(mem_ids_local) + 5, i, card_idx, istype), (0, *mem_ids_local[:3]), *remainder_mem_ids] + ) + + return out_str diff --git a/src/ada/fem/formats/sesam/write/writer.py b/src/ada/fem/formats/sesam/write/writer.py index c7f96e4b3..a5d5eb875 100644 --- a/src/ada/fem/formats/sesam/write/writer.py +++ b/src/ada/fem/formats/sesam/write/writer.py @@ -10,6 +10,7 @@ from ada.fem.exceptions.model_definition import DoesNotSupportMultiPart from .templates import top_level_fem_str +from .write_sets import sets_str from .write_utils import write_ff if TYPE_CHECKING: @@ -69,6 +70,7 @@ def to_fem(assembly, name, analysis_dir=None, metadata=None, model_data_only=Fal d.write(univec_str(part.fem)) d.write(nodes_str(part.fem)) d.write(mass_str(part.fem)) + d.write(sets_str(part.fem)) d.write(bc_str(part.fem) + bc_str(assembly.fem)) d.write(constraint_str(part.fem) + constraint_str(assembly.fem)) d.write(hinges_str(part.fem)) diff --git a/src/ada/fem/meshing/concepts.py b/src/ada/fem/meshing/concepts.py index 1f01ae5fc..4cfe598f6 100644 --- a/src/ada/fem/meshing/concepts.py +++ b/src/ada/fem/meshing/concepts.py @@ -325,7 +325,8 @@ def add_obj_to_elem_ref(el: Elem, obj: Shape | Beam | Plate | Pipe): # create a variable for all overlapping element ids overlapping_el_ids = existing_el_ids.intersection(el_ids) if overlapping_el_ids: - logger.warning("Overlapping element ids found") + logger.warning(f"Overlapping element ids found for {gmsh_data.obj.name}: {overlapping_el_ids}") + elements.update({el.id: el for el in entity_elements}) fem.elements = FemElements(elements.values(), fem_obj=fem) diff --git a/src/ada/fem/meshing/partitioning/partition_plates.py b/src/ada/fem/meshing/partitioning/partition_plates.py index bbd422bca..5467041ee 100644 --- a/src/ada/fem/meshing/partitioning/partition_plates.py +++ b/src/ada/fem/meshing/partitioning/partition_plates.py @@ -49,7 +49,7 @@ def partition_intersected_plates(plate_con: PlateConnections, gmsh_session: Gmsh intersecting_plates.add((pl2_dim, pl2_ent)) try: res, res_map = gmsh_session.model.occ.fragment( - list(intersecting_plates), [(pl1_dim, pl1_ent)], removeTool=False + list(intersecting_plates), [(pl1_dim, pl1_ent)], removeTool=True ) except Exception as e: logger.error(f"Error while fragmenting plate: {pl1.name} using {pl2.name} {e}") @@ -114,9 +114,9 @@ def split_plates_by_beams(gmsh_session: GmshSession): bm_gmsh_obj.entities = new_ents for ent in new_ents: int_bm_map[(ent[0], ent[1])] = bm_gmsh_obj - # for ent in old_entities: - # if ent not in new_ents: - # int_bm_map.pop((ent[0], ent[1]), None) + for ent in old_entities: + if ent not in new_ents: + int_bm_map.pop((ent[0], ent[1]), None) pl_gmsh_obj.entities = replaced_pl_entities gmsh_session.model.occ.synchronize() diff --git a/src/ada/fem/meshing/utils.py b/src/ada/fem/meshing/utils.py index 464be2488..ae6006cb0 100644 --- a/src/ada/fem/meshing/utils.py +++ b/src/ada/fem/meshing/utils.py @@ -242,7 +242,7 @@ def get_elements_from_entity(model: gmsh.model, ent, fem: FEM, dim) -> list[Elem def get_elements_from_entities(model: gmsh.model, entities, fem: FEM) -> list[Elem]: elements = [] - for dim, ent in entities: + for dim, ent in set(entities): try: elements += get_elements_from_entity(model, ent, fem, dim) except BaseException as e: diff --git a/src/ada/fem/sets.py b/src/ada/fem/sets.py index 9dfee059d..2973df1da 100644 --- a/src/ada/fem/sets.py +++ b/src/ada/fem/sets.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING, List, Union, Literal from ada.api.nodes import Node @@ -29,7 +29,14 @@ class FemSet(FemBase): TYPES = SetTypes - def __init__(self, name, members: None | list[Elem | Node], set_type=None, metadata=None, parent=None): + def __init__( + self, + name, + members: None | list[Elem | Node], + set_type: Literal["nset", "elset"] = None, + metadata=None, + parent=None, + ): super().__init__(name, metadata, parent) from ada.fem import Elem @@ -66,7 +73,7 @@ def add_members(self, members: List[Union[Elem, Node]]): self._members += members @property - def type(self): + def type(self) -> str: return self._set_type.lower() @property diff --git a/src/frontend/src/components/viewer/DynamicGridHelper.tsx b/src/frontend/src/components/viewer/DynamicGridHelper.tsx index 2db9121fd..b1f34b65e 100644 --- a/src/frontend/src/components/viewer/DynamicGridHelper.tsx +++ b/src/frontend/src/components/viewer/DynamicGridHelper.tsx @@ -13,9 +13,8 @@ const DynamicGridHelper: React.FC = () => { if (boundingBox) { const size = calculateGridSize(boundingBox); - const divisions = size; + const divisions = Math.max(10, Math.floor(size / 10)); grid = new ThreeGridHelper(size, divisions, 'white', 'gray'); - //grid.position.y = boundingBox.min.y - (size * 0.05); // Adjust grid position if needed } else { // Default grid if no bounding box is available grid = new ThreeGridHelper(10, 10, 'white', 'gray'); diff --git a/src/frontend/src/components/viewer/OrientationGizmo.tsx b/src/frontend/src/components/viewer/OrientationGizmo.tsx index 211250f15..80d7f8888 100644 --- a/src/frontend/src/components/viewer/OrientationGizmo.tsx +++ b/src/frontend/src/components/viewer/OrientationGizmo.tsx @@ -8,7 +8,7 @@ const OrientationGizmo = () => { // Rotate the gizmo so that Z is treated as up matrix={[1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]} > - {/**/} + {/**/} ); diff --git a/tests/core/fem/formats/sesam/test_fem_sesam.py b/tests/core/fem/formats/sesam/test_fem_sesam.py index 11eb8dcb5..7261cb309 100644 --- a/tests/core/fem/formats/sesam/test_fem_sesam.py +++ b/tests/core/fem/formats/sesam/test_fem_sesam.py @@ -1,4 +1,9 @@ +import ada +from ada.fem.containers import FemElements +from ada.fem.formats.sesam.read.read_sets import SetReader +from ada.fem.formats.sesam.write.write_sets import sets_str from ada.fem.formats.sesam.write.writer import write_ff +from ada.fem.shapes.definitions import LineShapes def test_write_ff(): @@ -17,3 +22,22 @@ def test_write_ff(): ] test_str += write_ff(fflag, ddata) # print(test_str) + + +def test_write_sets(): + + elements = [ + ada.fem.Elem(el_id, [ada.Node((el_id, 0, 0), el_id), ada.Node((el_id + 1, 0, 0), el_id + 1)], LineShapes.LINE) + for el_id in range(1, 2000) + ] + fem = ada.FEM("MyFem", elements=FemElements(elements)) + original_set = fem.add_set(ada.fem.FemSet("MySet", elements)) + result_str = sets_str(fem) + + return_fem = ada.FEM("MyFem", elements=fem.elements) + sr = SetReader(result_str, return_fem) + roundtripped_sets = sr.run() + assert len(roundtripped_sets) == 1 + + roundtripped_set = roundtripped_sets[0] + assert len(original_set.members) == len(roundtripped_set.members)