diff --git a/blueprints/geometry/line.py b/blueprints/geometry/line.py
index 55f125ff..0162da87 100644
--- a/blueprints/geometry/line.py
+++ b/blueprints/geometry/line.py
@@ -1,6 +1,5 @@
"""Line module."""
-from enum import Enum
from typing import Literal
import numpy as np
@@ -12,13 +11,6 @@
from blueprints.unit_conversion import RAD_TO_DEG
-class Reference(Enum):
- """Enum of the reference options start/end."""
-
- START = 0
- END = 1
-
-
class Line:
"""Represents a line in a 3D modelling space.
diff --git a/blueprints/structural_sections/concrete/covers.py b/blueprints/structural_sections/concrete/covers.py
new file mode 100644
index 00000000..19d8d936
--- /dev/null
+++ b/blueprints/structural_sections/concrete/covers.py
@@ -0,0 +1,46 @@
+"""Module with the representation of the covers for cross-sections."""
+
+from collections import defaultdict
+from dataclasses import asdict, dataclass
+
+from blueprints.type_alias import MM
+from blueprints.validations import raise_if_negative
+
+DEFAULT_COVER = 50 # mm
+
+
+@dataclass(frozen=True)
+class CoversRectangular:
+ """Representation of the covers of a rectangular cross-section."""
+
+ upper: MM = DEFAULT_COVER
+ right: MM = DEFAULT_COVER
+ lower: MM = DEFAULT_COVER
+ left: MM = DEFAULT_COVER
+
+ def __post_init__(self) -> None:
+ """Post initialization of the covers."""
+ self.validate()
+
+ def get_covers_info(self) -> str:
+ """Return a string with the covers of the cross-section."""
+ caption_text = "Cover:"
+
+ covers = defaultdict(list)
+
+ for key, value in asdict(self).items():
+ covers[value].append(key)
+
+ if len(covers) == 1:
+ return f"{caption_text} {self.upper:.0f} mm"
+
+ cover_texts = [caption_text]
+
+ for cover, names in covers.items():
+ cover_texts.append(f"{'|'.join(names)}: {cover:.0f} mm")
+
+ return "\n ".join(cover_texts)
+
+ def validate(self) -> None:
+ """Validate the covers."""
+ raise_if_negative(upper=self.upper, right=self.right, lower=self.lower, left=self.left)
diff --git a/blueprints/structural_sections/concrete/rebar.py b/blueprints/structural_sections/concrete/rebar.py
index 9bd04bac..000a3cf7 100644
--- a/blueprints/structural_sections/concrete/rebar.py
+++ b/blueprints/structural_sections/concrete/rebar.py
@@ -3,7 +3,7 @@
from dataclasses import dataclass
from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
-from blueprints.structural_sections.concrete.reinforced_concrete_sections.cross_section_shapes import CircularCrossSection
+from blueprints.structural_sections.cross_section_shapes import CircularCrossSection
from blueprints.type_alias import KG_M, RATIO
from blueprints.unit_conversion import MM2_TO_M2
@@ -17,6 +17,10 @@ class Rebar(CircularCrossSection):
----------
diameter : MM
Diameter of the bar (for example: ⌀12, ⌀16, ⌀20, etc.) [mm]
+ x : MM
+ x-coordinate in the cross-section [mm]
+ y : MM
+ y-coordinate in the cross-section [mm]
material : ReinforcementSteelMaterial
Representation of the properties of reinforcement steel suitable for use with NEN-EN 1992-1-1.
relative_start_position: RATIO
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/base.py b/blueprints/structural_sections/concrete/reinforced_concrete_sections/base.py
new file mode 100644
index 00000000..3dcae505
--- /dev/null
+++ b/blueprints/structural_sections/concrete/reinforced_concrete_sections/base.py
@@ -0,0 +1,203 @@
+"""Base class of all reinforced cross-sections."""
+
+from abc import ABC
+from functools import partial
+from typing import Callable
+
+from shapely import LineString
+
+from blueprints.materials.concrete import ConcreteMaterial
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.structural_sections.concrete.rebar import Rebar
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.reinforcement_configurations import (
+ ReinforcementConfiguration,
+)
+from blueprints.structural_sections.concrete.stirrups import StirrupConfiguration
+from blueprints.structural_sections.cross_section_shapes import CrossSection
+from blueprints.type_alias import KG_M, KG_M3, M3_M, MM2_M
+from blueprints.unit_conversion import M_TO_MM, MM3_TO_M3
+
+
+class ReinforcedCrossSection(ABC):
+ """Base class of all reinforced cross-sections."""
+
+ def __init__(
+ self,
+ cross_section: CrossSection,
+ concrete_material: ConcreteMaterial,
+ ) -> None:
+ """Initialize the reinforced cross-section.
+
+ Parameters
+ ----------
+ cross_section : CrossSection
+ Cross-section of the reinforced concrete section.
+ concrete_material : ConcreteMaterial
+ Material properties of the concrete.
+ """
+ self.cross_section = cross_section
+ self.concrete_material = concrete_material
+ self._reinforcement_configurations: list[tuple[LineString | Callable[..., LineString], ReinforcementConfiguration]] = []
+ self._single_longitudinal_rebars: list[Rebar] = []
+ self._stirrups: list[StirrupConfiguration] = []
+
+ @property
+ def longitudinal_rebars(self) -> list[Rebar]:
+ """Return a list of all longitudinal rebars."""
+ rebars: list[Rebar] = []
+
+ # add the single longitudinal rebars
+ rebars.extend(self._single_longitudinal_rebars)
+
+ # add the rebars from the reinforcement configurations
+ for line, configuration in self._reinforcement_configurations:
+ if callable(line):
+ # partial function with additional arguments where the line should be called to get the LineString
+ # the implementation will be made at that level and inserted here to produce the rebars needed.
+ # this keeps this ABC class clean and allows for a lot of flexibility in the implementation of the line.
+ # this has been done to be able to add any shape of line to the cross-section (e.g. a circle or any other in the future).
+ rebars.extend(configuration.to_rebars(line=line()))
+ else:
+ rebars.extend(configuration.to_rebars(line=line))
+
+ # check if all rebars are inside the cross-section.
+ # needed for the case where custom configurations are added to the RCS
+ for rebar in rebars:
+ if not self.cross_section.geometry.contains(other=rebar.geometry):
+ msg = f"Rebar (diameter={rebar.diameter}, x={rebar.x}, y={rebar.y}) is not (fully) inside the cross-section."
+ raise ValueError(msg)
+
+ return rebars
+
+ @property
+ def stirrups(self) -> list[StirrupConfiguration]:
+ """Return a list of all stirrups."""
+ return self._stirrups
+
+ @property
+ def reinforcement_weight_longitudinal_bars(self) -> KG_M:
+ """Total mass of the longitudinal reinforcement in the cross-section per meter length [kg/m]."""
+ return sum(rebar.weight_per_meter for rebar in self.longitudinal_rebars)
+
+ @property
+ def reinforcement_weight_stirrups(self) -> KG_M:
+ """Total mass of the stirrups' reinforcement in the cross-section per meter length [kg/m]."""
+ return sum(stirrup.weight_per_meter for stirrup in self._stirrups)
+
+ @property
+ def reinforcement_weight(self) -> KG_M:
+ """Total mass of the reinforcement in the cross-section per meter length [kg/m]."""
+ return self.reinforcement_weight_longitudinal_bars + self.reinforcement_weight_stirrups
+
+ @property
+ def reinforcement_area_longitudinal_bars(self) -> MM2_M:
+ """Total area of the longitudinal reinforcement in the cross-section per meter length [mm²/m]."""
+ return sum(rebar.area for rebar in self.longitudinal_rebars)
+
+ @property
+ def concrete_volume(self) -> M3_M:
+ """Total volume of the reinforced cross-section per meter length [m³/m]."""
+ length = M_TO_MM
+ return self.cross_section.area * length * MM3_TO_M3
+
+ @property
+ def weight_per_volume(self) -> KG_M3:
+ """Total mass of the cross-section per meter length (concrete_checks+reinforcement) [kg/m³]."""
+ return self.reinforcement_weight / self.concrete_volume
+
+ def get_present_steel_materials(self) -> list[ReinforcementSteelMaterial]:
+ """Return a list of all present steel materials in the cross-section."""
+ materials = [rebar.material for rebar in self.longitudinal_rebars]
+ materials.extend(stirrup.material for stirrup in self._stirrups)
+ return list(set(materials))
+
+ def add_longitudinal_rebar(
+ self,
+ rebar: Rebar,
+ ) -> Rebar:
+ """Adds a single reinforcement bar to the cross-section.
+
+ Parameters
+ ----------
+ rebar : Rebar
+ Rebar to be added to the cross-section.
+
+ Raises
+ ------
+ ValueError
+ If the rebar is not fully inside the cross-section.
+
+ Returns
+ -------
+ Rebar
+ Newly created Rebar
+ """
+ # check if given diameter/coordinates are fully inside the cross-section
+ if not rebar.geometry.within(self.cross_section.geometry):
+ msg = f"Rebar (diameter={rebar.diameter}, x={rebar.x}, y={rebar.y}) is not (fully) inside the cross-section."
+ raise ValueError(msg)
+
+ # add the rebar to the list of longitudinal rebars
+ self._single_longitudinal_rebars.append(rebar)
+
+ return rebar
+
+ def add_stirrup_configuration(self, stirrup: StirrupConfiguration) -> StirrupConfiguration:
+ """Add a stirrup configuration to the cross-section.
+
+ Parameters
+ ----------
+ stirrup : StirrupConfiguration
+ Configuration of stirrup reinforcement in the cross-section.
+
+ Returns
+ -------
+ StirrupConfiguration
+ Newly created Stirrup
+
+ Raises
+ ------
+ ValueError
+ If the stirrup is not fully inside the cross-section.
+ """
+ # check if the stirrup is inside the cross-section
+ stirrup_outside_edge = stirrup.geometry.buffer(distance=stirrup.diameter / 2)
+ if not self.cross_section.geometry.contains(stirrup_outside_edge):
+ msg = "Stirrup is not (fully) inside the cross-section."
+ raise ValueError(msg)
+
+ # add the stirrup to the list
+ self._stirrups.append(stirrup)
+
+ return stirrup
+
+ def add_reinforcement_configuration(
+ self,
+ line: LineString | Callable[..., LineString],
+ configuration: ReinforcementConfiguration,
+ *args,
+ **kwargs,
+ ) -> None:
+ """Add a reinforcement configuration to the cross-section.
+
+ Parameters
+ ----------
+ line : LineString | Callable[..., LineString]
+ Representing the path of the reinforcement in the cross-section.
+ Start of the line defines the first rebar of the configuration, end of the line defines the last rebar.
+ If a callable is given, it should return a LineString. The callable can take additional arguments.
+ Arguments can be passed to the callable using the *args and **kwargs.
+ configuration : ReinforcementConfiguration
+ Configuration of the reinforcement.
+ args : Any
+ Additional arguments for the callable line. If line is not a callable, these arguments are ignored.
+ kwargs : Any
+ Additional keyword arguments for the callable line. If line is not a callable, these arguments are ignored.
+
+ """
+ # check if the line is a callable and wrap it with the given arguments
+ if callable(line):
+ line = partial(line, *args, **kwargs) # type: ignore[misc]
+
+ # add the reinforcement configuration to the list
+ self._reinforcement_configurations.append((line, configuration))
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/__init__.py b/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/__init__.py
new file mode 100644
index 00000000..604728c3
--- /dev/null
+++ b/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/__init__.py
@@ -0,0 +1 @@
+"""Default plotters for Blueprint's reinforced concrete sections."""
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/rectangular.py b/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/rectangular.py
new file mode 100644
index 00000000..4d0164f5
--- /dev/null
+++ b/blueprints/structural_sections/concrete/reinforced_concrete_sections/plotters/rectangular.py
@@ -0,0 +1,415 @@
+"""Plotter for Reinforced Rectangular Cross-Sections."""
+
+# ruff: noqa: PLR0913, F821
+from typing import TypeVar
+
+from matplotlib import patches as mplpatches
+from matplotlib import pyplot as plt
+from matplotlib.axes import Axes
+from shapely import Point
+
+from blueprints.structural_sections.concrete.rebar import Rebar
+
+T = TypeVar("T", bound="RectangularReinforcedCrossSection") # type: ignore[name-defined]
+
+RCS_CROSS_SECTION_COLOR = (0.827, 0.827, 0.827)
+STIRRUP_COLOR = (0.412, 0.412, 0.412)
+REBAR_COLOR = (0.717, 0.255, 0.055)
+
+
+class RectangularCrossSectionPlotter:
+ """Plotter for Reinforced Rectangular Cross-Sections (RRCS)."""
+
+ def __init__(
+ self,
+ cross_section: T,
+ ) -> None:
+ """Initialize the RRCSPlotter.
+
+ Parameters
+ ----------
+ cross_section: RectangularReinforcedCrossSection
+ Reinforced cross-section to plot.
+ """
+ self.cross_section = cross_section
+ self.fig: plt.Figure | None = None
+ self.axes: list[Axes] = []
+
+ def plot(
+ self,
+ figsize: tuple[float, float] = (15.0, 8.0),
+ title: str | None = None,
+ font_size_title: float = 18.0,
+ font_size_legend: float = 10.0,
+ include_legend: bool = True,
+ font_size_dimension: float = 12.0,
+ custom_text_legend: str | None = None,
+ custom_text_width: str | None = None,
+ custom_text_height: str | None = None,
+ offset_line_width: float = 1.25,
+ offset_line_height: float = 1.2,
+ center_line_style: dict[str, float | str] | None = None,
+ show: bool = False,
+ axes_i: int = 0,
+ ) -> plt.Figure:
+ """Plots the cross-section.
+
+ Parameters
+ ----------
+ figsize: tuple[float, float]
+ Size of the plot window.
+ title: str
+ Title of the plot.
+ font_size_title: float
+ Font size of the title.
+ font_size_legend: float
+ Font size of the legend.
+ include_legend: bool
+ Include legend in the plot.
+ font_size_dimension: float
+ Font size of the dimensions.
+ custom_text_legend: str
+ Custom text for the legend.
+ custom_text_width: str
+ Custom text for the width dimension. Replaces the width of the cross-section with the custom text.
+ custom_text_height: str
+ Custom text for the height dimension. Replaces the height of the cross-section with the custom text.
+ offset_line_width: float
+ Offset of the width line.
+ offset_line_height: float
+ Offset of the height line.
+ center_line_style: dict[str, float | str] | None
+ Style of the center lines. Check matplotlib documentation for more information (Annotation-arrowprops).
+ show: bool
+ Show the plot.
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+
+ Returns
+ -------
+ plt.Figure
+ Matplotlib figure.
+ """
+ self._start_plot(figsize=figsize)
+ self._add_rectangle(axes_i=axes_i)
+ self._add_center_lines(axes_i=axes_i, style=center_line_style)
+ self._add_dimension_lines(
+ axes_i=axes_i,
+ font_size_dimension=font_size_dimension,
+ custom_text_height=custom_text_height,
+ custom_text_width=custom_text_width,
+ offset_line_width=offset_line_width,
+ offset_line_height=offset_line_height,
+ )
+ self._add_stirrups(axes_i=axes_i)
+ self._add_longitudinal_rebars(axes_i=axes_i)
+
+ # set limits and title
+ self.axes[axes_i].axis("off")
+ self.axes[axes_i].axis("equal")
+ self.axes[axes_i].set_title(
+ label=title or "",
+ fontdict={"fontsize": font_size_title},
+ )
+
+ if include_legend:
+ self._add_legend(
+ axes_i=axes_i,
+ font_size_legend=font_size_legend,
+ custom_legend_text=custom_text_legend,
+ )
+ if show:
+ plt.show() # pragma: no cover
+ assert self.fig is not None
+ return self.fig
+
+ def _start_plot(self, figsize: tuple[float, float] = (15.0, 8.0)) -> tuple[float, float]:
+ """Starts the plot by initialising a matplotlib plot window of the given size.
+
+ Parameters
+ ----------
+ figsize: tuple[float, float]
+ Size of the plot window.
+ """
+ plt.close("all")
+ self.fig = plt.figure(figsize=figsize)
+ self.axes = [self.fig.add_subplot()]
+ return self.fig.get_figwidth(), self.fig.get_figheight()
+
+ def _add_rectangle(
+ self,
+ edge_color: str = "black",
+ axes_i: int = 0,
+ ) -> mplpatches.Rectangle:
+ """Adds a rectangle to the plot.
+
+ Parameters
+ ----------
+ edge_color: str
+ Color of the edge of the rectangle. Use any matplotlib color.
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ """
+ patch = mplpatches.Rectangle(
+ xy=(-self.cross_section.width / 2, -self.cross_section.height / 2),
+ width=self.cross_section.width,
+ height=self.cross_section.height,
+ edgecolor=edge_color,
+ facecolor=RCS_CROSS_SECTION_COLOR,
+ fill=True,
+ lw=1,
+ )
+ self.axes[axes_i].add_patch(patch)
+ return patch
+
+ def _add_center_lines(self, axes_i: int = 0, style: dict[str, float | str] | None = None) -> None:
+ """Adds center lines to the plot.
+
+ Parameters
+ ----------
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ style: dict[str, float]
+ Style of the center lines. Check matplotlib documentation for more information (Annotation-arrowprops).
+ """
+ default_style = {"arrowstyle": "-", "linewidth": 0.8, "color": "gray", "linestyle": "dashdot"}
+ if style:
+ default_style.update(style)
+ offset_center_line = 1.05
+ self.axes[axes_i].annotate(
+ text="z",
+ xy=(0, (-self.cross_section.height / 2) * offset_center_line),
+ xytext=(0, (self.cross_section.height / 2) * offset_center_line),
+ arrowprops=default_style,
+ verticalalignment="bottom",
+ horizontalalignment="center",
+ )
+ self.axes[axes_i].annotate(
+ text="y",
+ xy=((self.cross_section.width / 2) * offset_center_line, 0),
+ xytext=(-(self.cross_section.width / 2) * offset_center_line, 0),
+ arrowprops=default_style,
+ verticalalignment="center",
+ horizontalalignment="right",
+ )
+
+ def _add_dimension_lines(
+ self,
+ axes_i: int = 0,
+ style: mplpatches.ArrowStyle | None = None,
+ offset_line_width: float = 1.25,
+ offset_line_height: float = 1.2,
+ custom_text_width: str | None = None,
+ custom_text_height: str | None = None,
+ font_size_dimension: float = 12.0,
+ ) -> None:
+ """Adds dimension lines to the plot.
+
+ Parameters
+ ----------
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ style: dict[str, float]
+ Style of the dimension lines. Check matplotlib documentation for more information (Annotation-arrowprops).
+ offset_line_width: float
+ Offset of the width line.
+ offset_line_height: float
+ Offset of the height line.
+ custom_text_width: str
+ Custom text for the width dimension. Replaces the width of the cross-section with the custom text.
+ custom_text_height: str
+ Custom text for the height dimension. Replaces the height of the cross-section with the custom text.
+ font_size_dimension: float
+ Font size of the dimensions.
+ """
+ # add the width dimension line
+ diameter_line_style = {
+ "arrowstyle": style or mplpatches.ArrowStyle(stylename="<->", head_length=0.5, head_width=0.5),
+ }
+ offset_width = (-self.cross_section.height / 2) * offset_line_width
+ self.axes[axes_i].annotate(
+ text="",
+ xy=(-self.cross_section.width / 2, offset_width),
+ xytext=(self.cross_section.width / 2, offset_width),
+ verticalalignment="center",
+ horizontalalignment="center",
+ arrowprops=diameter_line_style,
+ annotation_clip=False,
+ )
+ self.axes[axes_i].text(
+ s=custom_text_width or f"{self.cross_section.width:.0f} mm",
+ x=0,
+ y=offset_width,
+ verticalalignment="bottom",
+ horizontalalignment="center",
+ fontsize=font_size_dimension,
+ )
+
+ # add the height dimension line
+ offset_height = (-self.cross_section.width / 2) * offset_line_height
+ self.axes[axes_i].annotate(
+ text="",
+ xy=(offset_height, self.cross_section.height / 2),
+ xytext=(offset_height, -self.cross_section.height / 2),
+ verticalalignment="center",
+ horizontalalignment="center",
+ arrowprops=diameter_line_style,
+ rotation=90,
+ annotation_clip=False,
+ )
+ self.axes[axes_i].text(
+ s=custom_text_height or f"{self.cross_section.height:.0f} mm",
+ x=offset_height,
+ y=0,
+ verticalalignment="center",
+ horizontalalignment="right",
+ fontsize=font_size_dimension,
+ rotation=90,
+ )
+
+ def _add_stirrups(
+ self,
+ axes_i: int = 0,
+ ) -> None:
+ """Adds stirrups to the plot.
+
+ Parameters
+ ----------
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ """
+ for stirrup in self.cross_section.stirrups:
+ left_bottom = Point(stirrup.geometry.exterior.coords[0]) # left bottom point of the stirrup (center line)
+ self.axes[axes_i].add_patch(
+ mplpatches.Rectangle(
+ xy=(left_bottom.x - stirrup.diameter / 2, left_bottom.y - stirrup.diameter / 2),
+ width=stirrup.ctc_distance_legs + stirrup.diameter,
+ height=self.cross_section.height - self.cross_section.covers.upper - self.cross_section.covers.lower,
+ facecolor=STIRRUP_COLOR,
+ fill=True,
+ )
+ )
+ self.axes[axes_i].add_patch(
+ mplpatches.Rectangle(
+ xy=(left_bottom.x + stirrup.diameter / 2, left_bottom.y + stirrup.diameter / 2),
+ width=stirrup.ctc_distance_legs - stirrup.diameter,
+ height=self.cross_section.height - self.cross_section.covers.upper - self.cross_section.covers.lower - 2 * stirrup.diameter,
+ facecolor=RCS_CROSS_SECTION_COLOR,
+ fill=True,
+ )
+ )
+
+ def _add_longitudinal_rebars(
+ self,
+ axes_i: int = 0,
+ ) -> None:
+ """Adds longitudinal rebars to the plot.
+
+ Parameters
+ ----------
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ """
+ for rebar in self.cross_section.longitudinal_rebars:
+ self.axes[axes_i].add_patch(
+ mplpatches.Circle(
+ xy=(rebar.x, rebar.y),
+ radius=rebar.radius,
+ linewidth=1,
+ color=REBAR_COLOR,
+ )
+ )
+
+ def legend_text(self) -> str:
+ """Creates the legend text.
+
+ Returns
+ -------
+ str
+ Legend text.
+ """
+ # start building legend
+ main_steel_material_used = self.cross_section.get_present_steel_materials()[0].name
+ legend_text = f"{self.cross_section.concrete_material.concrete_class.value} - {main_steel_material_used}"
+
+ legend_text += self._add_stirrups_to_legend()
+ legend_text += self._add_longitudinal_rebars_to_legend()
+ legend_text += self._add_rebar_configurations_to_legend()
+ legend_text += self._add_single_longitudinal_rebars_to_legend()
+ legend_text += self._add_covers_info_to_legend()
+
+ return legend_text
+
+ def _add_stirrups_to_legend(self) -> str:
+ """Adds stirrups to the legend text."""
+ stirrups_text = ""
+ if self.cross_section.stirrups:
+ stirrups_text += f"\nStirrups ({sum(stirrup.as_w for stirrup in self.cross_section.stirrups):.0f} mm²/m):"
+ for stirrup in self.cross_section.stirrups:
+ stirrups_text += (
+ f"\n ⌀{stirrup.diameter}-{stirrup.distance} mm (b:{stirrup.ctc_distance_legs:.0f} mm) ({stirrup.as_w:.0f} " f"mm²/m)"
+ )
+ return stirrups_text
+
+ def _add_longitudinal_rebars_to_legend(self) -> str:
+ """Add longitudinal rebars to the legend text."""
+ longitudinal_rebars = ""
+ if self.cross_section.longitudinal_rebars:
+ longitudinal_rebars += f"\nReinforcement ({sum(rebar.area for rebar in self.cross_section.longitudinal_rebars):.0f} mm²/m): "
+ return longitudinal_rebars
+
+ def _add_single_longitudinal_rebars_to_legend(self) -> str:
+ """Add single longitudinal rebars to legend text."""
+ single_longitudinal_text = ""
+ if self.cross_section._single_longitudinal_rebars: # noqa: SLF001
+ rebar_diameters: dict[float, list[Rebar]] = {}
+ for rebar in self.cross_section._single_longitudinal_rebars: # noqa: SLF001
+ rebar_diameters.setdefault(rebar.diameter, []).append(rebar)
+ for diameter, rebars in rebar_diameters.items():
+ single_longitudinal_text += f"\n {len(rebars)}⌀{round(diameter, 2)} ({int(sum(rebar.area for rebar in rebars))} mm²/m)"
+ return single_longitudinal_text
+
+ def _add_rebar_configurations_to_legend(self) -> str:
+ """Add rebar configurations to legend text (quantity in line)."""
+ rebar_configurations_text = ""
+ if self.cross_section._reinforcement_configurations: # noqa: SLF001
+ for _, configuration in self.cross_section._reinforcement_configurations: # noqa: SLF001
+ rebar_configurations_text += f"\n {configuration!s} ({int(configuration.area)} mm²/m)"
+ return rebar_configurations_text
+
+ def _add_covers_info_to_legend(self) -> str:
+ """Add covers info to legend text."""
+ covers_text = ""
+ if self.cross_section.stirrups or self.cross_section.longitudinal_rebars:
+ covers_text += "\n" + self.cross_section.covers.get_covers_info()
+ return covers_text
+
+ def _add_legend(
+ self,
+ axes_i: int = 0,
+ custom_legend_text: str | None = None,
+ font_size_legend: float = 10.0,
+ offset_center_line: float = 1.05,
+ ) -> None:
+ """Adds the legend to the plot.
+
+ Parameters
+ ----------
+ axes_i: int
+ Index of the axes to plot on. Default is 0.
+ custom_legend_text: str
+ Custom legend text.
+ font_size_legend: float
+ Font size of the legend.
+ offset_center_line: float
+ Offset of the center line.
+ """
+ legend_text = custom_legend_text or self.legend_text()
+ self.axes[axes_i].annotate(
+ text=legend_text,
+ xy=((self.cross_section.width / 2) * offset_center_line, -self.cross_section.height / 2),
+ verticalalignment="bottom",
+ horizontalalignment="left",
+ fontsize=font_size_legend,
+ annotation_clip=False,
+ )
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/rectangular.py b/blueprints/structural_sections/concrete/reinforced_concrete_sections/rectangular.py
new file mode 100644
index 00000000..2fa427a9
--- /dev/null
+++ b/blueprints/structural_sections/concrete/reinforced_concrete_sections/rectangular.py
@@ -0,0 +1,334 @@
+"""Rectangular reinforced cross-section."""
+
+# ruff: noqa: PLR0913
+from typing import Literal
+
+from matplotlib import pyplot as plt
+from shapely import LineString, Point, Polygon
+
+from blueprints.materials.concrete import ConcreteMaterial
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.structural_sections.concrete.covers import CoversRectangular
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.base import ReinforcedCrossSection
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.plotters.rectangular import RectangularCrossSectionPlotter
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.reinforcement_configurations import ReinforcementByQuantity
+from blueprints.structural_sections.concrete.stirrups import StirrupConfiguration
+from blueprints.structural_sections.cross_section_shapes import RectangularCrossSection
+from blueprints.type_alias import DIMENSIONLESS, MM, RATIO
+
+
+class RectangularReinforcedCrossSection(ReinforcedCrossSection):
+ """Representation of a reinforced rectangular concrete cross-section like a beam.
+
+ Parameters
+ ----------
+ width : MM
+ The width of the rectangular cross-section [mm].
+ height : MM
+ The height of the rectangular cross-section [mm].
+ concrete_material : ConcreteMaterial
+ Material properties of the concrete.
+ covers : CoversRectangular, optional
+ The reinforcement covers for the cross-section [mm]. The default on all sides is 50 mm.
+ """
+
+ def __init__(
+ self,
+ width: MM,
+ height: MM,
+ concrete_material: ConcreteMaterial,
+ covers: CoversRectangular = CoversRectangular(),
+ ) -> None:
+ """Initialize the rectangular reinforced concrete section."""
+ super().__init__(
+ cross_section=RectangularCrossSection(
+ width=width,
+ height=height,
+ ),
+ concrete_material=concrete_material,
+ )
+ self.width = width
+ self.height = height
+ self.covers = covers
+ self.plotter = RectangularCrossSectionPlotter(cross_section=self)
+
+ def add_stirrup_along_edges(
+ self,
+ diameter: MM,
+ distance: MM,
+ material: ReinforcementSteelMaterial,
+ shear_check: bool = True,
+ torsion_check: bool = True,
+ mandrel_diameter_factor: DIMENSIONLESS | None = None,
+ anchorage_length: MM = 0.0,
+ relative_start_position: RATIO = 0.0,
+ relative_end_position: RATIO = 1.0,
+ ) -> StirrupConfiguration:
+ """Adds a stirrup configuration along the edges of the cross-section taking the covers into account. The created configuration goes around
+ the longitudinal rebars (if any).
+
+ Use .add_stirrup_configuration() to add a stirrup configuration of any shape, size, and position (as long as it is inside the cross-section).
+
+
+ Parameters
+ ----------
+ diameter: MM
+ Diameter of the stirrups [mm].
+ distance: MM
+ Longitudinal distance between stirrups [mm].
+ material : ReinforcementSteelMaterial
+ Representation of the properties of reinforcement steel suitable for use with NEN-EN 1992-1-1
+ shear_check: bool
+ Take stirrup into account in shear check
+ torsion_check: bool
+ Take stirrup into account in torsion check
+ mandrel_diameter_factor: DIMENSIONLESS
+ Inner diameter of mandrel as multiple of stirrup diameter [-]
+ (default: 4⌀ for ⌀<=16mm and 5⌀ for ⌀>16mm) Tabel 8.1Na NEN-EN 1992-1-1 Dutch National Annex.
+ anchorage_length: MM
+ Anchorage length [mm]
+ relative_start_position: RATIO
+ Relative position of the start of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1.
+ Default is 0 (start).
+ relative_end_position: RATIO
+ Relative position of the end of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1.
+ Default is 1 (end).
+
+
+ Returns
+ -------
+ StirrupConfiguration
+ Newly created stirrup configuration inside the cross-section.
+ """
+ # get the corners of the cross-section
+ min_x, min_y, max_x, max_y = self.cross_section.geometry.bounds
+
+ # create the corners of the stirrup configuration based on the covers present
+ left_bottom_corner = Point(min_x + self.covers.left + (diameter / 2), min_y + self.covers.lower + (diameter / 2))
+ left_top_corner = Point(min_x + self.covers.left + (diameter / 2), max_y - self.covers.upper - (diameter / 2))
+ right_top_corner = Point(max_x - self.covers.right - (diameter / 2), max_y - self.covers.upper - (diameter / 2))
+ right_bottom_corner = Point(max_x - self.covers.right - (diameter / 2), min_y + self.covers.lower + (diameter / 2))
+
+ return self.add_stirrup_configuration(
+ StirrupConfiguration(
+ geometry=Polygon([left_bottom_corner, left_top_corner, right_top_corner, right_bottom_corner]),
+ diameter=diameter,
+ distance=distance,
+ material=material,
+ shear_check=shear_check,
+ torsion_check=torsion_check,
+ mandrel_diameter_factor=mandrel_diameter_factor,
+ anchorage_length=anchorage_length,
+ based_on_cover=True,
+ relative_start_position=relative_start_position,
+ relative_end_position=relative_end_position,
+ )
+ )
+
+ def add_stirrup_in_center(
+ self,
+ width: MM,
+ diameter: MM,
+ distance: MM,
+ material: ReinforcementSteelMaterial,
+ shear_check: bool = True,
+ torsion_check: bool = True,
+ mandrel_diameter_factor: float | None = None,
+ anchorage_length: MM = 0.0,
+ relative_start_position: RATIO = 0.0,
+ relative_end_position: RATIO = 1.0,
+ ) -> StirrupConfiguration:
+ """Add stirrups to the center of the reinforced cross-section based on a given width (ctc of the legs). The created configuration goes around
+ the longitudinal rebars (if any).
+
+ Use .add_stirrup_configuration() to add a stirrup configuration of any shape, size, and position (as long as it is inside the cross-section).
+
+ Parameters
+ ----------
+ width: MM
+ Total width of the stirrup taken from the center lines of the legs [mm].
+ diameter: MM
+ Diameter of the stirrups [mm].
+ distance: MM
+ Longitudinal distance between stirrups [mm].
+ material : ReinforcementSteelMaterial
+ Representation of the properties of reinforcement steel suitable for use with NEN-EN 1992-1-1
+ shear_check: bool
+ Take stirrup into account in shear check
+ torsion_check: bool
+ Take stirrup into account in torsion check
+ mandrel_diameter_factor: DIMENSIONLESS
+ Inner diameter of mandrel as multiple of stirrup diameter [-]
+ (default: 4⌀ for ⌀<=16mm and 5⌀ for ⌀>16mm) Tabel 8.1Na NEN-EN 1992-1-1 Dutch National Annex.
+ anchorage_length: MM
+ Anchorage length [mm]
+ relative_start_position: RATIO
+ Relative position of the start of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1.
+ Default is 0 (start).
+ relative_end_position: RATIO
+ Relative position of the end of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1.
+ Default is 1 (end).
+
+ Returns
+ -------
+ StirrupConfiguration
+ Newly created stirrup configuration inside the cross-section.
+ """
+ # get the corners of the cross-section
+ _, min_y, _, max_y = self.cross_section.geometry.bounds
+
+ # create the corners of the stirrup configuration based on the covers present
+ left_bottom_corner = Point(-width / 2, min_y + self.covers.lower + (diameter / 2))
+ left_top_corner = Point(-width / 2, max_y - self.covers.upper - (diameter / 2))
+ right_top_corner = Point(width / 2, max_y - self.covers.upper - (diameter / 2))
+ right_bottom_corner = Point(width / 2, min_y + self.covers.lower + (diameter / 2))
+
+ return self.add_stirrup_configuration(
+ StirrupConfiguration(
+ geometry=Polygon([left_bottom_corner, left_top_corner, right_top_corner, right_bottom_corner]),
+ diameter=diameter,
+ distance=distance,
+ material=material,
+ shear_check=shear_check,
+ torsion_check=torsion_check,
+ mandrel_diameter_factor=mandrel_diameter_factor,
+ anchorage_length=anchorage_length,
+ based_on_cover=True,
+ relative_start_position=relative_start_position,
+ relative_end_position=relative_end_position,
+ )
+ )
+
+ def _get_reference_line(
+ self,
+ edge: Literal["upper", "right", "lower", "left"],
+ diameter: MM,
+ cover: MM | None = None,
+ corner_offset: MM = 0.0,
+ ) -> LineString:
+ """Get the reference line for the given edge of the cross-section.
+
+ Parameters
+ ----------
+ edge: Literal["upper", "right", "lower", "left"]
+ Edge of the cross-section.
+ diameter: MM
+ Diameter of the rebars [mm].
+ cover: MM, optional
+ Cover of the rebars [mm]. If not provided, the default cover on the given edge of the cross-section is used.
+ corner_offset: MM, optional
+ The offset of the first and last rebars from the corners of the cross-section towards the center of the cross-section [mm]. If not
+ provided, the rebars are to be placed at the corners taking into account the present covers and stirrups inside the cross-section.
+
+ Returns
+ -------
+ LineString
+ Reference line for the given edge of the cross-section.
+ """
+ # get cross-section corners
+ min_x, min_y, max_x, max_y = self.cross_section.geometry.bounds
+
+ # check if a custom cover is provided
+ upper_cover = cover if cover is not None else self.covers.upper
+ lower_cover = cover if cover is not None else self.covers.lower
+
+ # check if there is a stirrup configuration present and adjust the cover
+ max_stirrups_diameter = 0.0
+ if self._stirrups:
+ max_stirrups_diameter = max([stirrup.diameter for stirrup in self._stirrups])
+
+ # define corner positions of a bar inside the cross-section
+ upper_left = (
+ min_x + self.covers.left + max_stirrups_diameter + corner_offset + diameter / 2,
+ max_y - upper_cover - max_stirrups_diameter - diameter / 2,
+ )
+ upper_right = (
+ max_x - self.covers.right - max_stirrups_diameter - corner_offset - diameter / 2,
+ max_y - upper_cover - max_stirrups_diameter - diameter / 2,
+ )
+ lower_left = (
+ min_x + self.covers.left + max_stirrups_diameter + corner_offset + diameter / 2,
+ min_y + lower_cover + max_stirrups_diameter + diameter / 2,
+ )
+ lower_right = (
+ max_x - self.covers.right - max_stirrups_diameter - corner_offset - diameter / 2,
+ min_y + lower_cover + max_stirrups_diameter + diameter / 2,
+ )
+
+ match edge.lower():
+ case "upper":
+ start, end = upper_left, upper_right
+ case "right":
+ start, end = upper_right, lower_right
+ case "lower":
+ start, end = lower_left, lower_right
+ case "left":
+ start, end = upper_left, lower_left
+ case _: # pragma: no cover
+ msg = f"Edge '{edge}' is not supported. Supported edges are 'upper', 'right', 'lower', and 'left'."
+ raise ValueError(msg)
+
+ return LineString([start, end])
+
+ def add_longitudinal_reinforcement_by_quantity(
+ self,
+ n: int,
+ diameter: MM,
+ material: ReinforcementSteelMaterial,
+ edge: Literal["upper", "right", "lower", "left"],
+ cover: MM | None = None,
+ corner_offset: MM = 0.0,
+ ) -> None:
+ """Add longitudinal reinforcement to the cross-section based on the quantity configuration of rebars and a given edge of the cross-section.
+
+ for example: 5⌀12 on upper edge, 4⌀16 on lower edge, etc.
+
+ Parameters
+ ----------
+ n: int
+ Amount of longitudinal bars.
+ diameter: MM
+ Diameter of the rebars [mm].
+ material : ReinforcementSteelMaterial
+ Representation of the properties of reinforcement steel suitable for use with NEN-EN 1992-1-1.
+ edge: Literal["upper", "right", "lower", "left"]
+ Edge of the cross-section where the rebars are placed.
+ cover: MM, optional
+ Cover of the rebars [mm]. If not provided, the default cover on the given edge of the cross-section is used.
+ corner_offset: MM, optional
+ The offset of the first and last rebars from the corners of the cross-section towards the center of the cross-section [mm]. If not
+ provided, the rebars are to be placed at the corners taking into account the present covers and stirrups inside the cross-section.
+ """
+ line = self._get_reference_line(
+ edge=edge,
+ diameter=diameter,
+ cover=cover,
+ corner_offset=corner_offset,
+ )
+ assert line
+ return self.add_reinforcement_configuration(
+ line=self._get_reference_line,
+ configuration=ReinforcementByQuantity(
+ diameter=diameter,
+ material=material,
+ n=n,
+ ),
+ edge=edge,
+ cover=cover,
+ corner_offset=corner_offset,
+ diameter=diameter,
+ )
+
+ def plot(self, *args, **kwargs) -> plt.Figure:
+ """Plot the cross-section. Making use of the standard plotter.
+
+ If you want to use a custom plotter, use the .plotter attribute to plot the cross-section.
+
+ Parameters
+ ----------
+ *args
+ Additional arguments passed to the plotter.
+ **kwargs
+ Additional keyword arguments passed to the plotter.
+ """
+ return self.plotter.plot(*args, **kwargs)
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/reinforcement_configurations.py b/blueprints/structural_sections/concrete/reinforced_concrete_sections/reinforcement_configurations.py
new file mode 100644
index 00000000..e4ba24f1
--- /dev/null
+++ b/blueprints/structural_sections/concrete/reinforced_concrete_sections/reinforcement_configurations.py
@@ -0,0 +1,214 @@
+"""Module for the representation of reinforcement configurations in reinforced concrete sections."""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+
+import numpy as np
+from shapely import LineString
+
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.structural_sections.concrete.rebar import Rebar
+from blueprints.type_alias import DIMENSIONLESS, MM, MM2, MM2_M
+from blueprints.unit_conversion import M_TO_MM
+
+
+@dataclass(frozen=True)
+class ReinforcementConfiguration(ABC):
+ """Base class of all reinforcement configurations.
+
+ Parameters
+ ----------
+ diameter : MM
+ Diameter of the rebar [mm].
+ material : ReinforcementSteelMaterial
+ Representation of the properties of reinforcement steel suitable for use with NEN-EN 1992-1-1.
+ """
+
+ diameter: MM
+ material: ReinforcementSteelMaterial
+
+ def __post_init__(self) -> None:
+ """Post initialization of the reinforcement configuration."""
+ # diameter is a positive number
+ if self.diameter <= 0:
+ msg = f"Diameter of the rebar must be a positive number, got {self.diameter}"
+ raise ValueError(msg)
+
+ @property
+ @abstractmethod
+ def area(self) -> MM2:
+ """Each reinforcement configuration must have a resulting area."""
+
+ @abstractmethod
+ def to_rebars(self, line: LineString) -> list[Rebar]:
+ """Convert the reinforcement configuration to a list of rebars.
+
+ Parameters
+ ----------
+ line : LineString
+ Representing the path of the reinforcement in the section.
+ Start of the line defines the first rebar of the configuration, end of the line defines the last rebar.
+
+ Returns
+ -------
+ List[Rebar]
+ List of Rebar objects.
+ """
+
+
+@dataclass(kw_only=True, frozen=True)
+class ReinforcementByDistance(ReinforcementConfiguration):
+ """Representation of a reinforcement configuration given by center-to-center distance.
+ For example ⌀16-150, ⌀20-200, ⌀25-250, ⌀32-300, etc.
+
+ Parameters
+ ----------
+ center_to_center : MM
+ Maximum center-to-center distance between rebars [mm].
+ """
+
+ center_to_center: MM
+
+ def __post_init__(self) -> None:
+ """Post initialization of the reinforcement configuration."""
+ super().__post_init__()
+ self._validations()
+
+ def _validations(self) -> None:
+ """Validation of the reinforcement configuration."""
+ # center-to-center is at least the diameter
+ if self.center_to_center < self.diameter:
+ msg = f"Center-to-center distance must be at least the diameter of the rebar, got {self.center_to_center}"
+ raise ValueError(msg)
+
+ @property
+ def area(self) -> MM2_M:
+ """Area of the reinforcement configuration per meter [mm²/m]."""
+ return 0.25 * np.pi * self.diameter**2 * (M_TO_MM / self.center_to_center)
+
+ @property
+ def n_rebars_per_meter(self) -> DIMENSIONLESS:
+ """Number of rebars per meter [1/m]."""
+ return 1.0 * M_TO_MM / self.center_to_center
+
+ def to_rebars(self, line: LineString) -> list[Rebar]:
+ """Convert the reinforcement configuration to a list of rebars.
+
+ Parameters
+ ----------
+ line : LineString
+ Representing the path of the reinforcement in the section.
+ Start of the line defines the first rebar of the configuration, end of the line defines the last rebar.
+
+ Returns
+ -------
+ List[Rebar]
+ List of Rebar objects.
+ """
+ # max(int(n), 1) is used to ensure that at least one rebar is placed
+ rebars = []
+
+ # define the number of rebars based on the length of the line, minimum 1
+ n_rebars = line.length / self.center_to_center
+ n_rebars_applied = max(int(n_rebars), 1)
+
+ # calculate the space between the rebars
+ side_buffer = (line.length - (n_rebars_applied - 1) * self.center_to_center) / 2
+ distances = np.linspace(start=side_buffer, stop=line.length - side_buffer, num=n_rebars_applied)
+
+ # define the representative diameter of the rebar
+ reinforcement_area = 0.25 * np.pi * self.diameter**2 * n_rebars
+ repr_diameter = np.sqrt(reinforcement_area / (0.25 * np.pi * n_rebars_applied))
+
+ for distance in distances:
+ point = line.interpolate(distance)
+ rebars.append(
+ Rebar(
+ diameter=repr_diameter,
+ x=point.x,
+ y=point.y,
+ material=self.material,
+ )
+ )
+ return rebars
+
+ def __repr__(self) -> str:
+ """Representation of the reinforcement configuration."""
+ return f"{self.__class__.__name__}|{self!s}|{self.area:.0f} mm²/m"
+
+ def __str__(self) -> str:
+ """String representation of the reinforcement configuration."""
+ return f"⌀{self.diameter:.0f}-{self.center_to_center:.0f}"
+
+
+@dataclass(kw_only=True, frozen=True)
+class ReinforcementByQuantity(ReinforcementConfiguration):
+ """Representation of a reinforcement configuration given by quantity of rebars.
+ For example 4⌀16, 6⌀20, 8⌀25, 10⌀32, etc.
+
+ Parameters
+ ----------
+ n : int
+ Amount of longitudinal bars.
+ """
+
+ n: int
+
+ def __post_init__(self) -> None:
+ """Post initialization of the reinforcement configuration."""
+ super().__post_init__()
+ self._validations()
+
+ def _validations(self) -> None:
+ """Validation of the reinforcement configuration."""
+ # check that n is an integer
+ if not isinstance(self.n, int):
+ msg = f"Number of rebars must be an integer, got {self.n}"
+ raise TypeError(msg)
+
+ # check that n is at least 2
+ minimum_number_of_rebars = 1
+ if self.n < minimum_number_of_rebars:
+ msg = f"Number of rebars must be at least {minimum_number_of_rebars}, got {self.n}"
+ raise ValueError(msg)
+
+ @property
+ def area(self) -> MM2:
+ """Area of the reinforcement configuration [mm²]."""
+ return 0.25 * np.pi * self.diameter**2 * self.n
+
+ def to_rebars(self, line: LineString) -> list[Rebar]:
+ """Convert the reinforcement configuration to a list of rebars.
+
+ Parameters
+ ----------
+ line : LineString
+ Representing the path of the reinforcement in the section.
+ Start of the line defines the first rebar of the configuration, end of the line defines the last rebar.
+
+ Returns
+ -------
+ List[Rebar]
+ List of Rebar objects.
+ """
+ rebars = []
+ for index in range(self.n):
+ distance = index * line.length / (self.n - 1)
+ point = line.interpolate(distance)
+ rebars.append(
+ Rebar(
+ diameter=self.diameter,
+ x=point.x,
+ y=point.y,
+ material=ReinforcementSteelMaterial(),
+ )
+ )
+ return rebars
+
+ def __repr__(self) -> str:
+ """Representation of the reinforcement by quantity."""
+ return f"{self.__class__.__name__}|{self!s}|{self.area:.0f} mm²"
+
+ def __str__(self) -> str:
+ """String representation of the reinforcement by quantity."""
+ return f"{self.n}⌀{self.diameter:.0f}"
diff --git a/blueprints/structural_sections/concrete/stirrups.py b/blueprints/structural_sections/concrete/stirrups.py
new file mode 100644
index 00000000..4a6316d3
--- /dev/null
+++ b/blueprints/structural_sections/concrete/stirrups.py
@@ -0,0 +1,158 @@
+"""Stirrups module."""
+
+import numpy as np
+from shapely import Point, Polygon
+from shapely.geometry.polygon import orient
+
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.type_alias import DIMENSIONLESS, KG_M3, MM, MM2, MM2_M, RATIO
+from blueprints.unit_conversion import M_TO_MM, MM3_TO_M3
+
+STIRRUP_COLOR = (0.412, 0.412, 0.412)
+
+
+class StirrupConfiguration:
+ """Representation of a stirrup configuration.
+
+ Parameters
+ ----------
+ geometry: Polygon
+ Line that represents the center-line of the stirrup configuration (clockwise or counterclockwise).
+ diameter: MM
+ Diameter of the rebar making the stirrup [mm].
+ distance: MM
+ Longitudinal distance between stirrups [mm].
+ material: ReinforcementSteelMaterial
+ Reinforcement material.
+ shear_check: bool
+ Take stirrup into account in shear check
+ torsion_check: bool
+ Take stirrup into account in torsion check
+ mandrel_diameter_factor: MM
+ Inner diameter of mandrel as multiple of stirrup diameter [-]
+ (default: 4⌀ for ⌀<=16mm and 5⌀ for ⌀>16mm) Tabel 8.1Na NEN-EN 1992-1-1 Dutch National Annex.
+ anchorage_length: MM
+ Anchorage length [mm]
+ based_on_cover: bool
+ Default is False. This helps to categorise stirrups that a created based on the covers present in the cross-section.
+ relative_start_position: RATIO
+ Relative position of the start of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1.
+ Default is 0 (start).
+ relative_end_position: RATIO
+ Relative position of the end of the stirrup configuration inside the cross-section (longitudinal direction). Value between 0 and 1. Default
+ is 1 (end).
+ """
+
+ counter = 1
+
+ def __init__( # noqa: PLR0913
+ self,
+ geometry: Polygon,
+ diameter: MM,
+ distance: MM,
+ material: ReinforcementSteelMaterial,
+ shear_check: bool = True,
+ torsion_check: bool = True,
+ mandrel_diameter_factor: DIMENSIONLESS | None = None,
+ anchorage_length: MM = 0.0,
+ based_on_cover: bool = False,
+ relative_start_position: RATIO = 0.0,
+ relative_end_position: RATIO = 1.0,
+ n_vertices_used: int = 4,
+ cover_used: MM | None = None,
+ ) -> None:
+ """Initialisation of the stirrup."""
+ self.geometry = orient(polygon=geometry)
+ self.diameter = diameter
+ self.distance = distance
+ self.material = material
+ self.shear_check = shear_check
+ self.torsion_check = torsion_check
+ self.anchorage_length = anchorage_length
+ self._mandrel_diameter_factor = mandrel_diameter_factor
+ self.based_on_cover = based_on_cover
+ self._id = StirrupConfiguration.counter
+ self._validation_relative_position(relative_position=relative_start_position)
+ self._validation_relative_position(relative_position=relative_end_position)
+ self._relative_start_position = relative_start_position
+ self._relative_end_position = relative_end_position
+ self.n_vertices_used = n_vertices_used
+ self._cover_used = cover_used
+ self._amount_of_legs = 2
+ StirrupConfiguration.counter += 1
+
+ @property
+ def mandrel_diameter_factor(self) -> DIMENSIONLESS:
+ """Diameter factor of mandrel.
+ Standard values given by Dutch Annex Table 8.1Na - NEN-EN 1992-1-1+C2:2011/NB+A1:2020
+ (default: 4⌀ for ⌀<=16mm and 5⌀ for ⌀>16mm).
+ """
+ if self._mandrel_diameter_factor:
+ return self._mandrel_diameter_factor
+ return 5.0 if self.diameter > 16.0 else 4.0
+
+ @property
+ def as_w(self) -> MM2_M:
+ """Total cross-sectional area of the stirrup [mm²/m]."""
+ return self._amount_of_legs * self.area * (M_TO_MM / self.distance)
+
+ @property
+ def area(self) -> MM2:
+ """Area of the stirrup bar [mm²]."""
+ return 0.25 * np.pi * self.diameter**2
+
+ @property
+ def radius(self) -> MM:
+ """Radius of the stirrup bar [mm]."""
+ return self.diameter / 2
+
+ @property
+ def centroid(self) -> Point:
+ """Centroid of the stirrup bar [mm]."""
+ return self.geometry.centroid
+
+ @property
+ def weight_per_meter(self) -> KG_M3:
+ """Total mass of the stirrup per meter length in the longitudinal direction (concrete+reinforcement) [kg/m³]
+ (Weight of a single stirrup x amount of stirrups present in one meter length).
+ """
+ return self.material.density * self.geometry.length * self.area * MM3_TO_M3 * M_TO_MM / self.distance
+
+ @property
+ def ctc_distance_legs(self) -> MM:
+ """Distance between the legs of the stirrup taken form the center lines of the rebar [mm]."""
+ min_x, max_x = self.geometry.bounds[0], self.geometry.bounds[2]
+ return max_x - min_x
+
+ @property
+ def cover_used(self) -> float:
+ """Can be used to store the value of the cover used when adding the stirrup to the cross-section [mm]."""
+ return self._cover_used or 0.0
+
+ @property
+ def relative_start_position(self) -> RATIO:
+ """Relative position of the start of the stirrup configuration inside the cross-section. Value between 0 and 1."""
+ return self._relative_start_position
+
+ @property
+ def relative_end_position(self) -> RATIO:
+ """Relative position of the end of the stirrup configuration inside the cross-section. Value between 0 and 1."""
+ return self._relative_end_position
+
+ @staticmethod
+ def _validation_relative_position(relative_position: DIMENSIONLESS) -> None:
+ """Validation of the relative position of the stirrup."""
+ if relative_position < 0.0:
+ msg = "Relative position of the stirrup must be greater than or equal to zero"
+ raise ValueError(msg)
+ if relative_position > 1.0:
+ msg = "Relative position of the stirrup must be less than or equal to one"
+ raise ValueError(msg)
+
+ def __repr__(self) -> str:
+ """Representation of the stirrup."""
+ return f"Stirrup (id={self._id})|⌀{self.diameter}/{self.material.name}"
+
+ def __str__(self) -> str:
+ """String representation of the stirrup."""
+ return f"Stirrups ⌀{self.diameter}-{self.distance:.0f} mm | {self.material.name} | {self.as_w:.2f} mm²/m"
diff --git a/blueprints/structural_sections/concrete/reinforced_concrete_sections/cross_section_shapes.py b/blueprints/structural_sections/cross_section_shapes.py
similarity index 100%
rename from blueprints/structural_sections/concrete/reinforced_concrete_sections/cross_section_shapes.py
rename to blueprints/structural_sections/cross_section_shapes.py
diff --git a/blueprints/type_alias.py b/blueprints/type_alias.py
index cc317f4d..bffa79bf 100644
--- a/blueprints/type_alias.py
+++ b/blueprints/type_alias.py
@@ -38,6 +38,7 @@
#
MM2 = float
+MM2_M = float
CM2 = float
DM2 = float
M2 = float
@@ -48,6 +49,7 @@
MM3 = float
CM3 = float
M3 = float
+M3_M = float
#
#
diff --git a/docs/DOCS_README.md b/docs/DOCS_README.md
new file mode 100644
index 00000000..b089b555
--- /dev/null
+++ b/docs/DOCS_README.md
@@ -0,0 +1,11 @@
+# How to make Blueprints docs?
+
+## 1. Install requirements
+```bash
+pip install requirements_docs.txt
+```
+
+## 2. Run the script
+```bash
+docs\make html
+```
\ No newline at end of file
diff --git a/docs/data/__init__.py b/docs/data/__init__.py
deleted file mode 100644
index 0d9f9f6a..00000000
--- a/docs/data/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Data package for the documentation."""
diff --git a/docs/data/images/__init__.py b/docs/data/images/__init__.py
deleted file mode 100644
index 94168924..00000000
--- a/docs/data/images/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Images for the documentation."""
diff --git a/docs/source/data/images/rectangular_reinforced_cross_section.png b/docs/source/data/images/rectangular_reinforced_cross_section.png
new file mode 100644
index 00000000..cc888473
Binary files /dev/null and b/docs/source/data/images/rectangular_reinforced_cross_section.png differ
diff --git a/docs/source/data/images/rectangular_reinforced_cross_section_custom.png b/docs/source/data/images/rectangular_reinforced_cross_section_custom.png
new file mode 100644
index 00000000..6be5ac94
Binary files /dev/null and b/docs/source/data/images/rectangular_reinforced_cross_section_custom.png differ
diff --git a/docs/source/examples.rst b/docs/source/examples.rst
new file mode 100644
index 00000000..7f1be1e1
--- /dev/null
+++ b/docs/source/examples.rst
@@ -0,0 +1,23 @@
+Examples
+========
+
+Here are some examples of how to use the 'Blueprints'.
+
+Make a Rectangular Reinforced Cross-section
+-------------------------------------------
+Simply create a rectangular cross-section and add some reinforcement:
+
+.. literalinclude:: examples\rectangular_reinforced_cross_section.py
+
+.. image:: data/images/rectangular_reinforced_cross_section.png
+ :alt: Rectangular Reinforced Cross-section
+ :align: center
+
+
+And just to show that you can create any RCS you like, here is a custom one:
+
+.. literalinclude:: examples\rectangular_reinforced_cross_section_custom.py
+
+.. image:: data/images/rectangular_reinforced_cross_section_custom.png
+ :alt: Rectangular Reinforced Cross-section
+ :align: center
\ No newline at end of file
diff --git a/examples/__init__.py b/docs/source/examples/__init__.py
similarity index 100%
rename from examples/__init__.py
rename to docs/source/examples/__init__.py
diff --git a/docs/source/examples/rectangular_reinforced_cross_section.py b/docs/source/examples/rectangular_reinforced_cross_section.py
new file mode 100644
index 00000000..6bdad778
--- /dev/null
+++ b/docs/source/examples/rectangular_reinforced_cross_section.py
@@ -0,0 +1,61 @@
+"""Reinforced concrete cross-section example."""
+
+from blueprints.materials.concrete import ConcreteMaterial, ConcreteStrengthClass
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial, ReinforcementSteelQuality
+from blueprints.structural_sections.concrete.covers import CoversRectangular
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.rectangular import RectangularReinforcedCrossSection
+
+# Define a concrete material
+concrete = ConcreteMaterial(concrete_class=ConcreteStrengthClass.C35_45)
+
+# Define a reinforcement steel material
+steel = ReinforcementSteelMaterial(steel_quality=ReinforcementSteelQuality.B500B)
+
+# Define a rectangular reinforced cross-section
+cs = RectangularReinforcedCrossSection(
+ width=1000,
+ height=800,
+ covers=CoversRectangular(upper=45, right=30, lower=35, left=50),
+ concrete_material=concrete,
+)
+
+# Add reinforcement to the cross-section
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ edge="upper",
+ material=steel,
+)
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=4,
+ diameter=40,
+ edge="lower",
+ material=steel,
+)
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ edge="right",
+ material=steel,
+)
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ edge="left",
+ material=steel,
+)
+
+# Add stirrups to the cross-section
+cs.add_stirrup_along_edges(
+ diameter=8,
+ distance=150,
+ material=steel,
+)
+# Add stirrups to the cross-section
+cs.add_stirrup_along_edges(
+ diameter=12,
+ distance=300,
+ material=steel,
+)
+
+cs.plot(show=True)
diff --git a/docs/source/examples/rectangular_reinforced_cross_section_custom.py b/docs/source/examples/rectangular_reinforced_cross_section_custom.py
new file mode 100644
index 00000000..05f6c2d0
--- /dev/null
+++ b/docs/source/examples/rectangular_reinforced_cross_section_custom.py
@@ -0,0 +1,78 @@
+"""Custom rectangular reinforced cross-section example."""
+
+from shapely import LineString, Polygon
+
+from blueprints.materials.concrete import ConcreteMaterial, ConcreteStrengthClass
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial, ReinforcementSteelQuality
+from blueprints.structural_sections.concrete.covers import CoversRectangular
+from blueprints.structural_sections.concrete.rebar import Rebar
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.rectangular import RectangularReinforcedCrossSection
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.reinforcement_configurations import (
+ ReinforcementByDistance,
+ ReinforcementByQuantity,
+)
+from blueprints.structural_sections.concrete.stirrups import StirrupConfiguration
+
+# Define a concrete material
+concrete = ConcreteMaterial(concrete_class=ConcreteStrengthClass.C30_37)
+
+# Define a reinforcement steel material
+steel = ReinforcementSteelMaterial(steel_quality=ReinforcementSteelQuality.B500B)
+
+# Define a rectangular reinforced cross-section
+cs = RectangularReinforcedCrossSection(
+ width=600,
+ height=500,
+ covers=CoversRectangular(upper=45, right=30, lower=35, left=50),
+ concrete_material=concrete,
+)
+
+# Add reinforcement to the cross-section
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ edge="upper",
+ material=steel,
+)
+
+# add a second layer of reinforcement to the cross-section
+cs.add_longitudinal_reinforcement_by_quantity(
+ n=3,
+ diameter=16,
+ edge="upper",
+ material=steel,
+ cover=100,
+)
+
+# add reinforcement configurations to the cross-section in any position
+cs.add_reinforcement_configuration(
+ line=LineString([(50, -100), (150, 50)]),
+ configuration=ReinforcementByQuantity(diameter=20, n=3, material=steel),
+)
+
+cs.add_reinforcement_configuration(
+ line=LineString([(0, -180), (-250, 0)]),
+ configuration=ReinforcementByDistance(diameter=12, center_to_center=40, material=steel),
+)
+
+# Add stirrups to the cross-section
+cs.add_stirrup_configuration(
+ stirrup=StirrupConfiguration(
+ geometry=Polygon([(-200, -200), (-200, 200), (200, 200), (200, -200)]),
+ diameter=8,
+ distance=150,
+ material=steel,
+ ),
+)
+
+# Add a longitudinal rebar to the cross-section
+cs.add_longitudinal_rebar(
+ rebar=Rebar(
+ diameter=12,
+ x=0,
+ y=0,
+ material=steel,
+ )
+)
+
+cs.plot(show=True)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 3e40eac8..9ddcfe0d 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -2,6 +2,7 @@
:hidden:
:maxdepth: 2
+ examples
api
.. image:: _static/blueprints_banner.png
diff --git a/tests/structural_sections/concrete/reinforced_concrete_sections/test_cross_section_shapes.py b/tests/structural_sections/concrete/reinforced_concrete_sections/test_cross_section_shapes.py
index b39f57c5..0d5b020d 100644
--- a/tests/structural_sections/concrete/reinforced_concrete_sections/test_cross_section_shapes.py
+++ b/tests/structural_sections/concrete/reinforced_concrete_sections/test_cross_section_shapes.py
@@ -1,8 +1,9 @@
-"""Tests for cross section shapes."""
+"""Tests for cross-section shapes."""
import pytest
+from shapely import Polygon
-from blueprints.structural_sections.concrete.reinforced_concrete_sections.cross_section_shapes import CircularCrossSection, RectangularCrossSection
+from blueprints.structural_sections.cross_section_shapes import CircularCrossSection, RectangularCrossSection
class TestCircularCrossSection:
@@ -13,6 +14,10 @@ def circular_cross_section(self) -> CircularCrossSection:
"""Return a CircularCrossSection instance."""
return CircularCrossSection(diameter=200.0, x=0.0, y=0.0)
+ def test_geometry(self, circular_cross_section: CircularCrossSection) -> None:
+ """Test the geometry property of the CircularCrossSection class."""
+ assert isinstance(circular_cross_section.geometry, Polygon)
+
def test_area(self, circular_cross_section: CircularCrossSection) -> None:
"""Test the area property of the CircularCrossSection class."""
assert circular_cross_section.area == pytest.approx(expected=31415.92653, rel=1e-6)
@@ -35,6 +40,11 @@ def test_vertices(self, circular_cross_section: CircularCrossSection) -> None:
assert (first_vertex.x, first_vertex.y) == pytest.approx(expected=(100.0, 0.0), rel=1e-6)
assert (last_vertex.x, last_vertex.y) == pytest.approx(expected=(100.0, 0.0), rel=1e-6)
+ def test_wrong_input(self) -> None:
+ """Test the wrong input for the CircularCrossSection class."""
+ with pytest.raises(ValueError):
+ CircularCrossSection(diameter=-200.0, x=0.0, y=0.0)
+
class TestRectangularCrossSection:
"""Tests for the RectangularCrossSection class."""
diff --git a/tests/structural_sections/concrete/reinforced_concrete_sections/test_rectangular.py b/tests/structural_sections/concrete/reinforced_concrete_sections/test_rectangular.py
new file mode 100644
index 00000000..71b8bd34
--- /dev/null
+++ b/tests/structural_sections/concrete/reinforced_concrete_sections/test_rectangular.py
@@ -0,0 +1,215 @@
+"""Tests for Rectangular Reinforced Concrete Sections."""
+
+from typing import Literal
+
+import pytest
+from matplotlib import pyplot as plt
+from shapely import LineString, Polygon
+
+from blueprints.materials.concrete import ConcreteMaterial, ConcreteStrengthClass
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial, ReinforcementSteelQuality
+from blueprints.structural_sections.concrete.covers import CoversRectangular
+from blueprints.structural_sections.concrete.rebar import Rebar
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.rectangular import RectangularReinforcedCrossSection
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.reinforcement_configurations import ReinforcementByQuantity
+from blueprints.structural_sections.concrete.stirrups import StirrupConfiguration
+
+
+class TestRectangularReinforcedCrossSection:
+ """Tests for the RectangularReinforcedCrossSection class."""
+
+ @pytest.fixture
+ def rectangular_reinforced_cross_section(self) -> RectangularReinforcedCrossSection:
+ """Return a rectangular reinforced cross-section."""
+ # Define a concrete material
+ concrete = ConcreteMaterial(concrete_class=ConcreteStrengthClass.C35_45)
+
+ # Define a reinforcement steel material
+ steel = ReinforcementSteelMaterial(steel_quality=ReinforcementSteelQuality.B500B)
+
+ # Define a rectangular reinforced cross-section
+ cs = RectangularReinforcedCrossSection(
+ width=1000,
+ height=800,
+ covers=CoversRectangular(upper=45, right=30, lower=35, left=50),
+ concrete_material=concrete,
+ )
+
+ # Add reinforcement to the cross-section
+ cs.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ edge="upper",
+ material=steel,
+ )
+ cs.add_longitudinal_reinforcement_by_quantity(
+ n=4,
+ diameter=40,
+ edge="lower",
+ material=steel,
+ )
+
+ # Add stirrups to the cross-section
+ cs.add_stirrup_along_edges(
+ diameter=8,
+ distance=150,
+ material=steel,
+ )
+ # Add stirrups to the cross-section
+ cs.add_stirrup_along_edges(
+ diameter=12,
+ distance=300,
+ material=steel,
+ )
+
+ # Add a longitudinal rebar to the cross-section
+ cs.add_longitudinal_rebar(
+ rebar=Rebar(
+ diameter=12,
+ x=-250,
+ y=-100,
+ material=steel,
+ )
+ )
+
+ return cs
+
+ def test_add_stirrup_along_edges(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_stirrup_along_edges method."""
+ stirrup = rectangular_reinforced_cross_section.add_stirrup_along_edges(
+ diameter=8,
+ distance=150,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ )
+ assert stirrup in rectangular_reinforced_cross_section.stirrups
+
+ def test_add_stirrup_in_center(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_stirrup_in_center method."""
+ stirrup = rectangular_reinforced_cross_section.add_stirrup_in_center(
+ width=200,
+ diameter=8,
+ distance=150,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ )
+ assert stirrup in rectangular_reinforced_cross_section.stirrups
+
+ @pytest.mark.parametrize(
+ "edge",
+ [
+ "upper",
+ "right",
+ "lower",
+ "left",
+ ],
+ )
+ def test_add_longitudinal_reinforcement_by_quantity(
+ self,
+ rectangular_reinforced_cross_section: RectangularReinforcedCrossSection,
+ edge: Literal["upper", "right", "lower", "left"],
+ ) -> None:
+ """Test the add_longitudinal_reinforcement_by_quantity method."""
+ rectangular_reinforced_cross_section.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ edge=edge,
+ cover=23,
+ corner_offset=40,
+ )
+ assert len(rectangular_reinforced_cross_section.longitudinal_rebars) == 15
+
+ def test_add_longitudinal_reinforcement_by_quantity_wrong_edge(
+ self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection
+ ) -> None:
+ """Test the add_longitudinal_reinforcement_by_quantity method with wrong edge."""
+ with pytest.raises(ValueError):
+ rectangular_reinforced_cross_section.add_longitudinal_reinforcement_by_quantity(
+ n=5,
+ diameter=14,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ edge="wrong", # type: ignore[arg-type]
+ )
+
+ def test_plot(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the plot method."""
+ plot = rectangular_reinforced_cross_section.plot(show=False, center_line_style={"linewidth": 0.85})
+ assert isinstance(plot, plt.Figure)
+
+ def test_reinforcement_weight_longitudinal_bars(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the reinforcement_weight_longitudinal_bars method."""
+ expected_weight = 46.38828588400884 # kg/m
+ assert rectangular_reinforced_cross_section.reinforcement_weight_longitudinal_bars == pytest.approx(expected=expected_weight, rel=1e-4)
+
+ def test_reinforcement_weight_stirrups(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the reinforcement_weight_stirrups method."""
+ expected_weight = 18.1087 # kg/m
+ assert rectangular_reinforced_cross_section.reinforcement_weight_stirrups == pytest.approx(expected=expected_weight, rel=1e-4)
+
+ def test_reinforcement_weight(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the reinforcement_weight method."""
+ expected_weight = 64.4969 # kg/m
+ assert rectangular_reinforced_cross_section.reinforcement_weight == pytest.approx(expected=expected_weight, rel=1e-4)
+
+ def test_reinforcement_area_longitudinal_bars(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the reinforcement_area_longitudinal_bars method."""
+ expected_area = 5909.3357 # mm²/m
+ assert rectangular_reinforced_cross_section.reinforcement_area_longitudinal_bars == pytest.approx(expected=expected_area, rel=1e-4)
+
+ def test_concrete_volume(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the concrete_volume method."""
+ expected_volume = 0.8 # m³/m
+ assert rectangular_reinforced_cross_section.concrete_volume == pytest.approx(expected=expected_volume, rel=1e-4)
+
+ def test_weight_per_volume(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the weight_per_volume method."""
+ expected_weight_per_volume = 80.6211 # kg/m³
+ assert rectangular_reinforced_cross_section.weight_per_volume == pytest.approx(expected=expected_weight_per_volume, rel=1e-4)
+
+ def test_add_longitudinal_rebar_wrong_position(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_longitudinal_rebar method with wrong position."""
+ with pytest.raises(ValueError):
+ rectangular_reinforced_cross_section.add_longitudinal_rebar(
+ rebar=Rebar(
+ diameter=12,
+ x=2500,
+ y=1000,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ )
+ )
+
+ def test_add_stirrup_configuration_wrong_position(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_stirrup_configuration method with wrong position."""
+ with pytest.raises(ValueError):
+ rectangular_reinforced_cross_section.add_stirrup_configuration(
+ stirrup=StirrupConfiguration(
+ geometry=Polygon([(0, 0), (0, 100), (5000, 5000), (100, 0)]),
+ diameter=8,
+ distance=150,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ ),
+ )
+
+ def test_add_reinforcement_configuration_by_linestring(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_reinforcement_configuration method with a linestring."""
+ linestring = LineString([(-300, 200), (300, 200)])
+ rectangular_reinforced_cross_section.add_reinforcement_configuration(
+ line=linestring,
+ configuration=ReinforcementByQuantity(
+ diameter=12,
+ n=3,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ ),
+ )
+ assert len(rectangular_reinforced_cross_section.longitudinal_rebars) == 13
+
+ def test_rebar_not_in_cross_section(self, rectangular_reinforced_cross_section: RectangularReinforcedCrossSection) -> None:
+ """Test the add_longitudinal_rebar method with a rebar not in the cross-section."""
+ rebar = Rebar(
+ diameter=12,
+ x=2500,
+ y=1000,
+ material=rectangular_reinforced_cross_section.get_present_steel_materials()[0],
+ )
+ rectangular_reinforced_cross_section._single_longitudinal_rebars.append(rebar) # noqa: SLF001
+ with pytest.raises(ValueError):
+ _ = rectangular_reinforced_cross_section.longitudinal_rebars
diff --git a/tests/structural_sections/concrete/reinforced_concrete_sections/test_reinforcement_configurations.py b/tests/structural_sections/concrete/reinforced_concrete_sections/test_reinforcement_configurations.py
new file mode 100644
index 00000000..5e065193
--- /dev/null
+++ b/tests/structural_sections/concrete/reinforced_concrete_sections/test_reinforcement_configurations.py
@@ -0,0 +1,108 @@
+"""Tests reinforcement configurations."""
+
+from typing import Type
+
+import pytest
+from shapely import LineString
+
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.structural_sections.concrete.reinforced_concrete_sections.reinforcement_configurations import (
+ ReinforcementByDistance,
+ ReinforcementByQuantity,
+)
+
+
+class TestReinforcementByDistance:
+ """Tests for the reinforcement by distance configuration."""
+
+ @pytest.fixture
+ def reinforcement_by_distance(self) -> ReinforcementByDistance:
+ """Creates a reinforcement by distance configuration."""
+ return ReinforcementByDistance(
+ diameter=12,
+ center_to_center=100,
+ material=ReinforcementSteelMaterial(),
+ )
+
+ def test_n_rebars_per_meter(self, reinforcement_by_distance: ReinforcementByDistance) -> None:
+ """Test the number of rebars per meter."""
+ n_rebars_per_meter = reinforcement_by_distance.n_rebars_per_meter
+ assert n_rebars_per_meter == 10
+
+ def test_area(self, reinforcement_by_distance: ReinforcementByDistance) -> None:
+ """Test the area of the reinforcement."""
+ area = reinforcement_by_distance.area
+ assert area == pytest.approx(expected=1130.9733, rel=1e-4)
+
+ def test__repr__(self, reinforcement_by_distance: ReinforcementByDistance) -> None:
+ """Test the representation of the reinforcement."""
+ representation = repr(reinforcement_by_distance)
+ assert representation == "ReinforcementByDistance|⌀12-100|1131 mm²/m"
+
+ def test_wrong_ctc(self) -> None:
+ """Test the wrong center-to-center distance."""
+ with pytest.raises(ValueError):
+ ReinforcementByDistance(
+ diameter=12,
+ center_to_center=0,
+ material=ReinforcementSteelMaterial(),
+ )
+
+ def test_to_rebars(self, reinforcement_by_distance: ReinforcementByDistance) -> None:
+ """Test the conversion to rebars."""
+ line = LineString([(0, 0), (1000, 0)])
+ rebars = reinforcement_by_distance.to_rebars(line=line)
+ assert len(rebars) == 10
+ assert all(rebar.diameter == 12 for rebar in rebars)
+ assert all(rebar.material == ReinforcementSteelMaterial() for rebar in rebars)
+
+
+class TestReinforcementByQuantity:
+ """Tests for the reinforcement by quantity configuration."""
+
+ @pytest.fixture
+ def reinforcement_by_quantity(self) -> ReinforcementByQuantity:
+ """Creates a reinforcement by quantity configuration."""
+ return ReinforcementByQuantity(
+ diameter=12,
+ material=ReinforcementSteelMaterial(),
+ n=10,
+ )
+
+ def test_area(self, reinforcement_by_quantity: ReinforcementByQuantity) -> None:
+ """Test the area of the reinforcement."""
+ area = reinforcement_by_quantity.area
+ assert area == pytest.approx(expected=1130.9733, rel=1e-4)
+
+ @pytest.mark.parametrize("diameter", [0, -1])
+ def test_wrong_diameter(self, diameter: int) -> None:
+ """Test the wrong diameter."""
+ with pytest.raises(ValueError):
+ ReinforcementByQuantity(
+ diameter=diameter,
+ material=ReinforcementSteelMaterial(),
+ n=10,
+ )
+
+ @pytest.mark.parametrize(("wrong_n", "expected_error"), [(0, ValueError), (-1, ValueError), (1.5, TypeError)])
+ def test_wrong_n_float(self, wrong_n: int, expected_error: Type[BaseException]) -> None:
+ """Test the wrong number of rebars."""
+ with pytest.raises(expected_error):
+ ReinforcementByQuantity(
+ diameter=12,
+ material=ReinforcementSteelMaterial(),
+ n=wrong_n,
+ )
+
+ def test_to_rebars(self, reinforcement_by_quantity: ReinforcementByQuantity) -> None:
+ """Test the conversion to rebars."""
+ line = LineString([(0, 0), (1000, 0)])
+ rebars = reinforcement_by_quantity.to_rebars(line=line)
+ assert len(rebars) == 10
+ assert all(rebar.diameter == 12 for rebar in rebars)
+ assert all(rebar.material == ReinforcementSteelMaterial() for rebar in rebars)
+
+ def test__repr__(self, reinforcement_by_quantity: ReinforcementByQuantity) -> None:
+ """Test the representation of the reinforcement."""
+ representation = repr(reinforcement_by_quantity)
+ assert representation == "ReinforcementByQuantity|10⌀12|1131 mm²"
diff --git a/tests/structural_sections/concrete/test_covers.py b/tests/structural_sections/concrete/test_covers.py
new file mode 100644
index 00000000..1e5e7868
--- /dev/null
+++ b/tests/structural_sections/concrete/test_covers.py
@@ -0,0 +1,53 @@
+"""Tests for CoversRectangular class."""
+
+import pytest
+
+from blueprints.structural_sections.concrete.covers import CoversRectangular
+from blueprints.validations import NegativeValueError
+
+
+class TestCoversRectangular:
+ """Tests for the CoversRectangular class."""
+
+ def test_get_covers_info_with_all_equal_covers(self) -> None:
+ """Test if the method `get_covers_info` returns the correct string when all covers are equal."""
+ covers = CoversRectangular(upper=50.0, right=50.0, lower=50.0, left=50.0)
+ assert covers.get_covers_info() == "Cover: 50 mm"
+
+ def test_get_covers_info_with_different_covers(self) -> None:
+ """Test if the method `get_covers_info` returns the correct string when all covers are different."""
+ covers = CoversRectangular(upper=40.0, right=50.0, lower=60.0, left=70.0)
+ expected_output = "Cover:\n upper: 40 mm\n right: 50 mm\n lower: 60 mm\n left: 70 mm"
+ assert covers.get_covers_info() == expected_output
+
+ def test_get_covers_info_with_two_equal_covers(self) -> None:
+ """Test if the method `get_covers_info` returns the correct string when two covers are equal."""
+ # Upper and left covers are equal
+ covers = CoversRectangular(upper=50.0, right=60.0, lower=50.0, left=70.0)
+ expected_output = "Cover:\n upper|lower: 50 mm\n right: 60 mm\n left: 70 mm"
+ assert covers.get_covers_info() == expected_output
+
+ # Upper and right covers are equal adnd lower and left covers are equal
+ covers = CoversRectangular(upper=50.0, right=50.0, lower=60.0, left=60.0)
+ expected_output = "Cover:\n upper|right: 50 mm\n lower|left: 60 mm"
+ assert covers.get_covers_info() == expected_output
+
+ def test_get_covers_info_with_three_equal_covers(self) -> None:
+ """Test if the method `get_covers_info` returns the correct string when three covers are equal."""
+ covers = CoversRectangular(upper=50.0, right=50.0, lower=50.0, left=60.0)
+ expected_output = "Cover:\n upper|right|lower: 50 mm\n left: 60 mm"
+ assert covers.get_covers_info() == expected_output
+
+ @pytest.mark.parametrize(
+ "covers",
+ [
+ (50.0, 50.0, 50.0, -60.0),
+ (50.0, 50.0, -50.0, 60.0),
+ (50.0, -50.0, 50.0, 60.0),
+ (-50.0, 50.0, 50.0, 60.0),
+ ],
+ )
+ def test_covers_negative_value(self, covers: tuple[float, float, float, float]) -> None:
+ """Test if the method `validate` raises an error when a cover is negative."""
+ with pytest.raises(NegativeValueError):
+ CoversRectangular(upper=covers[0], right=covers[1], lower=covers[2], left=covers[3])
diff --git a/tests/structural_sections/concrete/test_stirrups.py b/tests/structural_sections/concrete/test_stirrups.py
new file mode 100644
index 00000000..8edd976a
--- /dev/null
+++ b/tests/structural_sections/concrete/test_stirrups.py
@@ -0,0 +1,110 @@
+"""Test for the stirrups configuration."""
+
+import pytest
+from shapely import Polygon
+
+from blueprints.materials.reinforcement_steel import ReinforcementSteelMaterial
+from blueprints.structural_sections.concrete.stirrups import StirrupConfiguration
+
+
+class TestStirrup:
+ """Tests for the stirrups configuration."""
+
+ @pytest.fixture
+ def stirrup(self) -> StirrupConfiguration:
+ """Creates a stirrup configuration."""
+ return StirrupConfiguration(
+ geometry=Polygon([(0, 0), (0, 100), (100, 100), (100, 0)]),
+ diameter=8,
+ distance=100,
+ material=ReinforcementSteelMaterial(),
+ )
+
+ def test_mandrel_diameter_factor(self, stirrup: StirrupConfiguration) -> None:
+ """Test the mandrel diameter factor."""
+ mandrel_diameter_factor = stirrup.mandrel_diameter_factor
+ assert mandrel_diameter_factor == 4
+
+ def test_mandrel_diameter_factor_none(self) -> None:
+ """Test the mandrel diameter factor."""
+ stirrup = StirrupConfiguration(
+ geometry=Polygon([(0, 0), (0, 100), (100, 100), (100, 0)]),
+ diameter=8,
+ distance=100,
+ material=ReinforcementSteelMaterial(),
+ mandrel_diameter_factor=3.5,
+ )
+ assert stirrup.mandrel_diameter_factor == 3.5
+
+ def test_as_w(self, stirrup: StirrupConfiguration) -> None:
+ """Test total cross-sectional area of the stirrup [mm²/m]."""
+ as_w = stirrup.as_w
+ assert as_w == pytest.approx(expected=1005.3096, rel=1e-4)
+
+ def test_area(self, stirrup: StirrupConfiguration) -> None:
+ """Test the area of the stirrup."""
+ area = stirrup.area
+ assert area == pytest.approx(expected=50.2655, rel=1e-4)
+
+ def test_radius(self, stirrup: StirrupConfiguration) -> None:
+ """Test the radius of the stirrup."""
+ radius = stirrup.radius
+ assert radius == pytest.approx(expected=4, rel=1e-4)
+
+ def test_centroid(self, stirrup: StirrupConfiguration) -> None:
+ """Test the centroid of the stirrup."""
+ centroid = stirrup.centroid
+ assert centroid.x == pytest.approx(expected=50, rel=1e-4)
+ assert centroid.y == pytest.approx(expected=50, rel=1e-4)
+
+ def test_weight_per_meter(self, stirrup: StirrupConfiguration) -> None:
+ """Test the weight per meter of the stirrup."""
+ weight_per_meter = stirrup.weight_per_meter
+ assert weight_per_meter == pytest.approx(expected=1.57833, rel=1e-4)
+
+ def test_ctc_distance_legs(self, stirrup: StirrupConfiguration) -> None:
+ """Test the distance between the legs of the stirrup."""
+ ctc_distance_legs = stirrup.ctc_distance_legs
+ assert ctc_distance_legs == pytest.approx(expected=100, rel=1e-4)
+
+ def test_cover_used(self, stirrup: StirrupConfiguration) -> None:
+ """Test the cover used."""
+ assert stirrup.cover_used == 0
+
+ def test_cover_used_changed(self) -> None:
+ """Test the cover used."""
+ stirrup = StirrupConfiguration(
+ geometry=Polygon([(0, 0), (0, 100), (100, 100), (100, 0)]), diameter=8, distance=100, material=ReinforcementSteelMaterial(), cover_used=10
+ )
+ assert stirrup.cover_used == 10
+
+ def test_relative_start_position(self, stirrup: StirrupConfiguration) -> None:
+ """Test the relative start position."""
+ assert stirrup.relative_start_position == 0.0
+
+ def test_relative_end_position(self, stirrup: StirrupConfiguration) -> None:
+ """Test the relative end position."""
+ assert stirrup.relative_end_position == 1.0
+
+ @pytest.mark.parametrize(("relative_start_position", "relative_end_position"), [(-1, 1), (-5, 1), (0, 2), (0, 5)])
+ def test_relative_positions_wrong(self, relative_start_position: float, relative_end_position: float) -> None:
+ """Test the relative positions."""
+ with pytest.raises(ValueError):
+ StirrupConfiguration(
+ geometry=Polygon([(0, 0), (0, 100), (100, 100), (100, 0)]),
+ diameter=8,
+ distance=100,
+ material=ReinforcementSteelMaterial(),
+ relative_start_position=relative_start_position,
+ relative_end_position=relative_end_position,
+ )
+
+ def test__repr__(self, stirrup: StirrupConfiguration) -> None:
+ """Test the representation of the stirrup."""
+ representation = repr(stirrup)
+ assert representation.endswith("|⌀8/B500B")
+
+ def test__str__(self, stirrup: StirrupConfiguration) -> None:
+ """Test the string representation of the stirrup."""
+ string_representation = str(stirrup)
+ assert string_representation == "Stirrups ⌀8-100 mm | B500B | 1005.31 mm²/m"
diff --git a/tests/utils/test_abc_enum_meta.py b/tests/utils/test_abc_enum_meta.py
index a6d3f93b..34f872c9 100644
--- a/tests/utils/test_abc_enum_meta.py
+++ b/tests/utils/test_abc_enum_meta.py
@@ -73,10 +73,7 @@ class InvalidMockConcreteEnum(MockAbstractEnum):
A = "a"
B = "b"
- assert (
- str(error_info.value)
- == "Can't instantiate abstract class 'InvalidMockConcreteEnum' without an implementation for abstract method 'test_method'"
- )
+ assert str(error_info.value).endswith("without an implementation for abstract method 'test_method'")
def test_abc_enum_meta_attribute_error(self) -> None:
"""Test instantiation of class with ABCEnumMeta where no abstract methods attribute is present."""