Skip to content

Commit 1d4dea9

Browse files
ccapraniclaude
andauthored
Fix InfluenceLines.get_il() IndexError for arbitrary poi values (#124)
get_il() threw IndexError when the point of interest did not land exactly on the result grid. Root cause: an overly tight floating-point tolerance (dx * 1e-6) and zero-valued padding indices at span boundaries that argmin could select. The fix uses per-member result lookup, skipping padding indices. Closes #89 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09ef145 commit 1d4dea9

File tree

3 files changed

+44
-7
lines changed

3 files changed

+44
-7
lines changed

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+
### Bug Fixes
6+
- Fix `InfluenceLines.get_il()` raising `IndexError` when the point of interest does not fall exactly on the result grid (#89). This occurred with longer spans where the default 100-point discretization produced a grid spacing that could not match arbitrary poi values. The fix uses per-member result lookup, avoiding both the overly tight floating-point tolerance and the zero-valued padding indices at span boundaries.

src/pycba/inf_lines.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,19 @@ def get_il(self, poi: float, load_effect: str) -> (np.ndarray, np.ndarray):
132132
self.ba.beam.no_fixed_restraints
133133
)
134134

135-
# idx = np.abs(x - poi).argmin()
135+
# Find the span containing the poi and the closest non-padding index
136+
# within that span. Padding indices (first/last per span) have M/V
137+
# forced to zero, so we must avoid them.
138+
ispan, _ = self.ba.beam.get_local_span_coords(poi)
139+
if ispan == -1:
140+
ispan = self.ba.beam.no_spans - 1
141+
span_x = self.vResults[0].vRes[ispan].x
142+
# Skip padding at indices 0 and -1; real data is at 1:-1
143+
idx_in_span = np.abs(span_x[1:-1] - poi).argmin() + 1
136144

137145
if load_effect.upper() == "V":
138-
dx = x[2] - x[1]
139-
idx = np.where(np.abs(x - poi) <= dx * 1e-6)[0][0]
140146
for i, res in enumerate(self.vResults):
141-
eta[i] = res.results.V[idx]
147+
eta[i] = res.vRes[ispan].V[idx_in_span]
142148

143149
elif load_effect.upper() == "R":
144150
#
@@ -182,10 +188,8 @@ def get_il(self, poi: float, load_effect: str) -> (np.ndarray, np.ndarray):
182188
eta[i] = res.R[mt_sup_idx]
183189

184190
else:
185-
dx = x[2] - x[1]
186-
idx = np.where(np.abs(x - poi) <= dx * 1e-6)[0][0]
187191
for i, res in enumerate(self.vResults):
188-
eta[i] = res.results.M[idx]
192+
eta[i] = res.vRes[ispan].M[idx_in_span]
189193

190194
return (np.array(self.pos), eta)
191195

tests/test_inf_lines.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,33 @@ def test_parse_beam_notation():
177177
assert eType == [1, 2, 1, 1]
178178

179179

180+
def test_il_arbitrary_poi():
181+
"""
182+
Regression test for issue #89: influence line plotting fails when the poi
183+
does not fall exactly on the result grid (e.g. longer spans with default
184+
100-point discretization).
185+
"""
186+
beam_string = "E30R30H30R30E"
187+
(L, EI, R, eType) = cba.parse_beam_string(beam_string)
188+
ils = cba.InfluenceLines(L, EI, R, eType)
189+
ils.create_ils(step=0.05)
190+
191+
# poi=40.0 does not land on the 0.3m result grid for 30m spans
192+
(x, y) = ils.get_il(40.0, "M")
193+
assert np.linalg.norm(y) > 0
194+
195+
(x, y) = ils.get_il(40.0, "V")
196+
assert np.linalg.norm(y) > 0
197+
198+
# Also verify at support locations
199+
(x, y) = ils.get_il(30.0, "M")
200+
assert np.linalg.norm(y) > 0
201+
202+
# Moment at hinge should be zero
203+
(x, y) = ils.get_il(60.0, "M")
204+
assert np.linalg.norm(y) == pytest.approx(0.0, abs=1e-10)
205+
206+
180207
def test_discretization():
181208
"""
182209
Confirm that poi rounding to find the closest idx for ILs works

0 commit comments

Comments
 (0)