Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = [
]
requires-python = ">=3.11"
dependencies = [
"load-distribution>=0.1.6",
"load-distribution>=0.1.7",
"pydantic>=2.0.0",
"safer>=5.1.0",
]
Expand Down
2 changes: 1 addition & 1 deletion src/loadbearing_wall/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
The wall model is parameterizable and can represent any material
"""

__version__ = "0.2.1"
__version__ = "0.3.0"

from loadbearing_wall.wall_model import LinearWallModel
from loadbearing_wall import *
82 changes: 67 additions & 15 deletions src/loadbearing_wall/geom_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,90 @@ def apply_spread_angle(
wall_height: float,
wall_length: float,
spread_angle: float,
w0: float,
x0: float,
w0: Optional[float] = None,
x0: Optional[float] = None,
w1: Optional[float] = None,
x1: Optional[float] = None,
) -> dict:
p: Optional[float] = None,
x: Optional[float] = None
) -> tuple[float, float, float, float]:
"""
Returns a dictionary representing the load described by
w0, w1, x0, x1. If only w0 and x0 are provided, the
load is assumed to be a point load.
w0, w1, x0, x1 (if distributed load) or p, x (if point
load).

The total spread cannot be longer than the wall length.

spread_angle is assumed to be in degrees
"""
angle_rads = math.radians(spread_angle)
spread_amount = wall_height * math.tan(angle_rads)
projected_x0 = max(0.0, x0 - spread_amount)
if x1 is None:
projected_x1 = min(wall_length, x0 + spread_amount)
else:
if None not in [w0, w1, x0, x1]:
projected_x0 = max(0.0, x0 - spread_amount)
projected_x1 = min(wall_length, x1 + spread_amount)
projected_length = projected_x1 - projected_x0
if x1 is not None:
original_length = x1 - x0
elif None not in [x, p]:
projected_x0 = max(0.0, x - spread_amount)
projected_x1 = min(wall_length, x + spread_amount)
original_length = 0
else:
original_length = 1
print(f"Weird condition: {locals()=}")

projected_length = projected_x1 - projected_x0

print(f"{projected_length=}")
ratio = original_length / projected_length
projected_w0 = w0 * ratio
if w1 is not None:

if None not in [w0, w1, x0, x1]:
projected_w0 = w0 * ratio
projected_w1 = w1 * ratio
elif None not in [x, p]:
projected_w0 = p / projected_length
projected_w1 = p / projected_length
print(f"{projected_w1=}")
return (
round_to_close_integer(projected_w0),
round_to_close_integer(projected_w1),
round_to_close_integer(projected_x0),
round_to_close_integer(projected_x1),
)


def apply_minimum_width(
magnitude: float,
location: float,
spread_width: float,
wall_length: float,
) -> tuple[float, float, float, float]:
"""
Returns a dictionary representing a distributed load
representing the point load converted to a distributed
load over the 'spread_width' in such a way that the
point load will be distributed an equal amount over
half of the spread_width on each side of point load.

If the point load location is 0 or wall_length, then
the point load will be a distributed load over half
of the spread_width (since there is not room for the
other half).

Load locations between zero/wall_length and half of the
spread_width will be linearly interpolated.
"""
assert spread_width <= wall_length
if location <= spread_width / 2:
projected_x0 = 0
projected_x1 = location + spread_width / 2
elif (wall_length - location) <= spread_width / 2:
projected_x0 = location - spread_width / 2
projected_x1 = wall_length
else:
projected_w1 = w0 * ratio
projected_x0 = location - spread_width / 2
projected_x1 = location + spread_width / 2

projected_x1 = location + spread_width / 2
projected_w0 = projected_w1 = magnitude / (projected_x1 - projected_x0)
print(f"{projected_w0=}")
return (
round_to_close_integer(projected_w0),
round_to_close_integer(projected_w1),
Expand Down
2 changes: 1 addition & 1 deletion src/loadbearing_wall/linear_reactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def from_projected_loads(

def extract_reaction_string(
self, xa: float, xb: float, case: str, dir: str
) -> Optional[list[LinearReaction]]:
):
"""
Returns a LinearReactionString representing the linear reactions that
exist between 'xa' and 'xb' extracted from self.
Expand Down
68 changes: 49 additions & 19 deletions src/loadbearing_wall/wall_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ class LinearWallModel(BaseModel):
height: float
length: float
vertical_spread_angle: float = 0.0
minimum_point_spread: float = 0.5
distributed_loads: dict = Field(default={})
point_loads: dict = Field(default={})
gravity_dir: str = "z"
inplane_dir: str = "x"
out_of_plane_dir: str = "y"
apply_spread_angle_gravity: bool = True
apply_spread_angle_inplane: bool = True
magnitude_point_key: str = 'p'
magnitude_start_key: str = "w0"
magnitude_end_key: str = "w1"
location_point_key: str = 'x'
location_start_key: str = "x0"
location_end_key: str = "x1"
reverse_reaction_force_direction: bool = True
_projected_loads: Optional[dict] = None
"""
A model of a linear load-bearing wall segment. The segment is assumed to be linear
Expand All @@ -34,6 +38,11 @@ class LinearWallModel(BaseModel):
spread through to the base of the wall according to this angle. The angle is
measured as deviation off of the direction of gravity (i.e. 0.0 is the in the
gravity direction and 30.0 is 30 degrees away from vertical)
'minimum_point_spread': For point loads applied without a spread angle, this is minimum width
over which the point load will be spread. Whether this value is realistic or not depends on
your context and the length unit system you are using. In other words, all applied point loads
will be converted to distributed loads spread over this distance unless a vertical spread
angle is applied.
'distributed_loads': A dictionary of loads. Can be set directly or using the .add_dist_load()
methods
'point_loads': A dictionary of loads. Can be set directly or using the .add_point_load()
Expand All @@ -44,30 +53,36 @@ class LinearWallModel(BaseModel):
used in the applied loads for applied loads in the inplane direction.
'out_of_plane_dir': A label used for the out_of_plane direction. Must match the direction labels
used in the applied loads for applied loads in the out_of_plane direction.
'magnitude_start_key': The key that will be used internally and in reaction results for the
'magnitude_point_key': The key that will be used internally to describe the magnitude of
point loads.
'magnitude_start_key': The key that will be used internally (and in reaction results) for the
start magnitude
'magnitude_end_key': The key that will be used internally and in reaction results for the
'magnitude_end_key': The key that will be used internally (and in reaction results) for the
end magnitude
'location_start_key': The key that will be used internally and in reaction results for the
'location_key': The key that will be used internally to describe the location of applied
point loads.
'location_start_key': The key that will be used internally (and in reaction results) for the
start location
'location_end_key': The key that will be used internally and in reaction results for the
'location_end_key': The key that will be used internally (and in reaction results) for the
end location
'reverse_reaction_force_direction': If True, the reactions will be represented with a load
of the opposite sign as the input load.
"""

