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/elements.py
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The elements module contains finite element classes
|
|
3
|
-
|
|
4
|
-
Currently the only element that is defined is a beam element.
|
|
5
|
-
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import Any, List, TYPE_CHECKING, Tuple
|
|
9
|
-
from warnings import warn
|
|
10
|
-
|
|
11
|
-
import matplotlib.pyplot as plt
|
|
12
|
-
import numpy as np
|
|
13
|
-
from scipy.misc import derivative
|
|
14
|
-
|
|
15
|
-
# local imports
|
|
16
|
-
from femethods.core._base_elements import BeamElement
|
|
17
|
-
from femethods.core._common import derivative as comm_derivative
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
-
from femethods.loads import Load # noqa: F401 (unused import)
|
|
21
|
-
from femethods.reactions import Reaction # noqa: F401 (unused import)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# noinspection PyPep8Naming
|
|
25
|
-
class Beam(BeamElement):
|
|
26
|
-
"""A Beam defines a beam element for analysis
|
|
27
|
-
|
|
28
|
-
A beam element is a slender member that is subjected to transverse loading.
|
|
29
|
-
It is assumed to have homogeneous properties, with a constant
|
|
30
|
-
cross-section.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Parameters:
|
|
34
|
-
length (:obj:`float`): the length of a beam. This is the total length
|
|
35
|
-
of the beam, this is not the length of the meshed
|
|
36
|
-
element. This must be a float that is greater than 0.
|
|
37
|
-
loads (:obj:`list`): list of load elements
|
|
38
|
-
reactions (:obj:`list`): list of reactions acting on the beam
|
|
39
|
-
E (:obj:`float`, optional): Young's modulus of the beam in units of
|
|
40
|
-
:math:`\\frac{force}{length^2}`. Defaults to 1.
|
|
41
|
-
The :math:`force` units used here are the same
|
|
42
|
-
units that are used in the input forces, and
|
|
43
|
-
calculated reaction forces. The :math:`length` unit
|
|
44
|
-
must be the same as the area moment of inertia
|
|
45
|
-
(**Ixx**) and the beam **length** units.
|
|
46
|
-
Ixx (:obj:`float`, optional): Area moment of inertia of the beam.
|
|
47
|
-
Defaults to 1. This is constant (constant cross-sectional
|
|
48
|
-
area) along the length of the beam. This is in units of
|
|
49
|
-
:math:`length^4`. This must be the same length unit of
|
|
50
|
-
Young's modulus (**E**) and the beam **length**.
|
|
51
|
-
|
|
52
|
-
"""
|
|
53
|
-
|
|
54
|
-
def __init__(
|
|
55
|
-
self,
|
|
56
|
-
length: float,
|
|
57
|
-
loads: List["Load"],
|
|
58
|
-
reactions: List["Reaction"],
|
|
59
|
-
E: float = 1,
|
|
60
|
-
Ixx: float = 1,
|
|
61
|
-
):
|
|
62
|
-
super().__init__(length, loads, reactions, E=E, Ixx=Ixx)
|
|
63
|
-
|
|
64
|
-
def deflection(self, x: float) -> np.float64:
|
|
65
|
-
"""Calculate deflection of the beam at location x
|
|
66
|
-
|
|
67
|
-
Parameters:
|
|
68
|
-
x (:obj:`float | int`): location along the length of the beam where
|
|
69
|
-
deflection should be calculated.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
:obj:`float`: deflection of the beam in units of the beam length
|
|
73
|
-
|
|
74
|
-
Raises:
|
|
75
|
-
:obj:`ValueError`: when the :math:`0\\leq x \\leq length` is False
|
|
76
|
-
:obj:`TypeError`: when x cannot be converted to a float
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
# TODO: store the lengths/node locations in the class so they only have
|
|
80
|
-
# to be assessed without recalculating
|
|
81
|
-
nodes = self.mesh.nodes
|
|
82
|
-
|
|
83
|
-
# validate that x is a valid by ensuring that x is
|
|
84
|
-
# - x is a number
|
|
85
|
-
# - 0 <= x <= length of beam
|
|
86
|
-
try:
|
|
87
|
-
x = float(x)
|
|
88
|
-
except ValueError:
|
|
89
|
-
raise TypeError(
|
|
90
|
-
f"Cannot calculate deflection with location of type: {type(x)}"
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
if x < 0 or self.length < x:
|
|
94
|
-
raise ValueError(
|
|
95
|
-
f"cannot calculate deflection at location {x} as "
|
|
96
|
-
f"it is outside of the beam!"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Using the given global x-value, determine the local x-value, length
|
|
100
|
-
# of active element, and the nodal displacements (vertical, angular)
|
|
101
|
-
# vector d
|
|
102
|
-
for i in range(len(self.mesh.lengths)):
|
|
103
|
-
if nodes[i] <= x <= nodes[i + 1]:
|
|
104
|
-
# this is the element where the global x-value falls into.
|
|
105
|
-
# Get the parameters in the local system and exit the loop
|
|
106
|
-
x_local = x - nodes[i]
|
|
107
|
-
L = self.mesh.lengths[i]
|
|
108
|
-
d = self.node_deflections[i * 2 : i * 2 + 4]
|
|
109
|
-
return self.shape(x_local, L).dot(d)[0]
|
|
110
|
-
|
|
111
|
-
def moment(self, x: float, dx: float = 1e-5, order: int = 9) -> np.float64:
|
|
112
|
-
"""Calculate the moment at location x
|
|
113
|
-
|
|
114
|
-
Calculate the moment in the beam at the global x value by taking
|
|
115
|
-
the second derivative of the deflection curve.
|
|
116
|
-
|
|
117
|
-
.. centered::
|
|
118
|
-
:math:`M(x) = E \\cdot Ixx \\cdot \\frac{d^2 v(x)}{dx^2}`
|
|
119
|
-
|
|
120
|
-
where :math:`M` is the moment, :math:`E` is Young's modulus and
|
|
121
|
-
:math:`Ixx` is the area moment of inertia.
|
|
122
|
-
|
|
123
|
-
.. note: When calculating the moment near the beginning of the beam
|
|
124
|
-
the moment calculation may be unreliable.
|
|
125
|
-
|
|
126
|
-
Parameters:
|
|
127
|
-
x (:obj:`int`): location along the beam where moment is calculated
|
|
128
|
-
dx (:obj:`float`, optional): spacing. Default is 1e-5
|
|
129
|
-
order (:obj:`int`, optional): number of points to use, must be odd.
|
|
130
|
-
Default is 9
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
:obj:`float`: moment in beam at location x
|
|
134
|
-
|
|
135
|
-
Raises:
|
|
136
|
-
:obj:`ValueError`: when the :math:`0\\leq x \\leq length` is False
|
|
137
|
-
:obj:`TypeError`: when x cannot be converted to a float
|
|
138
|
-
|
|
139
|
-
For more information on the parameters, see the scipy.misc.derivative
|
|
140
|
-
documentation.
|
|
141
|
-
"""
|
|
142
|
-
|
|
143
|
-
# TODO: Update so that moment can be calculated at both ends of beam
|
|
144
|
-
if x < 0.75:
|
|
145
|
-
# this cut-off was found experimentally. Anything less than this,
|
|
146
|
-
# and calculating the derivative is unreliable
|
|
147
|
-
warn("Calculated moments below 0.75 may be unreliable")
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
return (
|
|
151
|
-
self.E
|
|
152
|
-
* self.Ixx
|
|
153
|
-
* derivative(self.deflection, x, dx=dx, n=2, order=order)
|
|
154
|
-
)
|
|
155
|
-
except ValueError:
|
|
156
|
-
# there was an error, probably due to the central difference
|
|
157
|
-
# method attempting to calculate the moment near the ends of the
|
|
158
|
-
# beam. Determine whether the desired position is near the start
|
|
159
|
-
# or end of the beam, and use the forward/backward difference
|
|
160
|
-
# method accordingly
|
|
161
|
-
|
|
162
|
-
if x <= self.length / 2:
|
|
163
|
-
# the desired moment is near the beginning of the beam, use the
|
|
164
|
-
# forward difference method
|
|
165
|
-
method = "forward"
|
|
166
|
-
else:
|
|
167
|
-
# the desired moment is near the end of the beam, use the
|
|
168
|
-
# backward difference method
|
|
169
|
-
method = "backward"
|
|
170
|
-
return (
|
|
171
|
-
self.E
|
|
172
|
-
* self.Ixx
|
|
173
|
-
* comm_derivative(self.deflection, x, method=method, n=2)
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
def shear(self, x: float, dx: float = 0.01, order: int = 5) -> np.float64:
|
|
177
|
-
"""
|
|
178
|
-
Calculate the shear force in the beam at location x
|
|
179
|
-
|
|
180
|
-
Calculate the shear in the beam at the global x value by taking
|
|
181
|
-
the third derivative of the deflection curve.
|
|
182
|
-
|
|
183
|
-
.. centered::
|
|
184
|
-
:math:`V(x) = E \\cdot Ixx \\cdot \\frac{d^3 v(x)}{dx^3}`
|
|
185
|
-
|
|
186
|
-
where :math:`V` is the shear force, :math:`E` is Young's modulus and
|
|
187
|
-
:math:`Ixx` is the area moment of inertia.
|
|
188
|
-
|
|
189
|
-
.. note: When calculating the shear near the beginning of the beam
|
|
190
|
-
the shear calculation may be unreliable.
|
|
191
|
-
|
|
192
|
-
Parameters:
|
|
193
|
-
x (:obj:`int`): location along the beam where moment is calculated
|
|
194
|
-
dx (:obj:`float`, optional): spacing. Default is 0.01
|
|
195
|
-
order (:obj:`int`, optional): number of points to use, must be odd.
|
|
196
|
-
Default is 5
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
:obj:`float`: moment in beam at location x
|
|
200
|
-
|
|
201
|
-
Raises:
|
|
202
|
-
:obj:`ValueError`: when the :math:`0\\leq x \\leq length` is False
|
|
203
|
-
:obj:`TypeError`: when x cannot be converted to a float
|
|
204
|
-
|
|
205
|
-
For more information on the parameters, see the scipy.misc.derivative
|
|
206
|
-
documentation.
|
|
207
|
-
"""
|
|
208
|
-
return (
|
|
209
|
-
self.E
|
|
210
|
-
* self.Ixx
|
|
211
|
-
* derivative(self.deflection, x, dx=dx, n=3, order=order)
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
def bending_stress(self, x, dx=1, c=1):
|
|
215
|
-
"""
|
|
216
|
-
returns the bending stress at global coordinate x
|
|
217
|
-
|
|
218
|
-
.. deprecated:: 0.1.7a1
|
|
219
|
-
calculate bending stress as :obj:`Beam.moment(x) * c / Ixx`
|
|
220
|
-
|
|
221
|
-
"""
|
|
222
|
-
warn("bending_stress will be removed soon", DeprecationWarning)
|
|
223
|
-
return self.moment(x, dx=dx) * c / self.Ixx
|
|
224
|
-
|
|
225
|
-
@staticmethod
|
|
226
|
-
def __validate_plot_diagrams(diagrams, diagram_labels):
|
|
227
|
-
"""
|
|
228
|
-
Validate the parameters for the plot function
|
|
229
|
-
"""
|
|
230
|
-
|
|
231
|
-
# create default (and complete list of valid) diagrams that are
|
|
232
|
-
# implemented
|
|
233
|
-
default_diagrams = ("shear", "moment", "deflection")
|
|
234
|
-
if diagrams is None and diagram_labels is None:
|
|
235
|
-
# set both the diagrams and labels to their defaults
|
|
236
|
-
# no need for further validation of these values since they are
|
|
237
|
-
# set internally
|
|
238
|
-
return default_diagrams, default_diagrams
|
|
239
|
-
|
|
240
|
-
if diagrams is None and diagram_labels is not None:
|
|
241
|
-
raise ValueError("cannot set diagrams from labels")
|
|
242
|
-
|
|
243
|
-
if diagram_labels is None:
|
|
244
|
-
diagram_labels = diagrams
|
|
245
|
-
|
|
246
|
-
if len(diagrams) != len(diagram_labels):
|
|
247
|
-
raise ValueError(
|
|
248
|
-
"length of diagram_labels must match length of diagrams"
|
|
249
|
-
)
|
|
250
|
-
for diagram in diagrams:
|
|
251
|
-
if diagram not in default_diagrams:
|
|
252
|
-
raise ValueError(
|
|
253
|
-
f"values of diagrams must be in {default_diagrams}"
|
|
254
|
-
)
|
|
255
|
-
return diagrams, diagram_labels
|
|
256
|
-
|
|
257
|
-
def plot(
|
|
258
|
-
self,
|
|
259
|
-
n=250,
|
|
260
|
-
title="Beam Analysis",
|
|
261
|
-
diagrams=None,
|
|
262
|
-
diagram_labels=None,
|
|
263
|
-
**kwargs,
|
|
264
|
-
):
|
|
265
|
-
"""
|
|
266
|
-
Plot the deflection, moment, and shear along the length of the beam
|
|
267
|
-
|
|
268
|
-
The plot method will create a :obj:`matplotlib.pyplot` figure with the
|
|
269
|
-
deflection, moment, and shear diagrams along the length of the beam
|
|
270
|
-
element. Which of these diagrams, and their order may be customized.
|
|
271
|
-
|
|
272
|
-
Parameters:
|
|
273
|
-
n (:obj:`int`): defaults to `250`:
|
|
274
|
-
number of data-points to use in plots
|
|
275
|
-
title (:obj:`str`) defaults to 'Beam Analysis`
|
|
276
|
-
title on top of plot
|
|
277
|
-
diagrams (:obj:`tuple`): defaults to
|
|
278
|
-
`('shear', 'moment', 'deflection')`
|
|
279
|
-
tuple of diagrams to plot. All values in tuple must be strings,
|
|
280
|
-
and one of the defaults.
|
|
281
|
-
Valid values are :obj:`('shear', 'moment', 'deflection')`
|
|
282
|
-
diagram_labels (:obj:`tuple`): y-axis labels for subplots.
|
|
283
|
-
Must have the same length as `diagrams`
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
:obj:`tuple`:
|
|
287
|
-
Tuple of :obj:`matplotlib.pyplot` figure and list of axes in
|
|
288
|
-
the form :obj:`(figure, axes)`
|
|
289
|
-
|
|
290
|
-
.. note:: The plot method will create the figure handle, but will not
|
|
291
|
-
automatically show the figure.
|
|
292
|
-
To show the figure use :obj:`Beam.show()` or
|
|
293
|
-
:obj:`matplotlib.pyplot.show()`
|
|
294
|
-
|
|
295
|
-
.. versionchanged:: 0.1.7a1 Removed :obj:`bending_stress` parameter
|
|
296
|
-
.. versionchanged:: 0.1.7a1
|
|
297
|
-
Added :obj:`diagrams` and :obj:`diagram_labels` parameters
|
|
298
|
-
|
|
299
|
-
"""
|
|
300
|
-
|
|
301
|
-
kwargs.setdefault("title", "Beam Analysis")
|
|
302
|
-
kwargs.setdefault("grid", True)
|
|
303
|
-
kwargs.setdefault("xlabel", "Beam position, x")
|
|
304
|
-
kwargs.setdefault("fill", True)
|
|
305
|
-
kwargs.setdefault("plot_kwargs", {})
|
|
306
|
-
kwargs.setdefault("fill_kwargs", {"color": "b", "alpha": 0.25})
|
|
307
|
-
|
|
308
|
-
diagrams, diagram_labels = self.__validate_plot_diagrams(
|
|
309
|
-
diagrams, diagram_labels
|
|
310
|
-
)
|
|
311
|
-
fig, axes = plt.subplots(len(diagrams), 1, sharex="all")
|
|
312
|
-
if len(diagrams) == 1:
|
|
313
|
-
# make sure axes are iterable, even if there is only one
|
|
314
|
-
axes = [axes]
|
|
315
|
-
|
|
316
|
-
xd = np.linspace(0, self.length, n) # deflection
|
|
317
|
-
x, y = None, None
|
|
318
|
-
for ax, diagram, label in zip(axes, diagrams, diagram_labels):
|
|
319
|
-
if diagram == "deflection":
|
|
320
|
-
x = xd
|
|
321
|
-
y = [self.deflection(xi) for xi in x]
|
|
322
|
-
if diagram == "moment":
|
|
323
|
-
x = xd
|
|
324
|
-
y = [self.moment(xi, dx=self.length / (n + 3)) for xi in x]
|
|
325
|
-
if diagram == "shear":
|
|
326
|
-
x = np.linspace(0, self.length, n + 4)[2:-2]
|
|
327
|
-
y = [self.shear(xi, dx=self.length / (n + 4)) for xi in x]
|
|
328
|
-
|
|
329
|
-
# regardless of the diagram that is being plotted, the number of
|
|
330
|
-
# data points should always equal the number specified by user
|
|
331
|
-
assert len(x) == n, "x does not match n"
|
|
332
|
-
assert len(y) == n, "y does not match n"
|
|
333
|
-
|
|
334
|
-
ax.plot(x, y, **kwargs["plot_kwargs"])
|
|
335
|
-
if kwargs["fill"]:
|
|
336
|
-
ax.fill_between(x, y, 0, **kwargs["fill_kwargs"])
|
|
337
|
-
ax.set_ylabel(label)
|
|
338
|
-
ax.grid(kwargs["grid"])
|
|
339
|
-
|
|
340
|
-
locations = self.mesh.nodes # in global coordinate system
|
|
341
|
-
axes[-1].set_xlabel(kwargs["xlabel"])
|
|
342
|
-
axes[-1].set_xticks(locations)
|
|
343
|
-
|
|
344
|
-
fig.subplots_adjust(hspace=0.25)
|
|
345
|
-
fig.suptitle(title)
|
|
346
|
-
return fig, axes
|
|
347
|
-
|
|
348
|
-
@staticmethod
|
|
349
|
-
def show(*args: Any, **kwargs: Any) -> None:
|
|
350
|
-
"""Wrapper function for showing matplotlib figure
|
|
351
|
-
|
|
352
|
-
This method gives direct access to the matplotlib.pyplot.show function
|
|
353
|
-
so the calling code is not required to import matplotlib directly
|
|
354
|
-
just to show the plots
|
|
355
|
-
|
|
356
|
-
Parameters:
|
|
357
|
-
args/kwargs: args and kwargs are passed directly to
|
|
358
|
-
matplotlib.pyplot.show
|
|
359
|
-
"""
|
|
360
|
-
plt.show(*args, **kwargs) # pragma: no cover
|
|
361
|
-
|
|
362
|
-
def __str__(self) -> str:
|
|
363
|
-
assert self.loads is not None
|
|
364
|
-
assert self.reactions is not None
|
|
365
|
-
|
|
366
|
-
L = ""
|
|
367
|
-
for load in self.loads:
|
|
368
|
-
L += "Type: {}\n".format(load.name)
|
|
369
|
-
L += " Location: {}\n".format(load.location)
|
|
370
|
-
L += " Magnitude: {}\n".format(load.magnitude)
|
|
371
|
-
|
|
372
|
-
r = ""
|
|
373
|
-
for reaction in self.reactions:
|
|
374
|
-
r += "Type: {}\n".format(reaction.name)
|
|
375
|
-
r += " Location: {}\n".format(reaction.location)
|
|
376
|
-
r += " Force: {}\n".format(reaction.force)
|
|
377
|
-
r += " Moment: {}\n".format(reaction.moment)
|
|
378
|
-
|
|
379
|
-
msg = (
|
|
380
|
-
"PARAMETERS\n"
|
|
381
|
-
f"Length (length): {self.length}\n"
|
|
382
|
-
f"Young's Modulus (E): {self.E}\n"
|
|
383
|
-
f"Area moment of inertia (Ixx): {self.Ixx}\n"
|
|
384
|
-
f"LOADING\n"
|
|
385
|
-
f"{L}\n"
|
|
386
|
-
f"REACTIONS\n"
|
|
387
|
-
f"{r}\n"
|
|
388
|
-
)
|
|
389
|
-
return msg
|
femethods/loads.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module to define different loads
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
|
-
from femethods.core._common import Forces
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Load(Forces):
|
|
11
|
-
"""Base class for all load types
|
|
12
|
-
|
|
13
|
-
Used primarily for type checking the loads on input
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
name = ""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class PointLoad(Load):
|
|
20
|
-
"""
|
|
21
|
-
class specific to a point load
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
name = "point load"
|
|
25
|
-
|
|
26
|
-
def __init__(self, magnitude: Optional[float], location: float):
|
|
27
|
-
super().__init__(magnitude, location)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class MomentLoad(Load):
|
|
31
|
-
"""
|
|
32
|
-
class specific to a moment load
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
name = "moment load"
|
|
36
|
-
|
|
37
|
-
def __init__(self, magnitude: float, location: float):
|
|
38
|
-
super().__init__(magnitude, location)
|
femethods/reactions.py
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
The reactions module defines different reaction classes
|
|
3
|
-
|
|
4
|
-
A reaction is required to support an element to resist any input forces.
|
|
5
|
-
|
|
6
|
-
There are two types of reactions that are defined.
|
|
7
|
-
|
|
8
|
-
* PinnedReaction, allows rotational displacement only
|
|
9
|
-
* FixedReaction, does not allow any displacement
|
|
10
|
-
|
|
11
|
-
"""
|
|
12
|
-
from typing import Optional, Tuple
|
|
13
|
-
|
|
14
|
-
from femethods.core._common import Forces
|
|
15
|
-
|
|
16
|
-
BOUNDARY_CONDITIONS = Tuple[Optional[int], Optional[int]]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Reaction(Forces):
|
|
20
|
-
"""Base class for all reactions
|
|
21
|
-
|
|
22
|
-
The Reaction class defines general properties related to all reaction
|
|
23
|
-
types.
|
|
24
|
-
|
|
25
|
-
Parameters:
|
|
26
|
-
location (:obj:`float`): the axial location of the reaction along the
|
|
27
|
-
length of the beam.
|
|
28
|
-
|
|
29
|
-
.. note:: Any force or moment values that where calculated values are
|
|
30
|
-
invalidated (set to :obj:`None`) any time the location is set.
|
|
31
|
-
|
|
32
|
-
Attributes:
|
|
33
|
-
force (:obj:`float | None`): the force of the reaction after it has
|
|
34
|
-
been calculated
|
|
35
|
-
moment (:obj:`float | None`): The moment of the reaction after it has
|
|
36
|
-
been calculated
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
name = ""
|
|
40
|
-
|
|
41
|
-
def __init__(self, location: float):
|
|
42
|
-
super().__init__(magnitude=None, location=location)
|
|
43
|
-
self.force = None
|
|
44
|
-
self.moment = None
|
|
45
|
-
self._boundary: BOUNDARY_CONDITIONS = (None, None)
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def boundary(self) -> BOUNDARY_CONDITIONS:
|
|
49
|
-
return self._boundary
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def location(self) -> float:
|
|
53
|
-
"""
|
|
54
|
-
Location of the reaction along the length of the beam
|
|
55
|
-
|
|
56
|
-
The units of the length property is the same as the units of the beam
|
|
57
|
-
length.
|
|
58
|
-
|
|
59
|
-
The value of the location must be a positive value that is less than
|
|
60
|
-
or equal to the length of the beam, or it will raise a ValueError.
|
|
61
|
-
|
|
62
|
-
.. note:: The force and moment values are set to :obj:`None` any time
|
|
63
|
-
the location is set.
|
|
64
|
-
"""
|
|
65
|
-
return self._location
|
|
66
|
-
|
|
67
|
-
@location.setter
|
|
68
|
-
def location(self, location: float) -> None:
|
|
69
|
-
# The location is overloading the location property in Forces so that
|
|
70
|
-
# the reaction can be invalidated when the location is changed
|
|
71
|
-
if location < 0:
|
|
72
|
-
# location cannot be a negative number
|
|
73
|
-
raise ValueError("location must be positive!")
|
|
74
|
-
self.invalidate()
|
|
75
|
-
self._location = location
|
|
76
|
-
|
|
77
|
-
@property
|
|
78
|
-
def value(self) -> Tuple[Optional[float], Optional[float]]:
|
|
79
|
-
"""
|
|
80
|
-
Simple tuple of force and moment
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
:obj:`tuple` (force, moment)
|
|
84
|
-
"""
|
|
85
|
-
return self.force, self.moment
|
|
86
|
-
|
|
87
|
-
def invalidate(self) -> None:
|
|
88
|
-
"""Invalidate the reaction values
|
|
89
|
-
|
|
90
|
-
This will set the force and moment values to :obj:`None`
|
|
91
|
-
|
|
92
|
-
To be used whenever the parameters change and the reaction values are
|
|
93
|
-
no longer valid.
|
|
94
|
-
"""
|
|
95
|
-
self.force, self.moment = (None, None)
|
|
96
|
-
|
|
97
|
-
def __str__(self) -> str:
|
|
98
|
-
return (
|
|
99
|
-
f"{self.__class__.__name__}\n"
|
|
100
|
-
f" Location: {self.location}\n"
|
|
101
|
-
f" Force: {self.force}\n"
|
|
102
|
-
f" Moment: {self.moment}\n"
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def __repr__(self) -> str:
|
|
106
|
-
return f"{self.__class__.__name__}(location={self.location})"
|
|
107
|
-
|
|
108
|
-
def __eq__(self, other: object) -> bool:
|
|
109
|
-
|
|
110
|
-
if not isinstance(other, self.__class__):
|
|
111
|
-
return False
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
self.location == other.location
|
|
115
|
-
and self.force == other.force
|
|
116
|
-
and self.moment == other.moment
|
|
117
|
-
):
|
|
118
|
-
return True
|
|
119
|
-
|
|
120
|
-
return False
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
class PinnedReaction(Reaction):
|
|
124
|
-
"""
|
|
125
|
-
A PinnedReaction allows rotation displacements only
|
|
126
|
-
|
|
127
|
-
A PinnedReaction represents a pinned, frictionless pivot that can
|
|
128
|
-
resist motion both normal and axial directions to the beam. It will not
|
|
129
|
-
resist moments.
|
|
130
|
-
The deflection of a beam at the PinnedReaction is always zero, but
|
|
131
|
-
the angle is free to change
|
|
132
|
-
|
|
133
|
-
Parameters:
|
|
134
|
-
location (:obj:`float`): the axial location of the reaction along the
|
|
135
|
-
length of the beam
|
|
136
|
-
|
|
137
|
-
Attributes:
|
|
138
|
-
name (:obj:`str`): short name of the reaction (pinned). Used internally
|
|
139
|
-
|
|
140
|
-
.. warning:: The **name** attribute is used internally.
|
|
141
|
-
**Do not change this value!**
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
name = "pinned"
|
|
145
|
-
|
|
146
|
-
def __init__(self, location: float):
|
|
147
|
-
super().__init__(location)
|
|
148
|
-
# limit the vertical displacement but allow rotation
|
|
149
|
-
self._boundary: BOUNDARY_CONDITIONS = (0, None)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class FixedReaction(Reaction):
|
|
153
|
-
"""
|
|
154
|
-
A FixedReaction does not allow any displacement or change in angle
|
|
155
|
-
|
|
156
|
-
A FixedReaction resists both force and moments. The displacement and the
|
|
157
|
-
angle are both constrained and must be zero at the reaction point.
|
|
158
|
-
FixedReactions are typically applied at the ends of a Beam.
|
|
159
|
-
|
|
160
|
-
Parameters:
|
|
161
|
-
location (:obj:`float`): the axial location of the reaction along the
|
|
162
|
-
length of the beam
|
|
163
|
-
|
|
164
|
-
Attributes:
|
|
165
|
-
name (:obj:`str`): short name of the reaction (fixed). Used internally
|
|
166
|
-
|
|
167
|
-
.. warning:: The **name** attribute is used internally.
|
|
168
|
-
**Do not change this value!**
|
|
169
|
-
"""
|
|
170
|
-
|
|
171
|
-
name = "fixed"
|
|
172
|
-
|
|
173
|
-
def __init__(self, location: float):
|
|
174
|
-
super().__init__(location)
|
|
175
|
-
# do not allow vertical or rotational displacement
|
|
176
|
-
self._boundary: BOUNDARY_CONDITIONS = (0, 0)
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
femethods/__init__.py,sha256=KEvu1iKJntEfs4jna_RsP7ObqyaP7t2g13x1UvL-2Lk,157
|
|
2
|
-
femethods/elements.py,sha256=GRNfGbRYjBDsjmQLA7SSQ2QLkTWaXGlMsH5PWfPgT9U,14850
|
|
3
|
-
femethods/loads.py,sha256=FLoQ1js_vC1iBV0C_bVhXx2ey09uqGniMRjgJwrvDLg,679
|
|
4
|
-
femethods/mesh.py,sha256=aeGr0y25Yt7xD1qmfOF8cAMBd44qube394B5lFrRR4s,2979
|
|
5
|
-
femethods/reactions.py,sha256=dszKp84PtoyH99v-YI6pELp0cxBh-G3aXbBEZbyqHfI,5479
|
|
6
|
-
femethods/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
femethods/core/_base_elements.py,sha256=Ci03v7LntAgYNs3WhxIiqfLZG5IEMRg6vXxyvRD3kAg,15427
|
|
8
|
-
femethods/core/_common.py,sha256=8yOwIsTVqPuFmOTQRf5sSPV4hHu_52AbjTTIdlMC6hg,3461
|
|
9
|
-
tests/__init__.py,sha256=eoZ6GfifbqhMLNzjlqRDVil-yyBkOmVN9ujSgJWNBlY,15
|
|
10
|
-
tests/functional tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
tests/functional tests/settings.py,sha256=K2qEBPjQs-5KYkQyxMDMKhjNBLmEgo5K3kQPd8SgJqE,410
|
|
12
|
-
tests/functional tests/test_fixed_support_beams.py,sha256=zQ3Yo78zV5s3NSm9YUtNZfMUJkl6kwzix21dBOl_EQ0,878
|
|
13
|
-
tests/functional tests/test_simply_supported_beam.py,sha256=XDbmMpQlelC8pq9qKiMlpbc04KJ1e6ynvdvzjK3kfnQ,4067
|
|
14
|
-
tests/functional tests/validate.py,sha256=RcLoOnse-Mug71KstiteHL7EVUI5gcCOZIqf9sdOqRo,1351
|
|
15
|
-
femethods-0.1.7a2.dist-info/METADATA,sha256=b0zNovQTztaKzkPxw882JRQWW3dgVvnQbfdhhmDJwng,8675
|
|
16
|
-
femethods-0.1.7a2.dist-info/WHEEL,sha256=EVRjI69F5qVjm_YgqcTXPnTAv3BfSUr0WVAHuSP3Xoo,92
|
|
17
|
-
femethods-0.1.7a2.dist-info/top_level.txt,sha256=kHq0RoeYIm-d8AhLU7k4YhFsefCyRPywG-3nZWg0Du0,16
|
|
18
|
-
femethods-0.1.7a2.dist-info/RECORD,,
|
tests/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# coding=utf-8
|
|
File without changes
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Common constants and parameters to be used functional tests. This is done so that
|
|
3
|
-
the beams are all similar in size and loading.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
L = 120 # in, length of beam in inches
|
|
7
|
-
P = -1000 # lbs, load acting on beam
|
|
8
|
-
E = 29e6 # psi, Young's modulus
|
|
9
|
-
Ixx = 350 # in^4 area moment of inertia of beam
|
|
10
|
-
EI = E * Ixx # common constant
|
|
11
|
-
TOL = 1e-1 # allowable tolerance between exact and numerical solutions to pass
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Functional tests for beams with fixed supports
|
|
3
|
-
|
|
4
|
-
https://www.awc.org/pdf/codes-standards/publications/design-aids/AWC-DA6-BeamFormulas-0710.pdf
|
|
5
|
-
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
from settings import E, EI, Ixx, L, P, TOL
|
|
10
|
-
from validate import validate
|
|
11
|
-
|
|
12
|
-
from femethods.elements import Beam
|
|
13
|
-
from femethods.loads import PointLoad
|
|
14
|
-
from femethods.reactions import FixedReaction
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_cantilevered_beam_load_at_end():
|
|
18
|
-
"""fixed beam with concentrated load at free end
|
|
19
|
-
case 13
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
R = -P
|
|
23
|
-
M_max = P * L # at fixed end
|
|
24
|
-
|
|
25
|
-
d_max = P * L ** 3 / (3 * EI) # at free end
|
|
26
|
-
|
|
27
|
-
beam = Beam(
|
|
28
|
-
length=L,
|
|
29
|
-
loads=[PointLoad(magnitude=P, location=0)],
|
|
30
|
-
reactions=[FixedReaction(L)],
|
|
31
|
-
E=E,
|
|
32
|
-
Ixx=Ixx,
|
|
33
|
-
)
|
|
34
|
-
beam.solve()
|
|
35
|
-
|
|
36
|
-
validate(beam, loc=0, R=[(R, M_max)], M_loc=0, d_loc=d_max)
|
|
37
|
-
|
|
38
|
-
assert pytest.approx(beam.moment(L), rel=TOL) == M_max
|