Skip to content

Commit 7aaa822

Browse files
Merge pull request #6 from StructuralPython/features/point-loads
Features/point loads
2 parents 008d30d + 1ab629e commit 7aaa822

9 files changed

Lines changed: 661 additions & 500 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = [
77
]
88
requires-python = ">=3.11"
99
dependencies = [
10-
"load-distribution>=0.1.6",
10+
"load-distribution>=0.1.7",
1111
"pydantic>=2.0.0",
1212
"safer>=5.1.0",
1313
]

src/loadbearing_wall/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
The wall model is parameterizable and can represent any material
66
"""
77

8-
__version__ = "0.2.1"
8+
__version__ = "0.3.0"
99

1010
from loadbearing_wall.wall_model import LinearWallModel
1111
from loadbearing_wall import *

src/loadbearing_wall/geom_ops.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,90 @@ def apply_spread_angle(
66
wall_height: float,
77
wall_length: float,
88
spread_angle: float,
9-
w0: float,
10-
x0: float,
9+
w0: Optional[float] = None,
10+
x0: Optional[float] = None,
1111
w1: Optional[float] = None,
1212
x1: Optional[float] = None,
13-
) -> dict:
13+
p: Optional[float] = None,
14+
x: Optional[float] = None
15+
) -> tuple[float, float, float, float]:
1416
"""
1517
Returns a dictionary representing the load described by
16-
w0, w1, x0, x1. If only w0 and x0 are provided, the
17-
load is assumed to be a point load.
18+
w0, w1, x0, x1 (if distributed load) or p, x (if point
19+
load).
1820
1921
The total spread cannot be longer than the wall length.
2022
2123
spread_angle is assumed to be in degrees
2224
"""
2325
angle_rads = math.radians(spread_angle)
2426
spread_amount = wall_height * math.tan(angle_rads)
25-
projected_x0 = max(0.0, x0 - spread_amount)
26-
if x1 is None:
27-
projected_x1 = min(wall_length, x0 + spread_amount)
28-
else:
27+
if None not in [w0, w1, x0, x1]:
28+
projected_x0 = max(0.0, x0 - spread_amount)
2929
projected_x1 = min(wall_length, x1 + spread_amount)
30-
projected_length = projected_x1 - projected_x0
31-
if x1 is not None:
3230
original_length = x1 - x0
31+
elif None not in [x, p]:
32+
projected_x0 = max(0.0, x - spread_amount)
33+
projected_x1 = min(wall_length, x + spread_amount)
34+
original_length = 0
3335
else:
34-
original_length = 1
36+
print(f"Weird condition: {locals()=}")
37+
38+
projected_length = projected_x1 - projected_x0
39+
40+
print(f"{projected_length=}")
3541
ratio = original_length / projected_length
36-
projected_w0 = w0 * ratio
37-
if w1 is not None:
42+
43+
if None not in [w0, w1, x0, x1]:
44+
projected_w0 = w0 * ratio
3845
projected_w1 = w1 * ratio
46+
elif None not in [x, p]:
47+
projected_w0 = p / projected_length
48+
projected_w1 = p / projected_length
49+
print(f"{projected_w1=}")
50+
return (
51+
round_to_close_integer(projected_w0),
52+
round_to_close_integer(projected_w1),
53+
round_to_close_integer(projected_x0),
54+
round_to_close_integer(projected_x1),
55+
)
56+
57+
58+
def apply_minimum_width(
59+
magnitude: float,
60+
location: float,
61+
spread_width: float,
62+
wall_length: float,
63+
) -> tuple[float, float, float, float]:
64+
"""
65+
Returns a dictionary representing a distributed load
66+
representing the point load converted to a distributed
67+
load over the 'spread_width' in such a way that the
68+
point load will be distributed an equal amount over
69+
half of the spread_width on each side of point load.
70+
71+
If the point load location is 0 or wall_length, then
72+
the point load will be a distributed load over half
73+
of the spread_width (since there is not room for the
74+
other half).
75+
76+
Load locations between zero/wall_length and half of the
77+
spread_width will be linearly interpolated.
78+
"""
79+
assert spread_width <= wall_length
80+
if location <= spread_width / 2:
81+
projected_x0 = 0
82+
projected_x1 = location + spread_width / 2
83+
elif (wall_length - location) <= spread_width / 2:
84+
projected_x0 = location - spread_width / 2
85+
projected_x1 = wall_length
3986
else:
40-
projected_w1 = w0 * ratio
87+
projected_x0 = location - spread_width / 2
88+
projected_x1 = location + spread_width / 2
89+
90+
projected_x1 = location + spread_width / 2
91+
projected_w0 = projected_w1 = magnitude / (projected_x1 - projected_x0)
92+
print(f"{projected_w0=}")
4193
return (
4294
round_to_close_integer(projected_w0),
4395
round_to_close_integer(projected_w1),

src/loadbearing_wall/linear_reactions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def from_projected_loads(
9797

9898
def extract_reaction_string(
9999
self, xa: float, xb: float, case: str, dir: str
100-
) -> Optional[list[LinearReaction]]:
100+
):
101101
"""
102102
Returns a LinearReactionString representing the linear reactions that
103103
exist between 'xa' and 'xb' extracted from self.

