from typing import Any, Literal, Optional
from anastruct._types import SectionProps
from anastruct.preprocess.beam_class import Beam
from anastruct.vertex import Vertex
[docs]
class SimpleBeam(Beam):
"""Simple beam with a pin support at one end, and a roller support at the other."""
[docs]
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Simple Beam"
[docs]
def define_nodes(self) -> None:
self.nodes.append(Vertex(0.0, 0.0))
self.nodes.append(Vertex(self.dx * self.length, self.dy * self.length))
self.node_ids[0] = [1, 2]
[docs]
def define_supports(self) -> None:
self.support_definitions[1] = "pinned"
self.support_definitions[2] = "roller"
[docs]
class CantileverBeam(Beam):
"""Cantilever beam with a fixed support at one end and free at the other.
The ``cantilever_side`` parameter specifies which end is the free (unsupported)
end. For example, ``cantilever_side="right"`` means the right end is free and
the left end is fixed.
"""
[docs]
def __init__(
self,
length: float,
cantilever_side: Literal["left", "right"] = "right",
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
[docs]
self.cantilever_side = cantilever_side.lower()
if self.cantilever_side not in ["left", "right"]:
raise ValueError(
"cantilever_side must be either 'left' or 'right', "
f"got '{cantilever_side}'"
)
super().__init__(
length=length,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Cantilever Beam"
[docs]
def define_nodes(self) -> None:
self.nodes.append(Vertex(0.0, 0.0))
self.nodes.append(Vertex(self.dx * self.length, self.dy * self.length))
self.node_ids[0] = [1, 2]
[docs]
def define_supports(self) -> None:
self.support_definitions[2 if self.cantilever_side == "left" else 1] = "fixed"
[docs]
class RightCantileverBeam(CantileverBeam):
"""Cantilever beam with a fixed support at the left end and free (cantilevered) at the right."""
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
cantilever_side="right",
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Right Cantilever Beam"
[docs]
class LeftCantileverBeam(CantileverBeam):
"""Cantilever beam with a free (cantilevered) left end and a fixed support at the right."""
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
cantilever_side="left",
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Left Cantilever Beam"
[docs]
class MultiSpanBeam(Beam):
"""Simply supported multi-span beam. Assumes equal spans unless span_lengths provided."""
[docs]
def __init__(
self,
length: Optional[float] = None,
num_spans: Optional[int] = None,
span_lengths: Optional[list[float]] = None,
cantilevers: Optional[Literal["left", "right", "both"]] = None,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
if span_lengths is None and num_spans is None:
raise ValueError("Either num_spans or span_lengths must be provided.")
if span_lengths is not None and num_spans is not None:
raise ValueError("Only one of num_spans or span_lengths may be provided.")
if num_spans is not None and length is None:
raise ValueError("If num_spans is provided, length must also be provided.")
if cantilevers not in [None, "left", "right", "both"]:
raise ValueError(
"cantilevers must be either None, 'left', 'right', or 'both', "
f"got '{cantilevers}'"
)
if num_spans is not None and length is not None:
span_lengths = [length / num_spans] * num_spans
# Set attributes needed by define_supports() before super().__init__()
[docs]
self.num_spans = num_spans
[docs]
self.cantilevers = cantilevers
super().__init__(
span_lengths=span_lengths,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Multi-Span Beam"
[docs]
def define_nodes(self) -> None:
current_length = 0.0
self.nodes.append(Vertex(0.0, 0.0))
for i, span in enumerate(self.span_lengths):
current_length += span
self.nodes.append(
Vertex(
self.dx * current_length,
self.dy * current_length,
)
)
self.node_ids[i] = [i + 1, i + 2]
[docs]
def define_supports(self) -> None:
first_support = 1 if self.cantilevers in [None, "right"] else 2
last_support = (
len(self.span_lengths) + 1
if self.cantilevers in [None, "left"]
else len(self.span_lengths)
)
self.support_definitions[first_support] = "pinned"
for i in range(first_support + 1, last_support + 1):
self.support_definitions[i] = "roller"
[docs]
class TwoSpanBeam(MultiSpanBeam):
"""Simply supported two-span beam with equal spans."""
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
num_spans=2,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Two-Span Beam"
[docs]
class ThreeSpanBeam(MultiSpanBeam):
"""Simply supported three-span beam with equal spans."""
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
num_spans=3,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Three-Span Beam"
[docs]
class FourSpanBeam(MultiSpanBeam):
"""Simply supported four-span beam with equal spans."""
def __init__(
self,
length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
length=length,
num_spans=4,
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Four-Span Beam"
[docs]
class ProppedBeam(MultiSpanBeam):
"""Propped beam with an interior simple span and a cantilever on one side."""
[docs]
def __init__(
self,
interior_length: float,
cantilever_length: float,
cantilever_side: Literal["left", "right"] = "right",
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
if cantilever_side.lower() == "left":
span_lengths = [cantilever_length, interior_length]
elif cantilever_side.lower() == "right":
span_lengths = [interior_length, cantilever_length]
else:
raise ValueError(
"cantilever_side must be either 'left' or 'right', "
f"got '{cantilever_side}'"
)
super().__init__(
span_lengths=span_lengths,
cantilevers=cantilever_side,
angle=angle,
section=section,
)
[docs]
self.cantilever_side = cantilever_side.lower()
@property
[docs]
def type(self) -> str:
return "Propped Beam"
[docs]
class RightProppedBeam(ProppedBeam):
"""Propped beam with an interior simple span and a cantilever on the right side."""
def __init__(
self,
interior_length: float,
cantilever_length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
interior_length=interior_length,
cantilever_length=cantilever_length,
cantilever_side="right",
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Right Propped Beam"
[docs]
class LeftProppedBeam(ProppedBeam):
"""Propped beam with an interior simple span and a cantilever on the left side."""
def __init__(
self,
interior_length: float,
cantilever_length: float,
angle: float = 0.0,
section: Optional[SectionProps] = None,
) -> None:
super().__init__(
interior_length=interior_length,
cantilever_length=cantilever_length,
cantilever_side="left",
angle=angle,
section=section,
)
@property
[docs]
def type(self) -> str:
return "Left Propped Beam"
[docs]
def create_beam(beam_type: str, **kwargs: Any) -> Beam:
"""Factory function to create beam instances by type name.
Provides a convenient way to create beams without importing specific classes.
Type names are case-insensitive and can use underscores or hyphens as separators.
Args:
beam_type (str): The type of beam to create (e.g., "simple", "cantilever", "multi_span")
**kwargs: Arguments to pass to the beam constructor
Returns:
Beam: An instance of the requested beam type
Raises:
ValueError: If beam_type is not recognized
Examples:
>>> beam = create_beam("simple", length=10, section=section)
>>> beam = create_beam("cantilever", length=5, section=section)
"""
# Normalize the beam type name
normalized = beam_type.lower().replace("-", "_").replace(" ", "_")
# Map of normalized names to classes
beam_map = {
# Single-span beams
"simple": SimpleBeam,
"cantilever": CantileverBeam,
"right_cantilever": RightCantileverBeam,
"left_cantilever": LeftCantileverBeam,
# Multi-span beams
"multispan": MultiSpanBeam,
"multi_span": MultiSpanBeam,
"two_span": TwoSpanBeam,
"three_span": ThreeSpanBeam,
"four_span": FourSpanBeam,
"propped": ProppedBeam,
"right_propped": RightProppedBeam,
"left_propped": LeftProppedBeam,
}
if normalized not in beam_map:
available = sorted(set(beam_map.keys()))
raise ValueError(
f"Unknown beam type '{beam_type}'. Available types: {', '.join(available)}"
)
beam_class = beam_map[normalized]
assert issubclass(beam_class, Beam)
return beam_class(**kwargs)
__all__ = [
"SimpleBeam",
"CantileverBeam",
"RightCantileverBeam",
"LeftCantileverBeam",
"MultiSpanBeam",
"TwoSpanBeam",
"ThreeSpanBeam",
"FourSpanBeam",
"ProppedBeam",
"RightProppedBeam",
"LeftProppedBeam",
"create_beam",
]