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)