src/loadbearing_wall/wall_model.py

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ class LinearWallModel(BaseModel):
1212
height: float
1313
length: float
1414
vertical_spread_angle: float = 0.0
15+
minimum_point_spread: float = 0.5
1516
distributed_loads: dict = Field(default={})
1617
point_loads: dict = Field(default={})
1718
gravity_dir: str = "z"
1819
inplane_dir: str = "x"
1920
out_of_plane_dir: str = "y"
2021
apply_spread_angle_gravity: bool = True
2122
apply_spread_angle_inplane: bool = True
23+
magnitude_point_key: str = 'p'
2224
magnitude_start_key: str = "w0"
2325
magnitude_end_key: str = "w1"
26+
location_point_key: str = 'x'
2427
location_start_key: str = "x0"
2528
location_end_key: str = "x1"
29+
reverse_reaction_force_direction: bool = True
2630
_projected_loads: Optional[dict] = None
2731
"""
2832
A model of a linear load-bearing wall segment. The segment is assumed to be linear
@@ -34,6 +38,11 @@ class LinearWallModel(BaseModel):
3438
spread through to the base of the wall according to this angle. The angle is
3539
measured as deviation off of the direction of gravity (i.e. 0.0 is the in the
3640
gravity direction and 30.0 is 30 degrees away from vertical)
41+
'minimum_point_spread': For point loads applied without a spread angle, this is minimum width
42+
over which the point load will be spread. Whether this value is realistic or not depends on
43+
your context and the length unit system you are using. In other words, all applied point loads
44+
will be converted to distributed loads spread over this distance unless a vertical spread
45+
angle is applied.
3746
'distributed_loads': A dictionary of loads. Can be set directly or using the .add_dist_load()
3847
methods
3948
'point_loads': A dictionary of loads. Can be set directly or using the .add_point_load()
@@ -44,30 +53,36 @@ class LinearWallModel(BaseModel):
4453
used in the applied loads for applied loads in the inplane direction.
4554
'out_of_plane_dir': A label used for the out_of_plane direction. Must match the direction labels
4655
used in the applied loads for applied loads in the out_of_plane direction.
47-
'magnitude_start_key': The key that will be used internally and in reaction results for the
56+
'magnitude_point_key': The key that will be used internally to describe the magnitude of
57+
point loads.
58+
'magnitude_start_key': The key that will be used internally (and in reaction results) for the
4859
start magnitude
49-
'magnitude_end_key': The key that will be used internally and in reaction results for the
60+
'magnitude_end_key': The key that will be used internally (and in reaction results) for the
5061
end magnitude
51-
'location_start_key': The key that will be used internally and in reaction results for the
62+
'location_key': The key that will be used internally to describe the location of applied
63+
point loads.
64+
'location_start_key': The key that will be used internally (and in reaction results) for the
5265
start location
53-
'location_end_key': The key that will be used internally and in reaction results for the
66+
'location_end_key': The key that will be used internally (and in reaction results) for the
5467
end location
68+
'reverse_reaction_force_direction': If True, the reactions will be represented with a load
69+
of the opposite sign as the input load.
5570
"""
5671

5772
@classmethod
58-
def from_json(self, filepath: str | pathlib.Path):
73+
def from_json(cls, filepath: str | pathlib.Path):
5974
with safer.open(filepath) as file:
6075
json_data = file.read()
61-
return self.model_validate_json(json_data)
76+
return cls.model_validate_json(json_data)
6277

6378
def to_json(self, filepath: str | pathlib.Path, indent=2):
6479
json_data = self.model_dump_json(indent=indent)
6580
with safer.open(filepath, "w") as file:
6681
file.write(json_data)
6782

6883
@classmethod
69-
def from_dict(self, data: dict):
70-
return self.model_validate(data)
84+
def from_dict(cls, data: dict):
85+
return cls.model_validate(data)
7186

