Skip to content

Commit f23ab4a

Browse files
ccapraniclaude
andauthored
Add coincident load effects to Envelopes (#128)
* Add coincident load effects to Envelopes (#122) Track the co-existing value of the other effect (V or M) at the truck position that caused each envelope extreme. New attributes: Vco_Mmax, Vco_Mmin, Mco_Vmax, Mco_Vmin. Also adds coincident values to critical_values() output and updates zero_like()/augment(). Closes #122 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add documentation for coincident load effects Update Envelopes class docstring with all attributes including new coincident arrays. Update critical_values() and augment() docstrings. Add coincident effects demonstration to bridge analysis notebook. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add CHANGELOG entry for coincident load effects (#122) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2546790 commit f23ab4a

File tree

5 files changed

+272
-41
lines changed

5 files changed

+272
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
### Features
6+
- Add coincident load effects to `Envelopes` (#122). New attributes `Vco_Mmax`, `Vco_Mmin`, `Mco_Vmax`, `Mco_Vmin` track the co-existing value of the other effect (V or M) at the truck position that caused each envelope extreme. `critical_values()` output now includes `"Vco"` and `"Mco"` keys. Coincident values are preserved through `augment()` and `zero_like()`.
67
- Add trapezoidal (linearly varying) distributed load type (#101). Load type 5 supports both full-span `[span, 5, w1, w2]` and partial coverage `[span, 5, w1, w2, a, c]` where w1/w2 are intensities at positions a and a+c respectively. Also adds `BeamAnalysis.add_trap()` convenience method with optional `a` and `c` parameters.
78
- Add `pos_start` and `pos_end` parameters to `BridgeAnalysis.run_vehicle()` to restrict the vehicle traverse range (#53). Useful for transverse deck analyses where the vehicle is confined to specific lanes.
89
- Add `Envelopes.sum()` method for element-wise addition of compatible envelopes (#92). This enables superimposing load effects from different sources, e.g. a patterned UDL envelope with a moving vehicle envelope.

docs/source/notebooks/bridge.ipynb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,34 @@
303303
"bridge_analysis.static_vehicle(pos, True);"
304304
]
305305
},
306+
{
307+
"cell_type": "markdown",
308+
"id": "92ycitwmq6m",
309+
"source": "### Coincident Load Effects\n\nIn bridge design it is important to know not just the extreme value of one load effect, but also the value of the *other* effect at the same vehicle position. For example, when checking combined stresses at a section, we need the shear force that co-exists with the maximum bending moment. The `Envelopes` object provides these as the `Vco_Mmax`, `Vco_Mmin`, `Mco_Vmax`, and `Mco_Vmin` attributes.\n\nThe `critical_values` dictionary also includes coincident values via the `\"Vco\"` key (for moment entries) and `\"Mco\"` key (for shear entries):",
310+
"metadata": {}
311+
},
312+
{
313+
"cell_type": "code",
314+
"id": "592cinf69eo",
315+
"source": "Vco = cvals[\"Mmax\"][\"Vco\"]\nprint(f\"At {at:.2f} m, Mmax = {val:.1f} kNm with coincident V = {Vco:.1f} kN\")",
316+
"metadata": {},
317+
"execution_count": null,
318+
"outputs": []
319+
},
320+
{
321+
"cell_type": "markdown",
322+
"id": "d5ang668mz",
323+
"source": "The full coincident arrays are available on the envelope for plotting or further analysis. For example, to plot the coincident shear alongside the moment envelope:",
324+
"metadata": {}
325+
},
326+
{
327+
"cell_type": "code",
328+
"id": "hfm883qskp",
329+
"source": "fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(9, 6))\n\nax1.plot(env.x, env.Mmax, \"r\", label=\"Mmax\")\nax1.plot(env.x, env.Mmin, \"b\", label=\"Mmin\")\nax1.plot(env.x, env.Mco_Vmax, \"r--\", alpha=0.5, label=\"M co. Vmax\")\nax1.plot(env.x, env.Mco_Vmin, \"b--\", alpha=0.5, label=\"M co. Vmin\")\nax1.invert_yaxis()\nax1.grid()\nax1.legend()\nax1.set_ylabel(\"Bending Moment (kNm)\")\n\nax2.plot(env.x, env.Vmax, \"r\", label=\"Vmax\")\nax2.plot(env.x, env.Vmin, \"b\", label=\"Vmin\")\nax2.plot(env.x, env.Vco_Mmax, \"r--\", alpha=0.5, label=\"V co. Mmax\")\nax2.plot(env.x, env.Vco_Mmin, \"b--\", alpha=0.5, label=\"V co. Mmin\")\nax2.grid()\nax2.legend()\nax2.set_ylabel(\"Shear Force (kN)\")\nax2.set_xlabel(\"Distance along beam (m)\");",
330+
"metadata": {},
331+
"execution_count": null,
332+
"outputs": []
333+
},
306334
{
307335
"cell_type": "markdown",
308336
"id": "71f1bd0f-1a7e-442f-9bcd-d6aa5044e07b",
@@ -1429,4 +1457,4 @@
14291457
},
14301458
"nbformat": 4,
14311459
"nbformat_minor": 5
1432-
}
1460+
}

src/pycba/bridge.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,11 @@ def critical_values(
286286
) -> Dict[str, Dict[str, Union[float, np.ndarray]]]:
287287
"""
288288
From the envelopes output, returns the extreme values, their locations,
289-
and the position of the vehicle for each in a dictionary of dictionaries
289+
and the position of the vehicle for each in a dictionary of dictionaries.
290+
291+
Each moment entry (``Mmax``, ``Mmin``) also contains a ``"Vco"`` key with
292+
the coincident shear at the critical location. Each shear entry (``Vmax``,
293+
``Vmin``) contains a ``"Mco"`` key with the coincident moment.
290294
291295
Parameters
292296
----------
@@ -304,7 +308,8 @@ def critical_values(
304308
-------
305309
crit_values : Dict[str, Dict[str, Union[float, np.ndarray]]]
306310
A dictionary of dictionaries containing the critical values (i.e. extremes)
307-
of each of the load effects, both maximum and minimum.
311+
of each of the load effects, both maximum and minimum, along with
312+
coincident values of the other effect.
308313
"""
309314

310315
crit_values = {}
@@ -346,21 +351,25 @@ def critical_values(
346351
"val": Mmax,
347352
"at": env.x[env.Mmax.argmax()],
348353
"pos": [self.pos[i] for i in indx["Mmax"]],
354+
"Vco": env.Vco_Mmax[env.Mmax.argmax()],
349355
}
350356
crit_values["Mmin"] = {
351357
"val": Mmin,
352358
"at": env.x[env.Mmin.argmin()],
353359
"pos": [self.pos[i] for i in indx["Mmin"]],
360+
"Vco": env.Vco_Mmin[env.Mmin.argmin()],
354361
}
355362
crit_values["Vmax"] = {
356363
"val": Vmax,
357364
"at": env.x[env.Vmax.argmax()],
358365
"pos": [self.pos[i] for i in indx["Vmax"]],
366+
"Mco": env.Mco_Vmax[env.Vmax.argmax()],
359367
}
360368
crit_values["Vmin"] = {
361369
"val": Vmin,
362370
"at": env.x[env.Vmin.argmin()],
363371
"pos": [self.pos[i] for i in indx["Vmin"]],
372+
"Mco": env.Mco_Vmin[env.Vmin.argmin()],
364373
}
365374
crit_values["nsup"] = env.nsup
366375
for i in range(env.nsup):

src/pycba/results.py

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,35 @@ def _member_values(
188188

189189
class Envelopes:
190190
"""
191-
Envelopes load effects from a vector of BeamResults
191+
Envelopes load effects from a vector of BeamResults.
192+
193+
Attributes
194+
----------
195+
x : np.ndarray
196+
Coordinates along the beam.
197+
Vmax, Vmin : np.ndarray
198+
Maximum and minimum shear force envelopes.
199+
Mmax, Mmin : np.ndarray
200+
Maximum and minimum bending moment envelopes.
201+
Vco_Mmax : np.ndarray
202+
Shear coincident with the moment maximum at each point.
203+
Vco_Mmin : np.ndarray
204+
Shear coincident with the moment minimum at each point.
205+
Mco_Vmax : np.ndarray
206+
Moment coincident with the shear maximum at each point.
207+
Mco_Vmin : np.ndarray
208+
Moment coincident with the shear minimum at each point.
209+
Rmax, Rmin : np.ndarray
210+
Reaction history matrices (nsup x nres).
211+
Rmaxval, Rminval : np.ndarray
212+
Maximum and minimum reaction per support.
192213
"""
193214

194215
def __init__(self, vResults: List[MemberResults]):
195216
"""
196217
Constructs the envelope of each load effect given a vector of results for
197-
the beam.
218+
the beam. Also tracks coincident (co-existing) load effects: the value of
219+
the other effect (V or M) at the analysis that caused each envelope extreme.
198220
199221
Parameters
200222
----------
@@ -212,57 +234,67 @@ def __init__(self, vResults: List[MemberResults]):
212234
self.nres = len(vResults)
213235
self.nsup = len(vResults[0].R)
214236

215-
self.Vmax, self.Vmin = self._get_envelope_V()
216-
self.Mmax, self.Mmin = self._get_envelope_M()
237+
(
238+
self.Vmax, self.Vmin,
239+
self.Mmax, self.Mmin,
240+
self.Vco_Mmax, self.Vco_Mmin,
241+
self.Mco_Vmax, self.Mco_Vmin,
242+
) = self._get_envelopes_VM()
217243
self.Rmax, self.Rmin = self._get_envelope_R()
218244
self.Rmaxval = self.Rmax.max(axis=1)
219245
self.Rminval = self.Rmin.min(axis=1)
220246

221-
def _get_envelope_V(self) -> Tuple[np.ndarray, np.ndarray]:
247+
def _get_envelopes_VM(self) -> Tuple[
248+
np.ndarray, np.ndarray, np.ndarray, np.ndarray,
249+
np.ndarray, np.ndarray, np.ndarray, np.ndarray,
250+
]:
222251
"""
223-
Creates the envelopes for shear.
224-
225-
Parameters
226-
----------
227-
None
252+
Creates the envelopes for shear and moment, and tracks the coincident
253+
(co-existing) value of the other effect at the truck position that
254+
caused each extreme.
228255
229256
Returns
230257
-------
231-
Vmax : np.ndarray
232-
The vector of enveloped maximum values.
233-
Vmin : np.ndarray
234-
The vector of enveloped minimum values.
258+
Vmax, Vmin : np.ndarray
259+
Enveloped maximum and minimum shear.
260+
Mmax, Mmin : np.ndarray
261+
Enveloped maximum and minimum moment.
262+
Vco_Mmax, Vco_Mmin : np.ndarray
263+
Shear coincident with the moment extreme at each point.
264+
Mco_Vmax, Mco_Vmin : np.ndarray
265+
Moment coincident with the shear extreme at each point.
235266
"""
236267
Vmax = np.zeros(self.npts)
237268
Vmin = np.zeros(self.npts)
269+
Mmax = np.zeros(self.npts)
270+
Mmin = np.zeros(self.npts)
271+
272+
Vco_Mmax = np.zeros(self.npts)
273+
Vco_Mmin = np.zeros(self.npts)
274+
Mco_Vmax = np.zeros(self.npts)
275+
Mco_Vmin = np.zeros(self.npts)
238276

239277
for res in self.vResults:
240-
Vmax = np.maximum(Vmax, res.results.V)
241-
Vmin = np.minimum(Vmin, res.results.V)
242-
return (Vmax, Vmin)
278+
V = res.results.V
279+
M = res.results.M
243280

244-
def _get_envelope_M(self) -> Tuple[np.ndarray, np.ndarray]:
245-
"""
246-
Creates the envelopes for moment.
281+
mask = M > Mmax
282+
Vco_Mmax = np.where(mask, V, Vco_Mmax)
283+
Mmax = np.where(mask, M, Mmax)
247284

248-
Parameters
249-
----------
250-
None
285+
mask = M < Mmin
286+
Vco_Mmin = np.where(mask, V, Vco_Mmin)
287+
Mmin = np.where(mask, M, Mmin)
251288

252-
Returns
253-
-------
254-
Mmax : np.ndarray
255-
The vector of enveloped maximum values.
256-
Mmin : np.ndarray
257-
The vector of enveloped minimum values.
258-
"""
259-
Mmax = np.zeros(self.npts)
260-
Mmin = np.zeros(self.npts)
289+
mask = V > Vmax
290+
Mco_Vmax = np.where(mask, M, Mco_Vmax)
291+
Vmax = np.where(mask, V, Vmax)
261292

262-
for res in self.vResults:
263-
Mmax = np.maximum(Mmax, res.results.M)
264-
Mmin = np.minimum(Mmin, res.results.M)
265-
return (Mmax, Mmin)
293+
mask = V < Vmin
294+
Mco_Vmin = np.where(mask, M, Mco_Vmin)
295+
Vmin = np.where(mask, V, Vmin)
296+
297+
return (Vmax, Vmin, Mmax, Mmin, Vco_Mmax, Vco_Mmin, Mco_Vmax, Mco_Vmin)
266298

267299
def _get_envelope_R(self) -> Tuple[np.ndarray, np.ndarray]:
268300
"""
@@ -318,6 +350,10 @@ def zero_like(cls, env: Envelopes) -> Envelopes:
318350
zero_env.Vmin = np.zeros(env.npts)
319351
zero_env.Mmax = np.zeros(env.npts)
320352
zero_env.Mmin = np.zeros(env.npts)
353+
zero_env.Vco_Mmax = np.zeros(env.npts)
354+
zero_env.Vco_Mmin = np.zeros(env.npts)
355+
zero_env.Mco_Vmax = np.zeros(env.npts)
356+
zero_env.Mco_Vmin = np.zeros(env.npts)
321357
for i in range(env.nsup):
322358
zero_env.Rmax[i] = np.zeros(env.nres)
323359
zero_env.Rmin[i] = np.zeros(env.nres)
@@ -328,7 +364,8 @@ def zero_like(cls, env: Envelopes) -> Envelopes:
328364
def augment(self, env: Envelopes):
329365
"""
330366
Augments this set of envelopes with another compatible set, making this the
331-
envelopes of the two sets of envelopes.
367+
envelopes of the two sets of envelopes. Coincident values are updated to
368+
match whichever envelope governs at each point.
332369
333370
All envelopes must be from the same :class:`pycba.bridge.BridgeAnalysis` object.
334371
@@ -353,12 +390,24 @@ def augment(self, env: Envelopes):
353390

354391
if self.npts != env.npts or self.nsup != env.nsup:
355392
raise ValueError("Cannot augment with an inconsistent envelope")
393+
394+
# Track where envelope values will change BEFORE updating them
395+
vmax_update = env.Vmax > self.Vmax
396+
vmin_update = env.Vmin < self.Vmin
397+
mmax_update = env.Mmax > self.Mmax
398+
mmin_update = env.Mmin < self.Mmin
399+
356400
self.Vmax = np.maximum(self.Vmax, env.Vmax)
357401
self.Vmin = np.minimum(self.Vmin, env.Vmin)
358-
359402
self.Mmax = np.maximum(self.Mmax, env.Mmax)
360403
self.Mmin = np.minimum(self.Mmin, env.Mmin)
361404

405+
# Update coincident values where the envelope changed
406+
self.Vco_Mmax = np.where(mmax_update, env.Vco_Mmax, self.Vco_Mmax)
407+
self.Vco_Mmin = np.where(mmin_update, env.Vco_Mmin, self.Vco_Mmin)
408+
self.Mco_Vmax = np.where(vmax_update, env.Mco_Vmax, self.Mco_Vmax)
409+
self.Mco_Vmin = np.where(vmin_update, env.Mco_Vmin, self.Mco_Vmin)
410+
362411
self.Rmaxval = np.maximum(self.Rmaxval, env.Rmaxval)
363412
self.Rminval = np.minimum(self.Rminval, env.Rminval)
364413

0 commit comments

Comments
 (0)