@classmethod
def from_json(self, filepath: str | pathlib.Path):
def from_json(cls, filepath: str | pathlib.Path):
with safer.open(filepath) as file:
json_data = file.read()
return self.model_validate_json(json_data)
return cls.model_validate_json(json_data)

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

@classmethod
def from_dict(self, data: dict):
return self.model_validate(data)
def from_dict(cls, data: dict):
return cls.model_validate(data)

def dump_dict(self):
return self.model_dump(mode="json")
Expand Down Expand Up @@ -122,8 +137,8 @@ def add_point_load(
self.point_loads[dir].setdefault(case, [])
self.point_loads[dir][case].append(
{
self.magnitude_start_key: magnitude,
self.location_start_key: location,
self.magnitude_point_key: magnitude,
self.location_point_key: location,
}
)

Expand All @@ -140,6 +155,8 @@ def spread_loads(self) -> None:
w1 = self.magnitude_end_key
x0 = self.location_start_key
x1 = self.location_end_key
p = self.magnitude_point_key
x = self.location_point_key
for load_dir, load_cases in self.distributed_loads.items():
proj.setdefault(load_dir, {})
should_apply_spread_angle = (
Expand All @@ -159,10 +176,10 @@ def spread_loads(self) -> None:
self.height,
self.length,
self.vertical_spread_angle,
dist_load[w0],
dist_load[x0],
dist_load.get(w1),
dist_load.get(x1),
w0=dist_load[w0],
x0=dist_load[x0],
w1=dist_load.get(w1),
x1=dist_load.get(x1),
)
proj[load_dir][load_case].append(
{
Expand All @@ -173,6 +190,7 @@ def spread_loads(self) -> None:
}
)
else:
print(f"{dist_load=}")
proj[load_dir][load_case].append(dist_load)

for load_dir, load_cases in self.point_loads.items():
Expand All @@ -194,10 +212,8 @@ def spread_loads(self) -> None:
self.height,
self.length,
self.vertical_spread_angle,
point_load[w0],
point_load[x0],
point_load.get(w1),
point_load.get(x1),
p=point_load[p],
x=point_load[x],
)
proj[load_dir][load_case].append(
{
Expand All @@ -208,7 +224,21 @@ def spread_loads(self) -> None:
}
)
else:
proj[load_dir][load_case].append(point_load)
print(f"Min spread: {point_load=}")
projected_load = geom.apply_minimum_width(
point_load[p],
point_load[x],
self.minimum_point_spread,
self.length
)
proj[load_dir][load_case].append(
{
w0: projected_load[0],
w1: projected_load[1],
x0: projected_load[2],
x1: projected_load[3],
}
)

self._projected_loads = proj

Expand Down
18 changes: 11 additions & 7 deletions tempfile.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"height": 2.0,
"length": 4.0,
"vertical_spread_angle": 0.0,
"minimum_point_spread": 0.5,
"distributed_loads": {
"Fz": {
"D": [
Expand All @@ -26,22 +27,22 @@
"Fz": {
"D": [
{
"w1": 100.0,
"x1": 0.5
"p": 100.0,
"x": 2.0
}
],
"L": [
{
"w1": 100.0,
"x1": 0.5
"p": 100.0,
"x": 2.0
}
]
},
"Fx": {
"W": [
{
"w1": 2000,
"x1": 0.0
"p": 2000,
"x": 0.0
}
]
}
Expand All @@ -51,8 +52,11 @@
"out_of_plane_dir": "y",
"apply_spread_angle_gravity": true,
"apply_spread_angle_inplane": true,
"magnitude_point_key": "p",
"magnitude_start_key": "w1",
"magnitude_end_key": "w2",
"location_point_key": "x",
"location_start_key": "x1",
"location_end_key": "x2"
"location_end_key": "x2",
"reverse_reaction_force_direction": true
}
4 changes: 4 additions & 0 deletions tests/test_geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@


def test_apply_spread_angle():
ret = apply_spread_angle(1, 4, spread_angle=45, w0=10, x0=1, w1=10, x1=2)
assert ret == (
3.333333333333333, 3.333333333333333, 0, 3
)
ret = apply_spread_angle(4, 3, spread_angle=10, w0=10, x0=1, w1=10, x1=2)
assert ret == (
4.148317542163208,
Expand Down
Loading
Loading