Robustness audit: loadtracer 0.5.1#
Status: Fixes implemented in 0.6.0
Branch: refactor/consistency
Author: Connor Ferster
Date: 2026-09-18
1. Question#
A load rundown often breaks when the drawings vary even slightly. This audit answers:
Which inputs crash loadtracer, and why?
Which assumptions about the data are built into the code?
What has to change to make it robust?
It also looks for the opposite problem: inputs that don’t crash but give a wrong answer. Those turned out to be the more serious findings.
2. Method#
Fixture sweep. Every PDF in
../papermodels/tests/test_datawas built into aGeometryGraphwith papermodels 0.21 (JoistArrayModel, spacing 1.0), then run throughLoadTracer. The occupancies were synthetic:{"D": 1.0, "L": 2.5}for each occupancy found in the drawing.Shape survey. The
LoadedElement.model()output of every fixture was checked for:list-valued point load locations
[]extentsloads outside the member
zero-length loads
beams with fewer than two supports
Probes. Each suspected failure was reproduced against the 0.5.1 code with hand-built papermodel dicts (
tests/builders.py).Load conservation. For each load case, the total occupancy load applied to the graph has to arrive at the members at the bottom of the graph (see
tests/conservation.py). “Doesn’t crash” is too weak a test for a load takedown. This invariant is what found F8 and F13.
Every finding is pinned by a test in tests/test_sensitivity.py (unit level)
or tests/test_integration_papermodels.py (fixtures).
3. Results at a glance#
Fixture |
0.5.1 |
0.6.0 |
Load conserved (0.6.0) |
|---|---|---|---|
collector_extents.pdf |
|
runs |
yes (no loads drawn) |
collector_extents_walls.pdf |
runs |
runs |
yes |
frame_collectors_transfers.pdf |
|
runs |
yes |
geometry_testing.pdf |
|
runs |
yes (no loads drawn) |
horiz_extents.pdf |
runs |
runs |
yes |
intersections.pdf |
runs |
runs |
yes |
many_correspondents.pdf |
|
runs |
+7.3%: walls on overlapping supports (F13, reported) |
resi_dormers.pdf |
runs |
runs |
yes |
sketch_to_scale.pdf |
|
runs |
yes |
three-pages-on-one.pdf |
papermodels |
same |
n/a (upstream, U3) |
trib-areas-basic.pdf |
runs |
runs |
yes |
5 of the 10 fixtures that papermodels can build crashed in loadtracer 0.5.1.
In 0.6.0 all 10 run. Load is conserved exactly (to 1e-6) on 9 of them. On the
10th, the extra load comes only from walls whose supports overlap, and each
one is reported in LoadTracer.issues.
4. Findings#
“Silent” means that 0.5.1 returned a result without raising an error, and that result was wrong.
# |
Input |
0.5.1 symptom |
Root cause (0.5.1) |
Fix (0.6.0) |
|---|---|---|---|---|
F1 |
Beam with 0 supports, or 1 support |
0 supports: |
|
|
F2 |
Loads outside [0, L], or with zero length. Common: joist trib areas project 0.06–0.32 past the joist ends, and one fixture ( |
Distributed load 0.5 past each end: silent, 1.0 of 11.0 lost. Point load 0.2 past the end: silent, reactions |
Pynite ignores the part of a load outside the member. |
|
F3 |
Any exception while analysing a member |
|
Bare |
Error hierarchy in |
F4 |
A member with no load. The load area doesn’t reach it, or it’s a drawn member that isn’t loaded yet |
|
The member has no load cases, so Pynite has no combinations, so |
Reactions are keyed by support index and always list every support ( |
F5 |
A support with zero reaction (for example, a point load directly over the middle of three supports) |
Silent: the reaction indexes shift. The middle support’s reaction was reported as index 0 and applied to the member under the first support |
|
|
F6 |
Reaction missing for |
|
|
|
F7 |
Any transfer load |
Silent: each transfer load appears twice in |
|
The load lists are rebuilt from the deep copy, and each load appears exactly once. |
F8 |
A wall with both point loads and distributed loads |
Silent: the distributed part of the wall’s reaction never reaches the member below |
|
Each load keeps its own |
F9 |
Walls stacked on walls |
Silent: the load changes sign at every wall. A +5 load into |
Walls report reactions with the sign of the load, but frames and columns report the force on the support. loadbearing-wall 0.3.0 (the version in the 0.5.1 lock) accepts |
loadbearing-wall 0.5.0 applies the flag. loadtracer now requires |
F10 |
A wall over more than one member, or over a shorter member |
Silent: the whole wall reaction was applied to each member under it (+22.7% on |
|
|
F11 |
No |
|
|
Removed. Heights and elevations are no longer inputs; passing them gives a |
F12 |
Structural problems in the papermodel: unknown orientation, missing length, list-valued locations, |
|
No validation of the input. Model selection at |
|
F13 |
A wall linked to members that overlap under it |
Double counting (the remaining +7.3% on |
How the load divides between two supports under the same stretch of wall depends on their stiffness, which the graph doesn’t carry. |
Detected and reported, not resolved. The overlap is applied to both members and recorded in |
F14 |
Excluding a joist array by its parent tag ( |
Silent: the joists ( |
|
|
F15 |
A column under a wall (a distributed load on a point column) |
Silent: the load was dropped |
|
Distributed loads are added to the column as their resultants. |
5. Assumptions that remain (documented, not changed)#
Reaction order.
transfer_reaction_indexrefers to the source member’s supports sorted by location, which is how papermodels orders them. Supports that coincide are kept, so the indexes still line up, but only the first is modelled.Support fixity. The first support is a pin and the others are rollers, unless a support dict has a
fixity("pin","roller","fixed").Stiffness and continuity. Beams use a unit, uniform EI, and a beam over more than two supports is continuous. Loads are distributed as a prismatic continuous member would distribute them, which can differ from simple-span hand calculations. A heavily loaded long span can lift the end of a short adjacent span:
FB3.1insketch_to_scale.pdf(spans 13.4 + 4.9) puts 59.6 of uplift intoCT3.2in the L case.Walls. A wall’s reaction maps onto the member below in the same direction, from source extents to target extents. Both are measured from each member’s positive-x end, which is the papermodels convention. Walls use
minimum_point_spreadto spread point loads.Columns are concentric: point load locations are ignored.
Gravity only. The
inplaneandout_of_planemappings are wired through but no fixture or test uses them.Pressures. Occupancy loads are the pressure × the magnitudes from papermodels (trib widths). The
applied_area_ratioscaling for linear-reaction collectors was not audited.
6. Upstream recommendations#
# |
Project |
Recommendation |
|---|---|---|
U1 |
papermodels |
|
U2 |
papermodels |
A wall can be linked to members that overlap under it (F13). The graph should link each part of a wall to one support, or carry enough information to divide the load between them. |
U3 |
papermodels |
|
U4 |
papermodels |
Occupancy loads on joists commonly extend 0.06–0.32 past the joist ends (the trib band is wider than the joist line). loadtracer moves this load to the ends; confirm whether it’s intended. |
U5 |
papermodels |
Point transfer loads from joists carry no trib extents. |
U6 |
loadbearing_wall |
Fixed in 0.5.0: |
U7 |
pynite_tools |
|
U8 |
PyniteFEA |
Fixed in 3.x: 1.6.2 (the version in the 0.5.1 lock) imported |
U9 |
papermodels |
|
U10 |
papermodels |
Fixed on papermodels |
U11 |
papermodels |
papermodels’ |
7. Behaviour changes in 0.6.0#
Dependencies:
loadbearing-wall>=0.5.0. The lock is on PyniteFEA 3.2.0 and pynite-tools 0.7.0 (it was 1.6.2 / 0.6.7). The 0.5.1 symptoms in §4 were observed on the old lock; the 0.6.0 results in §3 are on the new one.LoadTracer(elevations=…, vertical_element_heights=…)is deprecated: the values are ignored, with a warning.pm_processing.pyis removed.Reactions are stored as
{support_index: {dir: {case: value}}}rather than{node_name: …}. Every support is included, and all six directions are listed for frames. Values are floats, where before they were rounded to nearby integers.LoadTracer.issuesrecords every input that was corrected; the rundown warns once (LoadTracerWarning) when there are any.LoadTracer.errorsrecords members that failed underon_error="collect".expand_occupancy_loads/expand_reaction_loadsno longer take null-load arguments, andLoadTracer.null_point_load/null_dist_loadare removed.Only transfer loads from excluded members are dropped. Loads whose source reactions are ignored stay in
expanded_pmwithout an expansion.