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/results.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Post-processing of a solved model.
|
|
2
|
+
|
|
3
|
+
Sign conventions follow :mod:`ebbef2p.model`: ``w`` positive downwards,
|
|
4
|
+
``theta = dw/dx``, bending moment ``M = -EI w''`` (positive when sagging),
|
|
5
|
+
shear force ``V = dM/dx``. Reactions (soil pressure and support reactions)
|
|
6
|
+
are positive when they act upwards on the beam, i.e. against positive loads.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import scipy.sparse as sp
|
|
14
|
+
from numpy.typing import ArrayLike, NDArray
|
|
15
|
+
|
|
16
|
+
from . import elements as el
|
|
17
|
+
from .mesh import Mesh, end_spring_stiffness
|
|
18
|
+
from .model import Model
|
|
19
|
+
|
|
20
|
+
_GAUSS_X, _GAUSS_W = np.polynomial.legendre.leggauss(4)
|
|
21
|
+
_GAUSS_XI = 0.5*(_GAUSS_X + 1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ElementEndValues:
|
|
26
|
+
"""A quantity evaluated at both ends of every element.
|
|
27
|
+
|
|
28
|
+
``x`` and ``values`` have length ``2 * n_elements``; consecutive pairs
|
|
29
|
+
belong to one element, so jumps at nodes (under point loads, at
|
|
30
|
+
changes of stiffness) are preserved.
|
|
31
|
+
"""
|
|
32
|
+
x: NDArray[np.float64]
|
|
33
|
+
values: NDArray[np.float64]
|
|
34
|
+
|
|
35
|
+
def max(self) -> float:
|
|
36
|
+
return float(np.max(self.values))
|
|
37
|
+
|
|
38
|
+
def min(self) -> float:
|
|
39
|
+
return float(np.min(self.values))
|
|
40
|
+
|
|
41
|
+
def at(self, x: float, side: Literal['left', 'right'] = 'left') -> float:
|
|
42
|
+
"""Value at a node, taken from the element on its left or right."""
|
|
43
|
+
idx = np.flatnonzero(np.isclose(self.x, x))
|
|
44
|
+
if idx.size == 0:
|
|
45
|
+
raise ValueError(f"no element end at x = {x}")
|
|
46
|
+
if side == 'left':
|
|
47
|
+
return float(self.values[idx[0]])
|
|
48
|
+
if side == 'right':
|
|
49
|
+
return float(self.values[idx[-1]])
|
|
50
|
+
raise ValueError("side must be 'left' or 'right'")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class ReactivePressure(ElementEndValues):
|
|
55
|
+
"""Soil reaction under the beam.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
x, values
|
|
60
|
+
Distributed pressure p = k w - (g w')' at element ends.
|
|
61
|
+
concentrated : list of (x, force)
|
|
62
|
+
Forces where the shear layer is cut: at the beam ends
|
|
63
|
+
(-g w' at the start, +g w' at the end, plus sqrt(k g) w if the soil
|
|
64
|
+
continues beyond the ends) and at interior nodes where g jumps.
|
|
65
|
+
resultant : float
|
|
66
|
+
Integral of p plus all concentrated forces (total soil reaction).
|
|
67
|
+
"""
|
|
68
|
+
concentrated: list[tuple[float, float]] = field(default_factory=list)
|
|
69
|
+
resultant: float = 0.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Solution:
|
|
74
|
+
model: Model
|
|
75
|
+
mesh: Mesh
|
|
76
|
+
u: NDArray[np.float64]
|
|
77
|
+
K: sp.csr_matrix
|
|
78
|
+
f: NDArray[np.float64]
|
|
79
|
+
element_stiffness: NDArray[np.float64]
|
|
80
|
+
element_loads: NDArray[np.float64]
|
|
81
|
+
fixed_dofs: NDArray[np.intp]
|
|
82
|
+
|
|
83
|
+
# -- nodal values ---------------------------------------------------
|
|
84
|
+
@property
|
|
85
|
+
def x(self) -> NDArray[np.float64]:
|
|
86
|
+
"""Node coordinates."""
|
|
87
|
+
return self.mesh.nodes
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def w(self) -> NDArray[np.float64]:
|
|
91
|
+
"""Nodal displacements (positive downwards)."""
|
|
92
|
+
return self.u[0::2]
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def theta(self) -> NDArray[np.float64]:
|
|
96
|
+
"""Nodal rotations dw/dx."""
|
|
97
|
+
return self.u[1::2]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def element_displacements(self) -> NDArray[np.float64]:
|
|
101
|
+
"""Degrees of freedom of every element, shape (n, 4)."""
|
|
102
|
+
return self.u[self.mesh.element_dofs]
|
|
103
|
+
|
|
104
|
+
def element_field(self, xi: ArrayLike, order: int = 0) -> NDArray[np.float64]:
|
|
105
|
+
"""``w`` (order 0) or its ``order``-th derivative at local points.
|
|
106
|
+
|
|
107
|
+
``xi`` holds local coordinates in [0, 1], the same in every element;
|
|
108
|
+
the result has shape (n_elements, len(xi)).
|
|
109
|
+
"""
|
|
110
|
+
xi = np.atleast_1d(np.asarray(xi, dtype=float))
|
|
111
|
+
l = self.mesh.lengths[:, None]
|
|
112
|
+
N = (el.shape_functions(xi, l) if order == 0
|
|
113
|
+
else el.shape_function_derivatives(xi, l, order))
|
|
114
|
+
return np.einsum('iep,ei->ep', N, self.element_displacements)
|
|
115
|
+
|
|
116
|
+
def deflection(self, x: ArrayLike) -> NDArray[np.float64]:
|
|
117
|
+
"""Displacement at arbitrary points, from the Hermite interpolation."""
|
|
118
|
+
x = np.atleast_1d(np.asarray(x, dtype=float))
|
|
119
|
+
e = self.mesh.element_at(x)
|
|
120
|
+
l = self.mesh.lengths[e]
|
|
121
|
+
xi = (x - self.mesh.nodes[e])/l
|
|
122
|
+
return np.einsum('im,mi->m', el.shape_functions(xi, l), self.element_displacements[e])
|
|
123
|
+
|
|
124
|
+
# -- internal forces --------------------------------------------------
|
|
125
|
+
def _element_end_forces(self) -> NDArray[np.float64]:
|
|
126
|
+
"""Forces exerted on each element by its nodes: K_e d_e - f_e."""
|
|
127
|
+
return (np.einsum('eij,ej->ei', self.element_stiffness, self.element_displacements)
|
|
128
|
+
- self.element_loads)
|
|
129
|
+
|
|
130
|
+
def _end_coords(self) -> NDArray[np.float64]:
|
|
131
|
+
return np.column_stack([self.mesh.nodes[:-1], self.mesh.nodes[1:]]).ravel()
|
|
132
|
+
|
|
133
|
+
def bending_moment(self) -> ElementEndValues:
|
|
134
|
+
"""Bending moment at element ends, from element equilibrium."""
|
|
135
|
+
F = self._element_end_forces()
|
|
136
|
+
M = np.column_stack([F[:, 1], -F[:, 3]]).ravel()
|
|
137
|
+
return ElementEndValues(self._end_coords(), M)
|
|
138
|
+
|
|
139
|
+
def shear_force(self) -> ElementEndValues:
|
|
140
|
+
"""Shear force in the beam at element ends, from element equilibrium.
|
|
141
|
+
|
|
142
|
+
The force carried by the foundation's shear layer (g w') is
|
|
143
|
+
excluded, so this is the shear force in the beam itself.
|
|
144
|
+
"""
|
|
145
|
+
F = self._element_end_forces()
|
|
146
|
+
th = self.theta
|
|
147
|
+
g = self.mesh.g
|
|
148
|
+
V = np.column_stack([-F[:, 0] - g[:, 0]*th[:-1],
|
|
149
|
+
F[:, 2] - g[:, 1]*th[1:]]).ravel()
|
|
150
|
+
return ElementEndValues(self._end_coords(), V)
|
|
151
|
+
|
|
152
|
+
# -- reactions --------------------------------------------------------
|
|
153
|
+
def reactive_pressure(self) -> ReactivePressure:
|
|
154
|
+
"""Soil reaction: distributed pressure and concentrated forces."""
|
|
155
|
+
mesh = self.mesh
|
|
156
|
+
l = mesh.lengths
|
|
157
|
+
k, g = mesh.k, mesh.g
|
|
158
|
+
dg = (g[:, 1] - g[:, 0])/l
|
|
159
|
+
w_end = self.element_field([0.0, 1.0])
|
|
160
|
+
theta_end = self.element_field([0.0, 1.0], order=1)
|
|
161
|
+
curvature_end = self.element_field([0.0, 1.0], order=2)
|
|
162
|
+
values = (k*w_end - g*curvature_end - dg[:, None]*theta_end).ravel()
|
|
163
|
+
|
|
164
|
+
flux = g*theta_end # g w' at both ends of every element
|
|
165
|
+
k_gauss = k[:, :1] + (k[:, 1:] - k[:, :1])*_GAUSS_XI
|
|
166
|
+
w_gauss = self.element_field(_GAUSS_XI)
|
|
167
|
+
resultant = float((0.5*l) @ ((k_gauss*w_gauss) @ _GAUSS_W)
|
|
168
|
+
- np.sum(flux[:, 1] - flux[:, 0]))
|
|
169
|
+
|
|
170
|
+
x0, x1 = mesh.nodes[0], mesh.nodes[-1]
|
|
171
|
+
f0, f1 = -flux[0, 0], flux[-1, 1]
|
|
172
|
+
if self.model.soil_beyond_ends:
|
|
173
|
+
k_left, k_right = end_spring_stiffness(mesh)
|
|
174
|
+
f0 += k_left*self.w[0]
|
|
175
|
+
f1 += k_right*self.w[-1]
|
|
176
|
+
|
|
177
|
+
concentrated = [(float(x0), float(f0))]
|
|
178
|
+
jumps = flux[:-1, 1] - flux[1:, 0]
|
|
179
|
+
scale = max(1e-300, np.max(np.abs(flux[:, 1])))
|
|
180
|
+
for e in np.flatnonzero(np.abs(jumps) > 1e-10*scale):
|
|
181
|
+
concentrated.append((float(mesh.nodes[e + 1]), float(jumps[e])))
|
|
182
|
+
concentrated.append((float(x1), float(f1)))
|
|
183
|
+
resultant += sum(force for _, force in concentrated)
|
|
184
|
+
|
|
185
|
+
return ReactivePressure(self._end_coords(), values,
|
|
186
|
+
concentrated=concentrated,
|
|
187
|
+
resultant=resultant)
|
|
188
|
+
|
|
189
|
+
def support_reactions(self) -> list[tuple[float, float, float]]:
|
|
190
|
+
"""Reactions at the supports as a list of (x, force, moment).
|
|
191
|
+
|
|
192
|
+
Positive when acting against positive loads (force upwards).
|
|
193
|
+
"""
|
|
194
|
+
r = self.f - self.K @ self.u
|
|
195
|
+
out: dict[float, tuple[float, float]] = {}
|
|
196
|
+
for dof in self.fixed_dofs:
|
|
197
|
+
node = dof // 2
|
|
198
|
+
x = float(self.mesh.nodes[node])
|
|
199
|
+
force, moment = out.get(x, (0.0, 0.0))
|
|
200
|
+
if dof % 2 == 0:
|
|
201
|
+
force = float(r[dof])
|
|
202
|
+
else:
|
|
203
|
+
moment = float(r[dof])
|
|
204
|
+
out[x] = (force, moment)
|
|
205
|
+
return [(x, F, M) for x, (F, M) in sorted(out.items())]
|
|
206
|
+
|
|
207
|
+
def equilibrium(self) -> dict[str, float]:
|
|
208
|
+
"""Vertical force balance: applied loads vs soil and support reactions."""
|
|
209
|
+
applied = sum(p.force for p in self.model.point_loads)
|
|
210
|
+
applied += sum(0.5*(d.q0 + d.q1)*(d.x1 - d.x0)
|
|
211
|
+
for d in self.model.distributed_loads)
|
|
212
|
+
soil = self.reactive_pressure().resultant
|
|
213
|
+
supports = sum(F for _, F, _ in self.support_reactions())
|
|
214
|
+
return dict(applied=applied, soil=soil, supports=supports,
|
|
215
|
+
residual=applied - soil - supports)
|
ebbef2p/solver.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Assembly and solution of the finite element system."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy.sparse as sp
|
|
7
|
+
import scipy.sparse.linalg as spla
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
from . import elements as el
|
|
11
|
+
from .exceptions import ModelError, UnstableModelError
|
|
12
|
+
from .mesh import Mesh, build_mesh, end_spring_stiffness
|
|
13
|
+
from .model import Model
|
|
14
|
+
from .results import Solution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def element_matrices(mesh: Mesh) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
18
|
+
"""Total element stiffness matrices and consistent load vectors.
|
|
19
|
+
|
|
20
|
+
Returns arrays of shape (n, 4, 4) and (n, 4).
|
|
21
|
+
"""
|
|
22
|
+
l = mesh.lengths
|
|
23
|
+
ke = (el.bending_stiffness(mesh.EI, l)
|
|
24
|
+
+ el.winkler_stiffness(mesh.k[:, 0], mesh.k[:, 1], l)
|
|
25
|
+
+ el.shear_stiffness(mesh.g[:, 0], mesh.g[:, 1], l))
|
|
26
|
+
fe = el.distributed_load_vector(mesh.q[:, 0], mesh.q[:, 1], l)
|
|
27
|
+
return ke, fe
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def assemble(model: Model, mesh: Mesh) -> tuple[sp.csr_matrix, NDArray[np.float64],
|
|
31
|
+
NDArray[np.float64], NDArray[np.float64]]:
|
|
32
|
+
"""Global stiffness matrix (CSR), load vector and element contributions."""
|
|
33
|
+
ndof = 2*(mesh.n_elements + 1)
|
|
34
|
+
ke, fe = element_matrices(mesh)
|
|
35
|
+
|
|
36
|
+
dofs = mesh.element_dofs
|
|
37
|
+
rows = np.repeat(dofs, 4, axis=1).ravel()
|
|
38
|
+
cols = np.tile(dofs, (1, 4)).ravel()
|
|
39
|
+
K = sp.coo_matrix((ke.ravel(), (rows, cols)), shape=(ndof, ndof)).tolil()
|
|
40
|
+
|
|
41
|
+
f = np.zeros(ndof)
|
|
42
|
+
np.add.at(f, dofs.ravel(), fe.ravel())
|
|
43
|
+
|
|
44
|
+
if model.soil_beyond_ends:
|
|
45
|
+
k_left, k_right = end_spring_stiffness(mesh)
|
|
46
|
+
K[0, 0] += k_left
|
|
47
|
+
K[ndof - 2, ndof - 2] += k_right
|
|
48
|
+
|
|
49
|
+
for p in model.point_loads:
|
|
50
|
+
i = mesh.node_index(p.x)
|
|
51
|
+
f[2*i] += p.force
|
|
52
|
+
f[2*i + 1] += p.moment
|
|
53
|
+
|
|
54
|
+
return K.tocsr(), f, ke, fe
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def constraints(model: Model, mesh: Mesh) -> tuple[NDArray[np.intp], NDArray[np.float64]]:
|
|
58
|
+
"""Prescribed degrees of freedom and their values."""
|
|
59
|
+
prescribed: dict[int, float] = {}
|
|
60
|
+
for s in model.supports:
|
|
61
|
+
i = mesh.node_index(s.x)
|
|
62
|
+
for offset, value in ((0, s.w), (1, s.theta)):
|
|
63
|
+
if value is None:
|
|
64
|
+
continue
|
|
65
|
+
dof = 2*i + offset
|
|
66
|
+
if dof in prescribed and prescribed[dof] != value:
|
|
67
|
+
raise ModelError(f"conflicting supports at x = {s.x}")
|
|
68
|
+
prescribed[dof] = float(value)
|
|
69
|
+
dofs = np.array(sorted(prescribed), dtype=int)
|
|
70
|
+
values = np.array([prescribed[d] for d in dofs], dtype=float)
|
|
71
|
+
return dofs, values
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_stability(model: Model, mesh: Mesh, fixed: NDArray[np.intp]) -> None:
|
|
75
|
+
"""Raise if the beam can move as a rigid body.
|
|
76
|
+
|
|
77
|
+
A foundation with k > 0 over a finite length restrains both
|
|
78
|
+
translation and rotation. Without it, the supports must prescribe w at
|
|
79
|
+
two distinct points, or w and theta at the same point.
|
|
80
|
+
"""
|
|
81
|
+
if np.any(mesh.k > 0):
|
|
82
|
+
return
|
|
83
|
+
w_nodes = {d // 2 for d in fixed if d % 2 == 0}
|
|
84
|
+
theta_nodes = {d // 2 for d in fixed if d % 2 == 1}
|
|
85
|
+
if len(w_nodes) >= 2 or (w_nodes and theta_nodes):
|
|
86
|
+
return
|
|
87
|
+
raise UnstableModelError(
|
|
88
|
+
"the beam is not sufficiently supported: without a Winkler "
|
|
89
|
+
"parameter k > 0 it needs w prescribed at two points, or w and "
|
|
90
|
+
"theta at one point")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def solve(model: Model, n_elements: int = 100) -> Solution:
|
|
94
|
+
"""Solve the static problem.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
model : Model
|
|
99
|
+
n_elements : int
|
|
100
|
+
Approximate number of elements along the beam.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
Solution
|
|
105
|
+
"""
|
|
106
|
+
mesh = build_mesh(model, n_elements)
|
|
107
|
+
K, f, ke, fe = assemble(model, mesh)
|
|
108
|
+
ndof = K.shape[0]
|
|
109
|
+
|
|
110
|
+
fixed, values = constraints(model, mesh)
|
|
111
|
+
check_stability(model, mesh, fixed)
|
|
112
|
+
free = np.setdiff1d(np.arange(ndof), fixed)
|
|
113
|
+
|
|
114
|
+
u = np.zeros(ndof)
|
|
115
|
+
u[fixed] = values
|
|
116
|
+
rhs = f[free] - K[free][:, fixed] @ values
|
|
117
|
+
with warnings.catch_warnings():
|
|
118
|
+
warnings.simplefilter("error", spla.MatrixRankWarning)
|
|
119
|
+
try:
|
|
120
|
+
u[free] = spla.spsolve(K[free][:, free].tocsc(), rhs)
|
|
121
|
+
except (spla.MatrixRankWarning, RuntimeError):
|
|
122
|
+
u[free] = np.nan
|
|
123
|
+
if not np.all(np.isfinite(u)):
|
|
124
|
+
raise UnstableModelError(
|
|
125
|
+
"the system is singular: the beam is not sufficiently supported "
|
|
126
|
+
"(add a foundation or supports)")
|
|
127
|
+
|
|
128
|
+
return Solution(model=model, mesh=mesh, u=u, K=K, f=f,
|
|
129
|
+
element_stiffness=ke, element_loads=fe,
|
|
130
|
+
fixed_dofs=fixed)
|
ebbef2p/vlasov.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Modified Vlasov foundation model (Vallabhan & Das, 1991).
|
|
2
|
+
|
|
3
|
+
The soil is an elastic layer of depth ``H`` on a rigid base, with Poisson's
|
|
4
|
+
ratio ``nu`` and Young's modulus varying linearly from ``E_top`` at the
|
|
5
|
+
surface to ``E_bottom`` at the base. The vertical displacement in the layer
|
|
6
|
+
is ``w(x) * phi(z)`` with
|
|
7
|
+
|
|
8
|
+
phi(z) = sinh(gamma*(1 - z/H)) / sinh(gamma),
|
|
9
|
+
|
|
10
|
+
and the foundation parameters follow from the layer properties and the
|
|
11
|
+
non-dimensional decay parameter ``gamma``:
|
|
12
|
+
|
|
13
|
+
k = (1-nu)/((1+nu)(1-2nu)) * int_0^H E(z) phi'(z)^2 dz
|
|
14
|
+
g = 1/(2(1+nu)) * int_0^H E(z) phi(z)^2 dz (g = 2t)
|
|
15
|
+
|
|
16
|
+
``gamma`` itself depends on the beam deflection, so it is found iteratively.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field, replace
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from .exceptions import ConvergenceError, ModelError
|
|
24
|
+
from .model import Model
|
|
25
|
+
from .results import _GAUSS_W, _GAUSS_XI, Solution
|
|
26
|
+
from .solver import solve
|
|
27
|
+
|
|
28
|
+
# Taylor coefficients in powers of gamma**2 (up to gamma**14) of the four
|
|
29
|
+
# gamma-dependent factors in VlasovSoil.parameters: k = C_k (E1 kA + dE kB),
|
|
30
|
+
# g = C_g (E1 gA + dE gB). Derived with sympy.
|
|
31
|
+
_SERIES = (
|
|
32
|
+
(1, 0, 1/45, -4/945, 1/1575, -8/93555, 1382/127702575, -8/6081075),
|
|
33
|
+
(1/2, -1/12, 1/60, -1/378, 1/2700, -1/20790, 691/116093250, -1/1403325),
|
|
34
|
+
(1/6, -1/45, 1/315, -2/4725, 1/18711, -1382/212837625, 2/2606175,
|
|
35
|
+
-14468/162820783125),
|
|
36
|
+
(1/24, -1/120, 1/756, -1/5400, 1/41580, -691/232186500, 1/2806650,
|
|
37
|
+
-3617/86837751000),
|
|
38
|
+
)
|
|
39
|
+
_SERIES_GAMMA = 0.3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class VlasovSoil:
|
|
44
|
+
"""Elastic layer for the modified Vlasov model.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
E_top : float
|
|
49
|
+
Young's modulus at the surface.
|
|
50
|
+
nu : float
|
|
51
|
+
Poisson's ratio, 0 <= nu < 0.5.
|
|
52
|
+
depth : float
|
|
53
|
+
Layer thickness H.
|
|
54
|
+
E_bottom : float, optional
|
|
55
|
+
Young's modulus at the base; defaults to ``E_top`` (homogeneous layer).
|
|
56
|
+
"""
|
|
57
|
+
E_top: float
|
|
58
|
+
nu: float
|
|
59
|
+
depth: float
|
|
60
|
+
E_bottom: float | None = None
|
|
61
|
+
|
|
62
|
+
def __post_init__(self) -> None:
|
|
63
|
+
if not 0 <= self.nu < 0.5:
|
|
64
|
+
raise ModelError("nu must satisfy 0 <= nu < 0.5")
|
|
65
|
+
if self.depth <= 0 or self.E_top <= 0:
|
|
66
|
+
raise ModelError("depth and E_top must be positive")
|
|
67
|
+
if self.E_bottom is not None and self.E_bottom <= 0:
|
|
68
|
+
raise ModelError("E_bottom must be positive")
|
|
69
|
+
|
|
70
|
+
def parameters(self, gamma: float) -> tuple[float, float]:
|
|
71
|
+
"""Foundation parameters ``(k, g)`` for a given ``gamma`` (g = 2t).
|
|
72
|
+
|
|
73
|
+
Below ``_SERIES_GAMMA`` the closed forms lose accuracy to
|
|
74
|
+
cancellation (the error in g grows like 1e-16/gamma**4), so their
|
|
75
|
+
Taylor series in gamma**2 is used instead; both agree to 1e-15 at
|
|
76
|
+
the switch.
|
|
77
|
+
"""
|
|
78
|
+
E1 = self.E_top
|
|
79
|
+
E2 = self.E_top if self.E_bottom is None else self.E_bottom
|
|
80
|
+
nu, H = self.nu, self.depth
|
|
81
|
+
if gamma < _SERIES_GAMMA:
|
|
82
|
+
kA, kB, gA, gB = (np.polynomial.polynomial.polyval(gamma**2, c)
|
|
83
|
+
for c in _SERIES)
|
|
84
|
+
else:
|
|
85
|
+
s2 = np.sinh(gamma)**2
|
|
86
|
+
kA = (2*gamma*np.sinh(2*gamma) + 4*gamma**2)/(8*s2)
|
|
87
|
+
kB = (np.cosh(2*gamma) - 1 + 2*gamma**2)/(8*s2)
|
|
88
|
+
gA = (2*gamma*np.sinh(2*gamma) - 4*gamma**2)/(16*gamma**2*s2)
|
|
89
|
+
gB = (np.cosh(2*gamma) - 1 - 2*gamma**2)/(16*gamma**2*s2)
|
|
90
|
+
k = (1 - nu)/((1 + nu)*(1 - 2*nu)*H)*(E1*kA + (E2 - E1)*kB)
|
|
91
|
+
g = H/(1 + nu)*(E1*gA + (E2 - E1)*gB)
|
|
92
|
+
return float(k), float(g)
|
|
93
|
+
|
|
94
|
+
def gamma_from_deflection(self, solution: Solution, k: float, g: float,
|
|
95
|
+
soil_beyond_ends: bool) -> float:
|
|
96
|
+
"""Update gamma from a deflected shape (Vallabhan & Das, 1991).
|
|
97
|
+
|
|
98
|
+
(gamma/H)^2 = (1-2nu)/(2(1-nu)) * int w'^2 dx / int w^2 dx
|
|
99
|
+
|
|
100
|
+
with the integrals extended beyond the beam ends when the soil
|
|
101
|
+
continues there (w decays as w_end*exp(-sqrt(k/g)*s)).
|
|
102
|
+
"""
|
|
103
|
+
half_l = 0.5*solution.mesh.lengths
|
|
104
|
+
int_w2 = float(half_l @ (solution.element_field(_GAUSS_XI)**2 @ _GAUSS_W))
|
|
105
|
+
int_dw2 = float(half_l @ (solution.element_field(_GAUSS_XI, order=1)**2 @ _GAUSS_W))
|
|
106
|
+
if soil_beyond_ends:
|
|
107
|
+
ends = solution.w[0]**2 + solution.w[-1]**2
|
|
108
|
+
alpha = np.sqrt(k/g)
|
|
109
|
+
int_w2 += ends/(2*alpha)
|
|
110
|
+
int_dw2 += alpha*ends/2
|
|
111
|
+
nu = self.nu
|
|
112
|
+
return float(self.depth*np.sqrt((1 - 2*nu)/(2*(1 - nu))*int_dw2/int_w2))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class VlasovResult:
|
|
117
|
+
"""Outcome of :func:`solve_vlasov`."""
|
|
118
|
+
solution: Solution
|
|
119
|
+
soil: VlasovSoil
|
|
120
|
+
gamma: float
|
|
121
|
+
k: float
|
|
122
|
+
g: float
|
|
123
|
+
history: list[float] = field(default_factory=list)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def t(self) -> float:
|
|
127
|
+
"""Vlasov's t (the second parameter is g = 2t)."""
|
|
128
|
+
return 0.5*self.g
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def solve_vlasov(model: Model, soil: VlasovSoil, n_elements: int = 100,
|
|
132
|
+
gamma0: float = 1.0, tol: float = 1e-6,
|
|
133
|
+
max_iter: int = 100) -> VlasovResult:
|
|
134
|
+
"""Solve a beam on a modified Vlasov foundation.
|
|
135
|
+
|
|
136
|
+
The foundation parameters are uniform along the beam and are found
|
|
137
|
+
iteratively together with ``gamma``. Any foundation already defined in
|
|
138
|
+
``model`` is not allowed, since the Vlasov layer replaces it.
|
|
139
|
+
|
|
140
|
+
Whether the layer continues beyond the beam ends is taken from
|
|
141
|
+
``model.soil_beyond_ends``; the same assumption is used in the
|
|
142
|
+
stiffness matrix and in the gamma update.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
model : Model
|
|
147
|
+
Beam, loads and supports.
|
|
148
|
+
soil : VlasovSoil
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
VlasovResult
|
|
153
|
+
"""
|
|
154
|
+
if model.foundation:
|
|
155
|
+
raise ModelError("solve_vlasov defines the foundation itself; "
|
|
156
|
+
"remove the foundation segments from the model")
|
|
157
|
+
x0, x1 = model.extent
|
|
158
|
+
beyond = model.soil_beyond_ends
|
|
159
|
+
|
|
160
|
+
def solve_with(gamma: float) -> tuple[Solution, float, float]:
|
|
161
|
+
k, g = soil.parameters(gamma)
|
|
162
|
+
trial = replace(model, foundation=[])
|
|
163
|
+
trial.add_foundation(x0, x1, k=k, g=g)
|
|
164
|
+
return solve(trial, n_elements), k, g
|
|
165
|
+
|
|
166
|
+
gamma = float(gamma0)
|
|
167
|
+
history = [gamma]
|
|
168
|
+
for _ in range(max_iter):
|
|
169
|
+
solution, k, g = solve_with(gamma)
|
|
170
|
+
new_gamma = soil.gamma_from_deflection(solution, k, g, beyond)
|
|
171
|
+
history.append(new_gamma)
|
|
172
|
+
if abs(new_gamma - gamma) < tol*max(1.0, gamma):
|
|
173
|
+
solution, k, g = solve_with(new_gamma)
|
|
174
|
+
return VlasovResult(solution, soil, new_gamma, k, g, history)
|
|
175
|
+
gamma = new_gamma
|
|
176
|
+
raise ConvergenceError(f"gamma did not converge in {max_iter} iterations "
|
|
177
|
+
f"(last values: {history[-3:]})")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ebbef2p-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Finite element analysis of Euler-Bernoulli beams on two-parameter elastic foundations (Winkler, Pasternak, modified Vlasov)
|
|
5
|
+
Project-URL: Homepage, https://github.com/bteodoru/ebbef2p-python
|
|
6
|
+
Project-URL: Documentation, https://ebbef2p-python.readthedocs.io
|
|
7
|
+
Project-URL: Issues, https://github.com/bteodoru/ebbef2p-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/bteodoru/ebbef2p-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Bogdan Teodoru <bteodoru@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: Pasternak,Vlasov,Winkler,beam on elastic foundation,finite element method,geotechnical engineering,soil-structure interaction
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Requires-Dist: numpy>=1.26
|
|
27
|
+
Requires-Dist: scipy>=1.11.3
|
|
28
|
+
Provides-Extra: plot
|
|
29
|
+
Requires-Dist: matplotlib>=3.10.7; extra == 'plot'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# EBBEF2p for Python
|
|
34
|
+
|
|
35
|
+
[](https://github.com/bteodoru/ebbef2p-python/actions/workflows/tests.yml)
|
|
36
|
+
[](https://badge.fury.io/py/ebbef2p-python)
|
|
37
|
+
[](https://pypi.org/project/ebbef2p-python/)
|
|
38
|
+
[](https://github.com/bteodoru/ebbef2p-python/blob/main/LICENSE)
|
|
39
|
+
[](https://ebbef2p-python.readthedocs.io/en/latest/?badge=latest)
|
|
40
|
+
|
|
41
|
+
EBBEF2p (Euler-Bernoulli Beams on Two-Parameter Elastic Foundations) is a
|
|
42
|
+
finite element package for static analysis of beams on Winkler, Pasternak
|
|
43
|
+
and modified Vlasov foundations.
|
|
44
|
+
|
|
45
|
+
* both foundation parameters may vary linearly along the beam, with jumps
|
|
46
|
+
between segments;
|
|
47
|
+
* the soil may continue beyond the beam ends (equivalent end springs);
|
|
48
|
+
* modified Vlasov model (Vallabhan & Das, 1991) with iterated decay
|
|
49
|
+
parameter and depth-dependent soil modulus;
|
|
50
|
+
* bending moments and shear forces from element equilibrium;
|
|
51
|
+
* soil pressure including the concentrated forces of the shear layer, with
|
|
52
|
+
a global equilibrium check.
|
|
53
|
+
|
|
54
|
+
EBBEF2p was originally written as a MATLAB toolbox.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install ebbef2p-python # NumPy and SciPy only
|
|
60
|
+
pip install "ebbef2p-python[plot]" # with Matplotlib for quick-look plots
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Python 3.12 or newer is required.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from ebbef2p import Model, solve
|
|
69
|
+
|
|
70
|
+
model = Model(soil_beyond_ends=True)
|
|
71
|
+
model.add_beam(0, 10, E=30e6, I=0.01)
|
|
72
|
+
model.add_foundation(0, 10, k=5000, g=2000) # g = 2t (Vlasov) = Gp (Pasternak)
|
|
73
|
+
model.add_point_load(5, force=100)
|
|
74
|
+
|
|
75
|
+
solution = solve(model, n_elements=100)
|
|
76
|
+
print(solution.w.max(), solution.bending_moment().max())
|
|
77
|
+
print(solution.equilibrium())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A modified Vlasov foundation is defined by the soil layer instead:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from ebbef2p import Model, VlasovSoil, solve_vlasov
|
|
84
|
+
|
|
85
|
+
model = Model(soil_beyond_ends=True)
|
|
86
|
+
model.add_beam(0, 100, E=432000, I=2.25).add_distributed_load(0, 100, 3)
|
|
87
|
+
result = solve_vlasov(model, VlasovSoil(E_top=144, nu=0.2, depth=25))
|
|
88
|
+
print(result.gamma, result.k, result.g)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
More in the [documentation](https://ebbef2p-python.readthedocs.io) and in
|
|
92
|
+
the `examples` folder.
|
|
93
|
+
|
|
94
|
+
## Verification
|
|
95
|
+
|
|
96
|
+
`tests/verification` compares EBBEF2p with independent references:
|
|
97
|
+
closed-form solutions for beams without foundation, Hetényi's long beam on
|
|
98
|
+
a Winkler foundation, the exact element stiffness and joint rotations of
|
|
99
|
+
Razaqpur & Shah (1991), the decay parameter gamma of Vallabhan & Das
|
|
100
|
+
(1991), numerical integration of the element matrices and Vlasov
|
|
101
|
+
parameters, and global equilibrium. The full table is in the
|
|
102
|
+
[documentation](https://ebbef2p-python.readthedocs.io/en/latest/theory/verification.html).
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
The project uses [uv](https://docs.astral.sh/uv/):
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
uv sync # environment with the package and the dev tools
|
|
110
|
+
uv run pytest # tests/unit and tests/verification
|
|
111
|
+
uv run ruff check .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and
|
|
115
|
+
[CHANGELOG.md](CHANGELOG.md) for the changes since 0.0.x, including how to
|
|
116
|
+
migrate from the old `Structure` API.
|
|
117
|
+
|
|
118
|
+
## Citing
|
|
119
|
+
|
|
120
|
+
If you use EBBEF2p in your work, please cite it using the metadata in
|
|
121
|
+
[CITATION.cff](CITATION.cff) (GitHub shows it under *Cite this
|
|
122
|
+
repository*).
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ebbef2p/__init__.py,sha256=-sA-yQqybcXHEXUWzh6cnAztdvhSiI-NJm7u64bENYA,613
|
|
2
|
+
ebbef2p/elements.py,sha256=OH2CCHXslm05G1mboPmZo5R4LUIPAFm8zDQ4889y9Aw,4431
|
|
3
|
+
ebbef2p/exceptions.py,sha256=ipc_lYRiWENw_-ipEWQpd4WvzenYbDc-3shbxeUrb68,632
|
|
4
|
+
ebbef2p/mesh.py,sha256=cvRsCHnCwD7eeJx2JGHD6IgCvUjUG4QbQkJGbEFV4fo,3816
|
|
5
|
+
ebbef2p/model.py,sha256=OinzyRDz_McZHf7MmOh79T86Tf9jekD2f95cIraLr3g,6681
|
|
6
|
+
ebbef2p/plotting.py,sha256=_kyMC-3xjM_Brp1CF5RKgXAm1xhZBrksyf4VP0i-jSM,1087
|
|
7
|
+
ebbef2p/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
ebbef2p/results.py,sha256=j7hAUaL3Prvr_daubhg8bzMcC40kbEM1Ld1sCxQCYqM,8484
|
|
9
|
+
ebbef2p/solver.py,sha256=9zb6n7Js3mD-Mk3wCZ44vbZY0y0rbwELjpnhMB3GZFI,4457
|
|
10
|
+
ebbef2p/vlasov.py,sha256=XUMx1q1sh8LqwJANagH2YN1i9k6TDXEJdSNlHT_kzNo,6609
|
|
11
|
+
ebbef2p_python-1.0.0.dist-info/METADATA,sha256=wXV-UVfbvVOVYdza1FaJNILP9WV9c6X7A6adww4YPKQ,5031
|
|
12
|
+
ebbef2p_python-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
13
|
+
ebbef2p_python-1.0.0.dist-info/licenses/LICENSE,sha256=6OUFGGXle37w9HVrLxxwjuCNw_OgHgZuMS9tb-EP8qY,1076
|
|
14
|
+
ebbef2p_python-1.0.0.dist-info/RECORD,,
|