PyCBA 0.5.2__tar.gz → 0.6.0__tar.gz
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.
- {pycba-0.5.2/src/PyCBA.egg-info → pycba-0.6.0}/PKG-INFO +6 -4
- {pycba-0.5.2 → pycba-0.6.0}/pyproject.toml +3 -2
- {pycba-0.5.2 → pycba-0.6.0/src/PyCBA.egg-info}/PKG-INFO +6 -4
- {pycba-0.5.2 → pycba-0.6.0}/src/PyCBA.egg-info/SOURCES.txt +1 -0
- pycba-0.6.0/src/pycba/__init__.py +23 -0
- pycba-0.6.0/src/pycba/analysis.py +567 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/beam.py +74 -37
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/load.py +34 -169
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/results.py +8 -4
- pycba-0.6.0/src/pycba/types.py +68 -0
- {pycba-0.5.2 → pycba-0.6.0}/tests/test_basic.py +144 -0
- pycba-0.5.2/src/pycba/__init__.py +0 -15
- pycba-0.5.2/src/pycba/analysis.py +0 -372
- {pycba-0.5.2 → pycba-0.6.0}/LICENSE +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/README.md +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/setup.cfg +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/setup.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/PyCBA.egg-info/dependency_links.txt +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/PyCBA.egg-info/requires.txt +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/PyCBA.egg-info/top_level.txt +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/bridge.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/inf_lines.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/pattern.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/utils.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/src/pycba/vehicle.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/tests/test_bridge.py +0 -0
- {pycba-0.5.2 → pycba-0.6.0}/tests/test_inf_lines.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: PyCBA
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Python Continuous Beam Analysis
|
|
5
5
|
Author-email: Colin Caprani <colin.caprani@monash.edu>
|
|
6
6
|
License: Apache 2.0
|
|
@@ -20,11 +20,12 @@ Classifier: Natural Language :: English
|
|
|
20
20
|
Classifier: Operating System :: POSIX :: Linux
|
|
21
21
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
22
22
|
Classifier: Operating System :: Microsoft :: Windows
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.9
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.10
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
-
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Requires-Python: >=3.9
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
License-File: LICENSE
|
|
30
31
|
Requires-Dist: matplotlib
|
|
@@ -32,6 +33,7 @@ Requires-Dist: numpy
|
|
|
32
33
|
Requires-Dist: scipy>=1.6.0
|
|
33
34
|
Provides-Extra: test
|
|
34
35
|
Requires-Dist: pytest>=6.2.2; extra == "test"
|
|
36
|
+
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
# PyCBA - Python Continuous Beam Analysis
|
|
37
39
|
|
|
@@ -21,12 +21,13 @@ classifiers = [
|
|
|
21
21
|
"Operating System :: POSIX :: Linux",
|
|
22
22
|
"Operating System :: MacOS :: MacOS X",
|
|
23
23
|
"Operating System :: Microsoft :: Windows",
|
|
24
|
-
"Programming Language :: Python :: 3.8",
|
|
25
24
|
"Programming Language :: Python :: 3.9",
|
|
26
25
|
"Programming Language :: Python :: 3.10",
|
|
27
26
|
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
28
29
|
]
|
|
29
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.9"
|
|
30
31
|
dependencies = [
|
|
31
32
|
"matplotlib",
|
|
32
33
|
"numpy",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: PyCBA
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Python Continuous Beam Analysis
|
|
5
5
|
Author-email: Colin Caprani <colin.caprani@monash.edu>
|
|
6
6
|
License: Apache 2.0
|
|
@@ -20,11 +20,12 @@ Classifier: Natural Language :: English
|
|
|
20
20
|
Classifier: Operating System :: POSIX :: Linux
|
|
21
21
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
22
22
|
Classifier: Operating System :: Microsoft :: Windows
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.9
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.10
|
|
26
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
27
|
-
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Requires-Python: >=3.9
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
License-File: LICENSE
|
|
30
31
|
Requires-Dist: matplotlib
|
|
@@ -32,6 +33,7 @@ Requires-Dist: numpy
|
|
|
32
33
|
Requires-Dist: scipy>=1.6.0
|
|
33
34
|
Provides-Extra: test
|
|
34
35
|
Requires-Dist: pytest>=6.2.2; extra == "test"
|
|
36
|
+
Dynamic: license-file
|
|
35
37
|
|
|
36
38
|
# PyCBA - Python Continuous Beam Analysis
|
|
37
39
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyCBA - Continuous Beam Analysis in Python
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.6.0"
|
|
6
|
+
|
|
7
|
+
from .analysis import BeamAnalysis
|
|
8
|
+
from .beam import Beam
|
|
9
|
+
from .load import (
|
|
10
|
+
LoadCNL,
|
|
11
|
+
MemberResults,
|
|
12
|
+
LoadMatrix,
|
|
13
|
+
LoadType,
|
|
14
|
+
parse_LM,
|
|
15
|
+
add_LM,
|
|
16
|
+
factor_LM,
|
|
17
|
+
)
|
|
18
|
+
from .results import BeamResults, Envelopes
|
|
19
|
+
from .inf_lines import InfluenceLines
|
|
20
|
+
from .utils import parse_beam_string
|
|
21
|
+
from .bridge import BridgeAnalysis
|
|
22
|
+
from .vehicle import Vehicle, make_train, VehicleLibrary
|
|
23
|
+
from .pattern import LoadPattern
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyCBA - Continuous Beam Analysis
|
|
3
|
+
|
|
4
|
+
Implements the direct stiffness method for linear-elastic continuous beam
|
|
5
|
+
analysis. The primary entry point is :class:`BeamAnalysis`, which assembles
|
|
6
|
+
the global stiffness matrix, applies boundary conditions (including prescribed
|
|
7
|
+
displacements / support settlements), solves for nodal displacements, and
|
|
8
|
+
recovers reactions and member load effects.
|
|
9
|
+
|
|
10
|
+
**PyCBA is unit-agnostic.** No conversions are performed; any internally
|
|
11
|
+
consistent set of units (e.g. kN/m/kNm, or N/mm/Nmm) may be used as long as
|
|
12
|
+
all inputs share the same system. The only exception is :meth:`BeamAnalysis.plot_results`,
|
|
13
|
+
which scales deflections by 1×10³ and labels the axis "mm" — that label is
|
|
14
|
+
only correct when the length unit is metres.
|
|
15
|
+
|
|
16
|
+
Sign conventions
|
|
17
|
+
----------------
|
|
18
|
+
* Vertical displacements and forces: **positive upward**.
|
|
19
|
+
* Rotations and moments: **positive counter-clockwise**.
|
|
20
|
+
* Settlement (prescribed displacement): negative value = downward.
|
|
21
|
+
* UDL / point loads: positive value = downward acting load.
|
|
22
|
+
* Moment loads: positive value = counter-clockwise.
|
|
23
|
+
|
|
24
|
+
Restraint vector ``R``
|
|
25
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
26
|
+
One entry per nodal DOF (vertical then rotational, node by node):
|
|
27
|
+
|
|
28
|
+
* ``-1`` — fully fixed (zero displacement unless overridden by ``D``).
|
|
29
|
+
* ``0`` — free.
|
|
30
|
+
* ``+k`` — elastic spring with stiffness *k* in consistent
|
|
31
|
+
force/length (translational) or force·length/angle (rotational) units.
|
|
32
|
+
|
|
33
|
+
An OO Python adaptation of CBA, originally written for MATLAB:
|
|
34
|
+
http://www.colincaprani.com/programming/matlab/
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from typing import Union, Optional
|
|
38
|
+
import numpy as np
|
|
39
|
+
import matplotlib.pyplot as plt
|
|
40
|
+
from .beam import Beam, LoadMatrix
|
|
41
|
+
from .results import BeamResults
|
|
42
|
+
from .load import add_LM
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BeamAnalysis:
|
|
46
|
+
"""
|
|
47
|
+
Direct-stiffness continuous beam analyser.
|
|
48
|
+
|
|
49
|
+
Assembles the global stiffness matrix from individual span stiffness
|
|
50
|
+
matrices, applies support boundary conditions (including elastic springs
|
|
51
|
+
and prescribed displacements), solves the linear system for nodal
|
|
52
|
+
displacements, and recovers support reactions and distributed load effects
|
|
53
|
+
along each member.
|
|
54
|
+
|
|
55
|
+
After calling :meth:`analyze`, results are available through
|
|
56
|
+
:attr:`beam_results`:
|
|
57
|
+
|
|
58
|
+
* ``beam_results.R`` — reactions at fully-fixed DOFs.
|
|
59
|
+
* ``beam_results.Rs`` — spring forces ``k_s × u_i`` at spring DOFs.
|
|
60
|
+
* ``beam_results.D`` — global nodal displacement vector.
|
|
61
|
+
* ``beam_results.results`` — concatenated member load-effect arrays
|
|
62
|
+
(``x``, ``M``, ``V``, ``D``, ``R``).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
L: np.ndarray,
|
|
68
|
+
EI: Union[float, np.ndarray],
|
|
69
|
+
R: np.ndarray,
|
|
70
|
+
LM: Optional[LoadMatrix] = None,
|
|
71
|
+
eletype: Optional[np.ndarray] = None,
|
|
72
|
+
D: Optional[list] = None,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Construct a beam analysis object.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
L : array_like of float
|
|
80
|
+
Span lengths. Length ``N`` for an ``N``-span beam.
|
|
81
|
+
EI : float or array_like of float
|
|
82
|
+
Flexural rigidity of each span. A scalar value is applied to all
|
|
83
|
+
spans; otherwise one value per span is required.
|
|
84
|
+
R : array_like of int or float
|
|
85
|
+
Nodal restraint vector, length ``2(N+1)``. Two entries per node
|
|
86
|
+
(vertical DOF then rotational DOF), ordered left to right:
|
|
87
|
+
|
|
88
|
+
* ``-1`` — fully restrained (zero displacement unless overridden
|
|
89
|
+
by ``D``).
|
|
90
|
+
* ``0`` — free.
|
|
91
|
+
* ``+k`` — elastic spring stiffness in consistent units.
|
|
92
|
+
|
|
93
|
+
LM : list of list, optional
|
|
94
|
+
Load matrix: a list of load descriptors, each of the form
|
|
95
|
+
``[span, load_type, value, a, c]``. Load types:
|
|
96
|
+
|
|
97
|
+
1. UDL — ``value`` is load intensity; set ``a = c = 0``.
|
|
98
|
+
2. Point load — ``value`` at distance ``a`` from the span start.
|
|
99
|
+
3. Partial UDL — ``value`` intensity from ``a`` for length ``c``.
|
|
100
|
+
4. Moment load — ``value`` at distance ``a`` from the span start.
|
|
101
|
+
|
|
102
|
+
eletype : array_like of int, optional
|
|
103
|
+
Element type for each span, controlling which end(s) carry moment:
|
|
104
|
+
|
|
105
|
+
1. Fixed–fixed (default).
|
|
106
|
+
2. Fixed–pinned (moment release at right end).
|
|
107
|
+
3. Pinned–fixed (moment release at left end).
|
|
108
|
+
4. Pinned–pinned (moment releases at both ends).
|
|
109
|
+
|
|
110
|
+
At an internal hinge, only one of the two members meeting at that
|
|
111
|
+
node should have a pinned end.
|
|
112
|
+
D : list, optional
|
|
113
|
+
Prescribed-displacement vector, length ``2(N+1)`` (same as ``R``).
|
|
114
|
+
Use ``None`` for DOFs whose displacement is unknown (the default).
|
|
115
|
+
Provide a float for DOFs with a known displacement (e.g. a support
|
|
116
|
+
settlement — negative = downward). Fixed supports (``R = -1``)
|
|
117
|
+
default to zero displacement unless ``D`` provides an explicit
|
|
118
|
+
override.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
ValueError
|
|
123
|
+
If ``R`` and ``D`` have different lengths, or ``EI`` is not
|
|
124
|
+
scalar and its length differs from ``len(L)``.
|
|
125
|
+
"""
|
|
126
|
+
self.npts = 100
|
|
127
|
+
self._beam_results = None
|
|
128
|
+
|
|
129
|
+
if eletype is None:
|
|
130
|
+
self.eletype = np.ones((len(L), 1))
|
|
131
|
+
else:
|
|
132
|
+
self.eletype = eletype
|
|
133
|
+
# Create the beam
|
|
134
|
+
self._beam = Beam(L=L, EI=EI, R=R, LM=LM, eletype=self.eletype, D=D)
|
|
135
|
+
|
|
136
|
+
self._n = self._beam.no_spans
|
|
137
|
+
self._no_nodes = self._n + 1
|
|
138
|
+
self._nDOF = 2 * self._no_nodes
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def beam_results(self) -> BeamResults:
|
|
142
|
+
"""
|
|
143
|
+
BeamResults : Post-analysis results object.
|
|
144
|
+
|
|
145
|
+
``None`` until :meth:`analyze` has been called successfully.
|
|
146
|
+
Provides nodal displacements (``D``), reactions at fixed supports
|
|
147
|
+
(``R``), spring forces (``Rs``), and per-member load-effect arrays
|
|
148
|
+
(``vRes``, ``results``).
|
|
149
|
+
"""
|
|
150
|
+
return self._beam_results
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def beam(self) -> Beam:
|
|
154
|
+
"""
|
|
155
|
+
Beam : The underlying :class:`~pycba.beam.Beam` object.
|
|
156
|
+
|
|
157
|
+
Provides direct access to span geometry, stiffness matrices,
|
|
158
|
+
restraints, and prescribed displacements.
|
|
159
|
+
"""
|
|
160
|
+
return self._beam
|
|
161
|
+
|
|
162
|
+
def set_loads(self, LM: LoadMatrix):
|
|
163
|
+
"""
|
|
164
|
+
Replace the current load matrix with a new one.
|
|
165
|
+
|
|
166
|
+
Any loads previously added via :meth:`add_udl`, :meth:`add_pl`,
|
|
167
|
+
:meth:`add_pudl`, or :meth:`add_ml` are discarded.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
LM : list of list
|
|
172
|
+
New load matrix in the same format as the ``LM`` argument of
|
|
173
|
+
:meth:`__init__`.
|
|
174
|
+
"""
|
|
175
|
+
self._beam.loads = LM
|
|
176
|
+
|
|
177
|
+
def add_udl(self, i_span: int, w: float):
|
|
178
|
+
"""
|
|
179
|
+
Append a full-span uniformly-distributed load.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
i_span : int
|
|
184
|
+
1-based span index.
|
|
185
|
+
w : float
|
|
186
|
+
Load intensity. Positive values act downward.
|
|
187
|
+
"""
|
|
188
|
+
load = [i_span, 1, w]
|
|
189
|
+
self._beam.add_load(load)
|
|
190
|
+
|
|
191
|
+
def add_pl(self, i_span: int, p: float, a: float):
|
|
192
|
+
"""
|
|
193
|
+
Append a point load.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
i_span : int
|
|
198
|
+
1-based span index.
|
|
199
|
+
p : float
|
|
200
|
+
Load magnitude. Positive values act downward.
|
|
201
|
+
a : float
|
|
202
|
+
Distance from the left end of the span to the load.
|
|
203
|
+
"""
|
|
204
|
+
load = [i_span, 2, p, a]
|
|
205
|
+
self._beam.add_load(load)
|
|
206
|
+
|
|
207
|
+
def add_pudl(self, i_span: int, w: float, a: float, c: float):
|
|
208
|
+
"""
|
|
209
|
+
Append a partial uniformly-distributed load.
|
|
210
|
+
|
|
211
|
+
Any portion of the load that extends beyond the end of the span is
|
|
212
|
+
silently ignored.
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
i_span : int
|
|
217
|
+
1-based span index.
|
|
218
|
+
w : float
|
|
219
|
+
Load intensity. Positive values act downward.
|
|
220
|
+
a : float
|
|
221
|
+
Distance from the left end of the span to the start of the load.
|
|
222
|
+
c : float
|
|
223
|
+
Length (cover) of the partial UDL.
|
|
224
|
+
"""
|
|
225
|
+
load = [i_span, 3, w, a, c]
|
|
226
|
+
self._beam.add_load(load)
|
|
227
|
+
|
|
228
|
+
def add_ml(self, i_span: int, m: float, a: float):
|
|
229
|
+
"""
|
|
230
|
+
Append a concentrated moment load.
|
|
231
|
+
|
|
232
|
+
Parameters
|
|
233
|
+
----------
|
|
234
|
+
i_span : int
|
|
235
|
+
1-based span index.
|
|
236
|
+
m : float
|
|
237
|
+
Moment magnitude. Positive values are counter-clockwise.
|
|
238
|
+
a : float
|
|
239
|
+
Distance from the left end of the span to the load.
|
|
240
|
+
"""
|
|
241
|
+
load = [i_span, 4, m, a]
|
|
242
|
+
self._beam.add_load(load)
|
|
243
|
+
|
|
244
|
+
def analyze(self, npts: Optional[int] = None) -> int:
|
|
245
|
+
"""
|
|
246
|
+
Execute the direct-stiffness analysis.
|
|
247
|
+
|
|
248
|
+
Assembles the unrestricted global stiffness matrix, validates the
|
|
249
|
+
model, applies boundary conditions (including spring supports and
|
|
250
|
+
prescribed displacements), solves for nodal displacements, and
|
|
251
|
+
recovers support reactions. Results are stored in
|
|
252
|
+
:attr:`beam_results`.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
npts : int, optional
|
|
257
|
+
Number of evaluation points along each member for computing
|
|
258
|
+
distributed load effects (bending moment, shear, deflection).
|
|
259
|
+
Must be greater than 3; defaults to 100 if omitted or ``≤ 3``.
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
int
|
|
264
|
+
``0`` on successful completion.
|
|
265
|
+
|
|
266
|
+
Raises
|
|
267
|
+
------
|
|
268
|
+
ValueError
|
|
269
|
+
If the model is invalid (see :meth:`_validate`) or if the
|
|
270
|
+
structure is geometrically unstable (see :meth:`_solver`).
|
|
271
|
+
"""
|
|
272
|
+
if npts and npts > 3:
|
|
273
|
+
self.npts = npts
|
|
274
|
+
|
|
275
|
+
restraints = self._beam.restraints
|
|
276
|
+
d_presc = self._beam.prescribed_displacements
|
|
277
|
+
fU = self._forces()
|
|
278
|
+
|
|
279
|
+
self._validate(restraints, d_presc, fU)
|
|
280
|
+
|
|
281
|
+
f = np.copy(fU)
|
|
282
|
+
ksysU = self._assemble()
|
|
283
|
+
ksys = np.copy(ksysU)
|
|
284
|
+
ksys, f = self._apply_bc(ksys, f)
|
|
285
|
+
d = self._solver(ksys, f)
|
|
286
|
+
r, rs = self._reactions(ksysU, d, fU)
|
|
287
|
+
|
|
288
|
+
self._beam_results = BeamResults(self._beam, d, r, self.npts, rs)
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
def _forces(self) -> np.ndarray:
|
|
292
|
+
"""
|
|
293
|
+
Build the unrestricted global nodal force vector from the load matrix.
|
|
294
|
+
|
|
295
|
+
Iterates over spans, retrieves each span's released end forces
|
|
296
|
+
(consistent nodal loads adjusted for element type / moment releases),
|
|
297
|
+
and accumulates them in the global vector with the sign reversal
|
|
298
|
+
required by the direct stiffness method (loads oppose the reactions
|
|
299
|
+
they generate).
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
f : np.ndarray, shape (nDOF,)
|
|
304
|
+
Unrestricted global nodal force vector.
|
|
305
|
+
"""
|
|
306
|
+
self._beam._set_loads()
|
|
307
|
+
|
|
308
|
+
f = np.zeros(self._nDOF)
|
|
309
|
+
|
|
310
|
+
for i in range(self._n):
|
|
311
|
+
dof_i = 2 * i
|
|
312
|
+
fmbr = self._beam.get_ref(i)
|
|
313
|
+
# Cumulatively apply forces in opposite direction
|
|
314
|
+
f[dof_i : dof_i + 4] -= fmbr
|
|
315
|
+
return f
|
|
316
|
+
|
|
317
|
+
def _validate(
|
|
318
|
+
self,
|
|
319
|
+
restraints: list,
|
|
320
|
+
d_presc: list,
|
|
321
|
+
fU: np.ndarray,
|
|
322
|
+
) -> None:
|
|
323
|
+
"""
|
|
324
|
+
Pre-analysis model validity check.
|
|
325
|
+
|
|
326
|
+
Currently checks for the one combination that is physically
|
|
327
|
+
inconsistent with the direct elimination method: a DOF that
|
|
328
|
+
simultaneously has a spring support, a prescribed displacement, *and*
|
|
329
|
+
a non-zero consistent nodal load. In that case the BC enforcement
|
|
330
|
+
sets ``f[i] = d_i``, silently overwriting the load — no warning is
|
|
331
|
+
possible after the fact, so this must be caught before the solve.
|
|
332
|
+
|
|
333
|
+
Parameters
|
|
334
|
+
----------
|
|
335
|
+
restraints : list
|
|
336
|
+
Beam restraint vector (same as ``R``).
|
|
337
|
+
d_presc : list
|
|
338
|
+
Prescribed-displacement vector (``None`` entries = free DOFs).
|
|
339
|
+
fU : np.ndarray, shape (nDOF,)
|
|
340
|
+
Unrestricted nodal force vector from :meth:`_forces`.
|
|
341
|
+
|
|
342
|
+
Raises
|
|
343
|
+
------
|
|
344
|
+
ValueError
|
|
345
|
+
If any DOF simultaneously carries a spring restraint, a prescribed
|
|
346
|
+
displacement, and a non-zero consistent nodal load.
|
|
347
|
+
"""
|
|
348
|
+
for i in range(self._nDOF):
|
|
349
|
+
if restraints[i] > 0 and d_presc[i] is not None and fU[i] != 0.0:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Invalid model at DOF {i}: a spring support, a prescribed "
|
|
352
|
+
f"displacement, and a non-zero external nodal load "
|
|
353
|
+
f"(fU[{i}] = {fU[i]}) cannot coexist. The elimination "
|
|
354
|
+
"method would silently discard the nodal load. "
|
|
355
|
+
"Remove the prescribed displacement, the spring, or the "
|
|
356
|
+
"external load at this DOF."
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _assemble(self) -> np.ndarray:
|
|
360
|
+
"""
|
|
361
|
+
Assemble the unrestricted global stiffness matrix.
|
|
362
|
+
|
|
363
|
+
Loops over spans and overlaps each span's 4×4 stiffness matrix into
|
|
364
|
+
the ``2(N+1) × 2(N+1)`` global matrix using the standard connectivity
|
|
365
|
+
pattern. Spring stiffnesses (``R > 0``) are added to the diagonal
|
|
366
|
+
here so that the returned matrix is the *complete* unrestricted system,
|
|
367
|
+
which is required by :meth:`_reactions` to recover spring forces
|
|
368
|
+
correctly.
|
|
369
|
+
|
|
370
|
+
Returns
|
|
371
|
+
-------
|
|
372
|
+
ksys : np.ndarray, shape (nDOF, nDOF)
|
|
373
|
+
Unrestricted global stiffness matrix including spring contributions.
|
|
374
|
+
"""
|
|
375
|
+
ksys = np.zeros((self._nDOF, self._nDOF))
|
|
376
|
+
|
|
377
|
+
for i in range(self._n):
|
|
378
|
+
kb = self._beam.get_span_k(i)
|
|
379
|
+
dof_i = 2 * i
|
|
380
|
+
ksys[dof_i : dof_i + 4, dof_i : dof_i + 4] += kb
|
|
381
|
+
|
|
382
|
+
# Add spring stiffness before copying so ksys (used for reactions)
|
|
383
|
+
# includes the spring contribution and spring forces are not lost.
|
|
384
|
+
r_vec = self._beam.restraints
|
|
385
|
+
for i in range(self._nDOF):
|
|
386
|
+
if r_vec[i] > 0:
|
|
387
|
+
ksys[i][i] += r_vec[i]
|
|
388
|
+
|
|
389
|
+
return ksys
|
|
390
|
+
|
|
391
|
+
def _apply_bc(self, k: np.ndarray, f: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
392
|
+
"""
|
|
393
|
+
Impose boundary conditions using the direct elimination method.
|
|
394
|
+
|
|
395
|
+
For each DOF with a known displacement ``d_i`` (either explicitly
|
|
396
|
+
prescribed via ``D`` or implicitly zero for fully-fixed supports),
|
|
397
|
+
the corresponding row and column are zeroed, the diagonal is set to
|
|
398
|
+
one, and the right-hand side is updated by subtracting
|
|
399
|
+
``k[:, i] * d_i`` before overwriting ``f[i] = d_i``. This preserves
|
|
400
|
+
the displacement field exactly and transfers the constraint contribution
|
|
401
|
+
to the remaining free DOFs.
|
|
402
|
+
|
|
403
|
+
Spring DOFs (``R > 0``) without a prescribed displacement are *not*
|
|
404
|
+
eliminated — their stiffness is already on the diagonal of ``k`` from
|
|
405
|
+
:meth:`_assemble` and they remain as free unknowns.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
k : np.ndarray, shape (nDOF, nDOF)
|
|
410
|
+
Unrestricted global stiffness matrix (modified in-place).
|
|
411
|
+
f : np.ndarray, shape (nDOF,)
|
|
412
|
+
Global nodal force vector (modified in-place).
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
k : np.ndarray, shape (nDOF, nDOF)
|
|
417
|
+
Restricted stiffness matrix with constrained rows/columns zeroed.
|
|
418
|
+
f : np.ndarray, shape (nDOF,)
|
|
419
|
+
Modified force vector incorporating prescribed displacement values.
|
|
420
|
+
"""
|
|
421
|
+
r = self._beam.restraints
|
|
422
|
+
d = self._beam.prescribed_displacements
|
|
423
|
+
for i in range(self._nDOF):
|
|
424
|
+
# Determine prescribed value: explicit settlement, or fixed support (= 0)
|
|
425
|
+
if d[i] is not None:
|
|
426
|
+
di = d[i]
|
|
427
|
+
elif r[i] < 0:
|
|
428
|
+
di = 0.0
|
|
429
|
+
else:
|
|
430
|
+
continue # free or spring-only DOF: nothing to eliminate
|
|
431
|
+
|
|
432
|
+
# Subtract full column i from RHS (including diagonal) before zeroing
|
|
433
|
+
f -= k[:, i] * di
|
|
434
|
+
k[i, :] = 0.0
|
|
435
|
+
k[:, i] = 0.0
|
|
436
|
+
k[i, i] = 1.0
|
|
437
|
+
f[i] = di
|
|
438
|
+
return k, f
|
|
439
|
+
|
|
440
|
+
def _reactions(self, k: np.ndarray, d: np.ndarray, f: np.ndarray) -> tuple:
|
|
441
|
+
"""
|
|
442
|
+
Recover support reactions and spring forces from the solved displacement field.
|
|
443
|
+
|
|
444
|
+
Uses the unrestricted global stiffness matrix ``k`` (i.e. ``ksysU``
|
|
445
|
+
from :meth:`analyze`) so that spring contributions are included in the
|
|
446
|
+
residual calculation.
|
|
447
|
+
|
|
448
|
+
For fully-fixed DOFs (``restraints[i] == -1``) the nodal residual
|
|
449
|
+
``(k @ d - f)[i]`` equals the support reaction directly, because those
|
|
450
|
+
DOFs have zero (or prescribed) displacement and no ambiguity with
|
|
451
|
+
spring terms.
|
|
452
|
+
|
|
453
|
+
For spring DOFs (``restraints[i] > 0``) the residual also contains
|
|
454
|
+
structural coupling and any applied nodal load, so it does *not* in
|
|
455
|
+
general equal the spring force alone. The spring force is therefore
|
|
456
|
+
computed explicitly as ``k_s * u_i``.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
k : np.ndarray, shape (nDOF, nDOF)
|
|
461
|
+
Unrestricted global stiffness matrix (including spring terms).
|
|
462
|
+
d : np.ndarray, shape (nDOF,)
|
|
463
|
+
Solved global nodal displacement vector (m, rad).
|
|
464
|
+
f : np.ndarray, shape (nDOF,)
|
|
465
|
+
Unrestricted nodal force vector ``fU`` from :meth:`_forces`.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
r : np.ndarray
|
|
470
|
+
Reactions at fully-fixed DOFs (``restraints[i] == -1``), in DOF
|
|
471
|
+
order.
|
|
472
|
+
rs : np.ndarray
|
|
473
|
+
Spring forces ``-k_s * u_i`` (upward positive) at spring DOFs
|
|
474
|
+
(``restraints[i] > 0``), in DOF order.
|
|
475
|
+
"""
|
|
476
|
+
residual = k @ d - f
|
|
477
|
+
restraints = self._beam.restraints
|
|
478
|
+
# For fixed DOFs the full nodal residual equals the support reaction,
|
|
479
|
+
# because d[i] = 0 leaves no ambiguity about which term dominates.
|
|
480
|
+
r = np.array([residual[i] for i in range(self._nDOF) if restraints[i] < 0])
|
|
481
|
+
# For spring DOFs, residual[i] = k_s*u_i + structural coupling - f_applied[i],
|
|
482
|
+
# so it is NOT purely the spring force when external nodal loads are present.
|
|
483
|
+
# Use -k_s*u_i explicitly: negative because the spring reaction is upward
|
|
484
|
+
# (positive) when the displacement is downward (negative).
|
|
485
|
+
rs = np.array(
|
|
486
|
+
[-restraints[i] * d[i] for i in range(self._nDOF) if restraints[i] > 0]
|
|
487
|
+
)
|
|
488
|
+
return r, rs
|
|
489
|
+
|
|
490
|
+
def _solver(self, A: np.ndarray, b: np.ndarray) -> np.ndarray:
|
|
491
|
+
"""
|
|
492
|
+
Solve the restricted linear system ``A x = b`` for nodal displacements.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
A : np.ndarray, shape (nDOF, nDOF)
|
|
497
|
+
Restricted global stiffness matrix from :meth:`_apply_bc`.
|
|
498
|
+
b : np.ndarray, shape (nDOF,)
|
|
499
|
+
Restricted force vector from :meth:`_apply_bc`.
|
|
500
|
+
|
|
501
|
+
Returns
|
|
502
|
+
-------
|
|
503
|
+
x : np.ndarray, shape (nDOF,)
|
|
504
|
+
Nodal displacement vector.
|
|
505
|
+
|
|
506
|
+
Raises
|
|
507
|
+
------
|
|
508
|
+
ValueError
|
|
509
|
+
If ``A`` is singular, indicating a geometrically unstable
|
|
510
|
+
structure (insufficient support restraints).
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
x = np.linalg.solve(A, b)
|
|
514
|
+
except np.linalg.LinAlgError as exc:
|
|
515
|
+
raise ValueError(
|
|
516
|
+
"Structure is geometrically unstable: the stiffness matrix is "
|
|
517
|
+
"singular. Check that sufficient support restraints are defined."
|
|
518
|
+
) from exc
|
|
519
|
+
return x
|
|
520
|
+
|
|
521
|
+
def plot_results(self):
|
|
522
|
+
"""
|
|
523
|
+
Plot bending moment, shear force, and deflection diagrams.
|
|
524
|
+
|
|
525
|
+
Produces a three-panel figure of bending moment, shear force, and
|
|
526
|
+
deflection along the beam. Bending moment is plotted with the
|
|
527
|
+
sagging-positive convention (y-axis inverted so sagging appears below
|
|
528
|
+
the beam line).
|
|
529
|
+
|
|
530
|
+
.. note::
|
|
531
|
+
The axis labels ("kNm", "kN", "mm") and the deflection scaling
|
|
532
|
+
(×1000) assume a kN / m unit system. If a different consistent
|
|
533
|
+
unit system is used the plots will still be correct in shape but
|
|
534
|
+
the labels and deflection axis values will need interpretation.
|
|
535
|
+
|
|
536
|
+
Has no effect and prints a warning if :meth:`analyze` has not been
|
|
537
|
+
called yet.
|
|
538
|
+
"""
|
|
539
|
+
if self._beam_results is None:
|
|
540
|
+
print("Nothing to plot - run analysis first")
|
|
541
|
+
return
|
|
542
|
+
res = self._beam_results.results
|
|
543
|
+
L = self._beam.length
|
|
544
|
+
|
|
545
|
+
fig, axs = plt.subplots(3, 1)
|
|
546
|
+
|
|
547
|
+
ax = axs[0]
|
|
548
|
+
ax.plot([0, L], [0, 0], "k", lw=2)
|
|
549
|
+
ax.plot(res.x, res.M, "r")
|
|
550
|
+
ax.invert_yaxis()
|
|
551
|
+
ax.grid()
|
|
552
|
+
ax.set_ylabel("Bending Moment (kNm)")
|
|
553
|
+
|
|
554
|
+
ax = axs[1]
|
|
555
|
+
ax.plot([0, L], [0, 0], "k", lw=2)
|
|
556
|
+
ax.plot(res.x, res.V, "r")
|
|
557
|
+
ax.grid()
|
|
558
|
+
ax.set_ylabel("Shear Force (kN)")
|
|
559
|
+
|
|
560
|
+
ax = axs[2]
|
|
561
|
+
ax.plot([0, L], [0, 0], "k", lw=2)
|
|
562
|
+
ax.plot(res.x, res.D * 1e3, "r")
|
|
563
|
+
ax.grid()
|
|
564
|
+
ax.set_ylabel("Deflection (mm)")
|
|
565
|
+
ax.set_xlabel("Distance along beam (m)")
|
|
566
|
+
|
|
567
|
+
plt.show()
|