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.
@@ -1,422 +0,0 @@
1
- """
2
- Module to define a general mesh element to be used for any FEM element, and
3
- the base element class that all FEM elements will be derived from
4
- """
5
-
6
- from abc import ABC, abstractmethod
7
- from typing import List, Optional, Tuple
8
- from warnings import warn
9
-
10
- import matplotlib.pyplot as plt
11
- import numpy as np
12
-
13
- # Importing loads is only used for checking the type. Find a better way to do
14
- # this without needing to import loads
15
- from femethods.loads import Load, PointLoad
16
- from femethods.mesh import Mesh
17
- from femethods.reactions import Reaction
18
-
19
- BOUNDARY_CONDITIONS = List[Tuple[Optional[int], Optional[int]]]
20
-
21
-
22
- # Allow upper case letters for variable names to match engineering conventions
23
- # for variables, such as E for Young's modulus and I for the polar moment of
24
- # inertia
25
- # noinspection PyPep8Naming
26
- class Base(ABC):
27
- """base object to be used as base for both FEM analysis"""
28
-
29
- def __init__(self, length: float, E: float = 1, Ixx: float = 1) -> None:
30
- self.length = length
31
- self.E = E # Young's modulus
32
- self.Ixx = Ixx # area moment of inertia
33
-
34
- @property
35
- def length(self) -> float:
36
- return self._length
37
-
38
- @length.setter
39
- def length(self, length: float) -> None:
40
- if length <= 0:
41
- # length must be a positive number
42
- raise ValueError("length must be positive!")
43
- self._length = length
44
-
45
- @property
46
- def E(self) -> float:
47
- return self._E
48
-
49
- @E.setter
50
- def E(self, E: float) -> None:
51
- if E <= 0:
52
- raise ValueError("Young's modulus must be positive!")
53
- self._E = E
54
-
55
- @property
56
- def Ixx(self) -> float:
57
- return self._Ixx
58
-
59
- @Ixx.setter
60
- def Ixx(self, Ixx: float) -> None:
61
- if Ixx <= 0:
62
- raise ValueError("Area moment of inertia must be positive!")
63
- self._Ixx = Ixx
64
-
65
-
66
- # Allow upper case letters for variable names to match engineering conventions
67
- # for variables, such as E for Young's modulus and I for the polar moment of
68
- # inertia
69
- # noinspection PyPep8Naming
70
- class Element(Base, ABC):
71
- """General element that will be inherited from for specific elements"""
72
-
73
- def __init__(self, length: float, E: float = 1, Ixx: float = 1) -> None:
74
- super().__init__(length, E, Ixx)
75
- self._node_deflections = None
76
- self._K = None # global stiffness matrix
77
- self._reactions: Optional[List[Reaction]] = None
78
- self._loads: Optional[List[Load]] = None
79
-
80
- @property
81
- def loads(self) -> Optional[List[Load]]:
82
- return self._loads
83
-
84
- @loads.setter
85
- def loads(self, loads: List[Load]) -> None:
86
- # validate that loads is a list of valid Loads
87
- for load in loads:
88
- if not isinstance(load, Load):
89
- raise TypeError(f"type {type(load)} is not of type Load")
90
-
91
- self.invalidate()
92
- self._loads = loads
93
- self.__validate_load_locations()
94
-
95
- def __validate_load_locations(self) -> bool:
96
- """All loads and reactions must have unique locations
97
-
98
- This function will validate that all loads do not line up with any
99
- reactions. If a load is aligned with a reaction, it is adjusted by a
100
- slight amount so it can be solved.
101
- :returns True if successful, False otherwise
102
- """
103
- assert self.reactions is not None
104
- assert self.loads is not None
105
-
106
- for reaction in self.reactions:
107
- for load in self.loads:
108
- if load.location == reaction.location:
109
- # the load is directly on the reaction. Offset the load
110
- # location a tiny amount so that it is very close, but not
111
- # exactly on the reaction.
112
- # This is done so that the global stiffness matrix
113
- # is calculated properly to give accurate results
114
-
115
- # offset the load towards the inside of the beam to be sure
116
- # the new load position is located on the beam.
117
- if reaction.location == 0:
118
- load.location += 1e-8
119
- warn(
120
- f"load location moved by 1e-8 to avoid reaction "
121
- f"at {reaction.location}"
122
- )
123
- else:
124
- load.location -= 1e-8
125
- warn(
126
- f"load location moved by -1e-8 to avoid reaction"
127
- f" at {reaction.location}"
128
- )
129
- return True
130
-
131
- @property
132
- def reactions(self) -> Optional[List[Reaction]]:
133
- return self._reactions
134
-
135
- @reactions.setter
136
- def reactions(self, reactions: List[Reaction]) -> None:
137
- for reaction in reactions:
138
- if not isinstance(reaction, Reaction):
139
- msg = f"type {type(reaction)} is not of type Reaction"
140
- raise TypeError(msg)
141
- self.invalidate()
142
- self._reactions = reactions
143
-
144
- @abstractmethod
145
- def remesh(self) -> None:
146
- """force a remesh calculation and invalidate any calculation results"""
147
- raise NotImplementedError("method must be overloaded")
148
-
149
- def invalidate(self) -> None:
150
- """invalidate the element to force resolving"""
151
- self._node_deflections = None
152
- self._K = None
153
- if self.reactions is not None:
154
- for reaction in self.reactions:
155
- reaction.invalidate()
156
-
157
- @property
158
- def K(self) -> np.array:
159
- """global stiffness matrix"""
160
- if self._K is None:
161
- self._K = self.stiffness_global()
162
- return self._K
163
-
164
- def solve(self) -> None:
165
- """solve the system the FEM system to define the nodal displacements
166
- and reaction forces.
167
- """
168
- self.__validate_load_locations()
169
- self.remesh()
170
- self._calc_node_deflections()
171
- self._get_reaction_values()
172
-
173
- @abstractmethod
174
- def _calc_node_deflections(self) -> None:
175
- raise NotImplementedError("must be overloaded!")
176
-
177
- @abstractmethod
178
- def _get_reaction_values(self) -> None:
179
- raise NotImplementedError("must be overloaded!")
180
-
181
- @abstractmethod
182
- def stiffness(self, L: float) -> None:
183
- """return local stiffness matrix, k, as numpy array evaluated with beam
184
- element length L, where L defaults to the length of the beam
185
- """
186
- raise NotImplementedError("Method must be overloaded!")
187
-
188
- @abstractmethod
189
- def stiffness_global(self) -> None:
190
- # Initialize the global stiffness matrix, then iterate over the
191
- # elements, calculate a local stiffness matrix, and add it to the
192
- # global stiffness matrix.
193
- raise NotImplementedError("Method must be overloaded!")
194
-
195
- @staticmethod
196
- def apply_boundary_conditions(
197
- k: np.array, bcs: BOUNDARY_CONDITIONS
198
- ) -> np.array:
199
- """
200
- Given the stiffness matrix 'k_local', and the boundary conditions as a list
201
- of tuples, apply the boundary conditions to the stiffness matrix by
202
- setting the rows and columns that correspond to the boundary conditions
203
- to zeros, with ones on the diagonal.
204
-
205
- The boundary conditions (bcs) are in the form
206
- bcs = [(displacement1, rotation1), (displacement2, rotation2)]
207
-
208
- For the boundary condition, if the conditional evaluates to None, then
209
- movement is allowed, otherwise no displacement is allowed.
210
-
211
- The boundary condition coordinates must match the stiffness matrix.
212
- That is, if the stiffness matrix is a local matrix, the boundary
213
- conditions must also be local.
214
-
215
- returns the adjusted stiffness matrix after the boundary
216
- conditions are applied
217
- """
218
-
219
- def apply(k_local: np.array, i: int) -> np.array:
220
- """sub function to apply the boundary condition at row/col i to
221
- stiffness matrix k_local
222
-
223
- return the stiffness matrix k_local with boundary conditions applied
224
- """
225
- k_local[i] = 0 # set entire row to zeros
226
- k_local[:, i] = 0 # set entire column to zeros
227
- k_local[i][i] = 1 # set diagonal to 1
228
- return k_local
229
-
230
- # TODO: Check the sizes of the boundary conditions and stiffness matrix
231
-
232
- for node, bc in enumerate(bcs):
233
- v, q = bc
234
- if v is not None:
235
- k = apply(k, node * 2)
236
- if q is not None:
237
- k = apply(k, node * 2 + 1)
238
- return k
239
-
240
-
241
- # Allow upper case letters for variable names to match engineering conventions
242
- # for variables, such as E for Young's modulus and I for the polar moment of
243
- # inertia
244
- # noinspection PyPep8Naming
245
- class BeamElement(Element):
246
- """base beam element"""
247
-
248
- def __init__(
249
- self,
250
- length: float,
251
- loads: List[Load],
252
- reactions: List[Reaction],
253
- E: float = 1,
254
- Ixx: float = 1,
255
- ):
256
- super().__init__(length, E, Ixx)
257
- self.reactions = reactions
258
- self.loads = loads # note loads are set after reactions
259
- self.mesh = Mesh(length, loads, reactions, 2)
260
-
261
- def remesh(self) -> None:
262
- assert self.loads is not None
263
- assert self.reactions is not None
264
- self.mesh = Mesh(self.length, self.loads, self.reactions, 2)
265
- self.invalidate()
266
-
267
- @property
268
- def node_deflections(self) -> np.ndarray:
269
- if self._node_deflections is None:
270
- self._node_deflections = self._calc_node_deflections()
271
- return self._node_deflections
272
-
273
- def __get_boundary_conditions(self) -> BOUNDARY_CONDITIONS:
274
- # Initialize the boundary conditions to None for each node, then
275
- # iterate over reactions and apply them to the boundary conditions
276
- # based on the reaction type.
277
- assert self.reactions is not None
278
- bc: BOUNDARY_CONDITIONS = [
279
- (None, None) for _ in range(len(self.mesh.nodes))
280
- ]
281
- for r in self.reactions:
282
- assert r is not None
283
- i = self.mesh.nodes.index(r.location)
284
- bc[i] = r.boundary
285
- return bc
286
-
287
- def _calc_node_deflections(self) -> np.ndarray:
288
- """solve for vertical and angular displacement at each node"""
289
- assert self.loads is not None
290
-
291
- # Get the boundary conditions from the reactions
292
- bc = self.__get_boundary_conditions()
293
-
294
- # Apply boundary conditions to global stiffness matrix. Note that the
295
- # boundary conditions are applied to a copy of the stiffness matrix to
296
- # avoid changing the property K, so it can still be used with further
297
- # calculations (ie, for calculating reaction values)
298
- kg = self.K.copy()
299
- kg = self.apply_boundary_conditions(kg, bc)
300
-
301
- # Use the same method of adding the input loads as the boundary
302
- # conditions. Start by initializing a numpy array to zero loads, then
303
- # iterate over the loads and add them to the appropriate index based on
304
- # the load type (force or moment)
305
- # noinspection PyUnresolvedReferences
306
- p = np.zeros((self.mesh.dof, 1))
307
- for ld in self.loads:
308
- i = self.mesh.nodes.index(ld.location)
309
- if isinstance(ld, PointLoad):
310
- p[i * 2][0] = ld.magnitude # input force
311
- else:
312
- p[i * 2 + 1][0] = ld.magnitude # input moment
313
-
314
- # Solve the global system of equations {p} = [K]*{d} for {d}
315
- # save the deflection vector for the beam, so the analysis can be
316
- # reused without recalculating the stiffness matrix.
317
- # This vector should be cleared anytime any of the beam parameters
318
- # gets changed.
319
- self._node_deflections = np.linalg.solve(kg, p)
320
- return self._node_deflections
321
-
322
- def _get_reaction_values(self) -> np.ndarray:
323
- """Calculate the nodal forces acting on the beam. Note that the forces
324
- will also include the input forces.
325
-
326
- reactions are calculated by solving the matrix equation
327
- {r} = [K] * {d}
328
-
329
- where
330
- - {r} is the vector of forces acting on the beam
331
- - [K] is the global stiffness matrix (without BCs applied)
332
- - {d} displacements of nodes
333
- """
334
- K = self.K # global stiffness matrix
335
- d = self.node_deflections # force displacement vector
336
-
337
- # noinspection PyUnresolvedReferences
338
- r = np.matmul(K, d)
339
- assert self.reactions is not None
340
-
341
- for ri in self.reactions:
342
- i = self.mesh.nodes.index(ri.location)
343
- force, moment = r[i * 2 : i * 2 + 2]
344
-
345
- # set the values in the reaction objects
346
- ri.force = force[0]
347
- ri.moment = moment[0]
348
- return r
349
-
350
- def shape(self, x: float, L: Optional[float] = None) -> np.ndarray:
351
- """return an array of the shape functions evaluated at x the local
352
- x-value
353
- """
354
- if L is None:
355
- L = self.length
356
- N1 = 1 / L ** 3 * (L ** 3 - 3 * L * x ** 2 + 2 * x ** 3)
357
- N2 = 1 / L ** 2 * (L ** 2 * x - 2 * L * x ** 2 + x ** 3)
358
- N3 = 1 / L ** 3 * (3 * L * x ** 2 - 2 * x ** 3)
359
- N4 = 1 / L ** 2 * (-L * x ** 2 + x ** 3)
360
- return np.array([N1, N2, N3, N4])
361
-
362
- def plot_shapes(self, n: int = 25) -> None: # pragma: no cover
363
- """plot shape functions for the with n data points"""
364
- x = np.linspace(0, self.length, n)
365
-
366
- # set up list of axes with a grid where the two figures in each column
367
- # share an x axis
368
- axes = []
369
- fig = plt.figure()
370
- axes.append(fig.add_subplot(221))
371
- axes.append(fig.add_subplot(222))
372
- axes.append(fig.add_subplot(223, sharex=axes[0]))
373
- axes.append(fig.add_subplot(224, sharex=axes[1]))
374
-
375
- N: List[List[int]] = [[], [], [], []]
376
- for xi in x:
377
- n_local = self.shape(xi)
378
- for i in range(4):
379
- N[i].append(n_local[i])
380
-
381
- for k in range(4):
382
- ax = axes[k]
383
- ax.grid(True)
384
- ax.plot(x, N[k], label=f"$N_{k + 1}$")
385
- ax.legend()
386
-
387
- fig.subplots_adjust(wspace=0.25, hspace=0)
388
- plt.show()
389
-
390
- def stiffness(self, L: float) -> np.ndarray:
391
- """return local stiffness matrix, k, as numpy array evaluated with beam
392
- element length L
393
- """
394
-
395
- E = self.E
396
- Ixx = self.Ixx
397
-
398
- k = np.array(
399
- [
400
- [12, 6 * L, -12, 6 * L],
401
- [6 * L, 4 * L ** 2, -6 * L, 2 * L ** 2],
402
- [-12, -6 * L, 12, -6 * L],
403
- [6 * L, 2 * L ** 2, -6 * L, 4 * L ** 2],
404
- ]
405
- )
406
- return E * Ixx / L ** 3 * k
407
-
408
- def stiffness_global(self) -> np.array:
409
- # Initialize the global stiffness matrix, then iterate over the
410
- # elements, calculate a local stiffness matrix, and add it to the
411
- # global stiffness matrix.
412
- # noinspection PyUnresolvedReferences
413
- kg = np.zeros((self.mesh.dof, self.mesh.dof))
414
- for e in range(self.mesh.num_elements):
415
- # iterate over all the elements and add the local stiffness matrix
416
- # to the global stiffness matrix at the proper index
417
- k = self.stiffness(self.mesh.lengths[e]) # local stiffness matrix
418
- i1, i2 = (e * 2, e * 2 + 4) # global slicing index
419
- kg[i1:i2, i1:i2] = kg[i1:i2, i1:i2] + k # current element
420
- self._K = kg
421
-
422
- return self._K
femethods/core/_common.py DELETED
@@ -1,117 +0,0 @@
1
- """
2
- Base module that contains base classes to be used by other modules
3
- """
4
-
5
- from abc import ABC
6
- from typing import Callable, Optional
7
-
8
- from numpy import float64
9
-
10
-
11
- class Forces(ABC):
12
- """Base class for all loads and reactions"""
13
-
14
- def __init__(
15
- self, magnitude: Optional[float], location: float = 0
16
- ) -> None:
17
- self.magnitude = magnitude
18
- self.location = location
19
-
20
- @property
21
- def magnitude(self) -> Optional[float]:
22
- return self._magnitude
23
-
24
- @magnitude.setter
25
- def magnitude(self, magnitude: float) -> None:
26
- if not isinstance(magnitude, (int, float, type(None))):
27
- raise TypeError("force value must be a number")
28
- self._magnitude = magnitude
29
-
30
- @property
31
- def location(self) -> float:
32
- return self._location
33
-
34
- @location.setter
35
- def location(self, location: float) -> None:
36
- if location < 0:
37
- # location must be positive to be a valid length/position
38
- raise ValueError("location must be positive!")
39
- self._location = location
40
-
41
- def __repr__(self) -> str:
42
- return (
43
- f"{self.__class__.__name__}(magnitude={self.magnitude}, "
44
- + f"location={self.location})"
45
- )
46
-
47
- def __add__(self, force2: "Forces") -> Optional["Forces"]:
48
-
49
- # assert to validate type checking for mypy
50
- assert self.magnitude is not None
51
- assert force2.magnitude is not None
52
-
53
- f1 = self.magnitude
54
- x1 = self.location
55
-
56
- f2 = force2.magnitude
57
- x2 = force2.location
58
-
59
- x = (f1 * x1 + f2 * x2) / (f1 + f2)
60
- return self.__class__(f1 + f2, x)
61
-
62
- def __eq__(self, other: object) -> bool:
63
- if not isinstance(other, self.__class__):
64
- return False
65
- if self.magnitude is None and other.magnitude is None:
66
- return self.location == other.location
67
- if self.magnitude is None or other.magnitude is None:
68
- return False
69
- return (
70
- self.magnitude * self.location == other.magnitude * other.location
71
- )
72
-
73
- def __sub__(self, force2: "Forces") -> Optional["Forces"]:
74
-
75
- assert self.magnitude is not None
76
- assert force2.magnitude is not None
77
-
78
- f1 = self.magnitude
79
- x1 = self.location
80
-
81
- f2 = force2.magnitude
82
- x2 = force2.location
83
-
84
- x = (f1 * x1 - f2 * x2) / (f1 - f2)
85
- return self.__class__(f1 - f2, x)
86
-
87
-
88
- def derivative(
89
- func: Callable, x0: float, n: int = 1, method: str = "forward"
90
- ) -> float64:
91
- """
92
- Calculate the nth derivative of function f at x0
93
-
94
- Calculate the 1st or 2nd order derivative of a function using
95
- the forward or backward method.
96
- """
97
-
98
- if n not in (1, 2):
99
- raise ValueError("n must be 1 or 2")
100
-
101
- # Note that the value for dx is set manually. This is because the ideal
102
- # values are not constant based on the method used.
103
- # TODO determine better method for choosing a more ideal dx value
104
- if method == "forward":
105
- dx = 1e-8
106
- if n == 1:
107
- return (func(x0 + dx) - func(x0)) / dx
108
- elif n == 2:
109
- return (func(x0 + 2 * dx) - 2 * func(x0 + dx) + func(x0)) / dx ** 2
110
- elif method == "backward":
111
- dx = 1e-5
112
- if n == 1:
113
- return (func(x0) - func(x0 - dx)) / dx
114
- elif n == 2:
115
- return (func(x0) - 2 * func(x0 - dx) + func(x0 - 2 * dx)) / dx ** 2
116
-
117
- raise ValueError(f'invalid method parameter "{method}"')