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/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
+ [![tests](https://github.com/bteodoru/ebbef2p-python/actions/workflows/tests.yml/badge.svg)](https://github.com/bteodoru/ebbef2p-python/actions/workflows/tests.yml)
36
+ [![PyPI version](https://badge.fury.io/py/ebbef2p-python.svg)](https://badge.fury.io/py/ebbef2p-python)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/ebbef2p-python)](https://pypi.org/project/ebbef2p-python/)
38
+ [![GitHub license](https://img.shields.io/github/license/bteodoru/ebbef2p-python)](https://github.com/bteodoru/ebbef2p-python/blob/main/LICENSE)
39
+ [![Documentation Status](https://readthedocs.org/projects/ebbef2p-python/badge/?version=latest)](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,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.34.2)
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020, Bogdan Teodoru
3
+ Copyright (c) 2020-2026, Bogdan Teodoru
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal