Skip to content

Commit 547acc5

Browse files
committed
Merge remote-tracking branch 'fea2/main'
2 parents 1010ccc + d05433a commit 547acc5

File tree

14 files changed

+228
-92
lines changed

14 files changed

+228
-92
lines changed

src/compas_fea2/UI/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
from __future__ import absolute_import
2-
from __future__ import division
3-
from __future__ import print_function
4-
51
from .viewer import FEA2Viewer
62

73
__all__ = [

src/compas_fea2/UI/viewer/scene.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
from __future__ import absolute_import
2-
from __future__ import division
3-
from __future__ import print_function
4-
51
from compas.colors import Color
62
from compas.colors import ColorMap
73
from compas.geometry import Vector

src/compas_fea2/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def change_setting(backend, setting, value):
6262
The new value for the setting.
6363
6464
Example usage:\n
65-
fea2 change-setting opensees exe "Applications/OpenSees3.5.0/bin/OpenSees"
65+
fea2 change-setting opensees exe "Applications/OpenSees3.7.0/bin/OpenSees"
6666
"""
6767
m = importlib.import_module("compas_fea2_" + backend.lower())
6868
env = os.path.join(m.HOME, "src", "compas_fea2_" + backend.lower(), ".env")

src/compas_fea2/model/elements.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,19 @@ def rigid(self) -> bool:
206206
def mass(self) -> float:
207207
return self.volume * self.section.material.density
208208

209-
def weight(self, g: float) -> float:
210-
return self.mass * g
209+
@property
210+
def g(self) -> float:
211+
if self.model:
212+
return self.model.g
213+
else:
214+
return self.g
215+
216+
@property
217+
def weight(self) -> float:
218+
if self.model.g:
219+
return self.mass * self.g
220+
else:
221+
raise "Gravity constant not defined"
211222

212223
@property
213224
def nodal_mass(self) -> List[float]:
@@ -356,9 +367,9 @@ def volume(self) -> float:
356367
def plot_section(self):
357368
self.section.plot()
358369

359-
def plot_stress_distribution(self, step: "Step", end: str = "end_1", nx: int = 100, ny: int = 100, *args, **kwargs):
360-
"""Plot the stress distribution along the element.
361-
370+
def plot_stress_distribution(self, step: "Step", end: str = "end_1", nx: int = 100, ny: int = 100, *args, **kwargs): # noqa: F821
371+
""" Plot the stress distribution along the element.
372+
362373
Parameters
363374
----------
364375
step : :class:`compas_fea2.model.Step`
@@ -412,9 +423,9 @@ def forces(self, step: "Step") -> "Result":
412423
r = self.section_forces_result(step)
413424
return r.forces
414425

415-
def moments(self, step: "Step") -> "Result":
416-
"""Get the moments result for the element.
417-
426+
def moments(self, step: "_Step") -> "Result": # noqa: F821
427+
""" Get the moments result for the element.
428+
418429
Parameters
419430
----------
420431
step : :class:`compas_fea2.model.Step`
@@ -491,8 +502,7 @@ def __init__(self, nodes: List["Node"], tag: str, element: Optional["_Element"]
491502
self._nodes = nodes
492503
self._tag = tag
493504
self._plane = Plane.from_three_points(*[node.xyz for node in nodes[:3]]) # TODO check when more than 3 nodes
494-
self._registration = element
495-
self._centroid = centroid_points([node.xyz for node in nodes])
505+
self._registration = element # FIXME: not updated when copying parts
496506

497507
@property
498508
def __data__(self):
@@ -549,7 +559,7 @@ def area(self) -> float:
549559

550560
@property
551561
def centroid(self) -> "Point":
552-
return self._centroid
562+
return centroid_points([node.xyz for node in self.nodes])
553563

554564
@property
555565
def nodes_key(self) -> List:
@@ -841,7 +851,7 @@ def __init__(
841851
if len(nodes) not in {4, 10}:
842852
raise ValueError("TetrahedronElement must have either 4 (C3D4) or 10 (C3D10) nodes.")
843853

844-
self.element_type = "C3D10" if len(nodes) == 10 else "C3D4"
854+
# self.element_type = "C3D10" if len(nodes) == 10 else "C3D4"
845855

846856
super().__init__(
847857
nodes=nodes,

src/compas_fea2/model/groups.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,6 @@ def group_by(self, key: Callable[[T], Any]) -> Dict[Any, "_Group"]:
136136
A dictionary where keys are the grouping values and values are `_Group` instances.
137137
"""
138138
sorted_members = self._members
139-
# try:
140-
# sorted_members = sorted(self._members, key=key)
141-
# except TypeError:
142-
# sorted_members = sorted(self._members, key=lambda x: x.key)
143139
grouped_members = {k: set(v) for k, v in groupby(sorted_members, key=key)}
144140
return {k: self.__class__(v, name=f"{self.name}") for k, v in grouped_members.items()}
145141

src/compas_fea2/model/materials/concrete.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
from math import log
22

3-
from compas_fea2.units import UnitRegistry
4-
from compas_fea2.units import units as u
5-
63
from .material import _Material
74

85

@@ -139,11 +136,6 @@ def __from_data__(cls, data):
139136
# FIXME: this is only working for the basic material properties.
140137
@classmethod
141138
def C20_25(cls, units, **kwargs):
142-
if not units:
143-
units = u(system="SI_mm")
144-
elif not isinstance(units, UnitRegistry):
145-
units = u(units)
146-
147139
return cls(fck=25 * units.MPa, E=30 * units.GPa, v=0.17, density=2400 * units("kg/m**3"), name="C20/25", **kwargs)
148140

149141

src/compas_fea2/model/materials/material.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,6 @@ def __init__(self, *, density: float, expansion: Optional[float] = None, name: O
303303
# ==============================================================================
304304
# non-linear general
305305
# ==============================================================================
306-
# @extend_docstring(_Material)
307306
class ElasticPlastic(ElasticIsotropic):
308307
"""Elastic and plastic, isotropic and homogeneous material.
309308

src/compas_fea2/model/model.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import Optional
1010
from typing import Set
1111
from typing import Union
12+
from typing import Tuple
1213

1314
from compas.datastructures import Graph
1415
from compas.geometry import Box
@@ -18,6 +19,7 @@
1819
from compas.geometry import Transformation
1920
from compas.geometry import bounding_box
2021
from compas.geometry import centroid_points
22+
from compas.geometry import centroid_points_weighted
2123
from pint import UnitRegistry
2224

2325
import compas_fea2
@@ -107,6 +109,8 @@ def __init__(self, description: Optional[str] = None, author: Optional[str] = No
107109
self._groups: Set[_Group] = set()
108110
self._problems: Set[Problem] = set()
109111

112+
self._constants: dict = {"g": None}
113+
110114
@property
111115
def __data__(self):
112116
return {
@@ -121,6 +125,7 @@ def __data__(self):
121125
"sections": [section.__data__ for section in self.sections],
122126
"problems": [problem.__data__ for problem in self.problems],
123127
"path": str(self.path) if self.path else None,
128+
"constants": self._constants,
124129
}
125130

126131
@classmethod
@@ -161,6 +166,7 @@ def __from_data__(cls, data):
161166
problem_classes = {cls.__name__: cls for cls in Problem.__subclasses__()}
162167
model._problems = {problem_classes[problem_data["class"]].__from_data__(problem_data) for problem_data in data.get("problems", [])}
163168
model._path = Path(data.get("path")) if data.get("path") else None
169+
model._constants = data.get("constants")
164170
return model
165171

166172
@classmethod
@@ -217,6 +223,18 @@ def constraints(self) -> Set[_Constraint]:
217223
def connectors(self) -> Set[Connector]:
218224
return self._connectors
219225

226+
@property
227+
def constants(self) -> dict:
228+
return self._constants
229+
230+
@property
231+
def g(self) -> float:
232+
return self.constants["g"]
233+
234+
@g.setter
235+
def g(self, value):
236+
self._constants["g"] = value
237+
220238
@property
221239
def materials_dict(self) -> dict[Union[_Part, "Model"], list[_Material]]:
222240
materials = {part: part.materials for part in filter(lambda p: not isinstance(p, RigidPart), self.parts)}
@@ -301,11 +319,24 @@ def bounding_box(self) -> Optional[Box]:
301319
return Box.from_bounding_box(bb)
302320

303321
@property
304-
def center(self) -> Point:
322+
def bb_center(self) -> Point:
305323
if self.bounding_box:
306-
return centroid_points(self.bounding_box.points)
324+
return Point(*centroid_points(self.bounding_box.points))
307325
else:
308-
return centroid_points(self.points)
326+
raise AttributeError("The model has no bounding box")
327+
328+
@property
329+
def center(self) -> Point:
330+
return Point(*centroid_points(self.points))
331+
332+
@property
333+
def centroid(self) -> Point:
334+
weights = []
335+
points = []
336+
for part in self.parts:
337+
points.append(part.centroid)
338+
weights.append(part.weight)
339+
return Point(*centroid_points_weighted(points=points, weights=weights))
309340

310341
@property
311342
def bottom_plane(self) -> Plane:
@@ -855,7 +886,12 @@ def find_node_by_name(self, name: str) -> Node:
855886

856887
@get_docstring(_Part)
857888
@part_method
858-
def find_closest_nodes_to_node(self, node: Node, distance: float, number_of_nodes: int = 1, plane: Optional[Plane] = None) -> NodesGroup:
889+
def find_closest_nodes_to_node(self, node: Node, number_of_nodes: int = 1, plane: Optional[Plane] = None) -> NodesGroup:
890+
pass
891+
892+
@get_docstring(_Part)
893+
@part_method
894+
def find_closest_nodes_to_point(self, point: Point, number_of_nodes: int = 1, plane: Optional[Plane] = None) -> NodesGroup:
859895
pass
860896

861897
@get_docstring(_Part)
@@ -1433,6 +1469,42 @@ def add_interfaces(self, interfaces):
14331469
"""
14341470
return [self.add_interface(interface) for interface in interfaces]
14351471

1472+
def extract_interfaces(self, max_origin_distance=0.01, tol=1e-6) -> list[Tuple[Plane, Plane]]:
1473+
"""Extract interfaces from the model.
1474+
1475+
Returns
1476+
-------
1477+
list[:class:`compas_fea2.model.Interface`]
1478+
List of interfaces extracted from the model.
1479+
1480+
"""
1481+
from compas.geometry import distance_point_plane_signed
1482+
import itertools
1483+
from typing import List
1484+
1485+
# --- Nested helper function to check coplanarity ---
1486+
def _are_two_planes_coplanar(pln1: Plane, pln2: Plane, tol: float) -> bool:
1487+
"""Checks if two COMPAS planes are geometrically the same."""
1488+
normals_collinear = abs(abs(pln1.normal.dot(pln2.normal)) - 1.0) < tol
1489+
if not normals_collinear:
1490+
return False
1491+
point_on_plane = abs(distance_point_plane_signed(pln1.point, pln2)) < tol
1492+
return point_on_plane
1493+
1494+
list_of_plane_lists = [p.extract_clustered_planes() for p in self.parts]
1495+
# 1. Flatten the list of lists into a single list of all planes.
1496+
all_planes_raw: List[Plane] = list(itertools.chain.from_iterable(list_of_plane_lists))
1497+
1498+
# 2. Get unique plane instances from the raw list.
1499+
coplanar_pairs: List[Tuple[Plane, Plane]] = []
1500+
for p1, p2 in itertools.combinations(all_planes_raw, 2):
1501+
if _are_two_planes_coplanar(p1, p2, tol):
1502+
# If planes are coplanar, then check the distance between their origin points
1503+
if max_origin_distance is None or p1.point.distance_to_point(p2.point) < max_origin_distance:
1504+
coplanar_pairs.append((p1, p2))
1505+
1506+
return coplanar_pairs
1507+
14361508
# ==============================================================================
14371509
# Summary
14381510
# ==============================================================================

0 commit comments

Comments
 (0)