7287
def dump_dict(self):
7388
return self.model_dump(mode="json")
@@ -122,8 +137,8 @@ def add_point_load(
122137
self.point_loads[dir].setdefault(case, [])
123138
self.point_loads[dir][case].append(
124139
{
125-
self.magnitude_start_key: magnitude,
126-
self.location_start_key: location,
140+
self.magnitude_point_key: magnitude,
141+
self.location_point_key: location,
127142
}
128143
)
129144

@@ -140,6 +155,8 @@ def spread_loads(self) -> None:
140155
w1 = self.magnitude_end_key
141156
x0 = self.location_start_key
142157
x1 = self.location_end_key
158+
p = self.magnitude_point_key
159+
x = self.location_point_key
143160
for load_dir, load_cases in self.distributed_loads.items():
144161
proj.setdefault(load_dir, {})
145162
should_apply_spread_angle = (
@@ -159,10 +176,10 @@ def spread_loads(self) -> None:
159176
self.height,
160177
self.length,
161178
self.vertical_spread_angle,
162-
dist_load[w0],
163-
dist_load[x0],
164-
dist_load.get(w1),
165-
dist_load.get(x1),
179+
w0=dist_load[w0],
180+
x0=dist_load[x0],
181+
w1=dist_load.get(w1),
182+
x1=dist_load.get(x1),
166183
)
167184
proj[load_dir][load_case].append(
168185
{
@@ -173,6 +190,7 @@ def spread_loads(self) -> None:
173190
}
174191
)
175192
else:
193+
print(f"{dist_load=}")
176194
proj[load_dir][load_case].append(dist_load)
177195

178196
for load_dir, load_cases in self.point_loads.items():
@@ -194,10 +212,8 @@ def spread_loads(self) -> None:
194212
self.height,
195213
self.length,
196214
self.vertical_spread_angle,
197-
point_load[w0],
198-
point_load[x0],
199-
point_load.get(w1),
200-
point_load.get(x1),
215+
p=point_load[p],
216+
x=point_load[x],
201217
)
202218
proj[load_dir][load_case].append(
203219
{
@@ -208,7 +224,21 @@ def spread_loads(self) -> None:
208224
}
209225
)
210226
else:
211-
proj[load_dir][load_case].append(point_load)
227+
print(f"Min spread: {point_load=}")
228+
projected_load = geom.apply_minimum_width(
229+
point_load[p],
230+
point_load[x],
231+
self.minimum_point_spread,
232+
self.length
233+
)
234+
proj[load_dir][load_case].append(
235+
{
236+
w0: projected_load[0],
237+
w1: projected_load[1],
238+
x0: projected_load[2],
239+
x1: projected_load[3],
240+
}
241+
)
212242

213243
self._projected_loads = proj
214244

tempfile.json

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"height": 2.0,
33
"length": 4.0,
44
"vertical_spread_angle": 0.0,
5+
"minimum_point_spread": 0.5,
56
"distributed_loads": {
67
"Fz": {
78
"D": [
@@ -26,22 +27,22 @@
2627
"Fz": {
2728
"D": [
2829
{
29-
"w1": 100.0,
30-
"x1": 0.5
30+
"p": 100.0,
31+
"x": 2.0
3132
}
3233
],
3334
"L": [
3435
{
35-
"w1": 100.0,
36-
"x1": 0.5
36+
"p": 100.0,
37+
"x": 2.0
3738
}
3839
]
3940
},
4041
"Fx": {
4142
"W": [
4243
{
43-
"w1": 2000,
44-
"x1": 0.0
44+
"p": 2000,
45+
"x": 0.0
4546
}
4647
]
4748
}
@@ -51,8 +52,11 @@
5152
"out_of_plane_dir": "y",
5253
"apply_spread_angle_gravity": true,
5354
"apply_spread_angle_inplane": true,
55+
"magnitude_point_key": "p",
5456
"magnitude_start_key": "w1",
5557
"magnitude_end_key": "w2",
58+
"location_point_key": "x",
5659
"location_start_key": "x1",
57-
"location_end_key": "x2"
60+
"location_end_key": "x2",
61+
"reverse_reaction_force_direction": true
5862
}

tests/test_geom.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33

44
def test_apply_spread_angle():
5+
ret = apply_spread_angle(1, 4, spread_angle=45, w0=10, x0=1, w1=10, x1=2)
6+
assert ret == (
7+
3.333333333333333, 3.333333333333333, 0, 3
8+
)
59
ret = apply_spread_angle(4, 3, spread_angle=10, w0=10, x0=1, w1=10, x1=2)
610
assert ret == (
711
4.148317542163208,

0 commit comments

Comments
 (0)