Skip to content

Commit 94dbf4b

Browse files
ccapraniclaude
andcommitted
Add Envelopes.sum() for element-wise envelope addition
Enables superposition of load effects from different sources, e.g. a patterned UDL envelope with a moving vehicle envelope. Based on PR #93 by @RoccoRaimo. Closes #92 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09ef145 commit 94dbf4b

3 files changed

Lines changed: 135 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
### Features
6+
- 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.

src/pycba/results.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,52 @@ def augment(self, env: Envelopes):
370370
self.Rmax = np.zeros((self.nsup, self.nres))
371371
self.Rmin = np.zeros((self.nsup, self.nres))
372372

373+
def sum(self, env: Envelopes):
374+
"""
375+
Adds another compatible set of envelopes to this one element-wise. This is
376+
useful for superimposing load effects from different sources, e.g. a patterned
377+
UDL envelope with a moving vehicle envelope.
378+
379+
All envelopes must be for the same beam geometry.
380+
381+
If the envelopes have a different number of analyses (due to differing vehicle
382+
lengths, for example), then only the reaction extremes are retained, and not
383+
the entire reaction history.
384+
385+
Parameters
386+
----------
387+
env : Envelopes
388+
A compatible :class:`pycba.results.Envelopes` object.
389+
390+
Raises
391+
------
392+
ValueError
393+
All envelopes must be for the same beam geometry.
394+
395+
Returns
396+
-------
397+
None.
398+
"""
399+
400+
if self.npts != env.npts or self.nsup != env.nsup:
401+
raise ValueError("Cannot sum with an inconsistent envelope")
402+
self.Vmax = np.add(self.Vmax, env.Vmax)
403+
self.Vmin = np.add(self.Vmin, env.Vmin)
404+
405+
self.Mmax = np.add(self.Mmax, env.Mmax)
406+
self.Mmin = np.add(self.Mmin, env.Mmin)
407+
408+
self.Rmaxval = np.add(self.Rmaxval, env.Rmaxval)
409+
self.Rminval = np.add(self.Rminval, env.Rminval)
410+
411+
if self.nres == env.nres:
412+
self.Rmax = np.add(self.Rmax, env.Rmax)
413+
self.Rmin = np.add(self.Rmin, env.Rmin)
414+
else:
415+
# Ensure no misleading results returned
416+
self.Rmax = np.zeros((self.nsup, self.nres))
417+
self.Rmin = np.zeros((self.nsup, self.nres))
418+
373419
def plot(self, each=False, **kwargs):
374420
"""
375421
Plots the envelopes of bending and shear.

tests/test_results.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
import numpy as np
3+
import pycba as cba
4+
5+
6+
def test_sum_compatible_envelopes():
7+
"""Test summing two compatible envelopes from load patterning and vehicle analysis"""
8+
L = [30, 30, 30]
9+
EI = 30 * 10e9 * 1e-6
10+
R = [-1, 0, -1, 0, -1, 0, -1, 0]
11+
12+
bridge = cba.BeamAnalysis(L, EI, R)
13+
14+
# Dead + permanent loads (factored)
15+
LMd = [
16+
[1, 1, 12.5 * 1.35 + 50 * 1.5, 0, 0],
17+
[2, 1, 12.5 * 1.35 + 50 * 1.5, 0, 0],
18+
[3, 1, 12.5 * 1.35 + 50 * 1.5, 0, 0],
19+
]
20+
21+
# Variable UDL loads
22+
LMq = [
23+
[1, 1, 13.5, 0, 0],
24+
[2, 1, 13.5, 0, 0],
25+
[3, 1, 13.5, 0, 0],
26+
]
27+
28+
# Create load pattern envelope
29+
loadpattern = cba.LoadPattern(bridge)
30+
loadpattern.set_dead_loads(LMd, 1.0, 1.0)
31+
loadpattern.set_live_loads(LMq, 1.50, 0)
32+
env_udl = loadpattern.analyze()
33+
34+
# Create vehicle envelope
35+
vehicle = cba.Vehicle([1.20], [600, 600])
36+
bridge_analysis = cba.BridgeAnalysis(bridge, vehicle)
37+
env_veh = bridge_analysis.run_vehicle(0.5)
38+
39+
# Sum the envelopes
40+
env_sum = cba.Envelopes.zero_like(env_udl)
41+
env_sum.sum(env_udl)
42+
env_sum.sum(env_veh)
43+
44+
# Verify: the summed envelope equals the element-wise sum of the individual ones
45+
assert env_sum.Mmax == pytest.approx(env_udl.Mmax + env_veh.Mmax)
46+
assert env_sum.Mmin == pytest.approx(env_udl.Mmin + env_veh.Mmin)
47+
assert env_sum.Vmax == pytest.approx(env_udl.Vmax + env_veh.Vmax)
48+
assert env_sum.Vmin == pytest.approx(env_udl.Vmin + env_veh.Vmin)
49+
assert env_sum.Rmaxval == pytest.approx(env_udl.Rmaxval + env_veh.Rmaxval)
50+
assert env_sum.Rminval == pytest.approx(env_udl.Rminval + env_veh.Rminval)
51+
52+
53+
def test_sum_incompatible_envelopes():
54+
"""Test that summing envelopes from different beam geometries raises ValueError"""
55+
L1 = [30, 30, 30]
56+
EI1 = 30 * 10e9 * 1e-6
57+
R1 = [-1, 0, -1, 0, -1, 0, -1, 0]
58+
bridge1 = cba.BeamAnalysis(L1, EI1, R1)
59+
60+
L2 = [20, 20]
61+
EI2 = 30 * 10e9 * 1e-6
62+
R2 = [-1, 0, -1, 0, -1, 0]
63+
bridge2 = cba.BeamAnalysis(L2, EI2, R2)
64+
65+
LMd1 = [[1, 1, 12.5, 0, 0], [2, 1, 12.5, 0, 0], [3, 1, 12.5, 0, 0]]
66+
LMq1 = [[1, 1, 13.5, 0, 0], [2, 1, 13.5, 0, 0], [3, 1, 13.5, 0, 0]]
67+
68+
LMd2 = [[1, 1, 12.5, 0, 0], [2, 1, 12.5, 0, 0]]
69+
LMq2 = [[1, 1, 13.5, 0, 0], [2, 1, 13.5, 0, 0]]
70+
71+
lp1 = cba.LoadPattern(bridge1)
72+
lp1.set_dead_loads(LMd1, 1.35, 1.0)
73+
lp1.set_live_loads(LMq1, 1.50, 0)
74+
env1 = lp1.analyze()
75+
76+
lp2 = cba.LoadPattern(bridge2)
77+
lp2.set_dead_loads(LMd2, 1.35, 1.0)
78+
lp2.set_live_loads(LMq2, 1.50, 0)
79+
env2 = lp2.analyze()
80+
81+
env_sum = cba.Envelopes.zero_like(env1)
82+
with pytest.raises(ValueError, match="Cannot sum with an inconsistent envelope"):
83+
env_sum.sum(env2)

0 commit comments

Comments
 (0)