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/core/_base_elements.py
DELETED
|
@@ -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}"')
|