"""
PyCBA - Continuous Beam Analysis - Bridge Crossing Module
"""
from __future__ import annotations # https://bit.ly/3KYiL2o
from typing import Optional, Union, Dict, List
import numpy as np
import matplotlib.pyplot as plt
from .analysis import BeamAnalysis
from .results import Envelopes, BeamResults
from .vehicle import Vehicle
from .load import add_LM
[docs]class BridgeAnalysis:
"""
Performs a bridge crossing analysis for a defined vehicle. The vehicle is moved
from the zero global x-coordinate of the beam until it has left the beam at the
far end.
Any loads already defined on the `BeamAnalysis` object are retained and
superimposed in each vehicle position analysis.
"""
def __init__(
self, ba: Optional[BeamAnalysis] = None, veh: Optional[Vehicle] = None
):
"""
Can instantiate with nothing and later add or define the objects, or
with instantiate with pre-defined bridge and vehicle objects.
Any loads already defined on the `BeamAnalysis` object are retained in
each vehicle position analysis.
Parameters
----------
ba : Optional[BeamAnalysis], optional
A :class:`pycba.analysis.BeamAnalysis` object. The default is None.
veh : Optional[Vehicle], optional
A :class:`pycba.bridge.Vehicle` object. The default is None.
Returns
-------
None.
"""
self.ba = ba
self.veh = veh
self.vResults = []
self.pos = []
self.static_LM = []
if self.ba:
self.static_LM = self.ba.beam.loads
[docs] def add_bridge(
self,
L: np.ndarray,
EI: Union[float, np.ndarray],
R: np.ndarray,
eletype: Optional[np.ndarray] = None,
):
"""
Create and add a beam to a bridge analysis
Parameters
----------
L : np.ndarray
A vector of span lengths.
EI : Union[float, np.ndarray]
A vector of member flexural rigidities.
R : np.ndarray
A vector describing the support conditions at each member end.
eletype : Optional[np.ndarray]
A vector of the member types. Defaults to a fixed-fixed element.
Returns
-------
ba : BeamAnalysis
A :class:`pycba.analysis.BeamAnalysis` object.
"""
self.ba = BeamAnalysis(L=L, EI=EI, R=R, eletype=eletype)
self.static_LM = self.ba.beam.loads
return self.ba
[docs] def set_bridge(self, ba: BeamAnalysis):
"""
Set the bridge for the bridge analysis.
Any loads already defined on the `BeamAnalysis` object are retained in
each vehicle position analysis.
Parameters
----------
ba : BeamAnalysis
A :class:`pycba.analysis.BeamAnalysis` object.
Returns
-------
None.
"""
self.ba = ba
self.static_LM = self.ba.beam.loads
[docs] def add_vehicle(self, axle_spacings: np.ndarray, axle_weights: np.ndarray):
"""
Create and add the vehicle to the analysis
Parameters
----------
axle_spacings : np.ndarray
A vector of axle spacings of length one fewer than the length of the
vector of axle weights
axle_weights : np.ndarray
A vector of axle weights, length one greater than the length of the
axle spacings vector.
Returns
-------
veh : Vehicle
A :class:`pycba.bridge.Vehicle` object.
"""
self.veh = Vehicle(axle_spacings, axle_weights)
return self.veh
[docs] def set_vehicle(self, veh: Vehicle):
"""
Set the vehicle for the bridge analysis.
Parameters
----------
veh : Vehicle
A :class:`pycba.bridge.Vehicle` object.
Returns
-------
None.
"""
self.veh = veh
[docs] def static_vehicle(self, pos: float, plotflag: bool = False) -> BeamResults:
"""
Performs a single analysis for the vehicle, static at a given position
Parameters
----------
pos : float
The location of the front axle of the vehicle in global beam coordinates.
plotflag : bool, optional
Whether or not to plot the results. The default is False.
Raises
------
ValueError
If a static beam analysis does not succeed, usually due to a beam
configuration error.
Returns
-------
ba: BeamResults
The `pycba.Beamresults` object containing the analysis results.
"""
self._check_objects()
out = self._single_analysis(pos)
if out != 0:
raise ValueError("Bridge analysis did not succeed")
return
if plotflag:
self.plot_static(pos)
return self.ba.beam_results
def _single_analysis(self, pos: float) -> int:
"""
Internal function for efficiency in run_vehicle - assumes Bridge and
Vehicle are already defined/checked in UI functions
Parameters
----------
pos : float
The location of the front axle of the vehicle in global beam coordinates.
Returns
-------
int
0 if the analysis succeeds.
"""
axle_positions = pos - self.veh.axle_coords
# Create the CBA Load Matrix, checking axle positions, etc
LM = []
for iaxle in range(self.veh.NoAxles):
load = self.veh.axw[iaxle]
ispan, pos_in_span = self.ba.beam.get_local_span_coords(
axle_positions[iaxle]
)
if ispan != -1:
LM.append([ispan + 1, 2, load, pos_in_span, 0])
# Now add any pre-existing loads on the beam
LM = add_LM(self.static_LM, LM)
self.ba.set_loads(LM)
return self.ba.analyze()
[docs] def run_vehicle(
self, step: float, plot_env: bool = False, plot_all: bool = False
) -> Envelopes:
"""
Runs the vehicle over the bridge performing a static analysis at each point
Parameters
----------
step : float
The distance increment to move the vehicle.
plot_env : bool, optional
Whether or not to plot the results envelope. The default is False.
plot_all : bool, optional
Whether or not to plot the results for each position as an animation.
The default is False.
Raises
------
ValueError
If a static beam analysis does not succeed, usually due to a beam
configuration error.
Returns
-------
Envelopes
The load effect envelopes for the traverse; a `pycba.Envelopes` object.
"""
self._check_objects()
self.pos = []
self.vResults = []
npts = round((self.ba.beam.length + self.veh.L) / step) + 1
if plot_all:
fig, axs = plt.subplots(2, 1, sharex=True)
for i in range(npts):
# load position
pos = i * step
self.pos.append(pos)
out = self._single_analysis(pos)
if out != 0:
raise ValueError("Bridge analysis did not succeed at {pos=}")
return
if plot_all:
self.plot_static(pos, axs)
plt.pause(0.01)
self.vResults.append(self.ba.beam_results)
env = Envelopes(self.vResults)
if plot_env:
self.plot_envelopes(env)
return env
[docs] def critical_values(
self, env: Envelopes
) -> Dict[str, Dict[str, Union[float, np.ndarray]]]:
"""
From the envelopes output, returns the extreme values, their locations,
and the position of the vehicle for each in a dictionary of dictionaries
Parameters
----------
env : Envelopes
An `pycba.Envelopes` object containing the results of a moving load
analysis.
Raises
------
ValueError
If the supplied envelope is inconsistent with the current
`pycba.bridge.BridgeAnalysis` object.
Returns
-------
crit_values : Dict[str, Dict[str, Union[float, np.ndarray]]]
A dictionary of dictionaries containing the critical values (i.e. extremes)
of each of the load effects, both maximum and minimum.
"""
crit_values = {}
indx = {}
Mmax = env.Mmax.max()
Mmin = env.Mmin.min()
Vmax = env.Vmax.max()
Vmin = env.Vmin.min()
# Find the indices of the critical vehicle positions
indx["Mmax"] = [
i
for i, res in enumerate(self.vResults)
if np.isclose(res.results.M.max(), Mmax)
]
indx["Mmin"] = [
i
for i, res in enumerate(self.vResults)
if np.isclose(res.results.M.min(), Mmin)
]
indx["Vmax"] = [
i
for i, res in enumerate(self.vResults)
if np.isclose(res.results.V.max(), Vmax)
]
indx["Vmin"] = [
i
for i, res in enumerate(self.vResults)
if np.isclose(res.results.V.min(), Vmin)
]
# Now check for any errors
if [] in indx.values():
raise ValueError("Envelope not from the current bridge analysis")
# Good to proceed
crit_values["Mmax"] = {
"val": Mmax,
"at": env.x[env.Mmax.argmax()],
"pos": [self.pos[i] for i in indx["Mmax"]],
}
crit_values["Mmin"] = {
"val": Mmin,
"at": env.x[env.Mmin.argmin()],
"pos": [self.pos[i] for i in indx["Mmin"]],
}
crit_values["Vmax"] = {
"val": Vmax,
"at": env.x[env.Vmax.argmax()],
"pos": [self.pos[i] for i in indx["Vmax"]],
}
crit_values["Vmin"] = {
"val": Vmin,
"at": env.x[env.Vmin.argmin()],
"pos": [self.pos[i] for i in indx["Vmin"]],
}
crit_values["nsup"] = env.nsup
for i in range(env.nsup):
crit_values[f"Rmax{i}"] = {
"val": env.Rmax[i, :].max(),
"pos": self.pos[env.Rmax[i, :].argmax()],
}
crit_values[f"Rmin{i}"] = {
"val": env.Rmin[i, :].min(),
"pos": self.pos[env.Rmin[i, :].argmin()],
}
return crit_values
[docs] def envelopes_ratios(
self, trial_env: Envelopes, ref_env: Envelopes
) -> Dict[str, np.ndarray]:
"""
Returns the ratios of two sets of envelopes considering zero values and
reactions. Note that ratios are only meaningful for any one location on the
beam, and so reaction envelopes ratios reduce to a scalar value. Ratios are
absolute, and zeroed if within 1e-3 absolute tolerance of zero.
Parameters
----------
trial_env : Envelopes
The numerator `pycba.Envelopes` object, usually from the vehicle seeking
access to the bridge.
ref_env : Envelopes
The denominator `pycba.Envelopes` object, usually from the reference or
benchamrk of acceptable load effects on the bridge. Can be from a single
notional vehicle, or a suite of such vehicles.
Raises
------
ValueError
The envelopes need to be from the same bridge analysis object.
Returns
-------
Dict[str,np.ndarray]
A dictionary of ratios between the two envelopes, considering the
maximum and minimum of each load effect.
"""
if ref_env.npts != trial_env.npts or ref_env.nsup != trial_env.nsup:
raise ValueError("Ratios can only be found for compatible envelopes")
def get_ratio(a, b):
"""
Zeroes infinities when a ref load effect is zero. b is reference vector
"""
return np.abs(
np.divide(
a,
b,
out=np.zeros_like(a),
where=~np.isclose(b, np.zeros_like(b), atol=1e-3, rtol=0.0),
)
)
env_ratios = {}
env_ratios["x"] = ref_env.x
env_ratios["Mmax"] = get_ratio(trial_env.Mmax, ref_env.Mmax)
env_ratios["Mmin"] = get_ratio(trial_env.Mmin, ref_env.Mmin)
env_ratios["Vmax"] = get_ratio(trial_env.Vmax, ref_env.Vmax)
env_ratios["Vmin"] = get_ratio(trial_env.Vmin, ref_env.Vmin)
env_ratios["nsup"] = ref_env.nsup
maxvals = get_ratio(trial_env.Rmaxval, ref_env.Rmaxval)
minvals = get_ratio(trial_env.Rminval, ref_env.Rminval)
for i in range(ref_env.nsup):
env_ratios[f"Rmax{i}"] = maxvals[i]
env_ratios[f"Rmin{i}"] = minvals[i]
return env_ratios
[docs] def plot_static(self, pos: float, axs: Optional[plt.Axes] = None):
"""
Plots for analysis of static vehicle
Parameters
----------
pos : float
The position of the front axle of the vehicle in global bridge
coordinates.
axs : Optional[plt.Axes], optional
The axes on which to plot; if None is supplied, one is created.
The default is None.
Returns
-------
None.
"""
res = self.ba.beam_results.results
L = self.ba.beam.length
if axs is None:
fig, axs = plt.subplots(3, 1, sharex=True)
ax0 = 1
if len(axs) == 2: # load effect only
ax0 = 0
else:
ax = axs[0]
ax.bar(pos - self.veh.axle_coords, self.veh.axw, color="r")
ax.set_ylabel("Axle Weights (kN)")
ax.grid()
ax = axs[ax0]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(res.x, -res.M, "r")
ax.grid()
ax.set_ylabel("Bending Moment (kNm)")
ax = axs[ax0 + 1]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(res.x, res.V, "r")
ax.grid()
ax.set_ylabel("Shear Force (kN)")
ax.set_xlabel("Distance along beam (m)")
[docs] def plot_envelopes(self, env: Envelopes):
"""
Plots the envelopes of load effects from a vehicle traverse analysis
Parameters
----------
env : Envelopes
An `pycba.Envelopes` object containing the results of a moving load
analysis.
Returns
-------
None
"""
L = self.ba.beam.length
x = env.x
nreactions = env.nsup
fig = plt.figure(constrained_layout=True, figsize=(10, 4))
subfigs = fig.subfigures(1, 2, wspace=0.07)
# Shear and moment in left panel
subfigs[0].suptitle("Stress Resultants")
axsLeft = subfigs[0].subplots(2, 1, sharex=True)
ax = axsLeft[0]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(x, env.Mmax, "r")
ax.plot(x, env.Mmin, "b")
ax.invert_yaxis()
ax.grid()
ax.set_ylabel("Bending Moment (kNm)")
ax = axsLeft[1]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(x, env.Vmax, "r")
ax.plot(x, env.Vmin, "b")
ax.grid()
ax.set_ylabel("Shear Force (kN)")
ax.set_xlabel("Distance along beam (m)")
# Reactions in right panel
subfigs[1].suptitle("Support Reactions")
# Check if consistent envelope
if len(self.pos) == env.Rmax.shape[1]:
axsRight = subfigs[1].subplots(nreactions, 1, sharex=True)
for i, ax in enumerate(axsRight):
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(self.pos, env.Rmax[i, :], "r")
ax.plot(self.pos, env.Rmin[i, :], "b")
ax.grid()
ax.set_ylabel(f"Reaction {i+1} (kN/kNm)")
axsRight[-1].set_xlabel("Position of Front Axle (m)")
else: # Otherwise envelope of envelopes
axsRight = subfigs[1].subplots(2, 1, sharex=True)
for i, (ax, le, col) in enumerate(
zip(axsRight, ["max", "min"], ["r", "b"])
):
r = eval(f"env.R{le}val") # kinda yuk!
ax.bar(np.arange(env.nsup), r, color=col)
ax.set_xticks(np.arange(env.nsup))
ax.set_xticklabels([f"R{i+1}" for i in range(env.nsup)])
ax.set_ylabel(f"Reactions [{le}]")
ax.grid()
axsRight[1].set_xlabel("Reaction ID")
[docs] def plot_ratios(self, env_ratios: Dict[str, np.ndarray]):
"""
Plots the output of :meth:`pycba.bridge.BridgeAnalysis.envelopes_ratios`.
Parameters
----------
env_ratios : Dict[str,np.ndarray]
The dictionary of envelopes ratios.
Raises
------
ValueError
Inconsistency in the dictionary entries.
Returns
-------
None.
"""
# Set of keys we want to confirm are present
check_keys = set(
["x", "Mmax", "Mmin", "Vmax", "Vmin", "nsup", "Rmax0", "Rmin0"]
)
if not check_keys.issubset(env_ratios.keys()):
raise ValueError(
"Dictionary argument does not contain sufficient ratios information"
)
x = env_ratios["x"]
L = x[-1]
nsup = env_ratios["nsup"]
fig = plt.figure(constrained_layout=True, figsize=(10, 4))
subfigs = fig.subfigures(1, 2, wspace=0.07)
# Shear and moment in left panel
subfigs[0].suptitle("Stress Resultants")
axsLeft = subfigs[0].subplots(2, 1, sharex=True)
ax = axsLeft[0]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(x, env_ratios["Mmax"], "r")
ax.plot(x, env_ratios["Mmin"], "b")
ax.grid()
ax.set_ylabel("Bending Moment Ratio")
ax = axsLeft[1]
ax.plot([0, L], [0, 0], "k", lw=2)
ax.plot(x, env_ratios["Vmax"], "r")
ax.plot(x, env_ratios["Vmin"], "b")
ax.grid()
ax.set_ylabel("Shear Force Ratio")
ax.set_xlabel("Distance along beam (m)")
# Reactions in right panel
subfigs[1].suptitle("Support Reactions")
axsRight = subfigs[1].subplots(2, 1, sharex=True)
for i, (ax, le, col) in enumerate(zip(axsRight, ["max", "min"], ["r", "b"])):
r = np.array([env_ratios[f"R{le}{i}"] for i in range(nsup)])
ax.bar(np.arange(nsup), r, color=col)
ax.set_xticks(np.arange(nsup))
ax.set_xticklabels([f"R{i+1}" for i in range(nsup)])
ax.set_ylabel(f"Reaction Ratio [{le}]")
ax.grid()
axsRight[1].set_xlabel("Reaction ID")
def _check_objects(self):
"""
Check that suitable objects are defined before an analysis is run.
Parameters
----------
None
Returns
-------
None
"""
if not self.ba:
raise ValueError("A bridge must be defined in advance")
if not self.veh:
raise ValueError("A vehicle must be defined in advance")