Skip to content

Commit

Permalink
fea: add support for sets export to sesam and auto-clean duplicate no…
Browse files Browse the repository at this point in the history
…des upon meshing finalization. And significantly improve auto-merge coincident nodes method
  • Loading branch information
Krande committed Oct 24, 2024
1 parent f84fd26 commit d42f2c7
Show file tree
Hide file tree
Showing 13 changed files with 118 additions and 22 deletions.
13 changes: 7 additions & 6 deletions src/ada/api/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
7 changes: 7 additions & 0 deletions src/ada/api/spatial/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/ada/fem/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/ada/fem/formats/sesam/read/read_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))

Expand All @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions src/ada/fem/formats/sesam/write/write_sets.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/ada/fem/formats/sesam/write/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion src/ada/fem/meshing/concepts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions src/ada/fem/meshing/partitioning/partition_plates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/ada/fem/meshing/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 10 additions & 3 deletions src/ada/fem/sets.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/frontend/src/components/viewer/DynamicGridHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/components/viewer/OrientationGizmo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
>
{/*<GizmoViewport axisColors={['red', 'green', 'blue']} labelColor="black" labels={["x", "z", "y"]}/>*/}
{/*<GizmoViewport axisColors={['red', 'blue', 'green']} labelColor="black" labels={["x", "z", "y"]}/>*/}
<GizmoViewcube color={"blue"} textColor={"white"}/>
</GizmoHelper>
);
Expand Down
24 changes: 24 additions & 0 deletions tests/core/fem/formats/sesam/test_fem_sesam.py
Original file line number Diff line number Diff line change
@@ -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():
Expand All @@ -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)

0 comments on commit d42f2c7

Please sign in to comment.