ebbef2p-python 0.0.2.dev1__py3-none-any.whl → 1.0.0__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.
- ebbef2p/__init__.py +24 -2
- ebbef2p/elements.py +117 -0
- ebbef2p/exceptions.py +23 -0
- ebbef2p/mesh.py +116 -0
- ebbef2p/model.py +189 -0
- ebbef2p/plotting.py +36 -0
- ebbef2p/results.py +215 -0
- ebbef2p/solver.py +130 -0
- ebbef2p/vlasov.py +177 -0
- ebbef2p_python-1.0.0.dist-info/METADATA +126 -0
- ebbef2p_python-1.0.0.dist-info/RECORD +14 -0
- {ebbef2p_python-0.0.2.dev1.dist-info → ebbef2p_python-1.0.0.dist-info}/WHEEL +1 -2
- {ebbef2p_python-0.0.2.dev1.dist-info → ebbef2p_python-1.0.0.dist-info/licenses}/LICENSE +1 -1
- ebbef2p/beam.py +0 -36
- ebbef2p/distributed_load.py +0 -16
- ebbef2p/elastic_foundation_support.py +0 -33
- ebbef2p/element.py +0 -76
- ebbef2p/helpers.py +0 -15
- ebbef2p/nodal_load.py +0 -5
- ebbef2p/nodal_support.py +0 -7
- ebbef2p/soil_conditions.py +0 -33
- ebbef2p/structure.py +0 -394
- ebbef2p/vlasov_foundation_parameters.py +0 -46
- ebbef2p_python-0.0.2.dev1.dist-info/METADATA +0 -40
- ebbef2p_python-0.0.2.dev1.dist-info/RECORD +0 -19
- ebbef2p_python-0.0.2.dev1.dist-info/top_level.txt +0 -2
- tests/test_beam.py +0 -13
- tests/test_structure.py +0 -25
- /tests/__init__.py → /ebbef2p/py.typed +0 -0
ebbef2p/__init__.py
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""EBBEF2p: Euler-Bernoulli beams on two-parameter elastic foundations."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import ConvergenceError, EBBEF2pError, ModelError, UnstableModelError
|
|
4
|
+
from .model import Model
|
|
5
|
+
from .results import ElementEndValues, ReactivePressure, Solution
|
|
6
|
+
from .solver import solve
|
|
7
|
+
from .vlasov import VlasovResult, VlasovSoil, solve_vlasov
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ConvergenceError",
|
|
11
|
+
"EBBEF2pError",
|
|
12
|
+
"ElementEndValues",
|
|
13
|
+
"Model",
|
|
14
|
+
"ModelError",
|
|
15
|
+
"ReactivePressure",
|
|
16
|
+
"Solution",
|
|
17
|
+
"UnstableModelError",
|
|
18
|
+
"VlasovResult",
|
|
19
|
+
"VlasovSoil",
|
|
20
|
+
"solve",
|
|
21
|
+
"solve_vlasov",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
ebbef2p/elements.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Element matrices for an Euler-Bernoulli beam on a two-parameter foundation.
|
|
2
|
+
|
|
3
|
+
Two-node element, degrees of freedom ``(w_i, theta_i, w_j, theta_j)`` with
|
|
4
|
+
``theta = dw/dx``, cubic Hermite interpolation. All foundation and load
|
|
5
|
+
quantities vary linearly along the element, so every matrix below is the
|
|
6
|
+
exact Galerkin integral for that variation.
|
|
7
|
+
|
|
8
|
+
The matrix functions accept scalars or arrays of element properties; for
|
|
9
|
+
arrays of shape ``(n,)`` they return a stack of shape ``(n, 4, 4)`` (or
|
|
10
|
+
``(n, 4)`` for load vectors).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from numpy.typing import ArrayLike, NDArray
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _stack(rows: list[list[ArrayLike]]) -> NDArray[np.float64]:
|
|
18
|
+
"""Matrix (or stack of matrices) from a nested list of broadcastable entries."""
|
|
19
|
+
entries = np.broadcast_arrays(*(np.asarray(v, dtype=float) for row in rows for v in row))
|
|
20
|
+
m = np.stack(entries, axis=-1)
|
|
21
|
+
return m.reshape((*m.shape[:-1], len(rows), len(rows[0])))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _rows(*entries: ArrayLike) -> NDArray[np.float64]:
|
|
25
|
+
"""Stack broadcastable entries along a new first axis."""
|
|
26
|
+
return np.stack(np.broadcast_arrays(*(np.asarray(v, dtype=float) for v in entries)))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def shape_functions(xi: ArrayLike, l: ArrayLike) -> NDArray[np.float64]:
|
|
30
|
+
"""Hermite shape functions at the local coordinate ``xi`` in [0, 1].
|
|
31
|
+
|
|
32
|
+
``xi`` and ``l`` broadcast against each other; the result has the four
|
|
33
|
+
functions along the first axis.
|
|
34
|
+
"""
|
|
35
|
+
xi, l = np.asarray(xi, dtype=float), np.asarray(l, dtype=float)
|
|
36
|
+
return _rows(
|
|
37
|
+
1 - 3*xi**2 + 2*xi**3,
|
|
38
|
+
l*xi*(1 - xi)**2,
|
|
39
|
+
3*xi**2 - 2*xi**3,
|
|
40
|
+
l*(xi**3 - xi**2),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def shape_function_derivatives(xi: ArrayLike, l: ArrayLike, order: int) -> NDArray[np.float64]:
|
|
45
|
+
"""Derivatives of the shape functions with respect to ``x``."""
|
|
46
|
+
xi, l = np.asarray(xi, dtype=float), np.asarray(l, dtype=float)
|
|
47
|
+
if order == 1:
|
|
48
|
+
return _rows(
|
|
49
|
+
(-6*xi + 6*xi**2)/l,
|
|
50
|
+
1 - 4*xi + 3*xi**2,
|
|
51
|
+
(6*xi - 6*xi**2)/l,
|
|
52
|
+
3*xi**2 - 2*xi,
|
|
53
|
+
)
|
|
54
|
+
if order == 2:
|
|
55
|
+
return _rows(
|
|
56
|
+
(-6 + 12*xi)/l**2,
|
|
57
|
+
(-4 + 6*xi)/l,
|
|
58
|
+
(6 - 12*xi)/l**2,
|
|
59
|
+
(-2 + 6*xi)/l,
|
|
60
|
+
)
|
|
61
|
+
if order == 3:
|
|
62
|
+
z = np.zeros_like(xi)
|
|
63
|
+
return _rows(12/l**3 + z, 6/l**2 + z, -12/l**3 + z, 6/l**2 + z)
|
|
64
|
+
raise ValueError("order must be 1, 2 or 3")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def bending_stiffness(EI: ArrayLike, l: ArrayLike) -> NDArray[np.float64]:
|
|
68
|
+
"""Flexural stiffness matrix, int EI N''^T N'' dx."""
|
|
69
|
+
EI, l = np.asarray(EI, dtype=float), np.asarray(l, dtype=float)
|
|
70
|
+
c = EI/l**3
|
|
71
|
+
return _stack([
|
|
72
|
+
[12*c, 6*l*c, -12*c, 6*l*c],
|
|
73
|
+
[6*l*c, 4*l**2*c, -6*l*c, 2*l**2*c],
|
|
74
|
+
[-12*c, -6*l*c, 12*c, -6*l*c],
|
|
75
|
+
[6*l*c, 2*l**2*c, -6*l*c, 4*l**2*c],
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def winkler_stiffness(k_i: ArrayLike, k_j: ArrayLike, l: ArrayLike) -> NDArray[np.float64]:
|
|
80
|
+
"""First-parameter foundation matrix, int k(x) N^T N dx, k linear."""
|
|
81
|
+
a, b, l = (np.asarray(v, dtype=float) for v in (k_i, k_j, l))
|
|
82
|
+
return _stack([
|
|
83
|
+
[l*(10*a + 3*b)/35, l**2*(15*a + 7*b)/420,
|
|
84
|
+
9*l*(a + b)/140, -l**2*(7*a + 6*b)/420],
|
|
85
|
+
[l**2*(15*a + 7*b)/420, l**3*(5*a + 3*b)/840,
|
|
86
|
+
l**2*(6*a + 7*b)/420, -l**3*(a + b)/280],
|
|
87
|
+
[9*l*(a + b)/140, l**2*(6*a + 7*b)/420,
|
|
88
|
+
l*(3*a + 10*b)/35, -l**2*(7*a + 15*b)/420],
|
|
89
|
+
[-l**2*(7*a + 6*b)/420, -l**3*(a + b)/280,
|
|
90
|
+
-l**2*(7*a + 15*b)/420, l**3*(3*a + 5*b)/840],
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def shear_stiffness(g_i: ArrayLike, g_j: ArrayLike, l: ArrayLike) -> NDArray[np.float64]:
|
|
95
|
+
"""Second-parameter foundation matrix, int g(x) N'^T N' dx, g linear.
|
|
96
|
+
|
|
97
|
+
``g`` is the shear parameter of the foundation: 2t in Vlasov's notation,
|
|
98
|
+
G_p in Pasternak's.
|
|
99
|
+
"""
|
|
100
|
+
a, b, l = (np.asarray(v, dtype=float) for v in (g_i, g_j, l))
|
|
101
|
+
return _stack([
|
|
102
|
+
[3*(a + b)/(5*l), b/10, -3*(a + b)/(5*l), a/10],
|
|
103
|
+
[b/10, l*(3*a + b)/30, -b/10, -l*(a + b)/60],
|
|
104
|
+
[-3*(a + b)/(5*l), -b/10, 3*(a + b)/(5*l), -a/10],
|
|
105
|
+
[a/10, -l*(a + b)/60, -a/10, l*(a + 3*b)/30],
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def distributed_load_vector(q_i: ArrayLike, q_j: ArrayLike, l: ArrayLike) -> NDArray[np.float64]:
|
|
110
|
+
"""Consistent nodal loads, int q(x) N dx, q linear."""
|
|
111
|
+
q_i, q_j, l = (np.asarray(v, dtype=float) for v in (q_i, q_j, l))
|
|
112
|
+
return _stack([[
|
|
113
|
+
l*(7*q_i + 3*q_j)/20,
|
|
114
|
+
l**2*(3*q_i + 2*q_j)/60,
|
|
115
|
+
l*(3*q_i + 7*q_j)/20,
|
|
116
|
+
-l**2*(2*q_i + 3*q_j)/60,
|
|
117
|
+
]])[..., 0, :]
|
ebbef2p/exceptions.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Exceptions raised by EBBEF2p.
|
|
2
|
+
|
|
3
|
+
Each one also derives from the built-in exception that describes it, so
|
|
4
|
+
``except ValueError`` and the like keep working.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EBBEF2pError(Exception):
|
|
11
|
+
"""Base class for all EBBEF2p errors."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModelError(EBBEF2pError, ValueError):
|
|
15
|
+
"""The problem definition is invalid or inconsistent."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UnstableModelError(EBBEF2pError, np.linalg.LinAlgError):
|
|
19
|
+
"""The beam can move as a rigid body: the system matrix is singular."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConvergenceError(EBBEF2pError, RuntimeError):
|
|
23
|
+
"""An iterative procedure (the gamma iteration) did not converge."""
|
ebbef2p/mesh.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Discretisation of a :class:`~ebbef2p.model.Model` into beam elements."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from itertools import pairwise
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
from .exceptions import ModelError
|
|
10
|
+
from .model import Model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Mesh:
|
|
15
|
+
"""Nodes and per-element properties.
|
|
16
|
+
|
|
17
|
+
Attributes
|
|
18
|
+
----------
|
|
19
|
+
nodes : (n+1,) array
|
|
20
|
+
EI : (n,) array
|
|
21
|
+
k, g : (n, 2) arrays
|
|
22
|
+
Foundation parameters at the start and end of every element.
|
|
23
|
+
q : (n, 2) array
|
|
24
|
+
Distributed load at the start and end of every element.
|
|
25
|
+
"""
|
|
26
|
+
nodes: NDArray[np.float64]
|
|
27
|
+
EI: NDArray[np.float64]
|
|
28
|
+
k: NDArray[np.float64]
|
|
29
|
+
g: NDArray[np.float64]
|
|
30
|
+
q: NDArray[np.float64]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def n_elements(self) -> int:
|
|
34
|
+
return len(self.nodes) - 1
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def lengths(self) -> NDArray[np.float64]:
|
|
38
|
+
return np.diff(self.nodes)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def element_dofs(self) -> NDArray[np.intp]:
|
|
42
|
+
"""Global degrees of freedom of every element, shape (n, 4)."""
|
|
43
|
+
return 2*np.arange(self.n_elements)[:, None] + np.arange(4)[None, :]
|
|
44
|
+
|
|
45
|
+
def node_index(self, x: float, tol: float = 1e-9) -> int:
|
|
46
|
+
"""Index of the node at ``x`` (raises if there is none)."""
|
|
47
|
+
span = self.nodes[-1] - self.nodes[0]
|
|
48
|
+
i = int(np.argmin(np.abs(self.nodes - x)))
|
|
49
|
+
if abs(self.nodes[i] - x) > tol*max(1.0, span):
|
|
50
|
+
raise ModelError(f"no node at x = {x}")
|
|
51
|
+
return i
|
|
52
|
+
|
|
53
|
+
def element_at(self, x: float | NDArray[np.float64]) -> NDArray[np.intp]:
|
|
54
|
+
"""Index of the element containing ``x`` (right-continuous)."""
|
|
55
|
+
i = np.searchsorted(self.nodes, x, side='right') - 1
|
|
56
|
+
return np.clip(i, 0, self.n_elements - 1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def end_spring_stiffness(mesh: Mesh) -> tuple[float, float]:
|
|
60
|
+
"""Springs representing the foundation beyond each beam end.
|
|
61
|
+
|
|
62
|
+
For a two-parameter foundation extending to infinity, the surface
|
|
63
|
+
deflection outside the beam decays as w_end*exp(-sqrt(k/g)*s), and that
|
|
64
|
+
soil reacts on the beam end with a force sqrt(k*g)*w_end. Returns the
|
|
65
|
+
spring stiffnesses at the start and end of the beam.
|
|
66
|
+
"""
|
|
67
|
+
return (float(np.sqrt(mesh.k[0, 0]*mesh.g[0, 0])),
|
|
68
|
+
float(np.sqrt(mesh.k[-1, 1]*mesh.g[-1, 1])))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _interp_segment(x, x0, x1, v0, v1):
|
|
72
|
+
return v0 + (v1 - v0)*(x - x0)/(x1 - x0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_mesh(model: Model, n_elements: int = 100) -> Mesh:
|
|
76
|
+
"""Mesh the model with roughly ``n_elements`` elements.
|
|
77
|
+
|
|
78
|
+
Every key point of the model (segment ends, loads, supports) becomes a
|
|
79
|
+
node; each interval between key points is divided uniformly so that the
|
|
80
|
+
element size is close to ``beam length / n_elements``.
|
|
81
|
+
"""
|
|
82
|
+
model.validate()
|
|
83
|
+
x_start, x_end = model.extent
|
|
84
|
+
h = (x_end - x_start)/max(int(n_elements), 1)
|
|
85
|
+
|
|
86
|
+
pts = model.key_points()
|
|
87
|
+
nodes = [pts[0]]
|
|
88
|
+
for a, b in pairwise(pts):
|
|
89
|
+
n = max(1, int(np.ceil((b - a)/h - 1e-9)))
|
|
90
|
+
nodes.extend(np.linspace(a, b, n + 1)[1:])
|
|
91
|
+
nodes = np.array(nodes)
|
|
92
|
+
|
|
93
|
+
n = len(nodes) - 1
|
|
94
|
+
xi, xj = nodes[:-1], nodes[1:]
|
|
95
|
+
xm = 0.5*(xi + xj)
|
|
96
|
+
|
|
97
|
+
EI = np.empty(n)
|
|
98
|
+
for b in model.beams:
|
|
99
|
+
inside = (xm > b.x0) & (xm < b.x1)
|
|
100
|
+
EI[inside] = b.EI
|
|
101
|
+
|
|
102
|
+
k = np.zeros((n, 2))
|
|
103
|
+
g = np.zeros((n, 2))
|
|
104
|
+
for f in model.foundation:
|
|
105
|
+
inside = (xm > f.x0) & (xm < f.x1)
|
|
106
|
+
for col, x in enumerate((xi, xj)):
|
|
107
|
+
k[inside, col] += _interp_segment(x[inside], f.x0, f.x1, *f.k)
|
|
108
|
+
g[inside, col] += _interp_segment(x[inside], f.x0, f.x1, *f.g)
|
|
109
|
+
|
|
110
|
+
q = np.zeros((n, 2))
|
|
111
|
+
for d in model.distributed_loads:
|
|
112
|
+
inside = (xm > d.x0) & (xm < d.x1)
|
|
113
|
+
for col, x in enumerate((xi, xj)):
|
|
114
|
+
q[inside, col] += _interp_segment(x[inside], d.x0, d.x1, d.q0, d.q1)
|
|
115
|
+
|
|
116
|
+
return Mesh(nodes=nodes, EI=EI, k=k, g=g, q=q)
|
ebbef2p/model.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Problem definition: beam segments, foundation, loads and supports.
|
|
2
|
+
|
|
3
|
+
Conventions
|
|
4
|
+
-----------
|
|
5
|
+
* ``x`` runs along the beam axis; ``w`` is the transverse displacement,
|
|
6
|
+
positive downwards (i.e. a settlement is positive).
|
|
7
|
+
* ``theta = dw/dx``.
|
|
8
|
+
* Forces and distributed loads are positive downwards. A concentrated moment
|
|
9
|
+
is positive when it does positive work on a positive ``theta``.
|
|
10
|
+
* No system of units is built in; use any consistent set.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from itertools import pairwise
|
|
15
|
+
from typing import Self
|
|
16
|
+
|
|
17
|
+
from .exceptions import ModelError
|
|
18
|
+
|
|
19
|
+
Pair = tuple[float, float]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class BeamSegment:
|
|
24
|
+
"""Beam portion ``[x0, x1]`` with constant bending stiffness."""
|
|
25
|
+
x0: float
|
|
26
|
+
x1: float
|
|
27
|
+
E: float
|
|
28
|
+
I: float
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def EI(self) -> float:
|
|
32
|
+
return self.E*self.I
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class FoundationSegment:
|
|
37
|
+
"""Two-parameter foundation acting on ``[x0, x1]``.
|
|
38
|
+
|
|
39
|
+
``k`` is the first (Winkler) parameter and ``g`` the second (shear)
|
|
40
|
+
parameter, both given as ``(value at x0, value at x1)`` and varying
|
|
41
|
+
linearly in between. ``g`` equals 2t in Vlasov's notation and G_p in
|
|
42
|
+
Pasternak's. Overlapping segments add up.
|
|
43
|
+
"""
|
|
44
|
+
x0: float
|
|
45
|
+
x1: float
|
|
46
|
+
k: Pair = (0.0, 0.0)
|
|
47
|
+
g: Pair = (0.0, 0.0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class PointLoad:
|
|
52
|
+
x: float
|
|
53
|
+
force: float = 0.0
|
|
54
|
+
moment: float = 0.0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class DistributedLoad:
|
|
59
|
+
"""Load per unit length on ``[x0, x1]``, varying linearly from q0 to q1."""
|
|
60
|
+
x0: float
|
|
61
|
+
x1: float
|
|
62
|
+
q0: float
|
|
63
|
+
q1: float
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Support:
|
|
68
|
+
"""Prescribed displacement and/or rotation at ``x``; ``None`` leaves it free."""
|
|
69
|
+
x: float
|
|
70
|
+
w: float | None = 0.0
|
|
71
|
+
theta: float | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pair(value: float | Pair) -> Pair:
|
|
75
|
+
if isinstance(value, (int, float)):
|
|
76
|
+
return (float(value), float(value))
|
|
77
|
+
a, b = value
|
|
78
|
+
return (float(a), float(b))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _optional_float(value: float | None) -> float | None:
|
|
82
|
+
return None if value is None else float(value)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class Model:
|
|
87
|
+
"""A beam on a two-parameter elastic foundation.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
soil_beyond_ends : bool
|
|
92
|
+
If True, the foundation continues indefinitely beyond both beam ends.
|
|
93
|
+
Its effect is represented by an equivalent spring sqrt(k*g) at each
|
|
94
|
+
end, with k and g taken at that end (Vallabhan & Das, 1991).
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
>>> m = Model()
|
|
99
|
+
>>> m.add_beam(0, 10, E=30e6, I=0.01)
|
|
100
|
+
>>> m.add_foundation(0, 10, k=5000, g=2000)
|
|
101
|
+
>>> m.add_point_load(5, force=100)
|
|
102
|
+
"""
|
|
103
|
+
beams: list[BeamSegment] = field(default_factory=list)
|
|
104
|
+
foundation: list[FoundationSegment] = field(default_factory=list)
|
|
105
|
+
point_loads: list[PointLoad] = field(default_factory=list)
|
|
106
|
+
distributed_loads: list[DistributedLoad] = field(default_factory=list)
|
|
107
|
+
supports: list[Support] = field(default_factory=list)
|
|
108
|
+
soil_beyond_ends: bool = False
|
|
109
|
+
|
|
110
|
+
# -- building -------------------------------------------------------
|
|
111
|
+
def add_beam(self, x0: float, x1: float, E: float, I: float) -> Self:
|
|
112
|
+
"""Add a beam segment with constant E and I."""
|
|
113
|
+
if x1 <= x0:
|
|
114
|
+
raise ModelError("beam segment must have x1 > x0")
|
|
115
|
+
if E <= 0 or I <= 0:
|
|
116
|
+
raise ModelError("E and I must be positive")
|
|
117
|
+
self.beams.append(BeamSegment(float(x0), float(x1), float(E), float(I)))
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def add_foundation(self, x0: float, x1: float, k: float | Pair = 0.0,
|
|
121
|
+
g: float | Pair = 0.0) -> Self:
|
|
122
|
+
"""Add a foundation segment; ``k`` and ``g`` are scalars or (start, end) pairs."""
|
|
123
|
+
if x1 <= x0:
|
|
124
|
+
raise ModelError("foundation segment must have x1 > x0")
|
|
125
|
+
k, g = _pair(k), _pair(g)
|
|
126
|
+
if min(k) < 0 or min(g) < 0:
|
|
127
|
+
raise ModelError("foundation parameters must be non-negative")
|
|
128
|
+
self.foundation.append(FoundationSegment(float(x0), float(x1), k, g))
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def add_point_load(self, x: float, force: float = 0.0, moment: float = 0.0) -> Self:
|
|
132
|
+
"""Add a concentrated force and/or moment at ``x``."""
|
|
133
|
+
self.point_loads.append(PointLoad(float(x), float(force), float(moment)))
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def add_distributed_load(self, x0: float, x1: float, q0: float,
|
|
137
|
+
q1: float | None = None) -> Self:
|
|
138
|
+
"""Add a load varying from ``q0`` at ``x0`` to ``q1`` at ``x1`` (uniform if q1 is None)."""
|
|
139
|
+
if x1 <= x0:
|
|
140
|
+
raise ModelError("distributed load must have x1 > x0")
|
|
141
|
+
q1 = q0 if q1 is None else q1
|
|
142
|
+
self.distributed_loads.append(DistributedLoad(float(x0), float(x1), float(q0), float(q1)))
|
|
143
|
+
return self
|
|
144
|
+
|
|
145
|
+
def add_support(self, x: float, w: float | None = 0.0,
|
|
146
|
+
theta: float | None = None) -> Self:
|
|
147
|
+
"""Prescribe ``w`` and/or ``theta`` at ``x`` (``None`` = free).
|
|
148
|
+
|
|
149
|
+
Defaults to a simple support (w = 0, rotation free). Use
|
|
150
|
+
``theta=0`` for a clamped end.
|
|
151
|
+
"""
|
|
152
|
+
if w is None and theta is None:
|
|
153
|
+
raise ModelError("a support must constrain w, theta or both")
|
|
154
|
+
self.supports.append(Support(float(x), _optional_float(w), _optional_float(theta)))
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
# -- queries ----------------------------------------------------------
|
|
158
|
+
@property
|
|
159
|
+
def extent(self) -> Pair:
|
|
160
|
+
"""(x_start, x_end) of the beam."""
|
|
161
|
+
if not self.beams:
|
|
162
|
+
raise ModelError("the model has no beam")
|
|
163
|
+
return (min(b.x0 for b in self.beams), max(b.x1 for b in self.beams))
|
|
164
|
+
|
|
165
|
+
def key_points(self) -> list[float]:
|
|
166
|
+
"""Coordinates that must coincide with mesh nodes."""
|
|
167
|
+
pts = set()
|
|
168
|
+
for b in self.beams:
|
|
169
|
+
pts.update((b.x0, b.x1))
|
|
170
|
+
for f in self.foundation:
|
|
171
|
+
pts.update((f.x0, f.x1))
|
|
172
|
+
for d in self.distributed_loads:
|
|
173
|
+
pts.update((d.x0, d.x1))
|
|
174
|
+
pts.update(p.x for p in self.point_loads)
|
|
175
|
+
pts.update(s.x for s in self.supports)
|
|
176
|
+
return sorted(pts)
|
|
177
|
+
|
|
178
|
+
def validate(self, tol: float = 1e-9) -> None:
|
|
179
|
+
"""Check that beam segments tile the beam and everything lies on it."""
|
|
180
|
+
if not self.beams:
|
|
181
|
+
raise ModelError("the model has no beam")
|
|
182
|
+
segs = sorted(self.beams, key=lambda b: b.x0)
|
|
183
|
+
for a, b in pairwise(segs):
|
|
184
|
+
if abs(a.x1 - b.x0) > tol:
|
|
185
|
+
raise ModelError(f"beam segments leave a gap or overlap at x = {a.x1}")
|
|
186
|
+
x0, x1 = self.extent
|
|
187
|
+
for x in self.key_points():
|
|
188
|
+
if x < x0 - tol or x > x1 + tol:
|
|
189
|
+
raise ModelError(f"x = {x} lies outside the beam [{x0}, {x1}]")
|
ebbef2p/plotting.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Quick-look plots of a :class:`~ebbef2p.results.Solution` (needs matplotlib)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .results import ElementEndValues, Solution
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def plot_solution(solution: Solution, axes=None):
|
|
9
|
+
"""Deflection, bending moment, shear force and soil pressure, stacked.
|
|
10
|
+
|
|
11
|
+
Returns the array of matplotlib axes.
|
|
12
|
+
"""
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
|
|
15
|
+
if axes is None:
|
|
16
|
+
_, axes = plt.subplots(4, 1, sharex=True, figsize=(7, 9))
|
|
17
|
+
|
|
18
|
+
xs = np.linspace(solution.x[0], solution.x[-1], 400)
|
|
19
|
+
panels = [
|
|
20
|
+
(xs, solution.deflection(xs), "w", True),
|
|
21
|
+
(*_pairs(solution.bending_moment()), "M", True),
|
|
22
|
+
(*_pairs(solution.shear_force()), "V", False),
|
|
23
|
+
(*_pairs(solution.reactive_pressure()), "p", True),
|
|
24
|
+
]
|
|
25
|
+
for ax, (x, y, label, invert) in zip(axes, panels, strict=True):
|
|
26
|
+
ax.plot(x, y)
|
|
27
|
+
ax.axhline(0, color="0.6", lw=0.8)
|
|
28
|
+
ax.set_ylabel(label)
|
|
29
|
+
if invert:
|
|
30
|
+
ax.invert_yaxis()
|
|
31
|
+
axes[-1].set_xlabel("x")
|
|
32
|
+
return axes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _pairs(values: ElementEndValues):
|
|
36
|
+
return values.x, values.values
|