Skip to content

Commit 737ef87

Browse files
Merge pull request #300 from isteinbrecher/arc_length_alternative
Add general functionality to compute arc lengths for created beam filaments (alternative to #298)
2 parents faa6d35 + 80d2cd0 commit 737ef87

16 files changed

+8022
-7664
lines changed

src/meshpy/core/element_beam.py

+47-21
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"""This file defines the base beam element in MeshPy."""
2323

2424
from typing import Any as _Any
25+
from typing import Callable as _Callable
26+
from typing import List as _List
2527
from typing import Optional as _Optional
2628

2729
import numpy as _np
@@ -30,6 +32,7 @@
3032
from meshpy.core.conf import mpy as _mpy
3133
from meshpy.core.element import Element as _Element
3234
from meshpy.core.node import NodeCosserat as _NodeCosserat
35+
from meshpy.core.rotation import Rotation as _Rotation
3336
from meshpy.core.vtk_writer import add_point_data_node_sets as _add_point_data_node_sets
3437

3538

@@ -51,34 +54,46 @@ def __init__(self, material=None, nodes=None):
5154
super().__init__(nodes=nodes, material=material)
5255

5356
def create_beam(
54-
self, beam_function, *, start_node=None, end_node=None, relative_twist=None
55-
):
57+
self,
58+
beam_function: _Callable,
59+
*,
60+
start_node: _Optional[_NodeCosserat] = None,
61+
end_node: _Optional[_NodeCosserat] = None,
62+
relative_twist: _Optional[_Rotation] = None,
63+
set_nodal_arc_length: bool = False,
64+
nodal_arc_length_offset: _Optional[float] = None,
65+
) -> _List[_NodeCosserat]:
5666
"""Create the nodes for this beam element. The function returns a list
5767
with the created nodes.
5868
5969
In the case of start_node and end_node, it is checked, that the
6070
function and the node have the same coordinates and rotations.
6171
62-
Args
63-
----
64-
beam_function: function(xi)
65-
Returns the position and rotation of the beam along the local
66-
coordinate xi.
67-
start_node: Node
68-
If this argument is given, this is the node of the beam at xi=-1.
69-
end_node: Node
70-
If this argument is given, this is the node of the beam at xi=1.
71-
relative_twist: Rotation
72-
Apply this relative rotation to all created nodes. This can be used to
73-
"twist" the created beam to match the rotation of given nodes.
72+
Args:
73+
beam_function: function(xi)
74+
Returns the position, rotation and (optionally) arc length of the
75+
beam along the local coordinate xi. If no arc lengths is provided,
76+
the returned value should simply be None.
77+
start_node: Node
78+
If this argument is given, this is the node of the beam at xi=-1.
79+
end_node: Node
80+
If this argument is given, this is the node of the beam at xi=1.
81+
relative_twist: Rotation
82+
Apply this relative rotation to all created nodes. This can be used to
83+
"twist" the created beam to match the rotation of given nodes.
84+
set_nodal_arc_length:
85+
Flag if the arc length in the created nodes should be set.
86+
nodal_arc_length_offset:
87+
Offset of the stored nodal arc length w.r.t. to the one generated
88+
by the function.
7489
"""
7590

7691
if len(self.nodes) > 0:
7792
raise ValueError("The beam should not have any local nodes yet!")
7893

79-
def check_node(node, pos, rot, name):
80-
"""Check if the given node matches with the position and
81-
rotation."""
94+
def check_node(node, pos, rot, arc_length, name):
95+
"""Check if the given node matches with the position and rotation
96+
and optionally also the arc length."""
8297

