Coverage for src/beamme/core/node.py: 100%

39 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 15:20 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2026 BeamMe Authors 

4# 

5# Permission is hereby granted, free of charge, to any person obtaining a copy 

6# of this software and associated documentation files (the "Software"), to deal 

7# in the Software without restriction, including without limitation the rights 

8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

9# copies of the Software, and to permit persons to whom the Software is 

10# furnished to do so, subject to the following conditions: 

11# 

12# The above copyright notice and this permission notice shall be included in 

13# all copies or substantial portions of the Software. 

14# 

15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 

21# THE SOFTWARE. 

22"""This module implements the class that represents one node in the Mesh.""" 

23 

24from typing import Any as _Any 

25 

26import numpy as _np 

27from numpy.typing import NDArray as _NDArray 

28 

29from beamme.core.conf import bme as _bme 

30from beamme.core.rotation import Rotation as _Rotation 

31 

32 

33class Node: 

34 """This object represents one node in the mesh.""" 

35 

36 node_type = _bme.node_type.node 

37 

38 def __init__(self, coordinates, *, is_middle_node=False) -> None: 

39 # Global index of this node in a mesh. 

40 self.i_global: None | int = None 

41 

42 # Coordinates of this node. 

43 self.coordinates = _np.array(coordinates, dtype=float) 

44 

45 # If this node is at the end of a line or curve (by default only those 

46 # nodes are checked for overlapping nodes). 

47 self.is_end_node = False 

48 

49 # If the node is in the middle of a beam element. 

50 self.is_middle_node = is_middle_node 

51 

52 # Lists with the objects that this node is linked to. 

53 self.element_link: list[_Any] = [] 

54 

55 # If this node is replaced, store a link to the remaining node. 

56 self.target_node: "Node" | None = None 

57 

58 def get_target_node(self) -> "Node": 

59 """Return the target node of this node. 

60 

61 Returns: 

62 If this node has a linked target node, then this target node is returned, 

63 otherwise this node is returned. 

64 """ 

65 if self.target_node is None: 

66 return self 

67 else: 

68 return self.target_node.get_target_node() 

69 

70 def unlink(self) -> None: 

71 """Reset the links to elements.""" 

72 self.element_link = [] 

73 

74 

75class NodeCosserat(Node): 

76 """This object represents a Cosserat node in the mesh, i.e., it contains three 

77 positions and three rotations.""" 

78 

79 node_type = _bme.node_type.cosserat 

80 

81 def __init__( 

82 self, 

83 coordinates, 

84 rotation: _Rotation, 

85 *, 

86 arc_length: float | None = None, 

87 **kwargs, 

88 ): 

89 super().__init__(coordinates, **kwargs) 

90 

91 # Rotation of this node. 

92 self.rotation = rotation.copy() 

93 

94 # Arc length along the filament that this beam is a part of 

95 self.arc_length = arc_length 

96 

97 def rotate( 

98 self, 

99 rotation: _Rotation, 

100 *, 

101 origin: _NDArray | list[float] | None = None, 

102 only_rotate_triads: bool = False, 

103 ) -> None: 

104 """Rotate this node. 

105 

106 Args: 

107 rotation: Rotation that will be applied to this node. 

108 origin: Point around which the node will be rotated. If None, the 

109 node will be rotated around the origin (0,0,0). 

110 only_rotate_triads: If True, only the rotation of this node will be 

111 affected, the position of the node stays the same. 

112 """ 

113 self.rotation = rotation * self.rotation 

114 

115 # Rotate the positions (around origin). 

116 if not only_rotate_triads: 

117 if origin is not None: 

118 self.coordinates = self.coordinates - origin 

119 self.coordinates = rotation * self.coordinates 

120 if origin is not None: 

121 self.coordinates = self.coordinates + origin 

122 

123 

124class ControlPoint(Node): 

125 """This object represents a control point with a weight in the mesh.""" 

126 

127 node_type = _bme.node_type.control_point 

128 

129 def __init__(self, coordinates, weight, **kwargs): 

130 super().__init__(coordinates, **kwargs) 

131 

132 # Weight of this node 

133 self.weight = weight