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 CHANGED
@@ -1,5 +1,8 @@
1
- __name__ = "femethods"
2
- __version__ = "0.1.7a2"
3
- __author__ = "Joseph Contreras Jr."
4
- __license__ = "MIT"
5
- __copyright__ = "Copyright 2020 Joseph Contreras Jr."
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"
@@ -0,0 +1,4 @@
1
+ # pylint: disable=F
2
+ # flake8: noqa
3
+
4
+ from .force import Force
femethods/mesh.py CHANGED
@@ -1,101 +1,245 @@
1
- """
2
- Mesh module that will define the mesh.
3
- """
4
-
5
- from typing import List, Sequence, TYPE_CHECKING
6
-
7
- if TYPE_CHECKING: # pragma: no cover
8
- from femethods.reactions import Reaction # noqa: F401 (unused import)
9
- from femethods.loads import Load # noqa: F401 (unused import)
10
-
11
-
12
- class Mesh(object):
13
- """define a mesh that will handle degrees-of-freedom (dof), element lengths
14
- etc.
15
-
16
- the input degree-of-freedom (dof) parameter is the degrees-of-freedom for
17
- a single element
18
- """
19
-
20
- def __init__(
21
- self,
22
- length: float,
23
- loads: List["Load"],
24
- reactions: List["Reaction"],
25
- dof: int,
26
- ):
27
- self._nodes = self.__get_nodes(length, loads, reactions)
28
- self._lengths = self.__get_lengths()
29
- self._num_elements = len(self.lengths)
30
- self._dof = dof * self.num_elements + dof
31
-
32
- @property
33
- def nodes(self) -> Sequence[float]:
34
- return self._nodes
35
-
36
- @property
37
- def dof(self) -> int:
38
- """
39
- Degrees of freedom of the entire beam
40
-
41
- Returns:
42
- :obj:`int`: Read-only. Number of degrees of freedom of the beam
43
- """
44
- return self._dof
45
-
46
- @property
47
- def lengths(self) -> List[float]:
48
- """
49
- List of lengths of mesh elements
50
-
51
- Returns:
52
- :obj:`list`: Read-only. List of lengths of local mesh elements
53
- """
54
- return self._lengths
55
-
56
- @property
57
- def num_elements(self) -> int:
58
- """
59
- Number of mesh elements
60
-
61
- Returns:
62
- :obj:`int`: Read-only. Number of elements in mesh
63
-
64
- """
65
-
66
- return self._num_elements
67
-
68
- def __get_lengths(self) -> List[float]:
69
- # Calculate the lengths of each element
70
- lengths: List[float] = []
71
- for k in range(len(self.nodes) - 1):
72
- lengths.append(self.nodes[k + 1] - self.nodes[k])
73
- return lengths
74
-
75
- @staticmethod
76
- def __get_nodes(
77
- length: float, loads: List["Load"], reactions: List["Reaction"]
78
- ) -> Sequence[float]:
79
- nodes: List[float] = [0] # ensure first node is always at zero (0)
80
-
81
- # Ignore the type checking for the for loop adding lists of loads and
82
- # lists of reactions. There is no + operator defined for these, but it
83
- # will combine the lists using the built in list addition. Which is the
84
- # desired behavior
85
- # noinspection PyTypeChecker,Mypy
86
- for item in loads + reactions: # type: ignore
87
- nodes.append(item.location)
88
- nodes.append(length) # ensure last node is at the end of the beam
89
- nodes = list(set(nodes)) # remove duplicates
90
- nodes.sort()
91
- return nodes
92
-
93
- def __str__(self) -> str:
94
- s = (
95
- "MESH PARAMETERS\n"
96
- f"Number of elements: {self.num_elements}\n"
97
- f"Node locations: {self.nodes}\n"
98
- f"Element Lengths: {self.lengths}\n"
99
- f"Total degrees of freedom: {self.dof}\n"
100
- )
101
- return s
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
@@ -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