femethods 0.1.7a2__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- femethods/__init__.py +8 -5
- femethods/core/__init__.py +4 -0
- femethods/mesh.py +245 -101
- femethods/validation.py +104 -0
- {femethods-0.1.7a2.dist-info → femethods-0.1.8.dist-info}/METADATA +281 -262
- femethods-0.1.8.dist-info/RECORD +9 -0
- {femethods-0.1.7a2.dist-info → femethods-0.1.8.dist-info}/WHEEL +1 -1
- femethods-0.1.8.dist-info/licenses/License.txt +7 -0
- {femethods-0.1.7a2.dist-info → femethods-0.1.8.dist-info}/top_level.txt +0 -1
- femethods/core/_base_elements.py +0 -422
- femethods/core/_common.py +0 -117
- femethods/elements.py +0 -389
- femethods/loads.py +0 -38
- femethods/reactions.py +0 -176
- femethods-0.1.7a2.dist-info/RECORD +0 -18
- tests/__init__.py +0 -1
- tests/functional tests/__init__.py +0 -0
- tests/functional tests/settings.py +0 -11
- tests/functional tests/test_fixed_support_beams.py +0 -38
- tests/functional tests/test_simply_supported_beam.py +0 -136
- tests/functional tests/validate.py +0 -44
femethods/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# pylint: disable=missing-module-docstring
|
|
2
|
+
# pylint: disable=F
|
|
3
|
+
# flake8: noqa
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .mesh import Mesh
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.8"
|
femethods/core/__init__.py
CHANGED
femethods/mesh.py
CHANGED
|
@@ -1,101 +1,245 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import numpy.typing as npt
|
|
5
|
+
|
|
6
|
+
from . import validation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Mesh:
|
|
10
|
+
"""
|
|
11
|
+
Mesh to handle degrees-of-freedom (dof) and element lengths
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
length : float
|
|
16
|
+
overall length of structure
|
|
17
|
+
locations : sequence
|
|
18
|
+
locations of nodes (loads and reactions)
|
|
19
|
+
node_dof : int
|
|
20
|
+
degrees-of-freedom for a single node
|
|
21
|
+
max_element_length : float | None
|
|
22
|
+
maximum allowed length of mesh element
|
|
23
|
+
min_element_count : int | None
|
|
24
|
+
minimum required number of elements
|
|
25
|
+
|
|
26
|
+
.. versionchanged:: 0.1.8a1 renamed :obj:`dof` parameter to :obj:`node_dof`
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
length: float,
|
|
32
|
+
locations: npt.NDArray[np.float64],
|
|
33
|
+
node_dof: int,
|
|
34
|
+
*,
|
|
35
|
+
max_element_length: Optional[float] = None,
|
|
36
|
+
min_element_count: Optional[int] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.length = length
|
|
39
|
+
self.locations = locations
|
|
40
|
+
self.node_dof = node_dof
|
|
41
|
+
|
|
42
|
+
self.__lengths: npt.NDArray[np.float64] | None = (
|
|
43
|
+
None # this will be lazy calculated on-demand
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self.max_element_length = max_element_length
|
|
47
|
+
self.min_element_count = min_element_count
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def node_dof(self) -> int:
|
|
51
|
+
"""
|
|
52
|
+
degrees of freedom for a single node
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
TypeError: when not a number
|
|
57
|
+
ValueError: when not a positive integer
|
|
58
|
+
"""
|
|
59
|
+
return self.__node_dof
|
|
60
|
+
|
|
61
|
+
@node_dof.setter
|
|
62
|
+
@validation.is_numeric
|
|
63
|
+
@validation.positive
|
|
64
|
+
def node_dof(self, value: int, /) -> None:
|
|
65
|
+
if value != int(value):
|
|
66
|
+
raise ValueError(f"node_dof must be an integer, not {value}")
|
|
67
|
+
self.__node_dof = value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def max_element_length(self) -> Optional[float]:
|
|
71
|
+
"""
|
|
72
|
+
maximum allowed length of mesh element
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
float: maximum allowed length of mesh element
|
|
77
|
+
"""
|
|
78
|
+
return self.__max_element_length
|
|
79
|
+
|
|
80
|
+
@max_element_length.setter
|
|
81
|
+
def max_element_length(self, value: Optional[float]) -> None:
|
|
82
|
+
if value is None:
|
|
83
|
+
self.__max_element_length = None
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if not isinstance(value, (int, float)):
|
|
87
|
+
raise TypeError(f"max_element_length must be a number, not {value}")
|
|
88
|
+
if value <= 0:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"max_element_length must be a positive number, not {value}"
|
|
91
|
+
)
|
|
92
|
+
self.__max_element_length = value
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def min_element_count(self) -> Optional[int]:
|
|
96
|
+
"""
|
|
97
|
+
Minimum required number of elements.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
int | None
|
|
102
|
+
minimum required number of elements
|
|
103
|
+
"""
|
|
104
|
+
return self.__min_element_count
|
|
105
|
+
|
|
106
|
+
@min_element_count.setter
|
|
107
|
+
def min_element_count(self, value: Optional[int]) -> None:
|
|
108
|
+
if value is None:
|
|
109
|
+
self.__min_element_count = None
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if not isinstance(value, int):
|
|
113
|
+
raise TypeError(f"min_element_count must be a number, not {value}")
|
|
114
|
+
if value <= 0:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"min_element_count must be a positive number, not {value}"
|
|
117
|
+
)
|
|
118
|
+
self.__min_element_count = value
|
|
119
|
+
|
|
120
|
+
def __max_length_nodes(
|
|
121
|
+
self, required_nodes: npt.NDArray[np.float64]
|
|
122
|
+
) -> npt.NDArray[np.float64]:
|
|
123
|
+
"""
|
|
124
|
+
get node locations when the max length of an element is specified
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if self.__max_element_length is None: # pragma: no cover
|
|
128
|
+
return required_nodes
|
|
129
|
+
|
|
130
|
+
# there is a requirements on the max length of elements. Create a copy of the
|
|
131
|
+
# nodes to be manipulated
|
|
132
|
+
nodes = np.copy(required_nodes)
|
|
133
|
+
|
|
134
|
+
# continually iterate over nodes until all elements have a length that is
|
|
135
|
+
# less than the max allowed
|
|
136
|
+
lengths = np.diff(nodes)
|
|
137
|
+
while self.__max_element_length < np.max(lengths):
|
|
138
|
+
# there is a node that is longer than allowed
|
|
139
|
+
for node, length in zip(nodes[:-1], lengths):
|
|
140
|
+
if not self.__max_element_length < length:
|
|
141
|
+
# current element is not longer than the allowed, no changes
|
|
142
|
+
# required
|
|
143
|
+
continue
|
|
144
|
+
# current element is too long, split it in half
|
|
145
|
+
nodes = np.append(nodes, node + length / 2)
|
|
146
|
+
|
|
147
|
+
# all nodes that were longer than allowed, have been split in half.
|
|
148
|
+
# recalculate node locations
|
|
149
|
+
nodes = np.unique(nodes)
|
|
150
|
+
nodes = np.sort(nodes)
|
|
151
|
+
|
|
152
|
+
# update lengths variable, this is required for the exit condition of
|
|
153
|
+
# the while loop that guarantees all elements end up shorter than the
|
|
154
|
+
# maximum allowed length
|
|
155
|
+
lengths = np.diff(nodes)
|
|
156
|
+
return nodes
|
|
157
|
+
|
|
158
|
+
def __min_count_nodes(
|
|
159
|
+
self, required_nodes: npt.NDArray[np.float64]
|
|
160
|
+
) -> npt.NDArray[np.float64]:
|
|
161
|
+
"""get node locations when min number of elements are specified"""
|
|
162
|
+
|
|
163
|
+
if self.__min_element_count is None: # pragma: no cover
|
|
164
|
+
return required_nodes
|
|
165
|
+
|
|
166
|
+
# there is a requirements on the number of elements. Create a copy of the
|
|
167
|
+
# nodes to be manipulated
|
|
168
|
+
nodes = np.copy(required_nodes)
|
|
169
|
+
|
|
170
|
+
# there is a limit on the minimum number of elements that must be used
|
|
171
|
+
while self.__min_element_count > np.size(nodes) - 1:
|
|
172
|
+
# there are not enough elements. Split the largest element in half
|
|
173
|
+
lengths = np.diff(nodes)
|
|
174
|
+
max_length = np.max(lengths)
|
|
175
|
+
for node, length in zip(nodes[:-1], lengths): # pragma: no branch
|
|
176
|
+
if length == max_length:
|
|
177
|
+
nodes = np.append(nodes, node + length / 2)
|
|
178
|
+
nodes = np.unique(nodes)
|
|
179
|
+
nodes = np.sort(nodes)
|
|
180
|
+
|
|
181
|
+
# this condition focuses on the NUMBER of elements, not the
|
|
182
|
+
# length. Therefore, only break the first element that equals
|
|
183
|
+
# the longest. Even if multiple elements have the max length,
|
|
184
|
+
# only the first one is split
|
|
185
|
+
break
|
|
186
|
+
return nodes
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def nodes(self) -> npt.NDArray[np.float64]:
|
|
190
|
+
"""location of nodes"""
|
|
191
|
+
# create set of locations of each load and reaction, and the ends of the beam.
|
|
192
|
+
# ensure first node is always at zero (0) (start of beam)
|
|
193
|
+
nodes__ = np.array([0, self.length])
|
|
194
|
+
nodes__ = np.append(nodes__, self.locations, axis=0)
|
|
195
|
+
nodes__ = np.unique(nodes__)
|
|
196
|
+
nodes__ = np.sort(nodes__)
|
|
197
|
+
|
|
198
|
+
if self.__max_element_length is not None:
|
|
199
|
+
# there is a maximum limit on the length of the elements.
|
|
200
|
+
nodes__ = self.__max_length_nodes(required_nodes=nodes__)
|
|
201
|
+
if self.__min_element_count is not None:
|
|
202
|
+
# there is a limit on the minimum number of elements that must be used
|
|
203
|
+
nodes__ = self.__min_count_nodes(required_nodes=nodes__)
|
|
204
|
+
|
|
205
|
+
return nodes__
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def dof(self) -> int:
|
|
209
|
+
"""
|
|
210
|
+
Degrees of freedom of the entire beam
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
int
|
|
215
|
+
Read-only. Number of degrees of freedom of the beam
|
|
216
|
+
"""
|
|
217
|
+
return self.node_dof * len(self.nodes)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def lengths(self) -> npt.NDArray[np.float64]:
|
|
221
|
+
"""
|
|
222
|
+
List of lengths of mesh elements
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
array_like
|
|
227
|
+
Read-only. List of lengths of local mesh elements
|
|
228
|
+
"""
|
|
229
|
+
if self.__lengths is not None: # pragma: no cover
|
|
230
|
+
return self.__lengths
|
|
231
|
+
|
|
232
|
+
# the lengths have not been calculated yet. Calculate the lengths of each
|
|
233
|
+
# element
|
|
234
|
+
self.__lengths = np.diff(self.nodes)
|
|
235
|
+
return self.__lengths
|
|
236
|
+
|
|
237
|
+
def __str__(self) -> str:
|
|
238
|
+
mesh_string = (
|
|
239
|
+
"MESH PARAMETERS\n"
|
|
240
|
+
f"Number of elements: {len(self.lengths)}\n"
|
|
241
|
+
f"Node locations: {self.nodes}\n"
|
|
242
|
+
f"Element Lengths: {self.lengths}\n"
|
|
243
|
+
f"Total degrees of freedom: {self.dof}\n"
|
|
244
|
+
)
|
|
245
|
+
return mesh_string
|
femethods/validation.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Misc validation decorators that are used for validation of class properties
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar
|
|
9
|
+
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
|
|
13
|
+
# value type: allow int-only OR float-only (and preserve it)
|
|
14
|
+
N = TypeVar("N", int, float)
|
|
15
|
+
|
|
16
|
+
# Assumes decorated function is called like: func(self, value, *args, **kwargs)
|
|
17
|
+
ValidatedFunc = Callable[Concatenate[Any, N, P], R]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_numeric(func: ValidatedFunc[N, P, R]) -> ValidatedFunc[N, P, R]:
|
|
21
|
+
"""
|
|
22
|
+
Validate property is numeric
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
TypeError: when input value is non-numeric
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@wraps(func)
|
|
29
|
+
def wrapper(self: Any, value: N, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
30
|
+
if not isinstance(value, (int, float)):
|
|
31
|
+
raise TypeError(f"{func.__name__} must be a number, not {value}")
|
|
32
|
+
return func(self, value, *args, **kwargs)
|
|
33
|
+
|
|
34
|
+
return wrapper
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def positive(func: ValidatedFunc[N, P, R]) -> ValidatedFunc[N, P, R]:
|
|
38
|
+
"""
|
|
39
|
+
Validate property is positive (excluding ``0``)
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: when input value is negative or ``0``
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@wraps(func)
|
|
46
|
+
def wrapper(self: Any, value: N, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
47
|
+
if value <= 0:
|
|
48
|
+
raise ValueError(f"{func.__name__} must be a positive number, not {value}")
|
|
49
|
+
return func(self, value, *args, **kwargs)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def non_positive(func: ValidatedFunc[N, P, R]) -> ValidatedFunc[N, P, R]:
|
|
55
|
+
"""
|
|
56
|
+
Validate property is non_positive (negative or ``0``)
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: when value is positive
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@wraps(func)
|
|
63
|
+
def wrapper(self: Any, value: N, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
64
|
+
if value > 0:
|
|
65
|
+
raise ValueError(f"{func.__name__} must be a negative or 0, not {value}")
|
|
66
|
+
return func(self, value, *args, **kwargs)
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def negative(func: ValidatedFunc[N, P, R]) -> ValidatedFunc[N, P, R]:
|
|
72
|
+
"""
|
|
73
|
+
Validate property is negative (excluding ``0``)
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: when value is ``0`` or positive
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@wraps(func)
|
|
80
|
+
def wrapper(self: Any, value: N, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
81
|
+
if value >= 0:
|
|
82
|
+
raise ValueError(f"{func.__name__} must be a negative number, not {value}")
|
|
83
|
+
return func(self, value, *args, **kwargs)
|
|
84
|
+
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def non_negative(func: ValidatedFunc[N, P, R]) -> ValidatedFunc[N, P, R]:
|
|
89
|
+
"""
|
|
90
|
+
Validate property is non_negative (must be ``0`` or positive)
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: when value is negative
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
@wraps(func)
|
|
97
|
+
def wrapper(self: Any, value: N, /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
98
|
+
if value < 0:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
f"{func.__name__} must be a positive or 0 number, not {value}"
|
|
101
|
+
)
|
|
102
|
+
return func(self, value, *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
return wrapper
|