8398
if _np.linalg.norm(pos - node.coordinates) > _mpy.eps_pos:
8499
raise ValueError(
@@ -88,31 +103,42 @@ def check_node(node, pos, rot, name):
88103
if not node.rotation == rot:
89104
raise ValueError(f"{name} rotation does not match with function!")
90105

106+
if arc_length is not None:
107+
if _np.abs(node.arc_length - arc_length) > _mpy.eps_pos:
108+
raise ValueError(
109+
f"Arc lengths don't match, got {node.arc_length} and {arc_length}"
110+
)
111+
91112
# Flags if nodes are given
92113
has_start_node = start_node is not None
93114
has_end_node = end_node is not None
94115

95116
# Loop over local nodes.
117+
arc_length = None
96118
for i, xi in enumerate(self.nodes_create):
97119
# Get the position and rotation at xi
98-
pos, rot = beam_function(xi)
120+
pos, rot, arc_length_from_function = beam_function(xi)
99121
if relative_twist is not None:
100122
rot = rot * relative_twist
123+
if set_nodal_arc_length:
124+
arc_length = arc_length_from_function + nodal_arc_length_offset
101125

102126
# Check if the position and rotation match existing nodes
103127
if i == 0 and has_start_node:
104-
check_node(start_node, pos, rot, "start_node")
128+
check_node(start_node, pos, rot, arc_length, "start_node")
105129
self.nodes = [start_node]
106130
elif (i == len(self.nodes_create) - 1) and has_end_node:
107-
check_node(end_node, pos, rot, "end_node")
131+
check_node(end_node, pos, rot, arc_length, "end_node")
108132

109133
# Create the node
110134
if (i > 0 or not has_start_node) and (
111135
i < len(self.nodes_create) - 1 or not has_end_node
112136
):
113137
is_middle_node = 0 < i < len(self.nodes_create) - 1
114138
self.nodes.append(
115-
_NodeCosserat(pos, rot, is_middle_node=is_middle_node)
139+
_NodeCosserat(
140+
pos, rot, is_middle_node=is_middle_node, arc_length=arc_length
141+
)
116142
)
117143

118144
# Get a list with the created nodes.

src/meshpy/core/node.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,15 @@ class NodeCosserat(Node):
124124
"""This object represents a Cosserat node in the mesh, i.e., it contains
125125
three positions and three rotations."""
126126

127-
def __init__(self, coordinates, rotation, **kwargs):
127+
def __init__(self, coordinates, rotation, *, arc_length=None, **kwargs):
128128
super().__init__(coordinates, **kwargs)
129129

130130
# Rotation of this node.
131131
self.rotation = rotation.copy()
132132

133+
# Arc length along the filament that this beam is a part of
134+
self.arc_length = arc_length
135+
133136
def rotate(self, rotation, *, origin=None, only_rotate_triads=False):
134137
"""Rotate this node.
135138

src/meshpy/mesh_creation_functions/beam_basic_geometry.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ def beam_function(xi):
9999
coordinate xi."""
100100
point_a = start_point + parameter_a * direction
101101
point_b = start_point + parameter_b * direction
102-
return (0.5 * (1 - xi) * point_a + 0.5 * (1 + xi) * point_b, rotation)
102+
pos = 0.5 * (1 - xi) * point_a + 0.5 * (1 + xi) * point_b
103+
arc_length = (
104+
0.5 * (1 - xi) * parameter_a + 0.5 * (1 + xi) * parameter_b
105+
) * line_length
106+
return (pos, rotation, arc_length)
103107

104108
return beam_function
105109

@@ -258,7 +262,7 @@ def beam_function(xi):
258262
arc_rotation = _Rotation(axis, phi)
259263
rot = arc_rotation * start_rotation
260264
pos = center + arc_rotation * distance
261-
return (pos, rot)
265+
return (pos, rot, phi * radius)
262266

263267
return beam_function
264268

src/meshpy/mesh_creation_functions/beam_curve.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def get_beam_position_and_rotation_at_xi(xi):
246246
self.S_start_newton = S
247247

248248
# Return the needed values for beam creation.
249-
return (pos, rot)
249+
return (pos, rot, S)
250250

251251
return get_beam_position_and_rotation_at_xi
252252

src/meshpy/mesh_creation_functions/beam_generic.py

+54-12
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ def create_beam_mesh_function(
5151
n_el: _Optional[int] = None,
5252
l_el: _Optional[float] = None,
5353
interval_length: _Optional[float] = None,
54+
set_nodal_arc_length: bool = False,
55+
nodal_arc_length_offset: _Optional[float] = None,
5456
node_positions_of_elements: _Optional[_List[float]] = None,
5557
start_node: _Optional[_Union[_NodeCosserat, _GeometrySet]] = None,
5658
end_node: _Optional[_Union[_NodeCosserat, _GeometrySet]] = None,
57-
close_beam: _Optional[bool] = False,
59+
close_beam: bool = False,
5860
vtk_cell_data: _Optional[_Dict[str, _Tuple]] = None,
5961
) -> _GeometryName:
6062
"""Generic beam creation function.
@@ -77,6 +79,10 @@ def create_beam_mesh_function(
7779
point_b (both within the interval) and return a function(xi) that
7880
calculates the position and rotation along the beam, where
7981
point_a -> xi = -1 and point_b -> xi = 1.
82+
83+
Usually, the Jacobian of the returned position field should be a unit
84+
vector. Otherwise, the nodes may be spaced in an undesired way. All
85+
standard mesh creation functions in MeshPy fulfill this property.
8086
interval:
8187
Start and end values for interval that will be used to create the
8288
beam.
@@ -90,6 +96,15 @@ def create_beam_mesh_function(
9096
created.
9197
interval_length:
9298
Total length of the interval. Is required when the option l_el is given.
99+
set_nodal_arc_length:
100+
Flag if the arc length along the beam filament is set in the created
101+
nodes. It is ensured that the arc length is consistent with possible
102+
given start/end nodes.
103+
nodal_arc_length_offset:
104+
Offset of the stored nodal arc length w.r.t. to the one generated by
105+
the function. Defaults to 0, the arc length set in the start node, or
106+
the arc length in the end node minus total length (such that the arc
107+
length at the end node matches).
93108
node_positions_of_elements:
94109
A list of normalized positions (within [0,1] and in ascending order)
95110
that define the boundaries of beam elements along the created curve.
@@ -138,6 +153,17 @@ def create_beam_mesh_function(
138153
'The arguments "close_beam" and "end_node" are mutually exclusive'
139154
)
140155

156+
if set_nodal_arc_length:
157+
if close_beam:
158+
raise ValueError(
159+
"The flags 'set_nodal_arc_length' and 'close_beam' are mutually exclusive."
160+
)
161+
elif nodal_arc_length_offset is not None:
162+
raise ValueError(
163+
'Providing the argument "nodal_arc_length_offset" without setting '
164+
'"set_nodal_arc_length" to True does not make sense.'
165+
)
166+
141167
# Cases where we have equally spaced elements
142168
if n_el is not None or l_el is not None:
143169
if l_el is not None:
@@ -150,13 +176,10 @@ def create_beam_mesh_function(
150176
elif n_el is None:
151177
raise ValueError("n_el should not be None at this point")
152178

153-
interval_node_positions_of_elements = [
154-
interval[0] + i_node * (interval[1] - interval[0]) / n_el
155-
for i_node in range(n_el + 1)
156-
]
179+
node_positions_of_elements = [i_node / n_el for i_node in range(n_el + 1)]
157180
# A list for the element node positions was provided
158181
elif node_positions_of_elements is not None:
159-
# Check that the given positions are in ascending order and start with 1 and end with 0
182+
# Check that the given positions are in ascending order and start with 0 and end with 1
160183
for index, value, name in zip([0, -1], [0, 1], ["First", "Last"]):
161184
if not _np.isclose(
162185
value,
@@ -174,9 +197,11 @@ def create_beam_mesh_function(
174197
raise ValueError(
175198
f"The given node_positions_of_elements must be in ascending order. Got {node_positions_of_elements}"
176199
)
177-
interval_node_positions_of_elements = interval[0] + (
178-
interval[1] - interval[0]
179-
) * _np.asarray(node_positions_of_elements)
200+
201+
# Get the scale the node positions to the interval coordinates
202+
interval_node_positions_of_elements = interval[0] + (
203+
interval[1] - interval[0]
204+
) * _np.asarray(node_positions_of_elements)
180205

181206
# We need to make sure we have the number of elements for the case a given end node
182207
n_el = len(interval_node_positions_of_elements) - 1
@@ -234,7 +259,7 @@ def get_relative_twist(rotation_node, rotation_function, name):
234259
start_node = _get_single_node(start_node)
235260
nodes = [start_node]
236261
check_given_node(start_node)
237-
_, start_rotation = function_over_whole_interval(-1.0)
262+
_, start_rotation, _ = function_over_whole_interval(-1.0)
238263
relative_twist_start = get_relative_twist(
239264
start_node.rotation, start_rotation, "start"
240265
)
@@ -243,9 +268,24 @@ def get_relative_twist(rotation_node, rotation_function, name):
243268
if end_node is not None:
244269
end_node = _get_single_node(end_node)
245270
check_given_node(end_node)
246-
_, end_rotation = function_over_whole_interval(1.0)
271+
_, end_rotation, _ = function_over_whole_interval(1.0)
247272
relative_twist_end = get_relative_twist(end_node.rotation, end_rotation, "end")
248273

274+
# Get the start value for the arc length functionality
275+
if set_nodal_arc_length:
276+
if nodal_arc_length_offset is not None:
277+
# Let's use the given value, if it does not match with possible given
278+
# start or end nodes, the check in the create beam function will detect
279+
# that.
280+
pass
281+
elif start_node is not None and start_node.arc_length is not None:
282+
nodal_arc_length_offset = start_node.arc_length
283+
elif end_node is not None and end_node.arc_length is not None:
284+
nodal_arc_length_offset = end_node.arc_length - interval_length
285+
else:
286+
# Default value
287+
nodal_arc_length_offset = 0.0
288+
249289
# Check if a relative twist has to be applied
250290
if relative_twist_start is not None and relative_twist_end is not None:
251291
if relative_twist_start == relative_twist_end:
@@ -262,7 +302,7 @@ def get_relative_twist(rotation_node, rotation_function, name):
262302
relative_twist = None
263303

264304
# Create the beams.
265-
for i_el in range(len(interval_node_positions_of_elements) - 1):
305+
for i_el in range(n_el):
266306
# If the beam is closed with itself, set the end node to be the
267307
# first node of the beam. This is done when the second element is
268308
# created, as the first node already exists here.
@@ -295,6 +335,8 @@ def get_relative_twist(rotation_node, rotation_function, name):
295335
start_node=first_node,
296336
end_node=last_node,
297337
relative_twist=relative_twist,
338+
set_nodal_arc_length=set_nodal_arc_length,
339+
nodal_arc_length_offset=nodal_arc_length_offset,
298340
)
299341
)
300342

src/meshpy/space_time/beam_to_space_time.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ class NodeCosseratSpaceTime(_NodeCosserat):
5454
We add the 4th dimension time as a class variable.
5555
"""
5656

57-
def __init__(self, coordinates, rotation, time, *, arc_length=None, **kwargs):
57+
def __init__(self, coordinates, rotation, time, **kwargs):
5858
super().__init__(coordinates, rotation, **kwargs)
5959
self.time = time
60-
self.arc_length = arc_length
6160

6261
def _get_dat(self):
6362
"""Return the line that represents this node in the input file.

0 commit comments

Comments
 (0)