From 5e800d65204fe38a257d1f818e76f3008b66843d Mon Sep 17 00:00:00 2001 From: Morten Engen Date: Tue, 18 Mar 2025 22:57:52 +0100 Subject: [PATCH 01/31] Add draft framework for shell sections --- structuralcodes/geometry/_shell_geometry.py | 49 ++++++++++++++++ structuralcodes/sections/_shell_section.py | 57 +++++++++++++++++++ .../section_integrators/_shell_integrator.py | 43 ++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 structuralcodes/geometry/_shell_geometry.py create mode 100644 structuralcodes/sections/_shell_section.py create mode 100644 structuralcodes/sections/section_integrators/_shell_integrator.py diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py new file mode 100644 index 00000000..f4f527fd --- /dev/null +++ b/structuralcodes/geometry/_shell_geometry.py @@ -0,0 +1,49 @@ +"""A concrete implementation of a shell geometry.""" + +import typing as t + +from ..core.base import Material +from ._geometry import Geometry + + +class ShellReinforcement(Geometry): + """A class for representing reinforcement in a shell geometry.""" + + _z: float + _n_bars: float + _cc_bars: float + _diameter_bar: float + _material: Material + _phi: float + + def __init__(self): + """Initialize a shell reinforcement.""" + raise NotImplementedError + + +class ShellGeometry(Geometry): + """A class for a shell with a thickness and material.""" + + _reinforcement: t.List[ShellReinforcement] + + def __init__( + self, + thickness: float, + material: Material, + name: t.Optional[str] = None, + group_label: t.Optional[str] = None, + ): + """Initialize a shell geometry.""" + super().__init__(name=name, group_label=group_label) + self._thickness = thickness + self._material = material + + def add_reinforcement( + self, + reinforcement: t.Union[ShellReinforcement, t.List[ShellReinforcement]], + ): + """Add reinforcement to the shell geometry.""" + if isinstance(reinforcement, ShellReinforcement): + self._reinforcement.append(reinforcement) + elif isinstance(reinforcement, list): + self._reinforcement.extend(reinforcement) diff --git a/structuralcodes/sections/_shell_section.py b/structuralcodes/sections/_shell_section.py new file mode 100644 index 00000000..5f613436 --- /dev/null +++ b/structuralcodes/sections/_shell_section.py @@ -0,0 +1,57 @@ +"""A concrete implementation of a shell section.""" + +import typing as t + +from numpy.typing import ArrayLike + +from ..core.base import Section, SectionCalculator +from ..geometry._shell_geometry import ShellGeometry +from .section_integrators._shell_integrator import ShellFiberIntegrator + + +class ShellSection(Section): + """A shell section.""" + + def __init__(self, geometry: ShellGeometry): + """Initialize a shell section.""" + super().__init__() + self._geometry = geometry + self.section_calculator = ShellSectionCalculator(section=self) + + @property + def geometry(self): + return self._geometry + + +class ShellSectionCalculator(SectionCalculator): + """A calculator for shell sections.""" + + section: ShellSection + + def __init__(self, section: ShellSection): + super().__init__(section=section) + self.integrator = ShellFiberIntegrator() + + def integrate_strain_profile( + self, + strain: ArrayLike, + integrate: t.Literal['stress', 'modulus'] = 'stress', + ): + """Integrate a strain profile returning stress resultants or tangent + section stiffness matrix. + """ + return self.integrator.integrate_strain_response_on_geometry( + geo=self.section.geometry, strain=strain, integrate=integrate + ) + + def calculate_strain_profile( + self, + nx: float, + ny: float, + nxy: float, + mx: float, + my: float, + mxy: float, + ): + """Get the strain plane for a given set of stress resultants.""" + raise NotImplementedError diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py new file mode 100644 index 00000000..e795a921 --- /dev/null +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -0,0 +1,43 @@ +"""A concrete implementation of a shell integrator.""" + +import typing as t + +from _section_integrator import SectionIntegrator +from numpy.typing import ArrayLike + +from ...geometry._shell_geometry import ShellGeometry + + +class ShellFiberIntegrator(SectionIntegrator): + """A concrete implementation of a fiber integrator for shell sections.""" + + def prepare_input( + self, + geo: ShellGeometry, + strain: ArrayLike, + integrate: t.Literal['stress', 'modulus'] = 'stress', + ): + raise NotImplementedError + + def integrate_stress(self, *prepared_input): + raise NotImplementedError + + def integrate_modulus(self, *prepared_input): + raise NotImplementedError + + def integrate_strain_response_on_geometry( + self, + geo: ShellGeometry, + strain: ArrayLike, + integrate: t.Literal['stress', 'modulus'] = 'stress', + ): + prepared_input = self.prepare_input( + geo=geo, strain=strain, integrate=integrate + ) + + # Return the calculated response + if integrate == 'stress': + return self.integrate_stress(prepared_input) + if integrate == 'modulus': + return self.integrate_modulus(prepared_input) + raise ValueError(f'Unknown integrate type: {integrate}') From 3dc2a25be3eb759d9b9b9946898de2a1a2e7f326 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Mon, 24 Mar 2025 17:42:14 +0100 Subject: [PATCH 02/31] feat: implement shell geometry (#228) * Add shell reinforcement * Add shell geometry --- structuralcodes/geometry/_shell_geometry.py | 76 +++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index f4f527fd..ea809ead 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -16,9 +16,56 @@ class ShellReinforcement(Geometry): _material: Material _phi: float - def __init__(self): + def __init__( + self, + z: float, + n_bars: float, + cc_bars: float, + diameter_bar: float, + material: Material, + phi: float, + name: t.Optional[str] = None, + group_label: t.Optional[str] = None, + ) -> None: """Initialize a shell reinforcement.""" - raise NotImplementedError + super().__init__(name, group_label) + + self._z = z + self._n_bars = n_bars + self._cc_bars = cc_bars + self._diameter_bar = diameter_bar + self._material = material + self._phi = phi + + @property + def z(self) -> float: + """Return the reinforcement position over the thickness.""" + return self._z + + @property + def n_bars(self) -> float: + """Return the number of bars per unit width.""" + return self._n_bars + + @property + def cc_bars(self) -> float: + """Return the spacing between bars.""" + return self._cc_bars + + @property + def diameter_bar(self) -> float: + """Return the bar diameter.""" + return self._diameter_bar + + @property + def material(self) -> Material: + """Return the material of the reinforcement.""" + return self._material + + @property + def phi(self) -> float: + """Return the orientation angle of the reinforcement.""" + return self._phi class ShellGeometry(Geometry): @@ -32,16 +79,37 @@ def __init__( material: Material, name: t.Optional[str] = None, group_label: t.Optional[str] = None, - ): + ) -> None: """Initialize a shell geometry.""" super().__init__(name=name, group_label=group_label) + + if thickness <= 0: + raise ValueError('Shell thickness must be positive.') + self._thickness = thickness self._material = material + self._reinforcement = [] + + @property + def thickness(self) -> float: + """Return the shell thickness.""" + return self._thickness + + @property + def material(self) -> Material: + """Return the material of the shell.""" + return self._material + + @property + def reinforcement(self) -> t.List[ShellReinforcement]: + """Return all reinforcement layers.""" + return self._reinforcement + def add_reinforcement( self, reinforcement: t.Union[ShellReinforcement, t.List[ShellReinforcement]], - ): + ) -> None: """Add reinforcement to the shell geometry.""" if isinstance(reinforcement, ShellReinforcement): self._reinforcement.append(reinforcement) From a9b5d7cae7457a64cb6bc03a1b5d1753bc239fce Mon Sep 17 00:00:00 2001 From: Morten Engen Date: Wed, 26 Mar 2025 07:51:04 +0100 Subject: [PATCH 03/31] Add placeholders for _repr_svg_ --- structuralcodes/geometry/_shell_geometry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index ea809ead..0c87285a 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -67,6 +67,10 @@ def phi(self) -> float: """Return the orientation angle of the reinforcement.""" return self._phi + def _repr_svg_(self) -> str: + """Returns the svg representation.""" + raise NotImplementedError + class ShellGeometry(Geometry): """A class for a shell with a thickness and material.""" @@ -115,3 +119,7 @@ def add_reinforcement( self._reinforcement.append(reinforcement) elif isinstance(reinforcement, list): self._reinforcement.extend(reinforcement) + + def _repr_svg_(self) -> str: + """Returns the svg representation.""" + raise NotImplementedError From 4646a06e6d108730762f7cf5e248e8f12cfd8c93 Mon Sep 17 00:00:00 2001 From: Morten Engen Date: Wed, 23 Apr 2025 21:11:34 +0200 Subject: [PATCH 04/31] Temp modify pipeline to be triggered on pr to this branch --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cd3cdb1e..001ac396 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,6 +6,7 @@ on: pull_request: branches: - 'dev' + - 'implement-shell-section' jobs: build: runs-on: ubuntu-latest From 9a056b63c89261c032df57be7af769ed7b7dbc96 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Mon, 28 Apr 2025 21:09:39 +0200 Subject: [PATCH 05/31] feat: add svg representation of shell reinforcement (#235) * Added validation for z-value and scaled the visualization * Removed 'return self' and 'return None' --------- Co-authored-by: Morten Engen --- structuralcodes/geometry/_shell_geometry.py | 129 +++++++++++++++++++- 1 file changed, 128 insertions(+), 1 deletion(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 0c87285a..c71bafa9 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -2,6 +2,8 @@ import typing as t +import numpy as np + from ..core.base import Material from ._geometry import Geometry @@ -120,6 +122,131 @@ def add_reinforcement( elif isinstance(reinforcement, list): self._reinforcement.extend(reinforcement) + # Validate each reinforcement layer + for r in reinforcement: + half_thickness = self._thickness / 2 + if not ( + -half_thickness + r.diameter_bar / 2 + <= r.z + <= half_thickness - r.diameter_bar / 2 + ): + raise ValueError( + f'Reinforcement at z = {r.z:.2f} mm is outside the' + f'range [-{half_thickness:.2f}, {half_thickness:.2f}] mm.' + ) + def _repr_svg_(self) -> str: """Returns the svg representation.""" - raise NotImplementedError + # Concrete dimensions and half sizes + w, h = 1000, 1000 + hw, hh = w / 2, h / 2 + # ViewBox for inner SVG elements + vb = f'{-hw - 100} {-hh - 100} {w + 200} {h + 200}' + + # Draw extended reinforcement lines + def draw_rebars(view: str) -> str: + elems = [] + for layer in self.reinforcement: + # Choose layers based on z-value (top vs bottom) + if (view == 'top' and layer.z >= 0) or ( + view == 'bottom' and layer.z < 0 + ): + phi = layer.phi + sp = layer.cc_bars + c, s = np.cos(phi), np.sin(phi) + # Perpendicular vector for shifting parallel lines + px, py = -s, c + # Color based on orientation + col = ( + 'red' + if np.isclose(phi % np.pi, 0) + else 'blue' + if np.isclose(phi % np.pi, np.pi / 2) + else 'green' + ) + n = int(w / sp) + 3 # Safety margin to take care of angels + L = 2000 # Extend lines + for i in range(-n // 2, n // 2 + 1): + ox = i * sp * px + oy = i * sp * py + x0, y0 = ox, oy + # Extended endpoints along the rebar direction + x1, y1 = x0 - L * c, y0 - L * s + x2, y2 = x0 + L * c, y0 + L * s + for j in range(int(layer.n_bars)): + extra = ( + j - (layer.n_bars - 1) / 2 + ) * layer.diameter_bar + sx, sy = extra * px, extra * py + p1 = (x1 + sx, y1 + sy) + p2 = (x2 + sx, y2 + sy) + elems.append( + f'' + ) + return ''.join(elems) + + # Build one view (top or bottom) + def build_view(view: str) -> str: + # Define a clipPath for the concrete area + clip_def = ( + f'' + f'' + f'' + ) + # Draw rebar lines and clip them to the concrete area + rebar_svg = ( + f'{draw_rebars(view)}' + ) + # Draw a background rectangle for the concrete (lightgray fill) + bg_rect = ( + f'' + ) + + # Draw a concrete outline and a label + outline = ( + f'' + ) + lbl = ( + f'' + + f'{"Top View" if view == "top" else "Bottom View"}' + + '' + ) + + return clip_def + bg_rect + rebar_svg + outline + lbl + + scale = 0.6 + + # Assemble both views side by side + gap = 50 # gap between views + sw, sh = w + 200, h + 200 # single view width/height + total_w, total_h = (sw * 2 + gap) * scale, sh * scale + + svg_parts = [ + f'' + # everything is grouped to scale + f'' + ] + + # Top-view (left) + svg_parts.append( + "" + f"{build_view('top')}" + ) + + # Bottom-view (right) + svg_parts.append( + f"" + f"{build_view('bottom')}" + ) + + svg_parts.append('') + return ''.join(svg_parts) From 045c617a95718d67517e949d422c3a756342921b Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Mon, 28 Apr 2025 21:11:50 +0200 Subject: [PATCH 06/31] feat: add Elastic2D material class (#234) * Add get_stress_2d and get_tangent_2d methods * Add separate elastic class for 2D operations * Add tests for Elastic2D methods get_stress and get_tangent. * Establish the C_matrix as an internal attribute and a property + various fixes * Change 'math.isclose...' to 'np.allclose...' * Verified ValueError and removed misplaced comment in get_stress method --------- Co-authored-by: Morten Engen --- .../materials/constitutive_laws/__init__.py | 3 + .../constitutive_laws/_elastic_2d.py | 80 +++++++++++ .../test_materials/test_constitutive_laws.py | 125 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 structuralcodes/materials/constitutive_laws/_elastic_2d.py diff --git a/structuralcodes/materials/constitutive_laws/__init__.py b/structuralcodes/materials/constitutive_laws/__init__.py index 9f268476..a9985857 100644 --- a/structuralcodes/materials/constitutive_laws/__init__.py +++ b/structuralcodes/materials/constitutive_laws/__init__.py @@ -5,6 +5,7 @@ from ...core.base import ConstitutiveLaw, Material from ._bilinearcompression import BilinearCompression from ._elastic import Elastic +from ._elastic_2d import Elastic2D from ._elasticplastic import ElasticPlastic from ._parabolarectangle import ParabolaRectangle from ._popovics import Popovics @@ -13,6 +14,7 @@ __all__ = [ 'Elastic', + 'Elastic2D', 'ElasticPlastic', 'ParabolaRectangle', 'BilinearCompression', @@ -25,6 +27,7 @@ CONSTITUTIVE_LAWS: t.Dict[str, ConstitutiveLaw] = { 'elastic': Elastic, + 'elastic2d': Elastic2D, 'elasticplastic': ElasticPlastic, 'elasticperfectlyplastic': ElasticPlastic, 'bilinearcompression': BilinearCompression, diff --git a/structuralcodes/materials/constitutive_laws/_elastic_2d.py b/structuralcodes/materials/constitutive_laws/_elastic_2d.py new file mode 100644 index 00000000..dc01a473 --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_elastic_2d.py @@ -0,0 +1,80 @@ +import typing as t + +import numpy as np +from numpy.typing import ArrayLike + +from ._elastic import Elastic + + +class Elastic2D(Elastic): + """Class for elastic constitutive law for 2D operations.""" + + def __init__( + self, + E: float, + nu: float, + name: t.Optional[str] = None, + ) -> None: + """Initialize an Elastic2D Material. + + Arguments: + E (float): The elastic modulus. + nu (float): Poisson's ratio. + name (str, optional): A descriptive name for the constitutive law. + """ + super().__init__(E=E, name=name) + self._nu = nu + + self._C_matrix: t.Optional[ArrayLike] = None + + @property + def E(self) -> float: + """Return the elastic modulus.""" + return self._E + + @property + def nu(self) -> float: + """Return Poisson's ratio. + + Note: + Including this property allows future checks or transformations + (e.g., ensuring 0 ≤ nu < 0.5). + """ + return self._nu + + @property + def C_matrix(self) -> np.ndarray: + """Return the 2D constitutive matrix.""" + if self._C_matrix is None: + E = self.E + nu = self.nu + + self._C_matrix = ( + E + / (1 - nu**2) + * np.array( + [ + [1.0, nu, 0.0], + [nu, 1.0, 0.0], + [0.0, 0.0, (1 - nu) / 2.0], + ] + ) + ) + return self._C_matrix + + def get_stress(self, eps: ArrayLike) -> np.ndarray: + """Return a 2D stress vector [sigma_x, sigma_y, tau_xy] + given a 2D strain vector [eps_x, epx_y, gamma_xy]. + """ + eps = np.atleast_1d(eps) + try: + return self.C_matrix @ eps + except ValueError as e: + raise ValueError( + 'The input strain vector must have a length of 3.' + ) from e + + def get_tangent(self, *args, **kwargs) -> np.ndarray: + """Return the 2D tangent stiffness matrix.""" + del args, kwargs + return self.C_matrix diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index a6b6e197..933bce76 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -9,6 +9,7 @@ from structuralcodes.materials.constitutive_laws import ( BilinearCompression, Elastic, + Elastic2D, ElasticPlastic, ParabolaRectangle, Popovics, @@ -92,6 +93,130 @@ def test_elastic_numpy(): assert np.allclose(sig, sig_expected) +# Elastic2D tests +@pytest.mark.parametrize( + 'E, nu, strain, expected', + [ + ( + 200000, + 0.25, + [0.001, 0.0, 0.0], + [ + 200000 / (1 - 0.25**2) * (0.001 + 0.25 * 0.0), + 200000 / (1 - 0.25**2) * (0.25 * 0.001 + 0.0), + 200000 / (2 * (1 + 0.25)) * 0.0, + ], + ), + ( + 200000, + 0.3, + [0.001, 0.0005, 0.0], + [ + 200000 / (1 - 0.3**2) * (0.001 + 0.3 * 0.0005), + 200000 / (1 - 0.3**2) * (0.3 * 0.001 + 0.0005), + 200000 / (2 * (1 + 0.3)) * 0.0, + ], + ), + ( + 210000, + 0.25, + [0.001, 0.0005, 0.002], + [ + 210000 / (1 - 0.25**2) * (0.001 + 0.25 * 0.0005), + 210000 / (1 - 0.25**2) * (0.25 * 0.001 + 0.0005), + 210000 / (2 * (1 + 0.25)) * 0.002, + ], + ), + ( + 210000, + 0.3, + [0.0, 0.0, 0.003], + [ + 210000 / (1 - 0.3**2) * (0.0 + 0.3 * 0.0), + 210000 / (1 - 0.3**2) * (0.3 * 0.0 + 0.0), + 210000 / (2 * (1 + 0.3)) * 0.003, + ], + ), + ], +) +def test_elastic2d_stress(E, nu, strain, expected): + """Test the get_stress method of Elastic2D.""" + mat = Elastic2D(E, nu) + stress = mat.get_stress(strain) + assert np.allclose(stress, expected) + + +def test_strain_input_for_stress(): + """Test the get_stress method of Elastic2D with invalid strain input.""" + mat = Elastic2D(200000, 0.25) + with pytest.raises(ValueError): + mat.get_stress([0.001, 0.002]) + + +@pytest.mark.parametrize( + 'E, nu, expected', + [ + ( + 200000, + 0.3, + 200000 + / (1 - 0.3**2) + * np.array( + [ + [1.0, 0.3, 0.0], + [0.3, 1.0, 0.0], + [0.0, 0.0, (1 - 0.3) / 2.0], + ] + ), + ), + ( + 210000, + 0.25, + 210000 + / (1 - 0.25**2) + * np.array( + [ + [1.0, 0.25, 0.0], + [0.25, 1.0, 0.0], + [0.0, 0.0, (1 - 0.25) / 2.0], + ] + ), + ), + ( + 200000, + 0.25, + 200000 + / (1 - 0.25**2) + * np.array( + [ + [1.0, 0.25, 0.0], + [0.25, 1.0, 0.0], + [0.0, 0.0, (1 - 0.25) / 2.0], + ] + ), + ), + ( + 210000, + 0.3, + 210000 + / (1 - 0.3**2) + * np.array( + [ + [1.0, 0.3, 0.0], + [0.3, 1.0, 0.0], + [0.0, 0.0, (1 - 0.3) / 2.0], + ] + ), + ), + ], +) +def test_elastic2d_tangent(E, nu, expected): + """Test the get_tangent method of Elastic2D.""" + mat = Elastic2D(E, nu) + tangent = mat.get_tangent() + assert np.allclose(tangent, expected) + + @pytest.mark.parametrize( 'E, fy, strain, expected', [ From 268b0ac2f42fa7d536d76b2a8f4b2428836d86a1 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Mon, 28 Apr 2025 21:32:15 +0200 Subject: [PATCH 07/31] feat: add shell fiber integrator (#237) * Add shell fiber integrator * Removed strain at fiber as argument for Elastic2D get_tangent method * Improve triangulation logic and various fixes to prepare_input per review comments * Use zip() in integrate_modulus and only generate default mesh_size when z_coords is None --------- Co-authored-by: Morten Engen --- .../section_integrators/_shell_integrator.py | 169 ++++++++++++++++-- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index e795a921..327018c8 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -1,11 +1,15 @@ -"""A concrete implementation of a shell integrator.""" +"""A concrete implementation of a shell integrator using fiber +discretization. +""" +import math import typing as t -from _section_integrator import SectionIntegrator -from numpy.typing import ArrayLike +import numpy as np +from numpy.typing import ArrayLike, NDArray from ...geometry._shell_geometry import ShellGeometry +from ._section_integrator import SectionIntegrator class ShellFiberIntegrator(SectionIntegrator): @@ -16,23 +20,164 @@ def prepare_input( geo: ShellGeometry, strain: ArrayLike, integrate: t.Literal['stress', 'modulus'] = 'stress', - ): - raise NotImplementedError + **kwargs, + ) -> t.Tuple[t.List[t.Tuple[np.ndarray, np.ndarray]], None]: + """Prepare general input to the integration of stress or material + modulus in the shell section. - def integrate_stress(self, *prepared_input): - raise NotImplementedError + Calculate the stress resultants or tangent section stiffness based on + strains at discrete fibers along the shell thickness. - def integrate_modulus(self, *prepared_input): - raise NotImplementedError + Arguments: + geo (ShellGeometry): The shell geometry object. + strain (ArrayLike): The strains and curvatures of the shell section + [eps_x, eps_y, gamma_xy, kappa_x, kappa_y, kappa_xy]. + integrate (str): A string indicating the quantity to integrate over + the shell section. It can be 'stress' or 'modulus'. When + 'stress' is selected, the return value will be the generalised + stress resultants: membrane forces and bending moments Nx, Ny, + Nxy, Mx, My, Mxy. When 'modulus' is selected, the return value + will be the section stiffness matrix K (6x6), which relates + generalised strains to stress resultants (default is 'stress'). + + Keyword Arguments: + z_coords (ArrayLike): The z-coordinates of the layers in the shell. + mesh_size (float): fraction of the total shell thickness for each + layer ([0,1]). Default is 0.01. + + + Returns: + Tuple: (prepared_input, z_coords) + """ + z_coords = kwargs.get('z_coords', None) + + t_total = geo.thickness + + if z_coords is None: + mesh_size = kwargs.get('mesh_size', 0.01) + if not (0 < mesh_size <= 1): + raise ValueError('mesh_size must be [0,1].') + n_layers = max(1, math.ceil(1 / mesh_size)) + dz = t_total / n_layers + z_coords = np.linspace( + -t_total / 2 + dz / 2, t_total / 2 - dz / 2, n_layers + ) + + material = geo.material + + prepared_input = [] + + IA = [] + + for z in z_coords: + fiber_strain = strain[:3] + z * strain[3:] + + if integrate == 'stress': + integrand = material.get_stress(fiber_strain) + elif integrate == 'modulus': + integrand = material.get_tangent(fiber_strain) + else: + raise ValueError(f'Unknown integrate type: {integrate}') + + IA.append(integrand * dz) + + prepared_input = [(np.array(z_coords), np.array(IA))] + + return prepared_input, z_coords + + def integrate_stress( + self, + prepared_input: t.List[t.Tuple[np.ndarray, np.ndarray]], + ) -> t.Tuple[float, float, float, float, float, float]: + """Integrate stresses over the shell thickness. + + Arguments: + prepared_input (List): The prepared input from .prepare_input(). + + Returns: + Tuple(float, float, float, float, float, float): + The stress resultants Nx, Ny, Nxy, Mx, My, Mxy + """ + z, stress_resultants = prepared_input[0] + + Nx = np.sum(stress_resultants[:, 0]) + Ny = np.sum(stress_resultants[:, 1]) + Nxy = np.sum(stress_resultants[:, 2]) + + Mx = np.sum(stress_resultants[:, 0] * z) + My = np.sum(stress_resultants[:, 1] * z) + Mxy = np.sum(stress_resultants[:, 2] * z) + + return Nx, Ny, Nxy, Mx, My, Mxy + + def integrate_modulus( + self, + prepared_input: t.List[t.Tuple[np.ndarray, np.ndarray]], + ) -> NDArray: + """Integrate material modulus over shell thickness to obtain shell + section stiffness. + + Arguments: + prepared_input (List): The prepared input from .prepare_input(). + + Returns: + NDArray: Section stiffness matrix (6x6). + """ + z, MA = prepared_input[0] + + A = np.zeros((3, 3)) + B = np.zeros((3, 3)) + D = np.zeros((3, 3)) + + for C_dz, z_i in zip(MA, z): + A += C_dz + B += z_i * C_dz + D += z_i**2 * C_dz + + return np.block([[A, B], [B, D]]) def integrate_strain_response_on_geometry( self, geo: ShellGeometry, strain: ArrayLike, integrate: t.Literal['stress', 'modulus'] = 'stress', - ): - prepared_input = self.prepare_input( - geo=geo, strain=strain, integrate=integrate + **kwargs, + ) -> t.Union[ + t.Tuple[float, float, float, float, float, float], np.ndarray + ]: + """Integrate strain profile through the shell thickness. + + This method evaluates the response of a shell section subjected to a + generalised strain state, either by integrating the stresses to obtain + resultant forces and moments, or by integrating the tangent stiffness + to obtain the section stiffness matrix. + + Arguments: + geo (ShellGeometry): The shell geometry including thickness and + material. + strain (ArrayLike): Generalised strain vector of the form: + [eps_x, eps_y, gamma_xy, kappa_x, kappa_y, kappa_xy] + integrate (str): Type of integration to perform. Must be one of: + - 'stress': returns the stress resultants (Nx, Ny, Nxy, Mx, My, + Mxy) + - 'modulus': returns the section stiffness matrix (6x6) + + Returns: + Union[Tuple[float, float, float, float, float, float], np.ndarray]: + - If integrate = 'stress': returns 6 stress resultants. + - If integrate = 'modulus': returns the 6x6 stiffness matrix. + + Example: + result = integrate_strain_response_on_geometry(geo, strain, + integrate='modulus') + 'result will be the 6x6 stiffness matrix of the shell. + + Raises: + ValueError: If `integrate` is not 'stress' or 'modulus'. + """ + # Prepare the general input based on the geometry and the input strains + prepared_input, _ = self.prepare_input( + geo=geo, strain=strain, integrate=integrate, **kwargs ) # Return the calculated response From b138099b3343b928b13f4404d17422f844e90a90 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Mon, 12 May 2025 07:24:30 +0200 Subject: [PATCH 08/31] fix: replace tangent stiffness with secant stiffness for 2D materials (#241) * Replace tangent stiffness with secant stiffness for 2D materials * Replace 'tangent' with 'secant' in tests --- .../materials/constitutive_laws/_elastic_2d.py | 4 ++-- .../sections/section_integrators/_shell_integrator.py | 6 +++--- tests/test_materials/test_constitutive_laws.py | 9 ++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_elastic_2d.py b/structuralcodes/materials/constitutive_laws/_elastic_2d.py index dc01a473..7dd36d0a 100644 --- a/structuralcodes/materials/constitutive_laws/_elastic_2d.py +++ b/structuralcodes/materials/constitutive_laws/_elastic_2d.py @@ -74,7 +74,7 @@ def get_stress(self, eps: ArrayLike) -> np.ndarray: 'The input strain vector must have a length of 3.' ) from e - def get_tangent(self, *args, **kwargs) -> np.ndarray: - """Return the 2D tangent stiffness matrix.""" + def get_secant(self, *args, **kwargs) -> np.ndarray: + """Return the 2D secant stiffness matrix.""" del args, kwargs return self.C_matrix diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index 327018c8..be343720 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -25,7 +25,7 @@ def prepare_input( """Prepare general input to the integration of stress or material modulus in the shell section. - Calculate the stress resultants or tangent section stiffness based on + Calculate the stress resultants or secant section stiffness based on strains at discrete fibers along the shell thickness. Arguments: @@ -75,7 +75,7 @@ def prepare_input( if integrate == 'stress': integrand = material.get_stress(fiber_strain) elif integrate == 'modulus': - integrand = material.get_tangent(fiber_strain) + integrand = material.get_secant(fiber_strain) else: raise ValueError(f'Unknown integrate type: {integrate}') @@ -149,7 +149,7 @@ def integrate_strain_response_on_geometry( This method evaluates the response of a shell section subjected to a generalised strain state, either by integrating the stresses to obtain - resultant forces and moments, or by integrating the tangent stiffness + resultant forces and moments, or by integrating the secant stiffness to obtain the section stiffness matrix. Arguments: diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 933bce76..89307c18 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -93,7 +93,6 @@ def test_elastic_numpy(): assert np.allclose(sig, sig_expected) -# Elastic2D tests @pytest.mark.parametrize( 'E, nu, strain, expected', [ @@ -210,11 +209,11 @@ def test_strain_input_for_stress(): ), ], ) -def test_elastic2d_tangent(E, nu, expected): - """Test the get_tangent method of Elastic2D.""" +def test_elastic2d_secant(E, nu, expected): + """Test the get_secant method of Elastic2D.""" mat = Elastic2D(E, nu) - tangent = mat.get_tangent() - assert np.allclose(tangent, expected) + secant = mat.get_secant() + assert np.allclose(secant, expected) @pytest.mark.parametrize( From 1c71fc4344148fa3b6c539438bd17d596e0c43c9 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Tue, 13 May 2025 09:28:55 +0200 Subject: [PATCH 09/31] feat: add tests for ShellGeometry and ShellReinforcement (#245) * Fix add_reinforcement to validate single ShellReinforcement object correctly * Add tests for ShellGeometry and ShellReinforcement, and add ._shell_geometry.py to __init__.py * Remove duplicate line --- structuralcodes/geometry/__init__.py | 3 + structuralcodes/geometry/_shell_geometry.py | 11 +- tests/test_geometry/test_shell.py | 155 ++++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 tests/test_geometry/test_shell.py diff --git a/structuralcodes/geometry/__init__.py b/structuralcodes/geometry/__init__.py index 5b9f1959..9870f3c1 100644 --- a/structuralcodes/geometry/__init__.py +++ b/structuralcodes/geometry/__init__.py @@ -14,6 +14,7 @@ add_reinforcement_circle, add_reinforcement_line, ) +from ._shell_geometry import ShellGeometry, ShellReinforcement from ._steel_sections import HE, IPE, IPN, UB, UBP, UC, UPN __all__ = [ @@ -34,4 +35,6 @@ 'CircularGeometry', 'add_reinforcement_circle', 'RectangularGeometry', + 'ShellGeometry', + 'ShellReinforcement', ] diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index c71bafa9..5f31befd 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -119,11 +119,18 @@ def add_reinforcement( """Add reinforcement to the shell geometry.""" if isinstance(reinforcement, ShellReinforcement): self._reinforcement.append(reinforcement) + to_validate = [reinforcement] elif isinstance(reinforcement, list): self._reinforcement.extend(reinforcement) + to_validate = reinforcement + else: + raise TypeError( + 'Reinforcement must be a ShellReinforcement or a list of ' + 'ShellReinforcement.' + ) # Validate each reinforcement layer - for r in reinforcement: + for r in to_validate: half_thickness = self._thickness / 2 if not ( -half_thickness + r.diameter_bar / 2 @@ -131,7 +138,7 @@ def add_reinforcement( <= half_thickness - r.diameter_bar / 2 ): raise ValueError( - f'Reinforcement at z = {r.z:.2f} mm is outside the' + f'Reinforcement at z = {r.z:.2f} mm is outside the ' f'range [-{half_thickness:.2f}, {half_thickness:.2f}] mm.' ) diff --git a/tests/test_geometry/test_shell.py b/tests/test_geometry/test_shell.py new file mode 100644 index 00000000..f96f4b81 --- /dev/null +++ b/tests/test_geometry/test_shell.py @@ -0,0 +1,155 @@ +"""Tests for the Geometry.""" + +import numpy as np +import pytest + +from structuralcodes.geometry import ( + PointGeometry, + ShellGeometry, + ShellReinforcement, +) +from structuralcodes.materials.concrete import ConcreteEC2_2004 +from structuralcodes.materials.constitutive_laws import ( + Elastic2D, + ElasticPlastic, +) + + +def test_shell_geometry(): + """Test the ShellGeometry class.""" + # Create a material to use + concrete = ConcreteEC2_2004(fck=35) + # Choose a constitutive law to use + const = Elastic2D(E=200000, nu=0.2) + + for mat in (concrete, const): + shell = ShellGeometry( + thickness=200, material=mat, name='Shell', group_label='Group1' + ) + assert shell.thickness == 200 + assert isinstance(shell.material, (ConcreteEC2_2004, Elastic2D)) + assert shell.name == 'Shell' + assert shell.group_label == 'Group1' + + +def test_negative_thickness_raises(): + """Test that a negative thickness raises a ValueError.""" + with pytest.raises(ValueError): + ShellGeometry(thickness=-200, material=Elastic2D(E=200000, nu=0.2)) + + +def test_shell_reinforcement(): + """Test the ShellReinforcement class.""" + z = -60 + n_bars = 4 + cc_bars = 500 + d = 16 + material = Elastic2D(E=200000, nu=0.3) + phi = np.pi / 4 + shell_reinforcement = ShellReinforcement( + z=z, + n_bars=n_bars, + cc_bars=cc_bars, + diameter_bar=d, + material=material, + phi=phi, + name='rebars_1', + group_label='group_A', + ) + assert shell_reinforcement.z == z + assert shell_reinforcement.n_bars == n_bars + assert shell_reinforcement.cc_bars == cc_bars + assert shell_reinforcement.diameter_bar == d + assert shell_reinforcement.material == material + assert shell_reinforcement.phi == phi + assert shell_reinforcement.name == 'rebars_1' + assert shell_reinforcement.group_label == 'group_A' + + +def test_add_reinforcement(): + """Test the add_reinforcement function.""" + # Create a shell geometry + shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + + # Create a reinforcement + reinf_1 = ShellReinforcement( + z=-60, + n_bars=4, + cc_bars=500, + diameter_bar=16, + material=Elastic2D(E=200000, nu=0.3), + phi=0, + ) + + reinf_2 = ShellReinforcement( + z=60, + n_bars=6, + cc_bars=600, + diameter_bar=20, + material=Elastic2D(E=200000, nu=0.3), + phi=np.pi / 4, + ) + + # Add a single reinforcement + shell.add_reinforcement(reinf_1) + assert len(shell.reinforcement) == 1 + assert shell.reinforcement[0] is reinf_1 + + # Add reinforcement as a list + shell.add_reinforcement([reinf_1, reinf_2]) + assert len(shell.reinforcement) == 3 + + +def test_add_reinforcement_invalid_type(): + """Test that adding a reinforcement of invalid type raises an error.""" + # Create a shell geometry + shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + + # Create a reinforcement + reinf_1 = PointGeometry( + np.array([2, 3]), 12, ElasticPlastic(E=200000, fy=500), name='Rebar' + ) + with pytest.raises(TypeError): + shell.add_reinforcement(reinf_1) + + +@pytest.mark.parametrize( + 'invalid_z', + [ + 95, # barely outside of shell (half_thickness=100, d/2=6-> max=94) + 250, # clearly outside the shell + ], +) +def test_add_reinforcement_invalid_z_value(invalid_z): + """Test that adding a reinforcement outside the shell raises an error.""" + shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + reinf = ShellReinforcement( + z=invalid_z, + n_bars=1, + cc_bars=100, + diameter_bar=12, + material=Elastic2D(E=200000, nu=0.3), + phi=0, + ) + with pytest.raises(ValueError): + shell.add_reinforcement([reinf]) + + +def test_repr_svg(): + """Test the SVG representation of the shell geometry.""" + # Create a shell geometry + shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + + # Add a reinforcement + reinf = ShellReinforcement( + z=-60, + n_bars=4, + cc_bars=500, + diameter_bar=16, + material=Elastic2D(E=200000, nu=0.3), + phi=0, + ) + shell.add_reinforcement(reinf) + # Get the SVG representation + svg = shell._repr_svg_() + assert ' Date: Wed, 14 May 2025 17:00:42 +0200 Subject: [PATCH 10/31] feat: add stiffness reinforcement (#246) * Add stiffness matrix for reinforcement * Changed names for global and local material matrix * Reinforcement integration in prepare_input and drop redundant geometry argument * Move transformation matrix from integrator to ShellReinforcement * Updated prepare_input for reinforcement --- structuralcodes/geometry/_shell_geometry.py | 12 +++++ .../section_integrators/_shell_integrator.py | 50 +++++++++++-------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 5f31befd..646723a0 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -69,6 +69,18 @@ def phi(self) -> float: """Return the orientation angle of the reinforcement.""" return self._phi + @property + def T(self) -> np.ndarray: + """Return the transformation matrix for the reinforcement.""" + c, s = np.cos(self.phi), np.sin(self.phi) + return np.array( + [ + [c * c, s * s, c * s], + [s * s, c * c, -c * s], + [-2 * c * s, 2 * c * s, c * c - s * s], + ] + ) + def _repr_svg_(self) -> str: """Returns the svg representation.""" raise NotImplementedError diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index be343720..edadbb58 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -24,7 +24,6 @@ def prepare_input( ) -> t.Tuple[t.List[t.Tuple[np.ndarray, np.ndarray]], None]: """Prepare general input to the integration of stress or material modulus in the shell section. - Calculate the stress resultants or secant section stiffness based on strains at discrete fibers along the shell thickness. @@ -45,14 +44,11 @@ def prepare_input( mesh_size (float): fraction of the total shell thickness for each layer ([0,1]). Default is 0.01. - Returns: Tuple: (prepared_input, z_coords) """ - z_coords = kwargs.get('z_coords', None) - + z_coords = kwargs.get('z_coords') t_total = geo.thickness - if z_coords is None: mesh_size = kwargs.get('mesh_size', 0.01) if not (0 < mesh_size <= 1): @@ -62,16 +58,12 @@ def prepare_input( z_coords = np.linspace( -t_total / 2 + dz / 2, t_total / 2 - dz / 2, n_layers ) - material = geo.material - prepared_input = [] - IA = [] - + z_list = [] for z in z_coords: fiber_strain = strain[:3] + z * strain[3:] - if integrate == 'stress': integrand = material.get_stress(fiber_strain) elif integrate == 'modulus': @@ -80,8 +72,29 @@ def prepare_input( raise ValueError(f'Unknown integrate type: {integrate}') IA.append(integrand * dz) + z_list.append(z) + + for r in geo.reinforcement: + z_r = r.z + As = r.n_bars * np.pi * (r.diameter_bar / 2) ** 2 + + fiber_strain = strain[:3] + z_r * strain[3:] + eps_sj = r.T @ fiber_strain + + if integrate == 'stress': + sig_sj = material.get_stress(eps_sj) + integrand = As * r.T.T @ np.array([sig_sj[0], 0, 0]) + elif integrate == 'modulus': + mod = material.get_secant(eps_sj) + integrand = r.T.T @ np.diag([mod[0][0], 0, 0]) @ r.T * As + else: + raise ValueError(f'Unknown integrate type: {integrate}') + + IA.append(integrand * z_r) + z_list.append(z_r) - prepared_input = [(np.array(z_coords), np.array(IA))] + MA = np.stack(IA, axis=0) + prepared_input = [(z_list, MA)] return prepared_input, z_coords @@ -99,15 +112,12 @@ def integrate_stress( The stress resultants Nx, Ny, Nxy, Mx, My, Mxy """ z, stress_resultants = prepared_input[0] - Nx = np.sum(stress_resultants[:, 0]) Ny = np.sum(stress_resultants[:, 1]) Nxy = np.sum(stress_resultants[:, 2]) - Mx = np.sum(stress_resultants[:, 0] * z) My = np.sum(stress_resultants[:, 1] * z) Mxy = np.sum(stress_resultants[:, 2] * z) - return Nx, Ny, Nxy, Mx, My, Mxy def integrate_modulus( @@ -124,15 +134,13 @@ def integrate_modulus( NDArray: Section stiffness matrix (6x6). """ z, MA = prepared_input[0] - A = np.zeros((3, 3)) B = np.zeros((3, 3)) D = np.zeros((3, 3)) - - for C_dz, z_i in zip(MA, z): - A += C_dz - B += z_i * C_dz - D += z_i**2 * C_dz + for C_layer, z_i in zip(MA, z): + A += C_layer + B += z_i * C_layer + D += z_i**2 * C_layer return np.block([[A, B], [B, D]]) @@ -146,7 +154,6 @@ def integrate_strain_response_on_geometry( t.Tuple[float, float, float, float, float, float], np.ndarray ]: """Integrate strain profile through the shell thickness. - This method evaluates the response of a shell section subjected to a generalised strain state, either by integrating the stresses to obtain resultant forces and moments, or by integrating the secant stiffness @@ -179,7 +186,6 @@ def integrate_strain_response_on_geometry( prepared_input, _ = self.prepare_input( geo=geo, strain=strain, integrate=integrate, **kwargs ) - # Return the calculated response if integrate == 'stress': return self.integrate_stress(prepared_input) From 0112057431df3fbff7586ced01cd94df3f1c7ec4 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Thu, 15 May 2025 13:00:46 +0200 Subject: [PATCH 11/31] Fix As calculation and updated notation for cc_bars (#249) --- structuralcodes/geometry/_shell_geometry.py | 2 +- .../sections/section_integrators/_shell_integrator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 646723a0..1577d6eb 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -46,7 +46,7 @@ def z(self) -> float: @property def n_bars(self) -> float: - """Return the number of bars per unit width.""" + """Return the number of bars per bundle.""" return self._n_bars @property diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index edadbb58..4fa87183 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -76,7 +76,7 @@ def prepare_input( for r in geo.reinforcement: z_r = r.z - As = r.n_bars * np.pi * (r.diameter_bar / 2) ** 2 + As = r.n_bars * np.pi * (r.diameter_bar / 2) ** 2 / r.cc_bars fiber_strain = strain[:3] + z_r * strain[3:] eps_sj = r.T @ fiber_strain From d5255306b7446568db50755c21ea7af60c3db860 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Thu, 15 May 2025 13:12:51 +0200 Subject: [PATCH 12/31] feat: add side view to _repr_svg in ShellGeometry (#244) * Add side view to repr_svg in ShellGeometry * Revert "Updated comment" This reverts commit 4ca1ad0cf5a32a94fe93a919cb14c8ad37380459, reversing changes made to 9823d2aced9b804f2e862e63e878c8a43942732d. * Updated files with errors * Fixed difference in not relevant file * Ignore too many statements in repr_svg --------- Co-authored-by: Morten Engen --- structuralcodes/geometry/_shell_geometry.py | 302 ++++++++++++++------ 1 file changed, 207 insertions(+), 95 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 1577d6eb..654a644e 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -154,118 +154,230 @@ def add_reinforcement( f'range [-{half_thickness:.2f}, {half_thickness:.2f}] mm.' ) - def _repr_svg_(self) -> str: + def _repr_svg_(self) -> str: # noqa: PLR0915 """Returns the svg representation.""" - # Concrete dimensions and half sizes + # overall drawing area dimensions w, h = 1000, 1000 + t_val = self.thickness + # half-width/height for centering hw, hh = w / 2, h / 2 - # ViewBox for inner SVG elements - vb = f'{-hw - 100} {-hh - 100} {w + 200} {h + 200}' + # single view dimensions + sw, sh = w + 200, h + 200 + # side view height based on thickness + sh_side = int(t_val + 200) + # main and side view ViewBox definitions + vb_main = f'{-hw - 100} {-hh - 100} {w + 200} {h + 200}' + vb_side = f'{-hw - 100} {-t_val / 2 - 100} {w + 200} {t_val + 200}' + # scaling and spacing between views + scale, gap = 0.6, 50 - # Draw extended reinforcement lines def draw_rebars(view: str) -> str: - elems = [] + # draw parallel rebar lines for top/bottom plan views + lines: t.List[str] = [] for layer in self.reinforcement: - # Choose layers based on z-value (top vs bottom) - if (view == 'top' and layer.z >= 0) or ( - view == 'bottom' and layer.z < 0 - ): - phi = layer.phi - sp = layer.cc_bars - c, s = np.cos(phi), np.sin(phi) - # Perpendicular vector for shifting parallel lines - px, py = -s, c - # Color based on orientation + # select only top or bottom layers + top = view == 'top' and layer.z >= 0 + bot = view == 'bottom' and layer.z < 0 + if not (top or bot): + continue + # orientation and spacing + phi, sp = layer.phi, layer.cc_bars + c, s = np.cos(phi), np.sin(phi) + # perpendicular offset vector + px, py = -s, c + # color by orientation + col = ( + 'red' + if np.isclose(phi % np.pi, 0) + else 'blue' + if np.isclose(phi % np.pi, np.pi / 2) + else 'green' + ) + n = int(w / sp) + 3 # linecount with safety margin to angles + L = 2000 # line length extension + for i in range(-n // 2, n // 2 + 1): + ox, oy = i * sp * px, i * sp * py + # line endpoints + x1, y1 = ox - L * c, oy - L * s + x2, y2 = ox + L * c, oy + L * s + for _ in range(int(layer.n_bars)): + # actual bar offset + lines.append( + f'' + ) + return ''.join(lines) + + def build_plan(view: str) -> str: + # define clipping area for concrete + clip = ( + '' + f'' + '' + ) + # concrete background + bg = ( + f'' + ) + # concrete outline + out = ( + f'' + ) + # view label + lbl = ( + f'' + f'{view} view' + ) + # rebar lines clipped to concrete area + body = ( + '' + + draw_rebars(view) + + '' + ) + return clip + bg + body + out + lbl + + def build_side(ax: str) -> str: + # side view perpendicular direction + span = w if ax == 'x' else h + half = span / 2 + t_ht = self.thickness + cid = 'clipSide' + ax + # clipping region for shell thickness + clip = ( + '' + f'' + '' + ) + # shell background + bg = ( + f'' + ) + # shell outline + out = ( + f'' + ) + # side view label + lbl = ( + f'' + f'Side {ax.upper()}-view' + ) + elems: t.List[str] = [] + for layer in self.reinforcement: + # select bars not parallel to this side-plane + perp = not ( + ( + ax == 'x' + and np.isclose(layer.phi % np.pi, np.pi / 2, atol=1e-2) + ) + or ( + ax == 'y' + and np.isclose(layer.phi % np.pi, 0, atol=1e-2) + ) + ) + if perp: + # draw circles for perp layers + step = layer.cc_bars or span + kmax = int(np.floor(half / step)) + xs = [k * step for k in range(-kmax, kmax + 1)] col = ( 'red' - if np.isclose(phi % np.pi, 0) + if np.isclose(layer.phi % np.pi, 0) else 'blue' - if np.isclose(phi % np.pi, np.pi / 2) + if np.isclose(layer.phi % np.pi, np.pi / 2) else 'green' ) - n = int(w / sp) + 3 # Safety margin to take care of angels - L = 2000 # Extend lines - for i in range(-n // 2, n // 2 + 1): - ox = i * sp * px - oy = i * sp * py - x0, y0 = ox, oy - # Extended endpoints along the rebar direction - x1, y1 = x0 - L * c, y0 - L * s - x2, y2 = x0 + L * c, y0 + L * s + for x in xs: for j in range(int(layer.n_bars)): - extra = ( + ex = ( j - (layer.n_bars - 1) / 2 ) * layer.diameter_bar - sx, sy = extra * px, extra * py - p1 = (x1 + sx, y1 + sy) - p2 = (x2 + sx, y2 + sy) + cy = -layer.z elems.append( - f'' + f'' ) - return ''.join(elems) - - # Build one view (top or bottom) - def build_view(view: str) -> str: - # Define a clipPath for the concrete area - clip_def = ( - f'' - f'' - f'' - ) - # Draw rebar lines and clip them to the concrete area - rebar_svg = ( - f'{draw_rebars(view)}' - ) - # Draw a background rectangle for the concrete (lightgray fill) - bg_rect = ( - f'' - ) - - # Draw a concrete outline and a label - outline = ( - f'' - ) - lbl = ( - f'' - + f'{"Top View" if view == "top" else "Bottom View"}' - + '' + else: + # draw lines for parallel layers + col = ( + 'red' + if np.isclose(layer.phi % np.pi, 0) + else 'blue' + if np.isclose(layer.phi % np.pi, np.pi / 2) + else 'green' + ) + y = -layer.z + for j in range(int(layer.n_bars)): + ex = (j - (layer.n_bars - 1) / 2) * layer.diameter_bar + x1 = -half + ex + x2 = half + ex + elems.append( + f'' + ) + body = ( + '' + ''.join(elems) + '' ) - - return clip_def + bg_rect + rebar_svg + outline + lbl - - scale = 0.6 - - # Assemble both views side by side - gap = 50 # gap between views - sw, sh = w + 200, h + 200 # single view width/height - total_w, total_h = (sw * 2 + gap) * scale, sh * scale - - svg_parts = [ - f'' - # everything is grouped to scale - f'' + return clip + bg + body + out + lbl + + # assemble all SVG fragments + svg = [ + f'' ] - - # Top-view (left) - svg_parts.append( - "" - f"{build_view('top')}" + # plan views + svg.append( + '' + build_plan('top') + '' ) - - # Bottom-view (right) - svg_parts.append( - f"" - f"{build_view('bottom')}" + svg.append( + '' # bottom plan + '' + build_plan('bottom') + '' ) - - svg_parts.append('') - return ''.join(svg_parts) + # side views + svg.append( + '' # side-x + '' + build_side('x') + '' + ) + svg.append( + '' # side-y + '' + build_side('y') + '' + ) + svg.append('') + return ''.join(svg) From 26b0b9b42d7ccf1e82b3fde6d6e2172a20bdc643 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Fri, 16 May 2025 12:45:21 +0200 Subject: [PATCH 13/31] feat: add ParabolaRectangle2D material class with tests (#242) * Add ParabolaRectangle2D material class * Add ParabolaRectangle2D material in __init__ * Split class into modular methods for transform, correction, and cracking * Add more tests to ParabolaRectangle2D class * Add coefficient for compressive-strength reduction due to lateral shear * Update tests accounting for compressive-strength reduction * Update docstrings * Fix computations related to principal strain direction and correct length of strain vectors as per review comments * Avoid division by zero in get_secant method * Update test results after corrections in commit 35898f8 --- .../materials/constitutive_laws/__init__.py | 3 + .../_parabolarectangle_2d.py | 173 ++++++++++++++++++ .../test_materials/test_constitutive_laws.py | 76 ++++++++ 3 files changed, 252 insertions(+) create mode 100644 structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py diff --git a/structuralcodes/materials/constitutive_laws/__init__.py b/structuralcodes/materials/constitutive_laws/__init__.py index a9985857..e2ff8f4e 100644 --- a/structuralcodes/materials/constitutive_laws/__init__.py +++ b/structuralcodes/materials/constitutive_laws/__init__.py @@ -8,6 +8,7 @@ from ._elastic_2d import Elastic2D from ._elasticplastic import ElasticPlastic from ._parabolarectangle import ParabolaRectangle +from ._parabolarectangle_2d import ParabolaRectangle2D from ._popovics import Popovics from ._sargin import Sargin from ._userdefined import UserDefined @@ -17,6 +18,7 @@ 'Elastic2D', 'ElasticPlastic', 'ParabolaRectangle', + 'ParabolaRectangle2D', 'BilinearCompression', 'Popovics', 'Sargin', @@ -32,6 +34,7 @@ 'elasticperfectlyplastic': ElasticPlastic, 'bilinearcompression': BilinearCompression, 'parabolarectangle': ParabolaRectangle, + 'parabolarectangle2d': ParabolaRectangle2D, 'popovics': Popovics, 'sargin': Sargin, } diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py new file mode 100644 index 00000000..8841752f --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -0,0 +1,173 @@ +"""Parabola-Rectangle 2D constitutive law.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +import typing as t + +import numpy as np +from numpy.typing import ArrayLike + +from ._parabolarectangle import ParabolaRectangle + + +class ParabolaRectangle2D(ParabolaRectangle): + """Class for parabola rectangle constitutive law in 2D. + + The stresses and strains are assumed negative in compression and positive + in tension. + """ + + def __init__( + self, + fc: float, + eps_0: float = -0.002, + eps_u: float = -0.0035, + n: float = 2.0, + nu: float = 0.2, + c_1: float = 0.8, + c_2: float = 100, + name: t.Optional[str] = None, + ) -> None: + """Initialize a Parabola-Rectangle 2D Material. + + Arguments: + fc (float): The strength of concrete in compression. + + Keyword Arguments: + eps_0 (float): Peak strain of concrete in compression. Default + value = -0.002. + eps_u (float): Ultimate strain of concrete in compression. Default + value = -0.0035. + n (float): Exponent for the pre-peak branch. Default value = 2. + name (str): A name for the constitutive law. + nu (float): Poisson's ratio. Default value = 0.2. + c_1 (float): First coefficient for the compressive-strength + reduction factor due to lateral tension. Default value = 0.8. + c_2 (float): Second coefficient for the compressive-strength + reduction factor due to lateral tension. Default value = 100. + """ + super().__init__(fc=fc, eps_0=eps_0, eps_u=eps_u, n=n, name=name) + self._nu = nu + self._c_1 = c_1 + self._c_2 = c_2 + + @property + def c_1(self) -> float: + """Return the first coefficient for the compressive-strength reduction + factor due to lateral tension. Default value = 0.8. + """ + return self._c_1 + + @property + def c_2(self) -> float: + """Return the second coefficient for the compressive-strength reduction + factor due to lateral tension. Default value = 100. + """ + return self._c_2 + + def transform(self, strain: ArrayLike) -> np.ndarray: + """Transform the strain vector to principal directions.""" + eps = np.atleast_1d(strain) + # Use arctan2 to obtain the angle in the correct quadrant + theta = 0.5 * np.arctan2(eps[2], eps[0] - eps[1]) + c = np.cos(theta) + s = np.sin(theta) + T = np.array( + [ + [c * c, s * s, s * c], + [s * s, c * c, -s * c], + [-2 * s * c, 2 * s * c, c * c - s * s], + ] + ) + return T, T @ eps + + def get_effective_principal_strains( + self, eps_p: ArrayLike, nu: float + ) -> np.ndarray: + """Compute the effective principal strains to include the influence of + Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete + Shells' by M. A. Polak and F. J. Vecchio (1993). + """ + denom = (1 + nu) * (1 - 2 * nu) + + P = np.array( + [ + [(1 - nu) / denom, nu / denom], + [nu / denom, (1 - nu) / denom], + ] + ) + + return P @ eps_p + + def check_cracked(self, eps_p: ArrayLike) -> bool: + """Check if the concrete is cracked. Returns True if any principal + strain (eps_p1, eps_p2) is greater than zero. + """ + return np.any(eps_p > 0) + + def strength_reduction_lateral_cracking(self, eps_p: ArrayLike) -> float: + """Return the compressive-strength reduction factor due to lateral + tension. This relation comes from Vecchio & Collins (1986), "The + Modified Compression-Field Theory for Reinforced Concrete Elements + Subjected to Shear. + """ + if not self.check_cracked(eps_p): + return 1 + + beta = 1 / (self.c_1 + self.c_2 * max(eps_p)) + + return min(beta, 1) + + def get_stress(self, strain: ArrayLike) -> np.ndarray: + """Return a 2D stress vector [sigma_x, sigma_y, tau_xy] + given a 2D strain vector [eps_x, epx_y, gamma_xy]. + """ + T, eps_p = self.transform(strain) + + # Neglect shear strain related to the principal strain direction + eps_p = eps_p[:2] + + nu = 0.0 if self.check_cracked(eps_p) else self._nu + + # Compressive-strength reduction factor due to lateral tension. + beta = self.strength_reduction_lateral_cracking(eps_p) + + # Include the influence of Poisson's ratio on the principal strains. + eps_pf = self.get_effective_principal_strains(eps_p, nu) + + # Compute the principal stresses from 1D parabola-rectangle law. + sig_p = super().get_stress(eps_pf) + + # Apply the compressive-strength reduction factor + sig_p[sig_p < 0] *= beta + + # Transform back to global coords + return T.T @ np.array([*sig_p, 0]) + + def get_secant(self, strain: ArrayLike) -> np.ndarray: + """Compute the 3x3 secant stiffness matrix C.""" + T, eps_p = self.transform(strain) + eps_p = eps_p[:2] + nu = 0.0 if self.check_cracked(eps_p) else self._nu + beta = self.strength_reduction_lateral_cracking(eps_p) + eps_pf = self.get_effective_principal_strains(eps_p, nu) + + sig_p = super().get_stress(eps_pf) + + # Avoid division by zero + tol = 1e-12 + if abs(eps_pf[0]) > tol: + E11 = (sig_p[0] * (beta if sig_p[0] < 0 else 1.0)) / eps_pf[0] + else: + E11 = self._fc * 2.0 / self._eps_0 + + if abs(eps_pf[1]) > tol: + E22 = (sig_p[1] * (beta if sig_p[1] < 0 else 1.0)) / eps_pf[1] + else: + E22 = self._fc * 2.0 / self._eps_0 + + E12 = (E11 + E22) / 2 + + Cp = np.diag([E11, E22, 0.5 * E12]) + + return T.T @ Cp @ T diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 89307c18..39c896b1 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -12,6 +12,7 @@ Elastic2D, ElasticPlastic, ParabolaRectangle, + ParabolaRectangle2D, Popovics, Sargin, UserDefined, @@ -278,6 +279,81 @@ def test_parabola_rectangle_floats(fc, eps_0, eps_u, strain, stress, tangent): assert math.isclose(mat.get_tangent(strain), tangent) +@pytest.mark.parametrize( + 'fc, eps_0, eps_u, strain, stress', + [ + ( + -30.0, + -0.002, + -0.0035, + [-0.0015, 0.0, 0.001], + [-26.64581728, -2.44270432, 8.06770432], + ), + ( + -45.0, + -0.002, + -0.0035, + [-0.003, 0.002, 0], + [-45, 0, 0], + ), + ( + -45.0, + -0.002, + -0.0035, + [-0.002, -0.0035, -0.001], + [-41.22113162, -3.77886838, 12.48075442], + ), + ( + -45, + -0.002, + -0.0035, + [0.001, -0.0015, -0.0045], + [-11.20993578, -32.37821129, -19.05144796], + ), + ( + -45, + -0.002, + -0.0035, + [0.02, 0.0, -0.01], + [-0.67731096, -12.153852, -2.86913526], + ), + ], +) +def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): + """Test the parabola-rectangle 2D material.""" + mat = ParabolaRectangle2D(fc, eps_0, eps_u) + assert np.allclose(mat.get_stress(strain), stress, atol=1e-3) + + +@pytest.mark.parametrize( + 'fc, eps_0, eps_u, nu, strain, expected', + [ + ( + -30.0, + -0.002, + -0.0035, + 0.0, + [-0.001, -0.001, 0.0], + [[22500, 0.0, 0.0], [0.0, 22500, 0.0], [0.0, 0.0, 0.5 * 22500]], + ), + ( + -45.0, + -0.002, + -0.0035, + 0.2, + [-0.003, 0.002, 0], + [[15000, 0, 0], [0, 0, 0], [0, 0, 3750]], + ), + ], +) +def test_get_secant(fc, eps_0, eps_u, nu, strain, expected): + """Test the secant stiffness matrix of the parabola-rectangle + 2D material. + """ + mat = ParabolaRectangle2D(fc, eps_0, eps_u, nu=nu) + assert np.allclose(mat.get_secant(strain), expected) + + @pytest.mark.parametrize( 'x, y, flag, strain, expected', [ From 2114306166e46cd3ba0c24a1dadd8f56544c18d8 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Fri, 16 May 2025 14:40:19 +0200 Subject: [PATCH 14/31] Remove z_r from IA (#251) --- .../sections/section_integrators/_shell_integrator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index 4fa87183..0e8efa44 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -90,7 +90,7 @@ def prepare_input( else: raise ValueError(f'Unknown integrate type: {integrate}') - IA.append(integrand * z_r) + IA.append(integrand) z_list.append(z_r) MA = np.stack(IA, axis=0) From ef854d77adfb9e56b9482d63db5c36b541d6753d Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Mon, 19 May 2025 20:27:48 +0200 Subject: [PATCH 15/31] Assign reinforcement as material in shell integrator (#252) --- .../sections/section_integrators/_shell_integrator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index 0e8efa44..e93b9d38 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -77,16 +77,17 @@ def prepare_input( for r in geo.reinforcement: z_r = r.z As = r.n_bars * np.pi * (r.diameter_bar / 2) ** 2 / r.cc_bars + material = r.material fiber_strain = strain[:3] + z_r * strain[3:] eps_sj = r.T @ fiber_strain if integrate == 'stress': - sig_sj = material.get_stress(eps_sj) - integrand = As * r.T.T @ np.array([sig_sj[0], 0, 0]) + sig_sj = material.get_stress(eps_sj[0]) + integrand = As * r.T.T @ np.array([sig_sj, 0, 0]) elif integrate == 'modulus': - mod = material.get_secant(eps_sj) - integrand = r.T.T @ np.diag([mod[0][0], 0, 0]) @ r.T * As + mod = material.get_secant(eps_sj[0]) + integrand = r.T.T @ np.diag([mod, 0, 0]) @ r.T * As else: raise ValueError(f'Unknown integrate type: {integrate}') From 5881893fc81905b99d1ed56eb77916d3f427e9d1 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Tue, 20 May 2025 09:04:35 +0200 Subject: [PATCH 16/31] feat: add shell section calculator and tests (#240) * Add shell section * Import shell classes to __init__ files * Make section_calculator a public attr of ShellSection * Make sure the layer information is stored * Make sure strain is an array * Draft iterative solver * Update tests to reflect how mesh_size is propagated * Make sure the stiffness matrix is returned correctly * Separate treatment of current and initial stiffness * Check number of iterations at the beginning of an iteration --------- Co-authored-by: Morten Engen --- structuralcodes/sections/__init__.py | 5 + structuralcodes/sections/_shell_section.py | 215 ++++++++++++++++-- .../sections/section_integrators/__init__.py | 2 + .../section_integrators/_shell_integrator.py | 20 +- tests/test_sections/test_shell_section.py | 155 +++++++++++++ 5 files changed, 373 insertions(+), 24 deletions(-) create mode 100644 tests/test_sections/test_shell_section.py diff --git a/structuralcodes/sections/__init__.py b/structuralcodes/sections/__init__.py index 6a302251..3dc1184f 100644 --- a/structuralcodes/sections/__init__.py +++ b/structuralcodes/sections/__init__.py @@ -2,10 +2,12 @@ from ._generic import GenericSection, GenericSectionCalculator from ._rc_utils import calculate_elastic_cracked_properties +from ._shell_section import ShellSection, ShellSectionCalculator from .section_integrators import ( FiberIntegrator, MarinIntegrator, SectionIntegrator, + ShellFiberIntegrator, integrator_factory, marin_integration, ) @@ -19,4 +21,7 @@ 'integrator_factory', 'marin_integration', 'calculate_elastic_cracked_properties', + 'ShellFiberIntegrator', + 'ShellSection', + 'ShellSectionCalculator', ] diff --git a/structuralcodes/sections/_shell_section.py b/structuralcodes/sections/_shell_section.py index 5f613436..9b1e3155 100644 --- a/structuralcodes/sections/_shell_section.py +++ b/structuralcodes/sections/_shell_section.py @@ -1,25 +1,47 @@ -"""A concrete implementation of a shell section.""" - import typing as t -from numpy.typing import ArrayLike +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.linalg import lu_factor, lu_solve from ..core.base import Section, SectionCalculator -from ..geometry._shell_geometry import ShellGeometry -from .section_integrators._shell_integrator import ShellFiberIntegrator +from ..geometry import ShellGeometry +from .section_integrators import ShellFiberIntegrator class ShellSection(Section): - """A shell section.""" + """This is the implementation of the shell class section. + + Attributes: + geometry ShellGeometry: The geometry of + the section. + name (str): The name of the section. + """ - def __init__(self, geometry: ShellGeometry): - """Initialize a shell section.""" - super().__init__() + def __init__( + self, + geometry: ShellGeometry, + name: t.Optional[str] = None, + **kwargs, + ) -> None: + """Initialize a ShellSection. + + Note: + The ShellSection uses a ShellSectionCalculator for all + calculations. The ShellSectionCalculator uses a + ShellFiberIntegrator for integrating over the thickness. + """ + if name is None: + name = 'ShellSection' + super().__init__(name) self._geometry = geometry - self.section_calculator = ShellSectionCalculator(section=self) + self.section_calculator = ShellSectionCalculator( + section=self, **kwargs + ) @property def geometry(self): + """Return the geometry of the section.""" return self._geometry @@ -27,23 +49,89 @@ class ShellSectionCalculator(SectionCalculator): """A calculator for shell sections.""" section: ShellSection + mesh_size: float + + def __init__( + self, + section: ShellSection, + **kwargs, + ) -> None: + """Initialize the ShellSectionCalculator. - def __init__(self, section: ShellSection): + Arguments: + section (ShellSection): The section object. + """ super().__init__(section=section) self.integrator = ShellFiberIntegrator() + self.mesh_size = kwargs.get('mesh_size', 0.01) + self.layers: t.Optional[t.Tuple] = None + + def _calculate_gross_section_properties(self): + pass + + def calculate_bending_strength(self): + pass + + def calculate_moment_curvature(self): + pass def integrate_strain_profile( self, strain: ArrayLike, integrate: t.Literal['stress', 'modulus'] = 'stress', - ): + ) -> t.Union[t.Tuple[float, float, float, float, float, float], NDArray]: """Integrate a strain profile returning stress resultants or tangent section stiffness matrix. + + Arguments: + strain (ArrayLike): Represents the deformation plane. The strain + should have six entries representing respectively: eps_x, + eps_y, gamma_xy, kappa_x, kappa_y, kappa_xy. + integrate (str): a string indicating the quantity to integrate over + the section. It can be 'stress' or 'modulus'. When 'stress' + is selected, the return value will be the stress resultants Nx, + Ny, Nxy, Mx, My and Mxy, while if 'modulus' is selected, the + return will be the tangent section stiffness matrix (default is + 'stress'). + + Returns: + Union(Tuple(float, float, float, float, float, float),NDArray): + Nx, Ny, Nxy, Mx, My and Mxy My when `integrate='stress'`, or a + numpy array representing the stiffness matrix then + `integrate='modulus'`. + + Examples: + result = self.integrate_strain_profile(strain,integrate='modulus') + # `result` will be the tangent stiffness matrix (a 6x6 numpy array) + + result = self.integrate_strain_profile(strain) + # `result` will be a tuple containing section forces (Nx, Ny, Nxy, + Mx, My, Mxy) + + Raises: + ValueError: If a unkown value is passed to the `integrate` + parameter. """ - return self.integrator.integrate_strain_response_on_geometry( - geo=self.section.geometry, strain=strain, integrate=integrate + result = self.integrator.integrate_strain_response_on_geometry( + geo=self.section.geometry, + strain=strain, + integrate=integrate, + mesh_size=self.mesh_size, + layers=self.layers, ) + # Save layers for future use + if self.layers is None and result[-1] is not None: + self.layers = result[-1] + + # Return the results without layers + if len(result) == 2: + # Return only the stiffness matrix + return result[0] + + # Return only the stress resultants + return result[:-1] + def calculate_strain_profile( self, nx: float, @@ -52,6 +140,99 @@ def calculate_strain_profile( mx: float, my: float, mxy: float, - ): - """Get the strain plane for a given set of stress resultants.""" - raise NotImplementedError + initial: bool = False, + max_iter: int = 10, + tol: float = 1e-6, + ) -> t.List[float]: + """Computes the strain profile for a given set of stress resultants. + + Args: + nx (float): Membrane force in x-direction. + ny (float): Membrane force in y-direction. + nxy (float): Membrane shear. + mx (float): Plate moment giving stresses in x-direction. + my (float): Plate moment giving stresses in y-direction. + mxy (float): Plate moment giving shear stresses in the xy-plane. + + Returns: + list(float): The strain response eps_x, eps_y, gamma_xy, chi_x, + chi_y and chi_xy. + """ + # Get the gometry + geom = self.section.geometry + + # Collect loads in a numpy array + loads = np.array([nx, ny, nxy, mx, my, mxy]) + + # Compute initial tangent stiffness matrix + stiffness, layers = ( + self.integrator.integrate_strain_response_on_geometry( + geom, + [0, 0, 0, 0, 0, 0], + integrate='modulus', + mesh_size=self.mesh_size, + layers=self.layers, + ) + ) + + # Save layers if needed + if self.layers is None and layers is not None: + self.layers = layers + + # Calculate strain plane with Newton Rhapson Iterative method + num_iter = 0 + strain = np.zeros(6) + + # Factorize once the stiffness matrix if using initial + if initial: + # LU factorization + lu, piv = lu_factor(stiffness) + + # Do Newton loops + while True: + # Check if number of iterations exceeds the maximum + if num_iter > max_iter: + raise StopIteration('Maximum number of iterations reached.') + + if initial: + # If the initial stiffness is used, we follow a regular + # Newton-Raphson scheme where we calculate the strain increment + # from the residual and the initial tangent stiffness matrix + + # Calculate response and residuals + response = np.array( + self.integrate_strain_profile(strain=strain) + ) + residual = loads - response + + # Solve using the decomposed matrix + delta_strain = lu_solve((lu, piv), residual) + + else: + # The the current stiffness is used, we use a secant stiffness + # and calculate a new total strain from which we derive the + # strain increment + + # Calculate the current stiffness + stiffness, _ = ( + self.integrator.integrate_strain_response_on_geometry( + geom, + strain, + integrate='modulus', + layers=self.layers, + ) + ) + + # Solve using the current stiffness + delta_strain = np.linalg.solve(stiffness, loads) - strain + + # Update the strain + strain += delta_strain + + num_iter += 1 + + # Check for convergence: + if np.linalg.norm(delta_strain) < tol: + break + + return strain.tolist() diff --git a/structuralcodes/sections/section_integrators/__init__.py b/structuralcodes/sections/section_integrators/__init__.py index e32f448b..0730f06f 100644 --- a/structuralcodes/sections/section_integrators/__init__.py +++ b/structuralcodes/sections/section_integrators/__init__.py @@ -4,6 +4,7 @@ from ._fiber_integrator import FiberIntegrator from ._marin_integrator import MarinIntegrator, marin_integration from ._section_integrator import SectionIntegrator +from ._shell_integrator import ShellFiberIntegrator __all__ = [ 'integrator_factory', @@ -11,4 +12,5 @@ 'MarinIntegrator', 'SectionIntegrator', 'marin_integration', + 'ShellFiberIntegrator', ] diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index e93b9d38..c37a2eb4 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -47,9 +47,12 @@ def prepare_input( Returns: Tuple: (prepared_input, z_coords) """ - z_coords = kwargs.get('z_coords') + strain = np.atleast_1d(strain) + + layers = kwargs.get('layers') + z_coords, dz = (None, None) if layers is None else layers t_total = geo.thickness - if z_coords is None: + if z_coords is None and dz is None: mesh_size = kwargs.get('mesh_size', 0.01) if not (0 < mesh_size <= 1): raise ValueError('mesh_size must be [0,1].') @@ -97,7 +100,7 @@ def prepare_input( MA = np.stack(IA, axis=0) prepared_input = [(z_list, MA)] - return prepared_input, z_coords + return prepared_input, (z_coords, dz) def integrate_stress( self, @@ -184,12 +187,15 @@ def integrate_strain_response_on_geometry( ValueError: If `integrate` is not 'stress' or 'modulus'. """ # Prepare the general input based on the geometry and the input strains - prepared_input, _ = self.prepare_input( - geo=geo, strain=strain, integrate=integrate, **kwargs + prepared_input, layers = self.prepare_input( + geo=geo, + strain=strain, + integrate=integrate, + **kwargs, ) # Return the calculated response if integrate == 'stress': - return self.integrate_stress(prepared_input) + return *self.integrate_stress(prepared_input), layers if integrate == 'modulus': - return self.integrate_modulus(prepared_input) + return self.integrate_modulus(prepared_input), layers raise ValueError(f'Unknown integrate type: {integrate}') diff --git a/tests/test_sections/test_shell_section.py b/tests/test_sections/test_shell_section.py new file mode 100644 index 00000000..7e8fe4f8 --- /dev/null +++ b/tests/test_sections/test_shell_section.py @@ -0,0 +1,155 @@ +"""Tests for the Shell Section.""" + +import numpy as np +import pytest + +from structuralcodes.geometry._shell_geometry import ShellGeometry +from structuralcodes.materials.constitutive_laws import Elastic2D +from structuralcodes.sections import ShellSection + +# Membrane and bending-strain parameter ranges +eps_x = np.linspace(0.0, 1.0e-3, 2) +eps_y = np.linspace(0.0, 1.0e-3, 2) +eps_xy = np.linspace(0.0, 1.0e-3, 2) +chi_x = np.linspace(0.0, 1.0e-6, 2) +chi_y = np.linspace(0.0, 1.0e-6, 2) +chi_xy = np.linspace(0.0, 1.0e-6, 2) + + +@pytest.mark.parametrize('eps_x', eps_x) +@pytest.mark.parametrize('eps_y', eps_y) +@pytest.mark.parametrize('eps_xy', eps_xy) +@pytest.mark.parametrize('chi_x', chi_x) +@pytest.mark.parametrize('chi_y', chi_y) +@pytest.mark.parametrize('chi_xy', chi_xy) +@pytest.mark.parametrize( + 'Ec, nu, thickness', + [(30000, 0.20, 200), (30000, 0.20, 600)], +) +def test_integrate_strain_profile( + Ec, nu, thickness, eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy +): + """Elastic plate: strains → stress-resultants.""" + material = Elastic2D(Ec, nu) + shell = ShellSection(ShellGeometry(thickness, material)) + + # Numerically integrated forces/moments + R = shell.section_calculator.integrate_strain_profile( + np.array([eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy]) + ) + + # Analytical forces/moments + A = Ec * thickness / (1 - nu**2) + B = Ec * thickness**3 / 12 / (1 - nu**2) + Nx = A * (eps_x + nu * eps_y) + Ny = A * (eps_y + nu * eps_x) + Nxy = Ec * thickness / (2 * (1 + nu)) * eps_xy + Mx = B * (chi_x + nu * chi_y) + My = B * (chi_y + nu * chi_x) + Mxy = Ec * thickness**3 / 12 / (2 * (1 + nu)) * chi_xy + + expected = np.array([Nx, Ny, Nxy, Mx, My, Mxy]) + + assert np.allclose(R, expected, rtol=1e-2, atol=1e-2) + + +def test_integrate_strain_profile_tangent(E=30000, nu=0.20, t=200): + """Elastic plate: tangent stiffness matrix.""" + shell = ShellSection(ShellGeometry(t, Elastic2D(E, nu))) + K = shell.section_calculator.integrate_strain_profile( + np.array([1e-3] * 3 + [1e-6] * 3), integrate='modulus' + ) + + A11 = E * t / (1 - nu**2) + A12 = nu * A11 + A33 = E * t / (2 * (1 + nu)) + A44 = E * t**3 / 12 / (1 - nu**2) + A14 = nu * A44 + A55 = E * t**3 / 12 / (2 * (1 + nu)) + + A = np.array( + [ + [A11, A12, 0, 0, 0, 0], + [A12, A11, 0, 0, 0, 0], + [0, 0, A33, 0, 0, 0], + [0, 0, 0, A44, A14, 0], + [0, 0, 0, A14, A44, 0], + [0, 0, 0, 0, 0, A55], + ] + ) + + assert np.allclose(K, A, rtol=1e-2) + + +def test_wrong_integrator(): + """Unknown keyword must raise ValueError.""" + shell = ShellSection(ShellGeometry(200, Elastic2D(30000, 0.20))) + with pytest.raises(ValueError): + shell.section_calculator.integrate_strain_profile( + np.zeros(6), integrate='tangent' + ) + + +# Loads for reverse strain-solution test +nx = np.linspace(-1e5, 1e5, 2) +ny = np.linspace(-1e5, 1e5, 2) +nxy = np.linspace(-1e5, 1e5, 2) +mx = np.linspace(-1e8, 1e8, 2) +my = np.linspace(-1e8, 1e8, 2) +mxy = np.linspace(-1e8, 1e8, 2) + + +@pytest.mark.parametrize('nx', nx) +@pytest.mark.parametrize('ny', ny) +@pytest.mark.parametrize('nxy', nxy) +@pytest.mark.parametrize('mx', mx) +@pytest.mark.parametrize('my', my) +@pytest.mark.parametrize('mxy', mxy) +@pytest.mark.parametrize('Ec, nu, t', [(30000, 0.20, 200), (30000, 0.20, 600)]) +def test_elastic_strain_profile(Ec, nu, t, nx, ny, nxy, mx, my, mxy): + """Loads → strains for an isotropic plate.""" + shell = ShellSection(ShellGeometry(t, Elastic2D(Ec, nu))) + eps = shell.section_calculator.calculate_strain_profile( + nx, ny, nxy, mx, my, mxy + ) + + sig_x = nx / t + sig_y = ny / t + tau_xy = nxy / t + eps_x = (sig_x - nu * sig_y) / Ec + eps_y = (sig_y - nu * sig_x) / Ec + eps_xy = 2 * (1 + nu) * tau_xy / Ec + D = Ec * t**3 / 12 + chi_x = (mx - nu * my) / D + chi_y = (my - nu * mx) / D + chi_xy = 2 * (1 + nu) * mxy / D + expected = np.array([eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy]) + + assert np.allclose(eps, expected, rtol=1e-4, atol=1e-3) + + +def test_default_equals_explicit_mesh_size(t=200): + """Default mesh_size (0.01) equals explicit 0.01.""" + shell_0 = ShellSection(ShellGeometry(t, Elastic2D(30_000, 0.20))) + K0 = shell_0.section_calculator.integrate_strain_profile( + np.zeros(6), integrate='modulus' + ) + shell_1 = ShellSection( + ShellGeometry(t, Elastic2D(30_000, 0.20)), mesh_size=0.01 + ) + K1 = shell_1.section_calculator.integrate_strain_profile( + np.zeros(6), integrate='modulus' + ) + assert np.allclose(K0, K1) + + +@pytest.mark.parametrize('invalid', [-0.2, 0.0, 1.5]) +def test_invalid_mesh_size_raises(invalid, t=200): + """mesh_size outside (0,1] → ValueError.""" + shell = ShellSection( + ShellGeometry(t, Elastic2D(30000, 0.20)), mesh_size=invalid + ) + with pytest.raises(ValueError): + shell.section_calculator.integrate_strain_profile( + np.zeros(6), integrate='stress' + ) From 5eb014863ef4d8b2eae8a6ba9eb18f85050fd9bd Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Wed, 21 May 2025 12:23:37 +0200 Subject: [PATCH 17/31] feat: make consistent stiffness matrix for ParabolaRectangle2D (#253) * Make the secant stiffness matrix consistent * Refine computation of secant stiffness * Add more tests for get_secant method * Update Poisson matrix as per review comments --- .../_parabolarectangle_2d.py | 54 +++++++++++++------ .../test_materials/test_constitutive_laws.py | 28 ++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 8841752f..41e6f662 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -81,6 +81,15 @@ def transform(self, strain: ArrayLike) -> np.ndarray: ) return T, T @ eps + def poisson_matrix(self, nu: float) -> np.ndarray: + """Return the Poisson's ratio matrix.""" + return (1 / (1 - nu**2)) * np.array( + [ + [1, nu], + [nu, 1], + ] + ) + def get_effective_principal_strains( self, eps_p: ArrayLike, nu: float ) -> np.ndarray: @@ -88,16 +97,7 @@ def get_effective_principal_strains( Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete Shells' by M. A. Polak and F. J. Vecchio (1993). """ - denom = (1 + nu) * (1 - 2 * nu) - - P = np.array( - [ - [(1 - nu) / denom, nu / denom], - [nu / denom, (1 - nu) / denom], - ] - ) - - return P @ eps_p + return self.poisson_matrix(nu) @ eps_p def check_cracked(self, eps_p: ArrayLike) -> bool: """Check if the concrete is cracked. Returns True if any principal @@ -149,7 +149,12 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: T, eps_p = self.transform(strain) eps_p = eps_p[:2] nu = 0.0 if self.check_cracked(eps_p) else self._nu + + # Compressive-strength reduction factor due to lateral tension. beta = self.strength_reduction_lateral_cracking(eps_p) + + # Poisson correction + P = self.poisson_matrix(nu) eps_pf = self.get_effective_principal_strains(eps_p, nu) sig_p = super().get_stress(eps_pf) @@ -157,17 +162,34 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: # Avoid division by zero tol = 1e-12 if abs(eps_pf[0]) > tol: - E11 = (sig_p[0] * (beta if sig_p[0] < 0 else 1.0)) / eps_pf[0] + E_11 = (sig_p[0] * (beta if sig_p[0] < 0 else 1.0)) / eps_pf[0] else: - E11 = self._fc * 2.0 / self._eps_0 + E_11 = self._fc * 2.0 / self._eps_0 if abs(eps_pf[1]) > tol: - E22 = (sig_p[1] * (beta if sig_p[1] < 0 else 1.0)) / eps_pf[1] + E_22 = (sig_p[1] * (beta if sig_p[1] < 0 else 1.0)) / eps_pf[1] else: - E22 = self._fc * 2.0 / self._eps_0 + E_22 = self._fc * 2.0 / self._eps_0 + + # Initial 2x2 secant + D = np.diag([E_11, E_22]) + C = P @ D + + # Ensure symmetry + C = 0.5 * (C + C.T) - E12 = (E11 + E22) / 2 + # Shear modulus + G_12 = np.array([[(E_11 + E_22) / (4 * (1 + nu))]]) # Shape (1, 1) - Cp = np.diag([E11, E22, 0.5 * E12]) + Z_21 = np.zeros((2, 1)) # Shape (2, 1) + Z_12 = np.zeros((1, 2)) # Shape (1, 2) + + # 3x3 secant stiffness matrix + Cp = np.block( + [ + [C, Z_21], + [Z_12, G_12], + ] + ) return T.T @ Cp @ T diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 39c896b1..901c753c 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -325,6 +325,22 @@ def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): assert np.allclose(mat.get_stress(strain), stress, atol=1e-3) +def test_get_secant_shape(): + """Test the secant stiffness matrix shape of the parabola-rectangle + 2D material. + """ + mat = ParabolaRectangle2D( + fc=45.0, eps_0=-0.002, eps_u=-0.0035, n=2.0, nu=0.2 + ) + C = mat.get_secant([0.0, 0.0, 0.0]) + # Shape + assert C.shape == (3, 3) + # Symmetry + assert np.allclose(C, C.T) + # Positive diagonal elements + assert np.all(np.diag(C) > 0) + + @pytest.mark.parametrize( 'fc, eps_0, eps_u, nu, strain, expected', [ @@ -344,6 +360,18 @@ def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): [-0.003, 0.002, 0], [[15000, 0, 0], [0, 0, 0], [0, 0, 3750]], ), + ( + -30.0, + -0.002, + -0.0035, + 0.2, + [-0.001, 0.0, 0.0], + [ + [2.31119792e04, 5.27343750e03, 0.0], + [5.27343750e03, 2.96223958e04, 0.0], + [0.0, 0.0, 1.05468750e04], + ], + ), ], ) def test_get_secant(fc, eps_0, eps_u, nu, strain, expected): From 0d84845d09f3dcb73d8a9e3580397bd8f27ccb05 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Mon, 26 May 2025 21:16:04 +0200 Subject: [PATCH 18/31] Fix stadium 2 stiffness for initial iterations (#257) --- structuralcodes/sections/_shell_section.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structuralcodes/sections/_shell_section.py b/structuralcodes/sections/_shell_section.py index 9b1e3155..06c7e252 100644 --- a/structuralcodes/sections/_shell_section.py +++ b/structuralcodes/sections/_shell_section.py @@ -232,7 +232,7 @@ def calculate_strain_profile( num_iter += 1 # Check for convergence: - if np.linalg.norm(delta_strain) < tol: + if np.linalg.norm(delta_strain) < tol and num_iter > 1: break return strain.tolist() From 38ddf8ca9f8b43c166fd9bcbea8761221c05c993 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Sun, 8 Jun 2025 20:27:20 +0200 Subject: [PATCH 19/31] Replace exponent factor in ParabolaRectangle2D (#259) --- .../materials/constitutive_laws/_parabolarectangle_2d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 41e6f662..262a369f 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -164,12 +164,12 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: if abs(eps_pf[0]) > tol: E_11 = (sig_p[0] * (beta if sig_p[0] < 0 else 1.0)) / eps_pf[0] else: - E_11 = self._fc * 2.0 / self._eps_0 + E_11 = self._fc * self._n / self._eps_0 if abs(eps_pf[1]) > tol: E_22 = (sig_p[1] * (beta if sig_p[1] < 0 else 1.0)) / eps_pf[1] else: - E_22 = self._fc * 2.0 / self._eps_0 + E_22 = self._fc * self._n / self._eps_0 # Initial 2x2 secant D = np.diag([E_11, E_22]) From baff1c8f9bf096d45c47df675b2d4fd7ed6efaa1 Mon Sep 17 00:00:00 2001 From: Kristoffer Kristensen Date: Sun, 8 Jun 2025 20:29:30 +0200 Subject: [PATCH 20/31] fix: replace misleading variable name in integrate_stress method (#258) * Fix misleading variable name in integrate_stress method * Changed variable name to fiber_stress --- .../section_integrators/_shell_integrator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index c37a2eb4..0f73492f 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -115,13 +115,13 @@ def integrate_stress( Tuple(float, float, float, float, float, float): The stress resultants Nx, Ny, Nxy, Mx, My, Mxy """ - z, stress_resultants = prepared_input[0] - Nx = np.sum(stress_resultants[:, 0]) - Ny = np.sum(stress_resultants[:, 1]) - Nxy = np.sum(stress_resultants[:, 2]) - Mx = np.sum(stress_resultants[:, 0] * z) - My = np.sum(stress_resultants[:, 1] * z) - Mxy = np.sum(stress_resultants[:, 2] * z) + z, fiber_stress = prepared_input[0] + Nx = np.sum(fiber_stress[:, 0]) + Ny = np.sum(fiber_stress[:, 1]) + Nxy = np.sum(fiber_stress[:, 2]) + Mx = np.sum(fiber_stress[:, 0] * z) + My = np.sum(fiber_stress[:, 1] * z) + Mxy = np.sum(fiber_stress[:, 2] * z) return Nx, Ny, Nxy, Mx, My, Mxy def integrate_modulus( From c1fceef0f936b609dd15dab4783c43b6a7e76cc2 Mon Sep 17 00:00:00 2001 From: Sara Sundal Schei Date: Tue, 26 Aug 2025 21:17:03 +0200 Subject: [PATCH 21/31] feat: add tests shell section (#256) * Add shell section * Import shell classes to __init__ files * Make section_calculator a public attr of ShellSection * Make sure the layer information is stored * Make sure strain is an array * Draft iterative solver * Update tests to reflect how mesh_size is propagated * Make sure the stiffness matrix is returned correctly * Separate treatment of current and initial stiffness * Check number of iterations at the beginning of an iteration * Add tests for shell section --------- Co-authored-by: Morten Engen --- tests/test_sections/test_shell_section.py | 152 +++++++++++++++++++++- 1 file changed, 150 insertions(+), 2 deletions(-) diff --git a/tests/test_sections/test_shell_section.py b/tests/test_sections/test_shell_section.py index 7e8fe4f8..33cba480 100644 --- a/tests/test_sections/test_shell_section.py +++ b/tests/test_sections/test_shell_section.py @@ -3,8 +3,15 @@ import numpy as np import pytest -from structuralcodes.geometry._shell_geometry import ShellGeometry -from structuralcodes.materials.constitutive_laws import Elastic2D +from structuralcodes.geometry._shell_geometry import ( + ShellGeometry, + ShellReinforcement, +) +from structuralcodes.materials.constitutive_laws import ( + Elastic2D, + ElasticPlastic, + ParabolaRectangle2D, +) from structuralcodes.sections import ShellSection # Membrane and bending-strain parameter ranges @@ -153,3 +160,144 @@ def test_invalid_mesh_size_raises(invalid, t=200): shell.section_calculator.integrate_strain_profile( np.zeros(6), integrate='stress' ) + + +def test_parabola_section(): + """ParabolaRectangle2D with mesh_size 0.5.""" + concrete = ParabolaRectangle2D(35, nu=0) + reinforcement = ElasticPlastic(200000, 500) + + Asx1 = ShellReinforcement(-157, 1, 300, 16, reinforcement, 0) + + geo = ShellGeometry(400, concrete) + geo.add_reinforcement([Asx1]) + + section = ShellSection(geo, mesh_size=0.1) + calculator = section.section_calculator + strain = calculator.calculate_strain_profile(-1000, 0, 0, 0, 0, 0) + + assert np.allclose( + strain, + np.array([-7.20105642e-05, 0, 0, 1.07581081e-08, 0, 0]), + rtol=1e-6, + atol=1e-7, + ) + + +@pytest.mark.parametrize( + 'nx,nxy,expected,tol', + [ + ( + 0, + 1000, + np.array([3.046432e-05, 3.079979e-05, 1.914840e-04, 0, 0, 0]), + 1e-4, + ), + ( + -1000, + 1000, + np.array([-6.191136e-05, -1.100878e-22, 1.269841e-04, 0, 0, 0]), + 0.001, + ), + ( + 1000, + 1000, + np.array([6.191136e-05, -1.100878e-22, 1.269841e-04, 0, 0, 0]), + 0.001, + ), + ], +) +def test_parabola_cracked(nx, nxy, expected, tol): + """Test parabola rectangle with nu = 0.""" + concrete = ParabolaRectangle2D(45, nu=0) + reinforcement = ElasticPlastic(200000, 500) + geo = ShellGeometry(350, concrete) + Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) + Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) + Asy1 = ShellReinforcement(-118, 1, 200, 12, reinforcement, np.pi / 2) + Asy2 = ShellReinforcement(118, 1, 200, 12, reinforcement, np.pi / 2) + geo.add_reinforcement([Asx1, Asx2, Asy1, Asy2]) + section = ShellSection(geo) + calculator = section.section_calculator + strain = calculator.calculate_strain_profile( + nx, 0, nxy, 0, 0, 0, initial=True, tol=tol + ) + + assert np.allclose( + strain, + expected, + rtol=1e-6, + atol=1e-7, + ) + + +@pytest.mark.parametrize( + 'nx,nxy,expected,tol', + [ + ( + 0, + 1000, + np.array([2.923437e-05, 2.961959e-05, 2.150748e-04, 0, 0, 0]), + 1e-4, + ), + ( + -1000, + 1000, + np.array([-6.187717e-05, 1.220713e-05, 1.523810e-04, 0, 0, 0]), + 0.001, + ), + ( + 1000, + 1000, + np.array([6.187717e-05, -1.220713e-05, 1.523810e-04, 0, 0, 0]), + 0.001, + ), + ], +) +def test_parabola_uncracked(nx, nxy, expected, tol): + """Test parabola rectangle with nu = 0.2.""" + concrete = ParabolaRectangle2D(45) + reinforcement = ElasticPlastic(200000, 500) + geo = ShellGeometry(350, concrete) + Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) + Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) + Asy1 = ShellReinforcement(-118, 1, 200, 12, reinforcement, np.pi / 2) + Asy2 = ShellReinforcement(118, 1, 200, 12, reinforcement, np.pi / 2) + geo.add_reinforcement([Asx1, Asx2, Asy1, Asy2]) + section = ShellSection(geo) + calculator = section.section_calculator + strain = calculator.calculate_strain_profile( + nx, 0, nxy, 0, 0, 0, initial=True, tol=tol + ) + + assert np.allclose( + strain, + expected, + rtol=1e-6, + atol=1e-7, + ) + + +def test_exceed_max_iterations(): + """Test that the maximum number of iterations is exceeded.""" + concrete = ParabolaRectangle2D(45) + reinforcement = ElasticPlastic(200000, 500) + geo = ShellGeometry(350, concrete) + Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) + Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) + Asy1 = ShellReinforcement(-118, 1, 200, 12, reinforcement, np.pi / 2) + Asy2 = ShellReinforcement(118, 1, 200, 12, reinforcement, np.pi / 2) + geo.add_reinforcement([Asx1, Asx2, Asy1, Asy2]) + section = ShellSection(geo) + + with pytest.raises( + StopIteration, match='Maximum number of iterations reached' + ): + section.section_calculator.calculate_strain_profile( + -1000, + 0, + 1000, + 0, + 0, + 0, + ) From 61d4b3708e56d47003bb21c0c53d0db53ce7d535 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:38:21 +0200 Subject: [PATCH 22/31] fix: accept only material in shell geometry (#273) * Update target values in new tests * Merge dev * Accept only materials in shell geometry and reinforcement * Update tests --- .github/workflows/build.yaml | 12 +- .vscode/settings.json | 33 +- .../quickstart_example.py | 0 .../usage_compute_stress_strain.py | 27 + .../usage_create_constitutive_law_factory.py | 10 + ...sage_create_elastic_material_from_other.py | 7 + .../usage_create_elasticplastic_material.py | 7 + .../usage_create_materials_factory.py | 22 + .../usage_create_specific_concrete.py | 11 + .../usage_create_specific_reinforcement.py | 5 + .../usage_create_surface_geometries.py | 40 + .../usage_create_surface_geometries_simple.py | 31 + ...e_surface_geometries_with_reinforcement.py | 114 +++ .../usage_steel_profile_in_geometry.py | 18 + docs/_example_code/usage_steel_profiles.py | 7 + docs/api/codes/ec2_2004/shear.md | 4 + docs/api/codes/mc2010/shear.md | 48 +- docs/api/geometry/geometry_creation.md | 34 +- docs/api/materials/base_materials.md | 4 +- docs/api/materials/basic_materials.md | 50 ++ docs/api/materials/concretes.md | 3 + docs/api/materials/constitutive_laws.md | 16 + docs/api/materials/index.md | 1 + docs/api/materials/reinforcement_steels.md | 9 + docs/conf.py | 14 +- docs/examples/index.md | 4 - docs/index.md | 10 +- docs/installation/index.md | 2 +- docs/quickstart/index.md | 58 +- docs/usage/geometries/hea100.svg | 1 + docs/usage/geometries/index.md | 152 ++++ docs/usage/geometries/t_shaped_geometry.svg | 1 + .../t_shaped_geometry_with_reinforcement.svg | 1 + docs/usage/index.md | 12 + docs/usage/materials/index.md | 173 +++++ docs/usage/materials/parabola_rectangle.png | Bin 0 -> 61967 bytes docs/usage/sections/index.md | 78 ++ docs/usage/sections/moment_curvature.png | Bin 0 -> 60719 bytes makefile | 16 +- pyproject.toml | 2 +- structuralcodes/__init__.py | 2 +- structuralcodes/codes/ec2_2004/__init__.py | 2 + structuralcodes/codes/ec2_2004/shear.py | 48 +- structuralcodes/codes/mc2010/__init__.py | 18 + .../mc2010/_concrete_creep_and_shrinkage.py | 4 +- .../codes/mc2010/_concrete_punching.py | 688 ++++++++---------- structuralcodes/core/base.py | 121 ++- structuralcodes/geometry/_circular.py | 13 +- structuralcodes/geometry/_geometry.py | 172 ++--- structuralcodes/geometry/_rectangular.py | 13 +- structuralcodes/geometry/_reinforcement.py | 23 +- structuralcodes/geometry/_shell_geometry.py | 11 + structuralcodes/materials/__init__.py | 3 +- structuralcodes/materials/basic/__init__.py | 11 + structuralcodes/materials/basic/_elastic.py | 69 ++ .../materials/basic/_elasticplastic.py | 92 +++ structuralcodes/materials/basic/_generic.py | 43 ++ .../materials/concrete/__init__.py | 3 + .../materials/concrete/_concrete.py | 11 +- .../materials/concrete/_concreteEC2_2004.py | 14 + .../materials/concrete/_concreteEC2_2023.py | 14 + .../materials/concrete/_concreteMC2010.py | 19 + .../materials/constitutive_laws/__init__.py | 3 + .../constitutive_laws/_elastic_2d.py | 6 + .../constitutive_laws/_elasticplastic.py | 4 +- .../constitutive_laws/_initial_strain.py | 130 ++++ .../_parabolarectangle_2d.py | 2 + .../materials/reinforcement/__init__.py | 6 + .../materials/reinforcement/_reinforcement.py | 11 +- .../reinforcement/_reinforcementEC2_2004.py | 16 +- .../reinforcement/_reinforcementEC2_2023.py | 16 +- .../reinforcement/_reinforcementMC2010.py | 16 +- structuralcodes/sections/_generic.py | 67 +- structuralcodes/sections/_rc_utils.py | 20 +- .../section_integrators/_fiber_integrator.py | 30 +- .../section_integrators/_marin_integrator.py | 43 +- .../section_integrators/_shell_integrator.py | 8 +- tests/test_core/test_rc_utils.py | 30 +- tests/test_ec2_2004/test_ec2_2004_shear.py | 17 + tests/test_geometry/test_add.py | 133 ++++ tests/test_geometry/test_circular.py | 8 +- tests/test_geometry/test_geometry.py | 55 +- tests/test_geometry/test_rectangular.py | 8 +- tests/test_geometry/test_shell.py | 74 +- tests/test_geometry/test_steel_sections.py | 31 +- .../test_materials/test_constitutive_laws.py | 104 +++ .../test_materials/test_marin_coefficiens.py | 60 ++ tests/test_materials/test_reinforcements.py | 88 +++ .../test_mc2010_concrete_punching.py | 435 +++++++++-- tests/test_sections/test_generic_section.py | 217 +++++- tests/test_sections/test_shell_section.py | 95 ++- tox.ini | 12 + 92 files changed, 3294 insertions(+), 882 deletions(-) rename docs/{quickstart => _example_code}/quickstart_example.py (100%) create mode 100644 docs/_example_code/usage_compute_stress_strain.py create mode 100644 docs/_example_code/usage_create_constitutive_law_factory.py create mode 100644 docs/_example_code/usage_create_elastic_material_from_other.py create mode 100644 docs/_example_code/usage_create_elasticplastic_material.py create mode 100644 docs/_example_code/usage_create_materials_factory.py create mode 100644 docs/_example_code/usage_create_specific_concrete.py create mode 100644 docs/_example_code/usage_create_specific_reinforcement.py create mode 100644 docs/_example_code/usage_create_surface_geometries.py create mode 100644 docs/_example_code/usage_create_surface_geometries_simple.py create mode 100644 docs/_example_code/usage_create_surface_geometries_with_reinforcement.py create mode 100644 docs/_example_code/usage_steel_profile_in_geometry.py create mode 100644 docs/_example_code/usage_steel_profiles.py create mode 100644 docs/api/materials/basic_materials.md delete mode 100644 docs/examples/index.md create mode 100644 docs/usage/geometries/hea100.svg create mode 100644 docs/usage/geometries/index.md create mode 100644 docs/usage/geometries/t_shaped_geometry.svg create mode 100644 docs/usage/geometries/t_shaped_geometry_with_reinforcement.svg create mode 100644 docs/usage/index.md create mode 100644 docs/usage/materials/index.md create mode 100644 docs/usage/materials/parabola_rectangle.png create mode 100644 docs/usage/sections/index.md create mode 100644 docs/usage/sections/moment_curvature.png create mode 100644 structuralcodes/materials/basic/__init__.py create mode 100644 structuralcodes/materials/basic/_elastic.py create mode 100644 structuralcodes/materials/basic/_elasticplastic.py create mode 100644 structuralcodes/materials/basic/_generic.py create mode 100644 structuralcodes/materials/constitutive_laws/_initial_strain.py create mode 100644 tests/test_geometry/test_add.py create mode 100644 tox.ini diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 001ac396..7dfbba64 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -21,10 +21,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: make deps - - name: Black - run: make form - - name: Lint - run: make lint - - name: Test - run: make test \ No newline at end of file + run: | + python -m pip install tox tox-uv + make tox \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 2f876d97..9f76d6d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,20 @@ { - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - - // Config for ruff extension in VSCode - "[python]": { - "editor.formatOnSave": true, // Enable auto format on save - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" // This lets ruff reorganize the imports - }, - "editor.defaultFormatter": "charliermarsh.ruff" - } + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + // Config for ruff extension in VSCode + "[python]": { + "editor.formatOnSave": true, // Enable auto format on save + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" // This lets ruff reorganize the imports + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "notebook.formatOnSave.enabled": true, + "notebook.codeActionsOnSave": { + "notebook.source.organizeImports": "explicit" + } } \ No newline at end of file diff --git a/docs/quickstart/quickstart_example.py b/docs/_example_code/quickstart_example.py similarity index 100% rename from docs/quickstart/quickstart_example.py rename to docs/_example_code/quickstart_example.py diff --git a/docs/_example_code/usage_compute_stress_strain.py b/docs/_example_code/usage_compute_stress_strain.py new file mode 100644 index 00000000..3c6e6172 --- /dev/null +++ b/docs/_example_code/usage_compute_stress_strain.py @@ -0,0 +1,27 @@ +"""Example code for computing stresses with a constitutive law.""" + +import matplotlib.pyplot as plt +import numpy as np + +from structuralcodes.materials.concrete import ConcreteEC2_2004 + +# Create a concrete object +concrete = ConcreteEC2_2004(fck=45, gamma_c=1.5, alpha_cc=0.85) + +# Create an array of strain values based on the ultimate compressive strain, +# and compute corresponding stresses +strains = np.linspace(concrete.constitutive_law.get_ultimate_strain()[0], 0) +stresses = concrete.constitutive_law.get_stress(strains) + +# Plot stress-strain relation +fig, ax = plt.subplots() + +ax.plot(strains, stresses, '-k') +ax.set_xlim(xmax=0) +ax.set_ylim(ymax=0) +ax.grid() +ax.set_xlabel(r'$\epsilon$ [-]') +ax.set_ylabel(r'$\sigma$ [MPa]') + +fig.tight_layout() +fig.savefig('parabola_rectangle.png', dpi=300) diff --git a/docs/_example_code/usage_create_constitutive_law_factory.py b/docs/_example_code/usage_create_constitutive_law_factory.py new file mode 100644 index 00000000..d8386c4a --- /dev/null +++ b/docs/_example_code/usage_create_constitutive_law_factory.py @@ -0,0 +1,10 @@ +"""Example code for creating a constitutive law using the factory.""" + +from structuralcodes.materials.concrete import ConcreteEC2_2004 +from structuralcodes.materials.constitutive_laws import create_constitutive_law + +# Create a concrete object, and a constitutive law +concrete = ConcreteEC2_2004(fck=45, gamma_c=1.5, alpha_cc=0.85) +constitutive_law = create_constitutive_law( + constitutive_law_name='bilinearcompression', material=concrete +) diff --git a/docs/_example_code/usage_create_elastic_material_from_other.py b/docs/_example_code/usage_create_elastic_material_from_other.py new file mode 100644 index 00000000..132b82b8 --- /dev/null +++ b/docs/_example_code/usage_create_elastic_material_from_other.py @@ -0,0 +1,7 @@ +"""Example code for creating an ElasticMaterial from another material.""" + +from structuralcodes.materials.basic import ElasticMaterial +from structuralcodes.materials.concrete import ConcreteEC2_2004 + +concrete = ConcreteEC2_2004(fck=45, alpha_cc=0.85, gamma_c=1.5) +elastic_concrete = ElasticMaterial.from_material(concrete) diff --git a/docs/_example_code/usage_create_elasticplastic_material.py b/docs/_example_code/usage_create_elasticplastic_material.py new file mode 100644 index 00000000..729937c6 --- /dev/null +++ b/docs/_example_code/usage_create_elasticplastic_material.py @@ -0,0 +1,7 @@ +"""Example code for creating an ElasticPlasticMaterial.""" + +from structuralcodes.materials.basic import ElasticPlasticMaterial + +material = ElasticPlasticMaterial( + E=200000, fy=500, density=7850, Eh=0, eps_su=0.03 +) diff --git a/docs/_example_code/usage_create_materials_factory.py b/docs/_example_code/usage_create_materials_factory.py new file mode 100644 index 00000000..b41d264a --- /dev/null +++ b/docs/_example_code/usage_create_materials_factory.py @@ -0,0 +1,22 @@ +"""Example code for creating material objects with the factory functions.""" + +from structuralcodes import set_design_code +from structuralcodes.codes import ec2_2004 +from structuralcodes.materials.concrete import create_concrete +from structuralcodes.materials.reinforcement import create_reinforcement + +fck = 45 +fyk = 500 +Es = 200000 +ductility_class = 'C' + +set_design_code('ec2_2004') +concrete = create_concrete(fck=45, alpha_cc=0.85, gamma_c=1.5) +reinforcement = create_reinforcement( + fyk=fyk, + Es=Es, + gamma_s=1.15, + **ec2_2004.reinforcement_duct_props( + fyk=fyk, ductility_class=ductility_class + ), +) diff --git a/docs/_example_code/usage_create_specific_concrete.py b/docs/_example_code/usage_create_specific_concrete.py new file mode 100644 index 00000000..28384eb4 --- /dev/null +++ b/docs/_example_code/usage_create_specific_concrete.py @@ -0,0 +1,11 @@ +"""Example code for creating specific concretes.""" + +from structuralcodes.materials.concrete import ConcreteEC2_2004 +from structuralcodes.materials.constitutive_laws import Sargin + +concrete = ConcreteEC2_2004(fck=45) + +concrete = ConcreteEC2_2004(fck=45, fcm=50, constitutive_law='sargin') + +constitutive_law = Sargin(fc=53, eps_c1=-0.002, eps_cu1=-0.003, k=2) +concrete = ConcreteEC2_2004(fck=45, constitutive_law=constitutive_law) diff --git a/docs/_example_code/usage_create_specific_reinforcement.py b/docs/_example_code/usage_create_specific_reinforcement.py new file mode 100644 index 00000000..59342e20 --- /dev/null +++ b/docs/_example_code/usage_create_specific_reinforcement.py @@ -0,0 +1,5 @@ +"""Example code for creating specific reinforcement.""" + +from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 + +reinforcement = ReinforcementEC2_2004(fyk=500, Es=200000, ftk=510, epsuk=0.06) diff --git a/docs/_example_code/usage_create_surface_geometries.py b/docs/_example_code/usage_create_surface_geometries.py new file mode 100644 index 00000000..215052d6 --- /dev/null +++ b/docs/_example_code/usage_create_surface_geometries.py @@ -0,0 +1,40 @@ +"""Example code for creating surface geometries.""" + +from shapely import Polygon + +from structuralcodes.geometry import SurfaceGeometry +from structuralcodes.materials.concrete import ConcreteEC2_2004 + +# Define parameters +fck = 45 +width_web = 250 +width_flange = 1000 +height_web = 650 +height_flange = 150 + +# Create material +concrete = ConcreteEC2_2004(fck=fck) + +# Create polygons +polygon_web = Polygon( + ( + (-width_web / 2, 0), + (width_web / 2, 0), + (width_web / 2, height_web), + (-width_web / 2, height_web), + ) +) +polygon_flange = Polygon( + ( + (-width_flange / 2, height_web), + (width_flange / 2, height_web), + (width_flange / 2, height_web + height_flange), + (-width_flange / 2, height_web + height_flange), + ), +) + +# Create a T-shaped polygon by taking the union of the two rectangles +polygon_t = polygon_web.union(polygon_flange) + +# Create surface geometry +t_geom = SurfaceGeometry(poly=polygon_t, material=concrete) diff --git a/docs/_example_code/usage_create_surface_geometries_simple.py b/docs/_example_code/usage_create_surface_geometries_simple.py new file mode 100644 index 00000000..4982c474 --- /dev/null +++ b/docs/_example_code/usage_create_surface_geometries_simple.py @@ -0,0 +1,31 @@ +"""Example code for creating surface geometries.""" + +from structuralcodes.geometry import RectangularGeometry +from structuralcodes.materials.concrete import ConcreteEC2_2004 + +# Define parameters +fck = 45 +width_web = 250 +width_flange = 1000 +height_web = 650 +height_flange = 150 + +# Create material +concrete = ConcreteEC2_2004(fck=fck) + +# Create surface geometries +web_geom = RectangularGeometry( + width=width_web, + height=height_web, + material=concrete, + origin=(0, height_web / 2), +) +flange_geom = RectangularGeometry( + width=width_flange, + height=height_flange, + material=concrete, + origin=(0, height_web + height_flange / 2), +) + +# Add surface geometries to create a T-shaped geometry +t_geom = web_geom + flange_geom diff --git a/docs/_example_code/usage_create_surface_geometries_with_reinforcement.py b/docs/_example_code/usage_create_surface_geometries_with_reinforcement.py new file mode 100644 index 00000000..6b7ea7f0 --- /dev/null +++ b/docs/_example_code/usage_create_surface_geometries_with_reinforcement.py @@ -0,0 +1,114 @@ +"""Example code for creating surface geometries.""" + +import matplotlib.pyplot as plt + +from structuralcodes.codes.ec2_2004 import reinforcement_duct_props +from structuralcodes.geometry import ( + RectangularGeometry, + add_reinforcement_line, +) +from structuralcodes.materials.concrete import ConcreteEC2_2004 +from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 +from structuralcodes.sections import GenericSection + +# Define parameters +fck = 30 +fyk = 500 +Es = 200000 +ductility_class = 'C' +width_web = 250 +width_flange = 1000 +height_web = 650 +height_flange = 150 +diameter_bar = 20 +cover = 50 +n_bars_layer = 3 + +# Create material +concrete = ConcreteEC2_2004(fck=fck) +reinforcement = ReinforcementEC2_2004( + fyk=fyk, + Es=Es, + **reinforcement_duct_props(fyk=fyk, ductility_class=ductility_class), +) + +# Create surface geometries +web_geom = RectangularGeometry( + width=width_web, + height=height_web, + material=concrete, + origin=(0, height_web / 2), +) +flange_geom = RectangularGeometry( + width=width_flange, + height=height_flange, + material=concrete, + origin=(0, height_web + height_flange / 2), +) + +# Add surface geometries to create a T-shaped geometry +t_geom = web_geom + flange_geom + +# Add two layers of reinforcement +for y_coord in ( + cover + diameter_bar / 2, + 2 * cover + 3 * diameter_bar / 2, +): + t_geom = add_reinforcement_line( + geo=t_geom, + coords_i=( + -width_web / 2 + cover + diameter_bar / 2, + y_coord, + ), + coords_j=( + width_web / 2 - cover - diameter_bar / 2, + y_coord, + ), + diameter=diameter_bar, + material=reinforcement, + n=n_bars_layer, + ) + +# Create a section +section_not_translated = GenericSection(geometry=t_geom) + +# Use the centroid of the section, given in the gross properties, to re-allign +# the geometry with the origin +t_geom = t_geom.translate(dy=-section_not_translated.gross_properties.cz) +section = GenericSection(geometry=t_geom) + +# Calculate the bending strength +bending_strength = section.section_calculator.calculate_bending_strength() + +# Calculate the limit axial loads +limit_axial_loads = section.section_calculator.calculate_limit_axial_load() + +# Calculate the interaction domain between axial force and bending moment +nm = section.section_calculator.calculate_nm_interaction_domain() + +# Calculate the moment-curvature relation +moment_curvature = section.section_calculator.calculate_moment_curvature() + +# Visualize nm interaction domain +fig_nm, ax_nm = plt.subplots() +ax_nm.plot(nm.m_y * 1e-6, nm.n * 1e-3, '-k') +ax_nm.grid() +ax_nm.set_xlabel(r'$M_{\mathrm{y}}$ [kNm]') +ax_nm.set_ylabel(r'$N$ [kN]') +ax_nm.set_xlim(xmax=0) +ax_nm.set_ylim(ymax=0) +fig_nm.tight_layout() + +# Visualize moment-curvature relation +fig_momcurv, ax_momcurv = plt.subplots() +ax_momcurv.plot( + -moment_curvature.chi_y * 1e3, -moment_curvature.m_y * 1e-6, '-k' +) +ax_momcurv.grid() +ax_momcurv.set_xlabel(r'$\chi_{\mathrm{y}}$ [1/m]') +ax_momcurv.set_ylabel(r'$M_{\mathrm{y}}$ [kNm]') +ax_momcurv.set_xlim(xmin=0) +ax_momcurv.set_ylim(ymin=0) +fig_momcurv.tight_layout() + +fig_momcurv.savefig('moment_curvature.png', dpi=300) diff --git a/docs/_example_code/usage_steel_profile_in_geometry.py b/docs/_example_code/usage_steel_profile_in_geometry.py new file mode 100644 index 00000000..f5ef9db7 --- /dev/null +++ b/docs/_example_code/usage_steel_profile_in_geometry.py @@ -0,0 +1,18 @@ +"""Example code for using a steel profile in a geometry.""" + +from structuralcodes.geometry import IPE, SurfaceGeometry +from structuralcodes.materials.basic import ElasticPlasticMaterial +from structuralcodes.sections import GenericSection + +# Create a profile +ipe100 = IPE('IPE100') + +# Create an elastic perfect plastic material +steel = ElasticPlasticMaterial(E=210000, fy=275 / 1.05, density=7850) + +# Create a geometry and a section +geom = SurfaceGeometry(poly=ipe100.polygon, material=steel) +section = GenericSection(geometry=geom) + +# Use the section calculator to calculate the bending strength +bending_strength = section.section_calculator.calculate_bending_strength() diff --git a/docs/_example_code/usage_steel_profiles.py b/docs/_example_code/usage_steel_profiles.py new file mode 100644 index 00000000..7da01c8f --- /dev/null +++ b/docs/_example_code/usage_steel_profiles.py @@ -0,0 +1,7 @@ +"""Example code for listing available steel profiles.""" + +from structuralcodes.geometry import HE + +HE.profiles() + +hea100 = HE('HEA100') diff --git a/docs/api/codes/ec2_2004/shear.md b/docs/api/codes/ec2_2004/shear.md index d70e5231..7b300c1c 100644 --- a/docs/api/codes/ec2_2004/shear.md +++ b/docs/api/codes/ec2_2004/shear.md @@ -29,3 +29,7 @@ The following functions are related to calculation of shear capacity with and wi ```{eval-rst} .. autofunction:: structuralcodes.codes.ec2_2004.Asw_max ``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.ec2_2004.Asw_s_required +``` diff --git a/docs/api/codes/mc2010/shear.md b/docs/api/codes/mc2010/shear.md index 92403d9e..b5b7326a 100644 --- a/docs/api/codes/mc2010/shear.md +++ b/docs/api/codes/mc2010/shear.md @@ -61,23 +61,23 @@ ## Punching ```{eval-rst} -.. autofunction:: structuralcodes.codes.mc2010.v_rd_punching +.. autofunction:: structuralcodes.codes.mc2010.b_0 ``` ```{eval-rst} -.. autofunction:: structuralcodes.codes.mc2010.v_rd_max_punching +.. autofunction:: structuralcodes.codes.mc2010.b_s ``` ```{eval-rst} -.. autofunction:: structuralcodes.codes.mc2010.v_rdc_punching +.. autofunction:: structuralcodes.codes.mc2010.b_sr ``` ```{eval-rst} -.. autofunction:: structuralcodes.codes.mc2010.v_rds_punching +.. autofunction:: structuralcodes.codes.mc2010.k_dg ``` ```{eval-rst} -.. autofunction:: structuralcodes.codes.mc2010.b_0 +.. autofunction:: structuralcodes.codes.mc2010.k_psi ``` ```{eval-rst} @@ -88,6 +88,44 @@ .. autofunction:: structuralcodes.codes.mc2010.psi_punching ``` +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.psi_punching_level_one +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.psi_punching_level_three +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.psi_punching_level_two +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.r_s +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.sigma_swd +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.v_rd_max_punching +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.v_rd_punching +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.v_rdc_punching +``` + +```{eval-rst} +.. autofunction:: structuralcodes.codes.mc2010.v_rds_punching +``` + + + ## Torsion ```{eval-rst} diff --git a/docs/api/geometry/geometry_creation.md b/docs/api/geometry/geometry_creation.md index 72c0971b..d590343b 100644 --- a/docs/api/geometry/geometry_creation.md +++ b/docs/api/geometry/geometry_creation.md @@ -1,15 +1,7 @@ (api-geometry-creation)= # Geometry creation -```{eval-rst} -.. autoclass:: structuralcodes.geometry.Geometry - - .. automethod:: __init__ - - .. autoproperty:: name - .. autoproperty:: group_label - -``` +## Point geometry ```{eval-rst} .. autoclass:: structuralcodes.geometry.PointGeometry @@ -30,6 +22,8 @@ ``` +## SurfaceGeometry + ```{eval-rst} .. autoclass:: structuralcodes.geometry.SurfaceGeometry @@ -49,6 +43,8 @@ ``` +## Compound geometry + ```{eval-rst} .. autoclass:: structuralcodes.geometry.CompoundGeometry @@ -65,11 +61,19 @@ ``` +## Line object + ```{eval-rst} .. autofunction:: structuralcodes.geometry.create_line_point_angle ``` +:::{note} + +This function is useful for creating a line which can be used with the {func}`split() ` and {func}`split_two_lines() ` methods. + +::: + ## Common geometries In this section the classes and methods for creating special and common geometries are described. Generally these are simply wrappers of base geometries. @@ -110,3 +114,15 @@ In this section the classes and methods for creating special and common geometri .. autofunction:: structuralcodes.geometry.add_reinforcement_circle ``` + +## Base geometry class + +```{eval-rst} +.. autoclass:: structuralcodes.geometry.Geometry + + .. automethod:: __init__ + + .. autoproperty:: name + .. autoproperty:: group_label + +``` diff --git a/docs/api/materials/base_materials.md b/docs/api/materials/base_materials.md index b9a475ad..9054b9a4 100644 --- a/docs/api/materials/base_materials.md +++ b/docs/api/materials/base_materials.md @@ -41,6 +41,8 @@ .. autoclass:: structuralcodes.core.base.ConstitutiveLaw .. autoproperty:: name - .. automethod:: preprocess_strains_with_limits + .. automethod:: get_stress + .. automethod:: get_tangent .. automethod:: get_secant + .. automethod:: get_ultimate_strain ``` \ No newline at end of file diff --git a/docs/api/materials/basic_materials.md b/docs/api/materials/basic_materials.md new file mode 100644 index 00000000..71c732e8 --- /dev/null +++ b/docs/api/materials/basic_materials.md @@ -0,0 +1,50 @@ +(api-basic-materials)= +# Basic materials + +(api-elastic-material)= +## Elastic material + +```{eval-rst} +.. autoclass:: structuralcodes.materials.basic.ElasticMaterial + + .. automethod:: __init__ + + .. autoproperty:: E + .. autoproperty:: constitutive_law + .. autoproperty:: name + .. autoproperty:: density + .. automethod:: from_material + +``` + +(api-elastic-plastic-material)= +## Elastic-plastic material + +```{eval-rst} +.. autoclass:: structuralcodes.materials.basic.ElasticPlasticMaterial + + .. automethod:: __init__ + + .. autoproperty:: E + .. autoproperty:: fy + .. autoproperty:: Eh + .. autoproperty:: eps_su + .. autoproperty:: constitutive_law + .. autoproperty:: name + .. autoproperty:: density + +``` + +(api-generic-material)= +## Generic material + +```{eval-rst} +.. autoclass:: structuralcodes.materials.basic.GenericMaterial + + .. automethod:: __init__ + + .. autoproperty:: constitutive_law + .. autoproperty:: name + .. autoproperty:: density + +``` diff --git a/docs/api/materials/concretes.md b/docs/api/materials/concretes.md index c4b81aad..089a2245 100644 --- a/docs/api/materials/concretes.md +++ b/docs/api/materials/concretes.md @@ -7,6 +7,7 @@ .. autofunction:: structuralcodes.materials.concrete.create_concrete ``` +(api-concrete-ec2-2004)= ## Eurocode 2 (2004) ```{eval-rst} @@ -36,6 +37,7 @@ .. autoproperty:: density ``` +(api-concrete-mc2010)= ## _fib_ Model Code 2010 ```{eval-rst} @@ -66,6 +68,7 @@ .. autoproperty:: density ``` +(api-concrete-ec2-2023)= ## Eurocode 2 (2023) ```{eval-rst} diff --git a/docs/api/materials/constitutive_laws.md b/docs/api/materials/constitutive_laws.md index cff12e3e..f85e3267 100644 --- a/docs/api/materials/constitutive_laws.md +++ b/docs/api/materials/constitutive_laws.md @@ -1,6 +1,7 @@ (api-constitutive-laws)= # Constitutive laws +(api-constitutive-law-factory)= ## Constitutive law factory ```{eval-rst} @@ -97,4 +98,19 @@ .. automethod:: get_ultimate_strain .. automethod:: set_ultimate_strain +``` + +## Initial strain + +```{eval-rst} +.. autoclass:: structuralcodes.materials.constitutive_laws.InitialStrain + + .. automethod:: __init__ + .. automethod:: get_stress + .. automethod:: get_tangent + .. automethod:: get_ultimate_strain + + .. autoproperty:: strain_compatibility + .. autoproperty:: wrapped_law + ``` \ No newline at end of file diff --git a/docs/api/materials/index.md b/docs/api/materials/index.md index 6674cc18..c3cf934c 100644 --- a/docs/api/materials/index.md +++ b/docs/api/materials/index.md @@ -4,6 +4,7 @@ :::{toctree} :maxdepth: 2 +Basic materials Concrete materials Reinforcement steel materials Constitutive laws diff --git a/docs/api/materials/reinforcement_steels.md b/docs/api/materials/reinforcement_steels.md index be8dfed1..679e440a 100644 --- a/docs/api/materials/reinforcement_steels.md +++ b/docs/api/materials/reinforcement_steels.md @@ -7,11 +7,14 @@ .. autofunction:: structuralcodes.materials.reinforcement.create_reinforcement ``` +(api-reinforcement-ec2-2004)= ## Eurocode 2 (2004) ```{eval-rst} .. autoclass:: structuralcodes.materials.reinforcement.ReinforcementEC2_2004 + .. automethod:: __init__ + .. automethod:: fyd .. automethod:: ftd .. automethod:: epsud @@ -28,11 +31,14 @@ .. autoproperty:: density ``` +(api-reinforcement-mc2010)= ## _fib_ Model Code 2010 ```{eval-rst} .. autoclass:: structuralcodes.materials.reinforcement.ReinforcementMC2010 + .. automethod:: __init__ + .. automethod:: fyd .. automethod:: ftd .. automethod:: epsud @@ -49,11 +55,14 @@ .. autoproperty:: density ``` +(api-reinforcement-ec2-2023)= ## Eurocode 2 (2023) ```{eval-rst} .. autoclass:: structuralcodes.materials.reinforcement.ReinforcementEC2_2023 + .. automethod:: __init__ + .. automethod:: fyd .. automethod:: ftd .. automethod:: epsud diff --git a/docs/conf.py b/docs/conf.py index 572fe800..519e3b67 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,6 +42,18 @@ 'docs', ] +# Sphinx-design configuration +sd_custom_directives = { + 'dropdown-syntax': { + 'inherit': 'dropdown', + 'argument': 'Syntax', + 'options': { + 'color': 'primary', + 'icon': 'code', + }, + } +} + # Options for HTML output html_title = project html_theme = 'furo' @@ -69,7 +81,7 @@ # Options for intersphinx intersphinx_mapping = { - 'shapely': ('https://shapely.readthedocs.io/en/stable/', None), + 'shapely': ('https://shapely.readthedocs.io/en/stable', None), } # Autodoc settings diff --git a/docs/examples/index.md b/docs/examples/index.md deleted file mode 100644 index 4a03a71c..00000000 --- a/docs/examples/index.md +++ /dev/null @@ -1,4 +0,0 @@ -(examples)= -# Examples - -We are working on a set of examples that demonstrate the use of the library. For now, have a look at the [quickstart](quickstart). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0a9bebf5..73419712 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ Installation Quickstart -Examples +Usage guide API reference Theory reference Contributing @@ -16,7 +16,7 @@ Versioning ::: -StructuralCodes is a Python library for structural engineering calculations. If you are new to StructuralCodes, you should check out our {ref}`quickstart `, our {ref}`examples `, and the {ref}`library structure `. +StructuralCodes is a Python library for structural engineering calculations. If you are new to StructuralCodes, you should check out our {ref}`quickstart `, our {ref}`usage guide `, and the {ref}`library structure `.
@@ -38,12 +38,12 @@ How to install StructuralCodes. Get started in 5 minutes. ::: -:::{grid-item-card} Examples -:link: examples +:::{grid-item-card} Usage guide +:link: usage :link-type: ref :margin: 2 2 auto auto :class-title: sd-fs-5 -Examples are step-by-step instructions on how to do specific tasks. +The usage guide walks through the library and demonstrates core functionality while solving specific problems. ::: :::{grid-item-card} API reference diff --git a/docs/installation/index.md b/docs/installation/index.md index 60c7535b..59c07ab6 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -1,7 +1,7 @@ (installation)= # Installation -`structuralcodes` is compatible with Python 3.8, 3.9, 3.10, 3.11 and 3.12, and is installed by typing: +`structuralcodes` is compatible with Python 3.9, 3.10, 3.11, 3.12, and 3.13, and is installed by typing: :::::{tab-set} ::::{tab-item} Python diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md index 153f743a..3f321044 100644 --- a/docs/quickstart/index.md +++ b/docs/quickstart/index.md @@ -5,10 +5,9 @@ This example shows how to use `structuralcodes` to calculate the response of a r Import relevant functions and classes: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 3-9 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 3-9 +::: :::{seealso} {ref}`Library structure `. @@ -16,10 +15,9 @@ Import relevant functions and classes: Set the active design code to Eurocode 2, 2004 (`ec2_2004`): -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 11-12 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 11-12 +::: :::{seealso} {func}`set_design_code() ` @@ -27,10 +25,9 @@ Set the active design code to Eurocode 2, 2004 (`ec2_2004`): Create a concrete and a reinforcement material: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 14-24 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 14-24 +::: :::{seealso} {ref}`Material reference `. @@ -42,10 +39,9 @@ The reinforcement {func}`create_reinforcement() ` based on a {class}`shapely.Polygon` and the {class}`Concrete ` created with {func}`create_concrete() `: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 26-39 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 26-39 +::: :::{seealso} {class}`shapely.Polygon` @@ -55,17 +51,15 @@ Create a {class}`SurfaceGeometry ` bas Add reinforcement to the geometry: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 41-62 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 41-62 +::: Create a {class}`GenericSection ` based on the geometry: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 64-65 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 64-65 +::: :::{seealso} {ref}`Section reference ` @@ -73,10 +67,9 @@ Create a {class}`GenericSection ` based Call the {func}`.calculate_moment_curvature() ` method on the {class}`GenericSectionCalculator ` to calculate the moment-curvature relation: -```{eval-rst} -.. literalinclude:: quickstart_example.py - :lines: 67-68 -``` +:::{literalinclude} ../_example_code/quickstart_example.py +:lines: 67-68 +::: :::{seealso} {ref}`Section calculator reference ` @@ -89,7 +82,8 @@ Call the {func}`.calculate_moment_curvature() \ No newline at end of file diff --git a/docs/usage/geometries/index.md b/docs/usage/geometries/index.md new file mode 100644 index 00000000..0469d6ea --- /dev/null +++ b/docs/usage/geometries/index.md @@ -0,0 +1,152 @@ +(usage-geometries)= +# Geometries + +## General + +Geometry objects are responsible for storing a polygon, a point, or a _compound_ of two or more of the former, and their respective material objects. Geometry objects are therefore the physical representation of a [section](#usage-sections). + +## Surface geometries + +A {class}`SurfaceGeometry ` is initialized by passing a {class}`shapely.Polygon` with an arbitrary shape, and a [material object](#api-materials). See the example below where we create a T-shaped geometry. + +Note that adding two or more {class}`SurfaceGeometry ` objects results in a {class}`CompoundGeometry ` object. + +(code-usage-surface-geometries)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_surface_geometries.py +:lines: 3- +:caption: Create surface geometries. +::: +:::: + +The final geometry is shown in the figure [below](#fig-usage-t-shaped-geometry). + +(fig-usage-t-shaped-geometry)= +:::{figure} t_shaped_geometry.svg + +The t-shaped geometry created with the code [above](#code-usage-surface-geometries). +::: + +:::{tip} + +If you work in a Jupyter notebook, and return the geometry object in the final line of a code cell, the shape is visualized in the output below the code cell ✨ +::: + +:::{tip} + +In the code [above](#code-usage-surface-geometries) we used [Shapely](https://shapely.readthedocs.io) to model the shape of the geometry. We created the t-shaped geometry by taking the union of two polygons, but we could just as well have created one t-shaped polygon directly. You can read more about geometry creation with Shapely [here](https://shapely.readthedocs.io/en/stable/geometry.html). + +If the t-shaped geometry was made with two different materials, say one material for the flange and one for the web, we could have created two geometry objects, e.g. one {class}`SurfaceGeometry ` for the flange and one {class}`SurfaceGeometry ` for the web, and added these together (`+`) to create a {class}`CompoundGeometry `. + +::: + +:::{admonition} Creating holes +:class: tip + +If you ever need to create a hole in a geometry, simply create a separate geometry object for the hole, and subtract it from the other. This operation returns a new {class}`CompoundGeometry ` with a hole ✅ + +If you prefer to do the modelling with Shapely, this is also possible. Simply subtract the polygon of the hole from the polygon of the base geometry, and use the result as input to a {class}`SurfaceGeometry `. +::: + +## Rectangular and circular geometries + +To simplify creating common geometrical shapes, we can use the {class}`RectangularGeometry ` or {class}`CircularGeometry ` classes. These classes are wrappers for creating surface geometries with either a rectangular or a circular shape. + +Using {class}`RectangularGeometry `, the above example can be simplified to the example below, where the changed lines are highlighted. + +(code-usage-surface-geometries-simple)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_surface_geometries_simple.py +:lines: 3- +:emphasize-lines: 1, 14-26 +:caption: Create surface geometries using wrapper class for rectangular shape. +::: +:::: + +## Point geometries and ways to add reinforcement + +A {class}`PointGeometry ` is a geometrical object defined by a coordinate, a diameter, and a material. Point geometries are used to represent reinforcement, and can be included in arbitrary locations, and in arbitrary numbers. + +There are several ways to create point geometries and add these to other geometry objects as reinforcement. Each of the following options results in a {class}`CompoundGeometry ` object holding the geometry object(s) that are reinforced, and the point geometries that act as reinforcement. + +- Create {class}`PointGeometry ` objects and add (`+`) them together with the geometry they reinforce. +- Use the {func}`add_reinforcement() ` function to add one {class}`PointGeometry ` to the geometry. +- Use the {func}`add_reinforcement_line() ` function to add point geometries along a line. This is useful for reinforcing geometries with straight edges. +- Use the {func}`add_reinforcement_circle() ` function to add point geometries along a circle. This is useful for reinforcing geometries with curved edges. + +The example below demonstrates how to add two layers of longitudinal reinforcement to the T-shaped geometry created above. The changed lines are highlighted. + +Notice how {func}`add_reinforcement_line() ` returns a new {class}`CompoundGeometry ` representing the T-shaped surface geometry _and_ the point geometries for the longitudinal reinforcement. The final geometry with reinforcement is shown in the figure [below](#fig-usage-t-shaped-geometry-with-reinforcement). + +(code-usage-surface-geometries-with-reinforcement)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_surface_geometries_with_reinforcement.py +:lines: 5-11, 13-70 +:emphasize-lines: 1-5, 7, 12-15, 18-20, 24-28, 47-65 +:caption: Create surface geometries with reinforcement. +::: +:::: + +(fig-usage-t-shaped-geometry-with-reinforcement)= +:::{figure} t_shaped_geometry_with_reinforcement.svg + +The t-shaped geometry with reinforcement created with the code [above](#code-usage-surface-geometries-with-reinforcement). +::: + +## Steel profiles + +StructuralCodes comes with a set of predefined steel profiles. A steel profile class is a wrapper around a {class}`shapely.Polygon` and exposes the most common elastic and plastic section properties. + +The following families of steel profiles are available: + +- {class}`IPE ` +- {class}`HE ` +- {class}`UB ` +- {class}`UC ` +- {class}`UBP ` +- {class}`IPN ` +- {class}`UPN ` + +The available profiles within each family can be listed by calling the `.profiles` method on the respective class. The code below shows how to list the available profiles in the {class}`HE ` family. + +(code-usage-list-steel-profiles)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_steel_profiles.py + :lines: 3-5 + :caption: List the available steel profiles in the HE family. +::: +:::: + +To create an HEA100 profile, simply pass the name of the profile to the class as shown below. + +(code-usage-create-steel-profile)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_steel_profiles.py + :lines: 3, 6-7 + :caption: List the available steel profiles in the HE family. +::: +:::: + +The figure [below](#fig-usage-hea100) shows the shape of the steel profile. + +(fig-usage-hea100)= +:::{figure} hea100.svg + +The shape of the steel profile created with the code [above](#code-usage-create-steel-profile). +::: + +Notice how all the predefined steel profiles expose thicknesses, widths, heights, radii, etc. as properties along with elastic and plastic section properties like {py:attr}`Iy `, {py:attr}`Wely `, {py:attr}`Wply `, and {py:attr}`iy `. + +:::::{tip} + +The steel profiles expose the underlying {class}`shapely.Polygon` as the `.polygon` property. This can be passed to a {class}`SurfaceGeometry ` along with a material for use in further calculations. See for example the following code where calculate the bending strength of an IPE100 profile. + +(code-usage-steel-profile-in-geometry)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_steel_profile_in_geometry.py + :lines: 3- + :caption: Use a steeø profile in a geometry and calculate the bending strength. +::: +:::: + +::::: diff --git a/docs/usage/geometries/t_shaped_geometry.svg b/docs/usage/geometries/t_shaped_geometry.svg new file mode 100644 index 00000000..ccc90a9f --- /dev/null +++ b/docs/usage/geometries/t_shaped_geometry.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/usage/geometries/t_shaped_geometry_with_reinforcement.svg b/docs/usage/geometries/t_shaped_geometry_with_reinforcement.svg new file mode 100644 index 00000000..eecc3c85 --- /dev/null +++ b/docs/usage/geometries/t_shaped_geometry_with_reinforcement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/usage/index.md b/docs/usage/index.md new file mode 100644 index 00000000..bb685a31 --- /dev/null +++ b/docs/usage/index.md @@ -0,0 +1,12 @@ +(usage)= +# Usage guide + +This section walks through the library and demonstrates core functionality while solving specific problems. Check out the [quickstart](quickstart) if you are in a hurry. + +:::{toctree} + +Materials +Geometries +Sections + +::: \ No newline at end of file diff --git a/docs/usage/materials/index.md b/docs/usage/materials/index.md new file mode 100644 index 00000000..ca5e8a50 --- /dev/null +++ b/docs/usage/materials/index.md @@ -0,0 +1,173 @@ +(usage-materials)= +# Materials + +## General + +The [material classes](#api-materials) in StructuralCodes are the natural starting point when building any structural design workflow. The material classes are specific to each design code, and they contain attributes that represent common properties from the design code. A material object also contains a [constitutive law](#api-constitutive-laws). The constitutive law is accessed from the [section calculator](#api-section-calculator) when integrating the strain response in a section. + +StructuralCodes contains material classes for representing [concrete](#usage-concrete-materials) and [reinforcement](#usage-reinforcement-materials), in addition to a collection of [basic materials](#usage-basic-materials) for representing arbitrary materials with given constitutive laws. + +To create concrete and reinforcement objects, we can either directly import specific classes as shown in the sections below, or we can set an active design code and use the factory functions. + +(code-usage-materials-factory)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_materials_factory.py +:lines: 3-22 +:caption: Create material objects by setting a design code and calling factory functions. +::: +:::: + +Notice how the factory functions accept the same keyword arguments as the constructors of the specific classes. This allows us to pass e.g. `gamma_c` and `alpha_cc` to `create_concrete`. Also notice how we are using {func}`reinforcement_duct_props() ` to get a dictionary with properties related to the specified ductility class of reinforcement. + +After creating a material object, its properties are easily accessed, e.g. `concrete.Ecm`. Most notably, the `.constitutive_law` attribute reveals the [constitutive law](#usage-constitutive-laws) of the material. + +:::{tip} + +All material classes are subclasses of the {class}`Material ` base class, and all constitutive laws are subclasses of the {class}`ConstitutiveLaw ` base class. These base classes define interfaces. This means that we can create our own custom classes as subclasses of these base classes, and as long as we implement the necessary methods defined by the interface, we can swap the builtin classes with our custom ones. This opens for a lot of flexibility inside StructuralCodes. +::: + +(usage-materials-are-immutable)= +:::{attention} + +The material objects in StructuralCodes are immutable. This means that as soon as a material object is created, the attributes of the object cannot be changed. +::: + +(usage-concrete-materials)= +## Concrete materials + +There are classes for representing concrete according to the [current version of Eurocode 2](#api-concrete-ec2-2004), [_fib_ Model Code 2010](#api-concrete-mc2010), and the [next generation of Eurocode 2](#api-concrete-ec2-2023). + +Import {class}`ConcreteEC2_2004 ` to create an object representing concrete according to Eurocode 2 (2004). + +(code-usage-concrete-ec2-2004)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_specific_concrete.py + :lines: 3, 5-6 + :caption: Create a concrete object according to Eurocode 2 (2004). +::: +:::: + +Notice how it is possible to override the default values of the properties by passing these to the constructor. + +(code-usage-concrete-ec2-2004-override-defaults)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_specific_concrete.py +:lines: 8 +:caption: Override default values in a concrete object. +::: +:::: + +To override the constitutive law, we can also pass a [constitutive law object](#api-constitutive-laws) valid for concrete. + +(code-usage-concrete-ec2-2004-override-constitutive-law)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_specific_concrete.py +:lines: 3-4, 9-11 +:caption: Override the constitutive law in a concrete object. +::: +:::: + +(usage-reinforcement-materials)= +## Reinforcement materials + +Reinforcement objects are created and used in a similar manner as the concrete objects, and there are reinforcement classes according to the [current version of Eurocode 2](#api-reinforcement-ec2-2004), [_fib_ Model Code 2010](#api-reinforcement-mc2010), and the [next generation of Eurocode 2](#api-reinforcement-ec2-2023). + +Import {class}`ReinforcementEC2_2004 ` to create a reinforcement object according to Eurocode 2 (2004). + +(code-usage-reinforcement-ec2-2004)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_specific_reinforcement.py +:lines: 3-5 +:caption: Create a reinforcement object according to Eurocode 2 (2004). +::: +:::: + +(usage-constitutive-laws)= +## Constitutive laws + +When initializing a material object it will include a [constitutive law](#api-constitutive-laws) object as well. The purpose of the constitutive law object is to provide a relation between stresses and strains, or modulus and strain. Therefore all constitutive law objects implement the `.get_stress()`, `.get_tangent()`, and `.get_secant()` methods. As shown above, the [concrete](#usage-concrete-materials) and [reinforcement](#usage-reinforcement-materials) classes define default constitutive laws, that can be overridden by the constructor. + +The example below shows how to access the ultimate strains of the constitutive law, create an array of linearly spaced strain values, and passing these strain values to the `.get_stress()` method to get corresponding stresses. Note that only compressive strains and stresses are computed in this example. In this example we are passing an array of strain values to `.get_stress()`, but the method also accepts a scalar argument. + +(code-usage-compute-stress)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_compute_stress_strain.py +:lines: 4-14 +:caption: Compute stresses with a constitutive law. +::: +:::: + +:::::{tip} + +Use your favourite plotting library to visualize the computed stresses. The code below shows how to produce the figure [below](#fig-usage-parabola-rectangle) with [Matplotlib](https://matplotlib.org/). + +(code-usage-visualize-stress-strain)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_compute_stress_strain.py +:lines: 3, 15-24 +:caption: Visualize stress-strain relation. +::: +:::: + +(fig-usage-parabola-rectangle)= +:::{figure} parabola_rectangle.png +:width: 75% + +The parabola-rectangle distribution computed with the code [above](#code-usage-compute-stress). +::: + +::::: + +To create a constitutive law, you can either use the [constitutive law factory](#api-constitutive-law-factory), or directly import a specific constitutive law class as shown in the [example above](#code-usage-concrete-ec2-2004-override-constitutive-law). You can call {func}`get_constitutive_laws_list() ` to get a list of all the available constitutive laws. + +The constitutive law factory takes a name and a material object as arguments, and returns a constitutive law object. For it to work, the material object which is used as argument needs to implement a dunder method related to the specified constitutive law, in order to provide the relevant parameters for the constitutive law. + +The example below shows how we can use the constitutive law factory to create a {class}`BilinearCompression ` object based on a {class}`ConcreteEC2_2004 ` object. Since the {class}`ConcreteEC2_2004 ` object implements the {meth}`__bilinearcompression__ ` method, the constitutive law factory can call this method to get a dictionary with the relevant parameters to create the constitutive law. This workflow can be used if you wish to compare several constitutive laws that are based on the same material. + +(code-usage-constitutive-law-factory)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_constitutive_law_factory.py +:lines: 3- +:caption: Create a constitutive law using the constitutive law factory. +::: +:::: + +:::{admonition} Customize the material behaviour +:class: tip + +There are a few ways to customize constitutive laws in StructuralCodes. + +The first is to simply provide user values to the parameters of the available constitutive laws. + +The second is to use the special {class}`UserDefined ` constitutive laws which allows you to provide two arrays in the constructor: one for strains and one for stresses, i.e. representing any arbitrary uniaxial relation between stresses and strains. + +The third is to create a new subclass of {class}`ConstitutiveLaw ` that represents your custom constitutive law. As long as you implement the `.get_stress()`, `.get_tangent()`, and `.get_ultimate_strain()` methods, you can use it in a material as basis for a geometry in a section calculation. + +::: + +(usage-basic-materials)= +## Basic materials + +To simplify creating materials with simple constitutive laws, StructuralCodes contains a set of [basic materials](#api-basic-materials), including {class}`ElasticMaterial `, {class}`ElasticPlasticMaterial `, and {class}`GenericMaterial `. + +The {class}`GenericMaterial ` is a material class that accepts any constitutive law. + +The {class}`ElasticMaterial ` and the {class}`ElasticPlasticMaterial ` are materials that are initialized with just the necessary data to also initialize an {class}`Elastic ` and an {class}`ElasticPlastic ` constitutive law, respectively. The example below shows how to create an {class}`ElasticPlasticMaterial `. + +(code-usage-create-elastic-plastic-material)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_elasticplastic_material.py +:lines: 3- +:caption: Create a material with an elastic-plastic constitutive law. +::: +:::: + +Note how you can use {func}`.from_material() ` on {class}`ElasticMaterial ` to create an elastic representation of any other material on the fly. This method makes use of the constitutive law of the other material to get the tangent modulus at zero strain, and passes this value to the constructor of the elastic material. See the example code below. This feature is used for example in {func}`calculate_elastic_cracked_properties() ` to represent the materials in a reinforced concrete geometry with elastic materials. + +(code-usage-create-elastic-material-from-other)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_elastic_material_from_other.py +:lines: 3- +:caption: Create a material with an elastic constitutive law based on another material. +::: +:::: \ No newline at end of file diff --git a/docs/usage/materials/parabola_rectangle.png b/docs/usage/materials/parabola_rectangle.png new file mode 100644 index 0000000000000000000000000000000000000000..42b6a0b0c605f3475361aedd87232f963642043b GIT binary patch literal 61967 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 J6@26V)9)JyekuR} literal 0 HcmV?d00001 diff --git a/docs/usage/sections/index.md b/docs/usage/sections/index.md new file mode 100644 index 00000000..33c6100a --- /dev/null +++ b/docs/usage/sections/index.md @@ -0,0 +1,78 @@ +(usage-sections)= +# Sections + +## General + +In StructuralCodes, a _Section_ is responsible for connecting a [geometry](#usage-geometries) object with an API for performing calculations on the geometry. + +The section contains a _SectionCalculator_ which contains the API for doing calculations on the geometry, including for example lower level methods for integrating a strain profile to stress resultants, or calculating a strain profile based on stress resultants, or higher level methods like calculating a moment-curvature relation, or an interaction diagram between moments and axial force. + +The SectionCalculator uses a _SectionIntegrator_ to integrate the strain response on a geometry. + +## The generic beam section + +The {class}`GenericSection ` takes a {class}`SurfaceGeometry ` or a {class}`CompoundGeometry ` as input, and is capable of calculating the response of an arbitrarily shaped geometry with arbitrary reinforcement layout, subject to stresses in the direction of the beam axis. + +In the example [below](#code-usage-generic-section), we continue the example with the T-shaped geometry. + +(usage-sections-gross-properties-tip)= +:::{tip} +If you are looking for the gross properties of the section, these are available at the `.gross_properties` property on the `Section` object ✅ +::: + +Since the axial force and the moments are all relative to the origin, we start by translating the geometry such that the centroid is alligned with the origin. + +Notice how we can use {py:meth}`.calculate_bending_strength() ` and {py:meth}`.calculate_limit_axial_load() ` to calculate the bending strength and the limit axial loads in tension and compression. + +Furthermore, we have the following methods: + +{py:meth}`.integrate_strain_profile() ` +: Calculate the stress resultants for a given strain profile. + +{py:meth}`.calculate_strain_profile() ` +: Calculate the strain profile for given stress resultants. + +{py:meth}`.calculate_moment_curvature() ` +: Calculate the moment-curvature relation. + +{py:meth}`.calculate_nm_interaction_domain() ` +: Calculate the interaction domain between axial load and bending moment. + +{py:meth}`.calculate_nmm_interaction_domain() ` +: Calculate the interaction domain between axial load and biaxial bending. + +{py:meth}`.calculate_mm_interaction_domain() ` +: Calculate the interaction domain between biaxial bending for a given axial load. + +See the {class}`GenericSectionCalculator ` for a complete list. + +(code-usage-generic-section)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_surface_geometries_with_reinforcement.py +:lines: 12-13, 72-90 +:caption: Create a section and perform calculations. +::: +:::: + +Notice how for example {py:meth}`.calculate_moment_curvature() ` returns a custom dataclass of type {class}`MomentCurvatureResults `. If we inspect this class further, we find among its attributes `chi_y` and `m_y`. These are the curvature and the moment about the selected global axis of the section. We also find the curvature and moment about the axis orthogonal to the global axis `chi_z` and `m_z`, and the axial strain at the level of the global axis `eps_axial`. All these attributes are stored as arrays, ready for visualization or further processing. + +:::::{tip} + +Use your favourite plotting library to visualize the results from the {class}`GenericSectionCalculator `. The code below shows how to plot the moment-curvature relation in the figure [below](#fig-usage-moment-curvature) with [Matplotlib](https://matplotlib.org/). Notice how we are plotting the negative values of the curvatures and moments due to the sign convention. + +(code-usage-visualize-moment-curvature)= +::::{dropdown-syntax} +:::{literalinclude} ../../_example_code/usage_create_surface_geometries_with_reinforcement.py +:lines: 3, 101-111 +:caption: Visualize the moment-curvature relation. +::: +:::: + +(fig-usage-moment-curvature)= +:::{figure} moment_curvature.png +:width: 75% + +The moment-curvature relation computed with the code [above](#code-usage-generic-section). +::: + +::::: \ No newline at end of file diff --git a/docs/usage/sections/moment_curvature.png b/docs/usage/sections/moment_curvature.png new file mode 100644 index 0000000000000000000000000000000000000000..b08adaa8666278e186d236cce3eb36ce9787eaa1 GIT binary patch literal 60719 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.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: OS Independent" diff --git a/structuralcodes/__init__.py b/structuralcodes/__init__.py index 2b3104a0..48a7b452 100644 --- a/structuralcodes/__init__.py +++ b/structuralcodes/__init__.py @@ -3,7 +3,7 @@ from . import codes, core, geometry, materials, sections from .codes import get_design_codes, set_design_code, set_national_annex -__version__ = '0.4.0' +__version__ = '0.5.0' __all__ = [ 'set_design_code', diff --git a/structuralcodes/codes/ec2_2004/__init__.py b/structuralcodes/codes/ec2_2004/__init__.py index 55fb8856..08723763 100644 --- a/structuralcodes/codes/ec2_2004/__init__.py +++ b/structuralcodes/codes/ec2_2004/__init__.py @@ -78,6 +78,7 @@ ) from .shear import ( Asw_max, + Asw_s_required, VEdmax_unreinf, VRdc, VRdc_prin_stress, @@ -90,6 +91,7 @@ 'As_min_2', 'As_min_p', 'Asw_max', + 'Asw_s_required', 'alpha_e', 'eps_sm_eps_cm', 'hc_eff', diff --git a/structuralcodes/codes/ec2_2004/shear.py b/structuralcodes/codes/ec2_2004/shear.py index 99504ec8..6825cd8b 100644 --- a/structuralcodes/codes/ec2_2004/shear.py +++ b/structuralcodes/codes/ec2_2004/shear.py @@ -112,8 +112,8 @@ def _theta(theta: float, cot_min: float = 1.0, cot_max: float = 2.5) -> None: 'Wrong value for theta is chosen. Theta has ' f'to be chosen such that 1/tan(theta) lies between ' f'{cot_min} and {cot_max}. This corresponds to an angle ' - f'between {round(math.degrees(math.atan(1/cot_min)),2)} ' - f'and {round(math.degrees(math.atan(1/cot_max)),2)} ' + f'between {round(math.degrees(math.atan(1 / cot_min)), 2)} ' + f'and {round(math.degrees(math.atan(1 / cot_max)), 2)} ' f'degrees, respectively. Current angle is set at {theta}' ' degrees.' ) @@ -152,7 +152,7 @@ def alpha_cw(Ned: float, Ac: float, fcd: float) -> float: value = 2.5 * (1 - sigma_cp / fcd) else: raise ValueError( - f'sigma_cp/fcd={sigma_cp/fcd}. Prestress has to be smaller' + f'sigma_cp/fcd={sigma_cp / fcd}. Prestress has to be smaller' ' than design compressive strength.' ) return value @@ -400,6 +400,46 @@ def VRds( ) +# Equation (6.8 & 6.13) -> Reinforcement ratio isolated from the equation. +def Asw_s_required( + Ved: float, + z: float, + theta: float, + fywd: float, + alpha: float = 90.0, +) -> float: + """Calculate the required shear reinforcement. + + EN 1992-1-1 (2005). Eq. (6.13) + + Args: + Ved (float): The shear force in N. + z (float): The inner lever arm of internal forces in mm. + theta (float): The angle of the compression strut in degrees. + fywd (float): The design strength of the shear reinforcement steel in + MPa. + + Keyword Args: + alpha (float): The angle of the shear reinforcement with respect to the + neutral axis in degrees. Default value = 90 degrees. + + Returns: + float: The amount of required shear reinforcement in mm2/mm. + + Raises: + ValueError: When theta < 21.8 degrees or theta > 45 degrees. + """ + theta = math.radians(theta) + alpha = math.radians(alpha) + return ( + Ved + / z + / fywd + / (1 / math.tan(theta) + 1.0 / math.tan(alpha)) + / math.sin(alpha) + ) + + # Equation (6.9 & 6.14) # For alpha == 90 degrees, Equation (6.14) reduces to Equation (6.9). def VRdmax( @@ -480,7 +520,7 @@ def Asw_max( bw (float): The smallest width of the cross-section in tension in mm. s (float): The centre-to-centre distance of the shear reinforcement in mm. - fwyd (float): The design strength of the shear reinforcement steel in + fywd (float): The design strength of the shear reinforcement steel in MPa. NEd (float): The normal force in the cross-section due to loading or prestress (NEd > 0 for compression) in N. diff --git a/structuralcodes/codes/mc2010/__init__.py b/structuralcodes/codes/mc2010/__init__.py index 27a9fafc..c3f941f6 100644 --- a/structuralcodes/codes/mc2010/__init__.py +++ b/structuralcodes/codes/mc2010/__init__.py @@ -55,8 +55,17 @@ ) from ._concrete_punching import ( b_0, + b_s, + b_sr, + k_dg, + k_psi, m_ed, psi_punching, + psi_punching_level_one, + psi_punching_level_three, + psi_punching_level_two, + r_s, + sigma_swd, v_rd_max_punching, v_rd_punching, v_rdc_punching, @@ -184,6 +193,15 @@ 's_tau_bu_split', 'f_stm', 'tau_yield', + 'b_s', + 'b_sr', + 'k_dg', + 'k_psi', + 'r_s', + 'psi_punching_level_one', + 'psi_punching_level_two', + 'psi_punching_level_three', + 'sigma_swd', ] __title__: str = 'fib Model Code 2010' diff --git a/structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py b/structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py index 250dd106..7a44b9b3 100644 --- a/structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +++ b/structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py @@ -95,13 +95,13 @@ def _check_initial_stress(sigma: float, fcm: float) -> None: raise ValueError( 'The stress level exceeds the range of application.' 'Maximum allowable stress is 0.6*fcm. Current stress level ' - f'is {round(abs(sigma)/fcm, 3)}*fcm.' + f'is {round(abs(sigma) / fcm, 3)}*fcm.' ) if abs(sigma) > 0.4 * fcm: warnings.warn( 'Initial stress is too high to consider the ' 'concrete as an aging linear visco-elastic material: ' - f'sigma = {round(abs(sigma)/fcm,3)}*fcm > 0.4*fcm. Nonlinear' + f'sigma = {round(abs(sigma) / fcm, 3)}*fcm > 0.4*fcm. Nonlinear' ' creep calculations are performed according to subclause ' '5.1.9.4.3 (d) of the fib Model Code 2010 to account for ' 'large compressive stresses.' diff --git a/structuralcodes/codes/mc2010/_concrete_punching.py b/structuralcodes/codes/mc2010/_concrete_punching.py index 442b3cce..1045816f 100644 --- a/structuralcodes/codes/mc2010/_concrete_punching.py +++ b/structuralcodes/codes/mc2010/_concrete_punching.py @@ -1,5 +1,6 @@ """Covers punching in Model code 2010, 7.3.5.1 to 7.3.5.4.""" +import typing as t import warnings from math import cos, pi, sin @@ -7,7 +8,7 @@ def b_0(v_ed: float, v_prep_d_max: float) -> float: """Gives the general output for b_0, shear-resisting control perimeter. - fib Model Code 2010, eq. (7.3-57). + fib Model Code 2010, Eq. (7.3-57). Args: V_ed (float): The acting shear force from the columns. @@ -20,11 +21,31 @@ def b_0(v_ed: float, v_prep_d_max: float) -> float: return v_ed / v_prep_d_max +def b_s( + l_x: float, + l_y: float, +) -> float: + """The width of the support strip for calculating m_ed. + + fib Model Code 2010, Eq. (7.3-76). + + Args: + l_x (float): The width in x direction that the collumn carries. + l_y (float): The width in y direction that the collumn carries. + + Returns: + float: The width of the support strip for calculating m_ed. + """ + r_sx = 0.22 * l_x # see MC2010 7.3.5.3 + r_sy = 0.22 * l_y # see MC2010 7.3.5.3 + l_min = min(l_x, l_y) + return min(1.5 * (r_sx * r_sy) ** 0.5, l_min) + + def m_ed( v_ed: float, e_u: float, - l_x: float, - l_y: float, + b_s: float, inner: bool, edge_par: bool, edge_per: bool, @@ -32,15 +53,13 @@ def m_ed( ) -> float: """The average bending moment acting in the support strip. - fib Model Code 2010, eq. (7.3-76), (7.3-71), (7.3-72), (7.3-73) - and (7.3-74). + fib Model Code 2010, Eq. (7.3-71), (7.3-72), (7.3-73) and (7.3-74). Args: v_ed (float): The acting shear force from the columns. e_u (float): Refers to the eccentricity of the resultant of shear forces with respect to the centroid. - l_x (float): The width in x direction that the collumn carries. - l_y (float): The width in y direction that the collumn carries. + b_s (float): The width of the support strip for calculating m_ed. inner (bool): Is true only if the column is a inner column. edge_par (bool): Is true only if the column is a edge column with tension reinforcement parallel to the edge. @@ -52,10 +71,6 @@ def m_ed( float: The bending moment acting in the support strip regardless of the position of the column. """ - r_sx = 0.22 * l_x - r_sy = 0.22 * l_y - l_min = min(l_x, l_y) - b_s = min(1.5 * (r_sx * r_sy) ** 0.5, l_min) if inner: return v_ed * ((1 / 8) + abs(e_u) / (2 * b_s)) if edge_par: @@ -67,296 +82,346 @@ def m_ed( raise ValueError('Placement is not defined, only one needs to be True') -def psi_punching( +def b_sr( + l_x: float, + l_y: float, +) -> float: + """The width of the support strip in the radial direction. + + fib Model Code 2010, 7.3.5.3. + + Args: + l_x (float): The width in x direction that the collumn carries. + l_y (float): The width in y direction that the collumn carries. + + Returns: + float: The width of the support strip in the radial direction. + """ + return min(l_x, l_y) + + +def r_s( + l_x: float, + l_y: float, + x_direction: bool, + is_level_three_approximation: bool = False, + column_edge_or_corner: bool = False, + b_sr: t.Optional[float] = None, +) -> float: + """The position where the radial bending moment is zero with respect to the + support axis. + + fib Model Code 2010, 7.3.5.3 and Eq. (7.3-78) for Level III of + Approximation. + + Args: + l_x (float): The width in x direction that the collumn carries. + l_y (float): The width in y direction that the collumn carries. + x_direction (bool): True if the radial bending moment is zero in the x + direction, False if it is in the y direction. + column_edge_or_corner (bool): True if the column is an edge or corner + column, False if it is an inner column. To be used for Level III of + Approximation. + b_sr (float): The width of the support strip in the radial direction. + + Returns: + float: The position where the radial bending moment is zero with + respect to the support axis. + """ + r_s = 0.22 * l_x if x_direction is True else 0.22 * l_y + + if column_edge_or_corner and is_level_three_approximation: + if b_sr is None: + raise ValueError( + 'b_sr is not defined for Level 3 of Approximation' + ) + return max(r_s, 0.67 * b_sr) + return r_s + + +def psi_punching_level_one( l_x: float, l_y: float, f_yd: float, - d: float, + d_eff: float, e_s: float, - approx_lvl_p: float, - v_ed: float, - e_u: float, - inner: bool, - edge_par: bool, - edge_per: bool, - corner: bool, +) -> float: + """The psi value for the punching level one. + + fib Model Code 2010, Eq. (7.3-70). + + Args: + r_s (float): The position where the radial bending moment is zero with + respect to the support axis. + f_yd (float): Design strength of reinforment steel in MPa. + d_eff (float): The mean value of the effective depth in mm. + e_s (float): The E_modulus for steel in MPa. + + Returns: + float: The psi value for the punching level one. + """ + r_s = max(0.22 * l_x, 0.22 * l_y) # MC2010 7.3.5.3 - Unique for (7.3-70) + return 1.5 * r_s * f_yd / (d_eff * e_s) + + +def psi_punching_level_two( + r_s: float, + f_yd: float, + d_eff: float, + e_s: float, + m_ed: float, m_rd: float, - x_direction: bool, + m_Pd: float = 0, ) -> float: - """The rotation of the slab around the supported area. + """The psi value for the punching level two. - fib Model Code 2010, eq. (7.3-70), (7.3-75) and (7.3-77). + fib Model Code 2010, Eq. (7.3-75) and (7.3-77). Args: - l_x (float): The distance between two columns in x direction. - l_y (float): The distance between two columns in y direction. + r_s (float): The position where the radial bending moment is zero with + respect to the support axis. f_yd (float): Design strength of reinforment steel in MPa. - d (float): The mean value of the effective depth in mm. + d_eff (float): The mean value of the effective depth in mm. e_s (float): The E_modulus for steel in MPa. - approx_lvl_p (float): The approx level for punching. - v_ed (float): The acting shear force from the columns. - e_u (float): Refers to the eccentricity of the resultant of shear - forces with respect to the centroid. - inner (bool): Is true only if the column is a inner column. - edge_par (bool): Is true only if the column is a edge column with - tension reinforcement parallel to the edge. - edge_per (bool): Is true only if the column is a edge column with - tension reinforcement perpendicular to the edge. - corner (bool): Is true only if the column is a corner column. + m_ed (float): The average bending moment acting in the support strip. m_rd (float): The design average strength per unit length in MPa. - m_pd: (float): The average decompresstion moment due to prestressing - in MPa. + m_Pd (float): Optional to cover Eq. (7.3-77) for prestressed slabs. + The average decompression moment over the width of the support + strip (b_s) due to prestressing. + + Returns: + float: The psi value for the punching level two. + """ + return (1.5 * r_s * f_yd / (d_eff * e_s)) * ( + (m_ed - m_Pd) / (m_rd - m_Pd) + ) ** 1.5 + + +def psi_punching_level_three( + psi_punching_level_two: float, + is_uncracked_model: bool = False, + is_moment_from_uncracked_model: bool = False, +) -> float: + """The psi value for the punching level three. + + fib Model Code 2010, Level III of Approximation Eq. (7.3-75) and Eq. + (7.3-77) with coefficient 1.2 instead of 1.5 under specific conditions. + + Args: + psi_punching_level_two (float): The psi value for the punching level 2. + is_uncracked_model (bool): True if r_s is calculated using a linear + elastic (uncracked) model. + is_moment_from_uncracked_model (bool): True if m_sd is calculated from + a linear elastic (uncracked) model as the average value of the + moment for design of the flexural reinforcement over the width of + the support strip (b_s). + + Returns: + float: The psi value for the punching level three. + """ + if is_uncracked_model and is_moment_from_uncracked_model: + return 1.2 / 1.5 * psi_punching_level_two + return psi_punching_level_two + + +def psi_punching( + psi_punching_level_one: float, + psi_punching_level_two: float, + psi_punching_level_three: float, + approx_lvl_p: float, +) -> float: + """The rotation of the slab around the supported area. + + fib Model Code 2010, Clause 7.3.5.4. + + Args: + psi_punching_level_one (float): The psi value for the punching level 1. + psi_punching_level_two (float): The psi value for the punching level 2. + psi_punching_level_three (float): The psi value for the punching level + 3. + approx_lvl_p (float): The approx level for punching. Returns: float: psi for the chosen approx level in punching. """ - r_s = max(0.22 * l_x, 0.22 * l_y) if approx_lvl_p == 1: - psi = 1.5 * r_s * f_yd / (d * e_s) + return psi_punching_level_one + if approx_lvl_p == 2: + return psi_punching_level_two + if approx_lvl_p == 3: + return psi_punching_level_three + raise ValueError('Approximation level is not defined') - elif approx_lvl_p == 2: - r_s = 0.22 * l_x if x_direction is True else 0.22 * l_y - psi = (1.5 * r_s * f_yd / (d * e_s)) * ( - (m_ed(v_ed, e_u, l_x, l_y, inner, edge_par, edge_per, corner)) - / (m_rd) - ) ** 1.5 - return psi +def k_dg( + d_g: float, +) -> float: + """Calculate k_dg factor for punching resistance. + + fib Model Code 2010, Eq. (7.3-62). + + Args: + d_g (float): Maximum size of aggregate. + + Returns: + float: k_dg factor. + """ + return max(32 / (16 + d_g), 0.75) + + +def k_psi( + k_dg: float, + d_eff: float, + psi_punching: float, +) -> float: + """Calculate k_psi factor for punching resistance. + + fib Model Code 2010, Eq. (7.3-63). + + Args: + k_dg (float): k_dg factor. + d_eff (float): The mean value of the effective depth in mm. + psi_punching (float): psi value from psi_punching. + + Returns: + float: k_psi factor. + """ + return min(1 / (1.5 + 0.9 * k_dg * d_eff * psi_punching), 0.6) def v_rdc_punching( - l_x: float, - l_y: float, - f_yd: float, - d: float, - e_s: float, - approx_lvl_p: float, - dg: float, - f_ck: float, + k_psi_val: float, + b_0: float, d_v: float, - v_ed: float, - e_u: float, - inner: bool, - edge_par: bool, - edge_per: bool, - corner: bool, - m_rd: float, - m_pd: float, - v_prep_d_max: float, + f_ck: float, gamma_c: float = 1.5, ) -> float: """Punching resistance from the concrete. - fib Model Code 2010, eq. (7.3-61), (7.3-62) and (7.3-63). + fib Model Code 2010, Eq. (7.3-61). Args: - l_x (float): The distance between two columns in x direction. - l_y (float): The distance between two columns in y direction. - f_yd (float): Design strength of reinforment steel in MPa. - d (float): The mean value of the effective depth in mm. - e_s (float): The E_modulus for steel in MPa. - approx_lvl_p (float): The approx level for punching. - dg (float): Maximum size of aggregate. - f_ck (float): Characteristic strength in MPa. + k_psi_val (float): k_psi value from k_psi. + b_0 (float): The shear-resisting control perimeter from b_0. d_v (float): The effective depth considering support in mm. - v_ed (float): The acting shear force from the columns. - e_u (float): Refers to the eccentricity of the resultant of shear - forces with respect to the centroid. - inner (bool): Is true only if the column is a inner column. - edge_par (bool): Is true only if the column is a edge column with - tension reinforcement parallel to the edge. - edge_per (bool): Is true only if the column is a edge column with - tension reinforcement perpendicular to the edge. - corner (bool): Is true only if the column is a corner column. - m_rd (float): The design average strength per unit length in MPa. - m_pd: (float): The average decompresstion moment due to prestressing - in MPa. - v_prep_d_max (float): The maximum shear force per unit length - perpendicular to the basic control parameter (Figure 7.3-24). + f_ck (float): Characteristic strength in MPa. gamma_c: Safety factor for concrete. Returns: float: v_rdc for punching with the right approx level. """ - k_dg = max(32 / (16 + dg), 0.75) - k_psi = min( - 1 - / ( - 1.5 - + 0.9 - * k_dg - * d - * psi_punching( - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - v_ed, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, - m_pd, - ) - ), - 0.6, + return k_psi_val * b_0 * d_v * (f_ck**0.5) / gamma_c + + +def f_ywd( + f_ywk: float, + gamma_s: float, +) -> float: + """Calculate f_ywd for punching resistance. + + fib Model Code 2010, Eq. (7.3-64). + + Args: + f_ywk (float): Characteristic yield strength of the shear reinforcement + in MPa. + gamma_s (float): Safety factor for reinforcement. + + Returns: + float: f_ywd for punching resistance. + """ + return f_ywk / gamma_s + + +def sigma_swd( + e_s: float, + psi_punching: float, + alpha: float, + f_bd: float, + d_eff: float, + f_ywd: float, + phi_w: float, +) -> float: + """Calculate sigma_swd for punching resistance. + + fib Model Code 2010, Eq. (7.3-65). + + Args: + e_s (float): The E_modulus for steel in MPa. + psi_punching (float): psi value from psi_punching. + alpha (float): Inclination of the stirrups in degrees. + f_bd (float): The design bond strength in MPa. + d_eff (float): The mean value of the effective depth in mm. + f_ywd (float): Design yield strength of the shear reinforcement in MPa. + phi_w (float): The diameter of the shear reinforcement. + + Returns: + float: sigma_swd. + """ + return min( + (e_s * psi_punching / 6) + * (sin(alpha * pi / 180) + cos(alpha * pi / 180)) + * (sin(alpha * pi / 180) + f_bd * d_eff / (f_ywd * phi_w)), + f_ywd, ) - return k_psi * b_0(v_ed, v_prep_d_max) * d_v * (f_ck**0.5) / gamma_c def v_rds_punching( + f_ywd: float, e_u: float, b_u: float, - l_x: float, - l_y: float, - f_yd: float, - d: float, - e_s: float, - approx_lvl_p: float, - v_ed: float, - inner: bool, - edge_par: bool, - edge_per: bool, - corner: bool, - m_rd: float, - m_pd: float, alpha: float, - f_bd: float, - f_ywk: float, - phi_w: float, + sigma_swd: float, a_sw: float, - gamma_s: float, -): + v_ed: float, +) -> float: """The punching resistance from shear reinforcement. - fib Model Code 2010, eq. (7.3-64) and (7.3-65). + fib Model Code 2010, Eq. (7.3-64). Args: - e_u (float): The ecentrisity of the result of shear forces - with respect to the centroid (Figure 7.3-27b). + f_ywd (float): Design yield strength of the shear reinforcement in MPa. + e_u (float): The ecentrisity of the result of shear forces with respect + to the centroid (Figure 7.3-27b). b_u (float): The diamter of a circle with same surface as the region inside the basic control perimeter (Figure 7.3-27b). - l_x (float): The distance between two columns in x direction. - l_y (float): The distance between two columns in y direction. - f_yd (float): Design strength of reinforment steel in MPa. - d (float): The mean value of the effective depth in mm. - e_s (float): The E_modulus for steel in MPa. - approx_lvl_p (float): The approx level for punching. - v_ed (float): The acting shear force from the columns. - inner (bool): Is true only if the column is a inner column. - edge_par (bool): Is true only if the column is a edge column with - tension reinforcement parallel to the edge. - edge_per (bool): Is true only if the column is a edge column with - tension reinforcement perpendicular to the edge. - corner (bool): Is true only if the column is a corner column. - m_rd (float): The design average strength per unit length in MPa. - m_pd: (float): The average decompresstion moment due to prestressing in - MPa. alpha (float): Inclination of the stirrups in degrees. - f_bd (float): The design bond strength in MPa. - f_ywk (float): Characteristic yield strength of the shear reinforcement - in MPa. - phi_w (float): The diameter of the shear reinforcement. + sigma_swd (float): sigma_swd from sigma_swd. a_sw (float): The area of the shear reinforcement in mm^2. - gamma_s (float): Safety factor for reinforcement. + v_ed (float): The acting shear force from the columns. Returns: float: Punching resistance that comes from reinforcement. """ - f_ywd = f_ywk / gamma_s k_e = 1 / (1 + e_u / b_u) - sigma_swd = min( - ( - e_s - * psi_punching( - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - v_ed, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, - m_pd, - ) - / 6 - ) - * (sin(alpha * pi / 180) + cos(alpha * pi / 180)) - * (sin(alpha * pi / 180) + f_bd * d / (f_ywd * phi_w)), - f_ywd, - ) - if (a_sw * k_e * f_ywd) < 0.5 * v_ed: warnings.warn( - """In order to ensure sufficent deformation capacity, - consider increasing the amount of punching shear reinforcement""" + 'Consider increasing punching shear reinforcement for sufficient ' + 'deformation capacity' ) return a_sw * k_e * sigma_swd * sin(alpha * pi / 180) def v_rd_max_punching( - l_x: float, - l_y: float, - f_yd: float, - d: float, - e_s: float, - approx_lvl_p: float, - v_ed: float, - e_u: float, - inner: bool, - edge_par: bool, - edge_per: bool, - dg: float, - corner: bool, - m_rd: float, - m_pd: float, - v_prep_d_max: float, d_v: float, f_ck: float, d_head: bool, stirrups_compression: bool, + b0_val: float, + k_psi_val: float, gamma_c: float = 1.5, ) -> float: """Finds the maximum value you can have for v_rd_punching. - fib Model Code 2010, eq. (7.3-68) and (7.3-69). + fib Model Code 2010, Eq. (7.3-69). Args: - l_x (float): The distance between two columns in x direction. - l_y (float): The distance between two columns in y direction. - f_yd (float): Design strength of reinforment steel in MPa. - d (float): The mean value of the effective depth in mm. - e_s (float): The E_modulus for steel in MPa. - approx_lvl_p (float): The approx level for punching. - v_ed (float): The acting shear force from the columns. - e_u (float): Refers to the eccentricity of the resultant of shear - forces with respect to the centroid. - inner (bool): Is true only if the column is a inner column. - edge_par (bool): Is true only if the column is a edge column with - tension reinforcement parallel to the edge. - edge_per (bool): Is true only if the column is a edge column with - tension reinforcement perpendicular to the edge. - dg (float): Maximum size of aggregate. - corner (bool): Is true only if the column is a corner column. - m_rd (float): The design average strength per unit length in MPa. - m_pd: (float): The average decompresstion moment due to prestressing in - MPa. - v_prep_d_max (float): The maximum shear force per unit length - perpendiculerer to the basic control parameter (Figure 7.3-24). d_v (float): The effective depth considering support in mm. f_ck (float): Characteristic strength in MPa. d_head (bool): True if diameter of heads is three times larger than. stirrups_compression: (bool): Stirrups with sufficient length at compression face, and bent on tension face. + b0_val (float): The shear-resisting control perimeter from b_0. + k_psi_val (float): k_psi value from k_psi. gamma_c (float): Safety factor for concrete. Return: @@ -369,175 +434,22 @@ def v_rd_max_punching( else: k_sys = 2 - k_dg = max(32 / (16 + dg), 0.75) - k_psi = min( - 1 - / ( - 1.5 - + 0.9 - * k_dg - * d - * psi_punching( - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - v_ed, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, - m_pd, - ) - ), - 0.6, - ) - return min( - (k_sys * k_psi * b_0(v_ed, v_prep_d_max) * d_v * f_ck**0.5) / gamma_c, - (b_0(v_ed, v_prep_d_max) * d_v * f_ck**0.5) / gamma_c, - ) + base_resistance = b0_val * d_v * (f_ck**0.5 / gamma_c) + return min(k_sys * k_psi_val * base_resistance, base_resistance) -def v_rd_punching( - e_u: float, - b_u: float, - l_x: float, - l_y: float, - f_yd: float, - d: float, - e_s: float, - approx_lvl_p: float, - v_ed: float, - inner: bool, - edge_par: bool, - edge_per: bool, - corner: bool, - m_rd: float, - m_pd: float, - alpha: float, - f_bd: float, - f_ywk: float, - phi_w: float, - a_sw: float, - dg: float, - f_ck: float, - d_v: float, - v_prep_d_max: float, - d_head: bool, - stirrups_compression: bool, - gamma_c: float = 1.5, - gamma_s: float = 1.15, -) -> float: - """The total resistance for punching, both Vrd,c and Vrd,s. - fib Model Code 2010, eq. (7.3-60). +def v_rd_punching(v_rd_c: float, v_rd_s: float, v_rd_max: float) -> float: + """The total resistance for punching. + + fib Model Code 2010, Eq. (7.3-60). Args: - e_u (float): The ecentrisity of the result of shear forces with respect - to the centroid (Figure 7.3-27b). - b_u (float): The diamter of a circle with same surface as the region - inside the basic control perimeter (Figure 7.3-27b). - l_x (float): The distance between two columns in x direction. - l_y (float): The distance between two columns in y direction. - f_yd (float): Design strength of reinforment steel in MPa. - d (float): The mean value of the effective depth in mm. - e_s (float): The E_modulus for steel in MPa. - approx_lvl_p (float): The approx level for punching. - v_ed (float): The acting shear force from the columns. - inner (bool): Is true only if the column is a inner column. - edge_par (bool): Is true only if the column is a edge column with - tension reinforcement parallel to the edge. - edge_per (bool): Is true only if the column is a edge column with - tension reinforcement perpendicular to the edge. - corner (bool): Is true only if the column is a corner column. - m_rd (float): The design average strength per unit length in MPa. - m_pd: (float): The average decompresstion moment due to prestressing in - MPa. - alpha (float): Inclination of the stirrups in degrees. - f_bd (float): The design bond strength in MPa. - f_ywk (float): Characteristic yield strength of the shear reinforcement - in MPa. - phi_w (float): The diameter of the shear reinforcement. - a_sw (float): The area of the shear reinforcement in mm^2. - dg (float): Maximum size of aggregate. - f_ck (float): Characteristic strength in MPa. - d_v (float): The effective depth considering support in mm. - v_prep_d_max (float): The maximum shear force per unit length - perpendicular to the basic control parameter (Figure 7.3-24). + v_rd_c: Concrete contribution to punching resistance. + v_rd_s: Shear reinforcement contribution to punching resistance. + v_rd_max: Maximum punching resistance. - Return: - float: The maximum allowed punching resistance, regardless of values - from v_rdc and v_rds. + Returns: + float: Total punching resistance as min(v_rd_c + v_rd_s, v_rd_max). """ - return min( - v_rdc_punching( - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - dg, - f_ck, - d_v, - v_ed, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, - v_prep_d_max, - gamma_c, - ) - + v_rds_punching( - e_u, - b_u, - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - v_ed, - inner, - edge_par, - edge_per, - corner, - m_rd, - m_pd, - alpha, - f_bd, - f_ywk, - phi_w, - a_sw, - gamma_s, - ), - v_rd_max_punching( - l_x, - l_y, - f_yd, - d, - e_s, - approx_lvl_p, - v_ed, - e_u, - inner, - edge_par, - edge_per, - dg, - corner, - m_rd, - m_pd, - v_prep_d_max, - d_v, - f_ck, - d_head, - stirrups_compression, - gamma_c, - ), - ) + return min(v_rd_c + v_rd_s, v_rd_max) diff --git a/structuralcodes/core/base.py b/structuralcodes/core/base.py index 0714541f..be025515 100644 --- a/structuralcodes/core/base.py +++ b/structuralcodes/core/base.py @@ -15,21 +15,48 @@ class Material(abc.ABC): """Abstract base class for materials.""" _constitutive_law = None - - def __init__(self, density: float, name: t.Optional[str] = None) -> None: + _initial_strain: t.Optional[float] = None + _initial_stress: t.Optional[float] = None + _strain_compatibility: t.Optional[bool] = None + + def __init__( + self, + density: float, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + name: t.Optional[str] = None, + ) -> None: """Initializes an instance of a new material. Args: - density (float): density of the material in kg/m3 + density (float): Density of the material in kg/m3. Keyword Args: + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. name (Optional[str]): descriptive name of the material + + Raise: + ValueError: if both initial_strain and initial_stress are provided """ self._density = abs(density) + if initial_strain is not None and initial_stress is not None: + raise ValueError( + 'Both initial_strain and initial_stress cannot be provided.' + ) + self._initial_strain = initial_strain + self._initial_stress = initial_stress + self._strain_compatibility = strain_compatibility self._name = name if name is not None else 'Material' @property - def constitutive_law(self): + def constitutive_law(self) -> ConstitutiveLaw: """Returns the ConstitutiveLaw of the object.""" return self._constitutive_law @@ -43,6 +70,88 @@ def density(self): """Returns the density of the material in kg/m3.""" return self._density + @property + def initial_strain(self): + """Returns the initial strain of the material.""" + return self._initial_strain + + @property + def initial_stress(self): + """Returns the initial stress of the material.""" + return self._initial_stress + + @property + def strain_compatibility(self): + """Returns the strain compatibility of the material. + + If true (default), the strain compatibility is enforced + haveing the same strain as in all other materials of the + section at the same point. If false, the strain compatibility + is not enforced and the initial strain is applied to the section + independently. + """ + return self._strain_compatibility + + def _apply_initial_strain(self): + """Wraps the current constitutive law to apply initial strain.""" + strain_compatibility = ( + self._strain_compatibility + if self._strain_compatibility is not None + else True + ) + if self._initial_stress is not None: + # Specified a stress, compute the strain from it + self._initial_strain_from_stress() + if self._initial_strain is not None: + # Lazy import to avoid circular dependency + from structuralcodes.materials.constitutive_laws import ( # noqa: PLC0415 + InitialStrain, + ) + + if self._initial_stress is None: + # Compute the stress from the strain + self._initial_stress = self._constitutive_law.get_stress( + self._initial_strain + ) + + self._constitutive_law = InitialStrain( + self._constitutive_law, + self._initial_strain, + strain_compatibility, + ) + + def _initial_strain_from_stress(self): + """Computes the initial strain from the initial stress. + + This function is called internally so it assumes that the + initial stress is not None + """ + # Iteratively compute the initial strain that gives the desired + # initial stress. Note that the wrapped law can be nonlinear + tol = 1e-12 + max_iter = 100 + target_stress = self._initial_stress + strain = 0.0 + stress = self._constitutive_law.get_stress(strain) + d_stress = target_stress - stress + num_iter = 0 + while abs(d_stress) > tol and num_iter < max_iter: + tangent = self._constitutive_law.get_tangent(strain) + if tangent == 0: + raise ValueError( + 'Tangent modulus = 0 during initial strain computation.' + ) + d_strain = d_stress / tangent + strain += d_strain + stress = self._constitutive_law.get_stress(strain) + d_stress = target_stress - stress + num_iter += 1 + + if abs(d_stress) > tol: + raise RuntimeError('Failed to converge for given initial stress.') + + self._initial_strain = strain + class ConstitutiveLaw(abc.ABC): """Abstract base class for constitutive laws.""" @@ -150,7 +259,9 @@ def find_x_lim(x, y): eps = np.concatenate((eps_neg, eps_pos)) sig = self.get_stress(eps) - from structuralcodes.materials.constitutive_laws import UserDefined + from structuralcodes.materials.constitutive_laws import ( # noqa: PLC0415 + UserDefined, + ) return UserDefined(eps, sig) diff --git a/structuralcodes/geometry/_circular.py b/structuralcodes/geometry/_circular.py index 2d3754eb..6f5b4764 100644 --- a/structuralcodes/geometry/_circular.py +++ b/structuralcodes/geometry/_circular.py @@ -12,7 +12,7 @@ from numpy.typing import ArrayLike from shapely import Polygon -from structuralcodes.core.base import ConstitutiveLaw, Material +from structuralcodes.core.base import Material from ._geometry import SurfaceGeometry @@ -37,9 +37,8 @@ class CircularGeometry(SurfaceGeometry): def __init__( self, diameter: float, - material: t.Union[Material, ConstitutiveLaw], + material: Material, n_points: int = 20, - density: t.Optional[float] = None, concrete: bool = False, origin: t.Optional[ArrayLike] = None, name: t.Optional[str] = None, @@ -49,14 +48,9 @@ def __init__( Arguments: diameter (float): The diameter of the geometry. - material (Union(Material, ConstitutiveLaw)): A Material or - ConsitutiveLaw class applied to the geometry. + material (Material): A Material class applied to the geometry. n_points (int): The number of points used to discretize the circle as a shapely `Polygon` (default = 20). - density (Optional(float)): When a ConstitutiveLaw is passed as - material, the density can be provided by this argument. When - material is a Material object the density is taken from the - material. concrete (bool): Flag to indicate if the geometry is concrete. origin (Optional(ArrayLike)): The center point of the circle. (0.0, 0.0) is used as default. @@ -84,7 +78,6 @@ def __init__( super().__init__( poly=polygon, material=material, - density=density, concrete=concrete, name=name, group_label=group_label, diff --git a/structuralcodes/geometry/_geometry.py b/structuralcodes/geometry/_geometry.py index e9186e8d..5e6bef73 100644 --- a/structuralcodes/geometry/_geometry.py +++ b/structuralcodes/geometry/_geometry.py @@ -18,9 +18,9 @@ ) from shapely.ops import split -from structuralcodes.core.base import ConstitutiveLaw, Material +from structuralcodes.core.base import Material +from structuralcodes.materials.basic import ElasticMaterial from structuralcodes.materials.concrete import Concrete -from structuralcodes.materials.constitutive_laws import Elastic class Geometry: @@ -72,13 +72,24 @@ def return_global_counter_and_increase(cls): @staticmethod def from_geometry( geo: Geometry, - new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None, + new_material: t.Optional[Material] = None, ) -> Geometry: """Create a new geometry with a different material.""" raise NotImplementedError( 'This method should be implemented by subclasses' ) + def __add__(self, other: Geometry) -> CompoundGeometry: + """Add operator "+" for geometries. + + Arguments: + other (Geometry): The other geometry to add. + + Returns: + CompoundGeometry: A new CompoundGeometry. + """ + return CompoundGeometry([self, other]) + class PointGeometry(Geometry): """Class for a point geometry with material. @@ -91,8 +102,7 @@ def __init__( self, point: t.Union[Point, ArrayLike], diameter: float, - material: t.Union[Material, ConstitutiveLaw], - density: t.Optional[float] = None, + material: Material, name: t.Optional[str] = None, group_label: t.Optional[str] = None, ): @@ -105,12 +115,7 @@ def __init__( point (Union(Point, ArrayLike)): A couple of coordinates or a shapely Point object. diameter (float): The diameter of the point. - material (Union(Material, ConstitutiveLaw)): The material for the - point (this can be a Material or a ConstitutiveLaw). - density (Optional(float)): When a ConstitutiveLaw is passed as - material, the density can be providen by this argument. When - the material is a Material object the density is taken from the - material. + material (Material): The material for the point. name (Optional(str)): The name to be given to the object. group_label (Optional(str)): A label for grouping several objects (default is None). @@ -131,22 +136,12 @@ def __init__( warn_str += ' discarded' warnings.warn(warn_str) point = Point(coords) - if not isinstance(material, Material) and not isinstance( - material, ConstitutiveLaw - ): + if not isinstance(material, Material): raise TypeError( - f'mat should be a valid structuralcodes.base.Material \ - or structuralcodes.base.ConstitutiveLaw object. \ + f'mat should be a valid structuralcodes.base.Material object. \ {repr(material)}' ) - # Pass a constitutive law to the PointGeometry - self._density = density - if isinstance(material, Material): - self._density = material.density - self._material = material.constitutive_law - elif isinstance(material, ConstitutiveLaw): - self._material = material - + self._material = material self._point = point self._diameter = diameter self._area = np.pi * diameter**2 / 4.0 @@ -162,14 +157,14 @@ def area(self) -> float: return self._area @property - def material(self) -> ConstitutiveLaw: + def material(self) -> Material: """Returns the point material.""" return self._material @property def density(self) -> float: """Returns the density.""" - return self._density + return self.material.density @property def x(self) -> float: @@ -204,7 +199,6 @@ def translate(self, dx: float = 0.0, dy: float = 0.0) -> PointGeometry: point=affinity.translate(self._point, dx, dy), diameter=self._diameter, material=self._material, - density=self._density, name=self._name, group_label=self._group_label, ) @@ -231,7 +225,6 @@ def rotate( ), diameter=self._diameter, material=self._material, - density=self._density, name=self._name, group_label=self._group_label, ) @@ -239,16 +232,15 @@ def rotate( @staticmethod def from_geometry( geo: PointGeometry, - new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None, + new_material: t.Optional[Material] = None, ) -> PointGeometry: """Create a new PointGeometry with a different material. Arguments: geo (PointGeometry): The geometry. - new_material (Optional(Union(Material, ConstitutiveLaw))): A new - material to be applied to the geometry. If new_material is - None an Elastic material with same stiffness as the original - material is created. + new_material (Optional(Material)): A new material to be applied to + the geometry. If new_material is None an Elastic material with + same stiffness as the original material is created. Returns: PointGeometry: The new PointGeometry. @@ -261,24 +253,21 @@ def from_geometry( raise TypeError('geo should be a PointGeometry') if new_material is not None: # provided a new_material - if not isinstance(new_material, Material) and not isinstance( - new_material, ConstitutiveLaw - ): + if not isinstance(new_material, Material): raise TypeError( f'new_material should be a valid structuralcodes.base.\ - Material or structuralcodes.base.ConstitutiveLaw object. \ + Material object. \ {repr(new_material)}' ) else: # new_material not provided, assume elastic material with same # elastic modulus - new_material = Elastic(E=geo.material.get_tangent(eps=0)) + new_material = ElasticMaterial.from_material(geo.material) return PointGeometry( point=geo._point, diameter=geo._diameter, material=new_material, - density=geo._density, name=geo._name, group_label=geo._group_label, ) @@ -331,13 +320,12 @@ class SurfaceGeometry(Geometry): holes. """ - _material: ConstitutiveLaw + _material: Material def __init__( self, poly: Polygon, - material: t.Union[Material, ConstitutiveLaw], - density: t.Optional[float] = None, + material: Material, concrete: bool = False, name: t.Optional[str] = None, group_label: t.Optional[str] = None, @@ -346,11 +334,7 @@ def __init__( Arguments: poly (shapely.Polygon): A Shapely polygon. - material (Union(Material, ConstitutiveLaw)): A Material or - ConsitutiveLaw class applied to the geometry. - density (Optional(float)): When a ConstitutiveLaw is passed as mat, - the density can be provided by this argument. When mat is a - Material object the density is taken from the material. + material (Material): A Material applied to the geometry. concrete (bool): Flag to indicate if the geometry is concrete. name (Optional(str)): The name to be given to the object. group_label (Optional(str)): A label for grouping several objects. @@ -362,24 +346,15 @@ def __init__( f'poly need to be a valid shapely.geometry.Polygon object. \ {repr(poly)}' ) - if not isinstance(material, Material) and not isinstance( - material, ConstitutiveLaw - ): + if not isinstance(material, Material): raise TypeError( - f'mat should be a valid structuralcodes.base.Material \ - or structuralcodes.base.ConstitutiveLaw object. \ + f'mat should be a valid structuralcodes.base.Material object. \ {repr(material)}' ) self._polygon = poly - # Pass a constitutive law to the SurfaceGeometry - self._density = density - if isinstance(material, Material): - self._density = material.density - if isinstance(material, Concrete): - concrete = True - material = material.constitutive_law - self._material = material + if isinstance(material, Concrete): + concrete = True self._concrete = concrete @property @@ -403,11 +378,11 @@ def centroid(self) -> t.Tuple[float, float]: @property def density(self) -> float: """Returns the density.""" - return self._density + return self.material.density @property - def material(self) -> ConstitutiveLaw: - """Returns the Constitutive law.""" + def material(self) -> Material: + """Returns the material.""" return self._material @property @@ -506,17 +481,6 @@ def split_two_lines( # get the intersection return self.polygon.intersection(lines_polygon) - def __add__(self, other: Geometry) -> CompoundGeometry: - """Add operator "+" for geometries. - - Arguments: - other (Geometry): The other geometry to add. - - Returns: - CompoundGeometry: A new CompoundGeometry. - """ - return CompoundGeometry([self, other]) - def __sub__(self, other: Geometry) -> SurfaceGeometry: """Add operator "-" for geometries. @@ -527,7 +491,6 @@ def __sub__(self, other: Geometry) -> SurfaceGeometry: SurfaceGeometry: The resulting SurfaceGeometry. """ material = self.material - density = self._density # if we subtract a point from a surface we obtain the same surface sub_polygon = self.polygon @@ -540,9 +503,7 @@ def __sub__(self, other: Geometry) -> SurfaceGeometry: for g in other.geometries: sub_polygon = sub_polygon - g.polygon - return SurfaceGeometry( - poly=sub_polygon, material=material, density=density - ) + return SurfaceGeometry(poly=sub_polygon, material=material) def _repr_svg_(self) -> str: """Returns the svg representation.""" @@ -561,7 +522,6 @@ def translate(self, dx: float = 0.0, dy: float = 0.0) -> SurfaceGeometry: return SurfaceGeometry( poly=affinity.translate(self.polygon, dx, dy), material=self.material, - density=self._density, concrete=self.concrete, ) @@ -589,23 +549,21 @@ def rotate( self.polygon, angle, origin=point, use_radians=use_radians ), material=self.material, - density=self._density, concrete=self.concrete, ) @staticmethod def from_geometry( geo: SurfaceGeometry, - new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None, + new_material: t.Optional[Material] = None, ) -> SurfaceGeometry: """Create a new SurfaceGeometry with a different material. Arguments: geo (SurfaceGeometry): The geometry. - new_material: (Optional(Union(Material, ConstitutiveLaw))): A new - material to be applied to the geometry. If new_material is None - an Elastic material with same stiffness of the original - material is created. + new_material: (Optional(Material)): A new material to be applied to + the geometry. If new_material is None an Elastic material with + same stiffness of the original material is created. Returns: SurfaceGeometry: The new SurfaceGeometry. @@ -618,22 +576,18 @@ def from_geometry( raise TypeError('geo should be a SurfaceGeometry') if new_material is not None: # provided a new_material - if not isinstance(new_material, Material) and not isinstance( - new_material, ConstitutiveLaw - ): + if not isinstance(new_material, Material): raise TypeError( f'new_material should be a valid structuralcodes.base.\ - Material or structuralcodes.base.ConstitutiveLaw object. \ + Material object. \ {repr(new_material)}' ) else: # new_material not provided, assume elastic material with same # elastic modulus - new_material = Elastic(E=geo.material.get_tangent(eps=0)) + new_material = ElasticMaterial.from_material(geo.material) - return SurfaceGeometry( - poly=geo.polygon, material=new_material, density=geo._density - ) + return SurfaceGeometry(poly=geo.polygon, material=new_material) # here we can also add static methods like: # from_points @@ -648,14 +602,12 @@ def from_geometry( def _process_geometries_multipolygon( geometries: MultiPolygon, - materials: t.Optional[ - t.Union[t.List[Material], Material, ConstitutiveLaw] - ], + materials: t.Optional[t.Union[t.List[Material], Material]], ) -> list[Geometry]: """Process geometries for initialization.""" checked_geometries = [] # a MultiPolygon is provided - if isinstance(materials, (ConstitutiveLaw, Material)): + if isinstance(materials, Material): for g in geometries.geoms: checked_geometries.append( SurfaceGeometry(poly=g, material=materials) @@ -698,20 +650,23 @@ class CompoundGeometry(Geometry): properties. """ - geometries: t.List[Geometry] + geometries: t.List[t.Union[SurfaceGeometry, PointGeometry]] def __init__( self, - geometries: t.Union[t.List[Geometry], MultiPolygon], + geometries: t.Union[ + t.List[t.Union[SurfaceGeometry, PointGeometry, CompoundGeometry]], + MultiPolygon, + ], materials: t.Optional[t.Union[t.List[Material], Material]] = None, ) -> None: """Creates a compound geometry. Arguments: geometries (Union(List(Geometry), MultiPolygon)): A list of - Geometry objects (i.e. PointGeometry or SurfaceGeometry) or a - shapely MultiPolygon object (in this latter case also a list of - materials should be given). + Geometry objects (i.e. PointGeometry, SurfaceGeometry or + CompoundGeometry) or a shapely MultiPolygon object (in this + latter case also a list of materials should be given). materials (Optional(List(Material), Material)): A material (applied to all polygons) or a list of materials. In this case the number of polygons should match the number of materials. @@ -834,17 +789,6 @@ def rotate( processed_geoms.append(pg.rotate(angle, point, use_radians)) return CompoundGeometry(geometries=processed_geoms) - def __add__(self, other: Geometry) -> CompoundGeometry: - """Add operator "+" for geometries. - - Arguments: - other (Geometry): The other geometry to add. - - Returns: - CompoundGeometry: A new CompoundGeometry. - """ - return CompoundGeometry([self, other]) - def __sub__(self, other: Geometry) -> CompoundGeometry: """Add operator "-" for geometries. @@ -868,7 +812,7 @@ def __sub__(self, other: Geometry) -> CompoundGeometry: @staticmethod def from_geometry( geo: CompoundGeometry, - new_material: t.Optional[t.Union[Material, ConstitutiveLaw]] = None, + new_material: t.Optional[Material] = None, ) -> CompoundGeometry: """Create a new CompoundGeometry with a different material. diff --git a/structuralcodes/geometry/_rectangular.py b/structuralcodes/geometry/_rectangular.py index 7e0d179e..21974945 100644 --- a/structuralcodes/geometry/_rectangular.py +++ b/structuralcodes/geometry/_rectangular.py @@ -11,7 +11,7 @@ from numpy.typing import ArrayLike from shapely import Polygon -from structuralcodes.core.base import ConstitutiveLaw, Material +from structuralcodes.core.base import Material from ._geometry import SurfaceGeometry @@ -28,8 +28,7 @@ def __init__( self, width: float, height: float, - material: t.Union[Material, ConstitutiveLaw], - density: t.Optional[float] = None, + material: Material, concrete: bool = False, origin: t.Optional[ArrayLike] = None, name: t.Optional[str] = None, @@ -40,12 +39,7 @@ def __init__( Arguments: width (float): The width of the geometry. height (float): The height of the geometry. - material (Union(Material, ConstitutiveLaw)): A Material or - ConsitutiveLaw class applied to the geometry. - density (Optional(float)): When a ConstitutiveLaw is passed as - material, the density can be provided by this argument. When - material is a Material object the density is taken from the - material. + material (Material): A Material class applied to the geometry. concrete (bool): Flag to indicate if the geometry is concrete. When passing a Material as material, this is automatically inferred. origin (Optional(ArrayLike)): The center point of the rectangle. @@ -84,7 +78,6 @@ def __init__( super().__init__( poly=polygon, material=material, - density=density, concrete=concrete, name=name, group_label=group_label, diff --git a/structuralcodes/geometry/_reinforcement.py b/structuralcodes/geometry/_reinforcement.py index c981ec7f..05faf7b5 100644 --- a/structuralcodes/geometry/_reinforcement.py +++ b/structuralcodes/geometry/_reinforcement.py @@ -6,7 +6,7 @@ import numpy as np from shapely import Point -from structuralcodes.core.base import ConstitutiveLaw, Material +from structuralcodes.core.base import Material from ._geometry import CompoundGeometry, PointGeometry, SurfaceGeometry @@ -15,7 +15,7 @@ def add_reinforcement( geo: t.Union[SurfaceGeometry, CompoundGeometry], coords: t.Tuple[float, float], diameter: float, - material: t.Union[Material, ConstitutiveLaw], + material: Material, group_label: t.Optional[str] = None, ) -> CompoundGeometry: """Add a single bar given coordinate. @@ -25,8 +25,7 @@ def add_reinforcement( reinforcement. coords (Tuple(float, float)): A tuple with cordinates of bar. diameter (float): The diameter of the reinforcement. - material (Union(Material, ConstitutiveLaw)): A material or a - constitutive law for the behavior of the reinforcement. + material (Material): A material for the reinforcement. group_label (Optional(str)): A label for grouping several objects (default is None). @@ -45,7 +44,7 @@ def add_reinforcement_line( coords_i: t.Tuple[float, float], coords_j: t.Tuple[float, float], diameter: float, - material: t.Union[Material, ConstitutiveLaw], + material: Material, n: int = 0, s: float = 0.0, first: bool = True, @@ -60,9 +59,8 @@ def add_reinforcement_line( coords_i (Tuple(float, float)): Coordinates of the initial point of line. coords_j (Tuple(float, float)): Coordinates of the final point of line. - diamter (float): The diameter of the bars. - material (Union(Material, ConstitutiveLaw)): A valid material or - constitutive law. + diameter (float): The diameter of the bars. + material (Material): A material for the reinforcement. n (int): The number of bars to be distributed inside the line (default = 0). s (float): The distance between the bars (default = 0). @@ -80,8 +78,6 @@ def add_reinforcement_line( CompoundGeometry: A compound geometry with the original geometry and the reinforcement. """ - from math import floor - p1 = np.array(coords_i) p2 = np.array(coords_j) distance = np.linalg.norm(p2 - p1) @@ -102,7 +98,7 @@ def add_reinforcement_line( elif s > 0: # Provided the spacing # 1. Compute the number of bars - n = floor(distance / s) + 1 + n = math.floor(distance / s) + 1 # 2. Distribute the bars centered in the segment d = (n - 1) * s p1 = p1 + v * (distance - d) / 2.0 @@ -130,7 +126,7 @@ def add_reinforcement_circle( center: t.Tuple[float, float], radius: float, diameter: float, - material: t.Union[Material, ConstitutiveLaw], + material: Material, n: int = 0, s: float = 0.0, first: bool = True, @@ -152,8 +148,7 @@ def add_reinforcement_circle( radius (float): Radius of the circle line where reinforcement will be added. diameter (float): The diameter of the bars. - material (Union(Material, ConstitutiveLaw)): A valid material or - constitutive law. + material (Material): A material for the reinforcement. n (int): The number of bars to be distributed inside the line (default = 0). s (float): The distance between the bars (default = 0). diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 654a644e..975e76e0 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -32,6 +32,11 @@ def __init__( """Initialize a shell reinforcement.""" super().__init__(name, group_label) + if not isinstance(material, Material): + raise TypeError( + 'Material should be a valid structuralcodes.base.Material' + f' object. {repr(material)}' + ) self._z = z self._n_bars = n_bars self._cc_bars = cc_bars @@ -104,6 +109,12 @@ def __init__( if thickness <= 0: raise ValueError('Shell thickness must be positive.') + if not isinstance(material, Material): + raise TypeError( + 'Material should be a valid structuralcodes.base.Material' + f' object. {repr(material)}' + ) + self._thickness = thickness self._material = material diff --git a/structuralcodes/materials/__init__.py b/structuralcodes/materials/__init__.py index 82a7305d..5dcac1d6 100644 --- a/structuralcodes/materials/__init__.py +++ b/structuralcodes/materials/__init__.py @@ -1,9 +1,10 @@ """Main entry point for materials.""" -from . import concrete, constitutive_laws, reinforcement +from . import basic, concrete, constitutive_laws, reinforcement __all__ = [ 'concrete', 'constitutive_laws', 'reinforcement', + 'basic', ] diff --git a/structuralcodes/materials/basic/__init__.py b/structuralcodes/materials/basic/__init__.py new file mode 100644 index 00000000..481679f0 --- /dev/null +++ b/structuralcodes/materials/basic/__init__.py @@ -0,0 +1,11 @@ +"""A collection of basic material classes.""" + +from ._elastic import ElasticMaterial +from ._elasticplastic import ElasticPlasticMaterial +from ._generic import GenericMaterial + +__all__ = [ + 'ElasticMaterial', + 'ElasticPlasticMaterial', + 'GenericMaterial', +] diff --git a/structuralcodes/materials/basic/_elastic.py b/structuralcodes/materials/basic/_elastic.py new file mode 100644 index 00000000..40951294 --- /dev/null +++ b/structuralcodes/materials/basic/_elastic.py @@ -0,0 +1,69 @@ +"""A material class with elastic properties.""" + +import typing as t + +from ...core.base import Material +from ..constitutive_laws import create_constitutive_law + + +class ElasticMaterial(Material): + """A material class with elastic properties.""" + + _E: float + + def __init__( + self, + E: float, + density: float, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[float] = None, + name: t.Optional[str] = None, + ): + """Initialize a material with an elastic plastic constitutive law. + + Arguments: + E (float): The Young's modulus. + density (float): The density. + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. + name (str, optional): The name of the material, default value None. + """ + super().__init__( + density=density, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + name=name if name else 'ElasticMaterial', + ) + self._E = E + self._constitutive_law = create_constitutive_law('elastic', self) + self._apply_initial_strain() + + @property + def E(self) -> float: + """Returns the Young's modulus.""" + return self._E + + def __elastic__(self) -> dict: + """Returns kwargs for creating an elastic constitutive law.""" + return {'E': self.E} + + @classmethod + def from_material(cls, other_material: Material): + """Create an elastic material based on another material.""" + # Create name of elastic material + name = other_material.name + if name is not None: + name += '_elastic' + + return cls( + E=other_material.constitutive_law.get_tangent(eps=0), + density=other_material.density, + name=name, + ) diff --git a/structuralcodes/materials/basic/_elasticplastic.py b/structuralcodes/materials/basic/_elasticplastic.py new file mode 100644 index 00000000..b1e39b49 --- /dev/null +++ b/structuralcodes/materials/basic/_elasticplastic.py @@ -0,0 +1,92 @@ +"""A material class with elastic plastic properties.""" + +import typing as t + +from ...core.base import Material +from ..constitutive_laws import create_constitutive_law + + +class ElasticPlasticMaterial(Material): + """A material class with elastic plastic properties.""" + + _E: float + _fy: float + _Eh: float + _eps_su: float + + def __init__( + self, + E: float, + fy: float, + density: float, + Eh: float = 0, + eps_su: t.Optional[float] = None, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[float] = None, + name: t.Optional[str] = None, + ): + """Initialize a material with an elastic plastic constitutive law. + + Arguments: + E (float): The Young's modulus. + fy (float): The yield stress. + density (float): The density. + Eh (float, optional): The hardening modulus, default value 0. + eps_su (float, optional): The ultimate strain, default value None. + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. + name (str, optional): The name of the material, default value None. + """ + super().__init__( + density=density, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + name=name if name else 'ElasticPlasticMaterial', + ) + self._E = E + self._fy = fy + self._Eh = Eh + self._eps_su = eps_su + + self._constitutive_law = create_constitutive_law( + 'elasticplastic', self + ) + self._apply_initial_strain() + + @property + def E(self) -> float: + """Returns the Young's modulus.""" + return self._E + + @property + def fy(self) -> float: + """Returns the yield stress.""" + return self._fy + + @property + def Eh(self) -> float: + """Returns the hardening modulus.""" + return self._Eh + + @property + def eps_su(self) -> float: + """Returns the ultimate strain.""" + return self._eps_su + + def __elasticplastic__(self) -> dict: + """Returns kwargs for ElasticPlastic constitutive law with strain + hardening. + """ + return { + 'E': self.E, + 'fy': self.fy, + 'Eh': self.Eh, + 'eps_su': self.eps_su, + } diff --git a/structuralcodes/materials/basic/_generic.py b/structuralcodes/materials/basic/_generic.py new file mode 100644 index 00000000..1e3b4b28 --- /dev/null +++ b/structuralcodes/materials/basic/_generic.py @@ -0,0 +1,43 @@ +"""A generic material that could hold any type of constitutive law.""" + +import typing as t + +from ...core.base import ConstitutiveLaw, Material + + +class GenericMaterial(Material): + """A material class that accepts any constitutive law.""" + + def __init__( + self, + density: float, + constitutive_law: ConstitutiveLaw, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, + name: t.Optional[str] = None, + ): + """Initialize a material with a constitutive law. + + Arguments: + density (float): The density. + constitutive_law (ConstitutiveLaw): The constitutive law of the + material. + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. + name (str, optional): The name of the material, default value None. + """ + super().__init__( + density=density, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + name=name if name else 'GenericMaterial', + ) + self._constitutive_law = constitutive_law + self._apply_initial_strain() diff --git a/structuralcodes/materials/concrete/__init__.py b/structuralcodes/materials/concrete/__init__.py index 3bc053ef..715a79ac 100644 --- a/structuralcodes/materials/concrete/__init__.py +++ b/structuralcodes/materials/concrete/__init__.py @@ -49,6 +49,9 @@ def create_concrete( desired standard. If None (default) the globally used design standard will be adopted. Otherwise the design standard specified will be used for the instance of the material. + **kwargs: Other valid keyword arguments that are collected and passed + to the specific concrete material. Please inspect the documentation + of the other concrete materials to see valid arguments. Raises: ValueError: if the design code is not valid or does not cover concrete diff --git a/structuralcodes/materials/concrete/_concrete.py b/structuralcodes/materials/concrete/_concrete.py index 0d4f52e7..4e9890a3 100644 --- a/structuralcodes/materials/concrete/_concrete.py +++ b/structuralcodes/materials/concrete/_concrete.py @@ -21,10 +21,19 @@ def __init__( density: float = 2400, gamma_c: t.Optional[float] = None, existing: t.Optional[bool] = False, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, ) -> None: """Initializes an abstract concrete material.""" name = name if name is not None else 'Concrete' - super().__init__(density=density, name=name) + super().__init__( + density=density, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + name=name, + ) self._fck = abs(fck) if existing: diff --git a/structuralcodes/materials/concrete/_concreteEC2_2004.py b/structuralcodes/materials/concrete/_concreteEC2_2004.py index ac20b844..28306441 100644 --- a/structuralcodes/materials/concrete/_concreteEC2_2004.py +++ b/structuralcodes/materials/concrete/_concreteEC2_2004.py @@ -46,6 +46,9 @@ def __init__( ConstitutiveLaw, ] ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, fcm: t.Optional[float] = None, fctm: t.Optional[float] = None, fctk_5: t.Optional[float] = None, @@ -80,6 +83,13 @@ def __init__( law type for concrete. (valid options for string: 'elastic', 'parabolarectangle', 'bilinearcompression', 'sargin', 'popovics'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. fcm (float, optional): The mean compressive strength. fctm (float, optional): The mean tensile strength. fctk_5 (float, optional): The 5% fractile for the tensile strength. @@ -130,6 +140,9 @@ def __init__( density=density, existing=False, gamma_c=gamma_c, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._alpha_cc = alpha_cc self._fcm = abs(fcm) if fcm is not None else None @@ -165,6 +178,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for concrete.' ) + self._apply_initial_strain() def __post_init__(self): """Validator for the attributes that are set in the constructor.""" diff --git a/structuralcodes/materials/concrete/_concreteEC2_2023.py b/structuralcodes/materials/concrete/_concreteEC2_2023.py index f4f92cf5..c2b52e34 100644 --- a/structuralcodes/materials/concrete/_concreteEC2_2023.py +++ b/structuralcodes/materials/concrete/_concreteEC2_2023.py @@ -48,6 +48,9 @@ def __init__( ConstitutiveLaw, ] ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, fcm: t.Optional[float] = None, fctm: t.Optional[float] = None, fctk_5: t.Optional[float] = None, @@ -80,6 +83,13 @@ def __init__( law type for concrete. (valid options for string: 'elastic', 'parabolarectangle', 'bilinearcompression', 'sargin', 'popovics'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. fcm (float, optional): The mean compressive strength. fctm (float, optional): The mean tensile strength. fctk_5 (float, optional): The 5% fractile for the tensile strength. @@ -124,6 +134,9 @@ def __init__( density=density, existing=False, gamma_c=gamma_c, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._kE = kE self._strength_dev_class = strength_dev_class.strip().lower() @@ -158,6 +171,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for concrete.' ) + self._apply_initial_strain() def __post_init__(self): """Validator for the attributes that are set in the constructor.""" diff --git a/structuralcodes/materials/concrete/_concreteMC2010.py b/structuralcodes/materials/concrete/_concreteMC2010.py index a8921524..37bc1156 100644 --- a/structuralcodes/materials/concrete/_concreteMC2010.py +++ b/structuralcodes/materials/concrete/_concreteMC2010.py @@ -48,6 +48,9 @@ def __init__( ConstitutiveLaw, ] ] = 'parabolarectangle', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, fcm: t.Optional[float] = None, fctm: t.Optional[float] = None, fctkmin: t.Optional[float] = None, @@ -77,6 +80,18 @@ def __init__( alpha_cc (float, optional): A factor for considering long-term effects on the strength, and effects that arise from the way the load is applied. + consitutive_law (ConstitutiveLaw | str): A valid ConstitutiveLaw + object for concrete or a string defining a valid constitutive + law type for concrete. (valid options for string: 'elastic', + 'parabolarectangle', 'bilinearcompression', 'sargin', + 'popovics'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. fcm (float, optional): The mean compressive strength. fctm (float, optional): The mean tensile strength. fctkmin (float, optional): The minimum tensile strength. @@ -126,6 +141,9 @@ def __init__( name=name, density=density, gamma_c=gamma_c, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._alpha_cc = alpha_cc self._fcm = abs(fcm) if fcm is not None else None @@ -162,6 +180,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for concrete.' ) + self._apply_initial_strain() def __post_init__(self): """Validator for the attributes that are set in the constructor.""" diff --git a/structuralcodes/materials/constitutive_laws/__init__.py b/structuralcodes/materials/constitutive_laws/__init__.py index e2ff8f4e..244e8926 100644 --- a/structuralcodes/materials/constitutive_laws/__init__.py +++ b/structuralcodes/materials/constitutive_laws/__init__.py @@ -7,6 +7,7 @@ from ._elastic import Elastic from ._elastic_2d import Elastic2D from ._elasticplastic import ElasticPlastic +from ._initial_strain import InitialStrain from ._parabolarectangle import ParabolaRectangle from ._parabolarectangle_2d import ParabolaRectangle2D from ._popovics import Popovics @@ -23,6 +24,7 @@ 'Popovics', 'Sargin', 'UserDefined', + 'InitialStrain', 'get_constitutive_laws_list', 'create_constitutive_law', ] @@ -37,6 +39,7 @@ 'parabolarectangle2d': ParabolaRectangle2D, 'popovics': Popovics, 'sargin': Sargin, + 'initialstrain': InitialStrain, } diff --git a/structuralcodes/materials/constitutive_laws/_elastic_2d.py b/structuralcodes/materials/constitutive_laws/_elastic_2d.py index 7dd36d0a..9548dc6c 100644 --- a/structuralcodes/materials/constitutive_laws/_elastic_2d.py +++ b/structuralcodes/materials/constitutive_laws/_elastic_2d.py @@ -9,6 +9,12 @@ class Elastic2D(Elastic): """Class for elastic constitutive law for 2D operations.""" + __materials__: t.Tuple[str] = ( + 'concrete', + 'steel', + 'rebars', + ) + def __init__( self, E: float, diff --git a/structuralcodes/materials/constitutive_laws/_elasticplastic.py b/structuralcodes/materials/constitutive_laws/_elasticplastic.py index ccf249b5..84c53470 100644 --- a/structuralcodes/materials/constitutive_laws/_elasticplastic.py +++ b/structuralcodes/materials/constitutive_laws/_elasticplastic.py @@ -34,8 +34,8 @@ def __init__( Keyword Arguments: Eh (float): The hardening modulus. - eps_su (float): The ultimate strain. - name (str): A descriptive name for the constitutive law. + eps_su (float, optional): The ultimate strain. + name (str, optional): A descriptive name for the constitutive law. """ name = name if name is not None else 'ElasticPlasticLaw' super().__init__(name=name) diff --git a/structuralcodes/materials/constitutive_laws/_initial_strain.py b/structuralcodes/materials/constitutive_laws/_initial_strain.py new file mode 100644 index 00000000..076c2270 --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_initial_strain.py @@ -0,0 +1,130 @@ +"""Initial strain constitutive law.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +import typing as t + +import numpy as np +from numpy.typing import ArrayLike + +from ...core.base import ConstitutiveLaw + + +class InitialStrain(ConstitutiveLaw): + """Class for initial strain Constitutive Law.""" + + _strain_compatibility: bool = True + + __materials__: t.Tuple[str] = ( + 'steel', + 'rebars', + 'concrete', + ) + + _wrapped_law: ConstitutiveLaw = None + + def __init__( + self, + constitutive_law: ConstitutiveLaw, + initial_strain: float, + strain_compatibility: bool = True, + name: t.Optional[str] = None, + ) -> None: + """Initialize an Initial Strain Constitutive Law. + + This constitutive law is a wrapper for another constitutive law + that assigns an initial strain. + + Arguments: + constitutive_law (ConstitutiveLaw): Wrapped constitutive law. + initial_strain (float): The initial strain to be applied. + strain_compatibility (bool): If True, the strain compatibility is + enforced, otherwise the strain compatibility is released. This + is helpful for instance for modelling unbonded tendons. + Default value True. + """ + name = name if name is not None else 'InitialStrainLaw' + super().__init__(name=name) + if not isinstance(constitutive_law, ConstitutiveLaw): + raise TypeError( + f'Expected a ConstitutiveLaw instance, ' + f'got {type(constitutive_law)}' + ) + self._wrapped_law = constitutive_law + self._initial_strain = initial_strain + self._initial_stress = self._wrapped_law.get_stress(initial_strain) + self._strain_compatibility = strain_compatibility + + @property + def strain_compatibility(self) -> bool: + """Return the strain compatibility status.""" + return self._strain_compatibility + + @property + def wrapped_law(self) -> ConstitutiveLaw: + """Return the wrapped constitutive law.""" + return self._wrapped_law + + def get_stress( + self, eps: t.Union[float, ArrayLike] + ) -> t.Union[float, ArrayLike]: + """Return the stress given strain.""" + stress = self._wrapped_law.get_stress(eps + self._initial_strain) + if not self._strain_compatibility: + # If strain compatibility is enforced, return initial stress + return np.ones_like(stress) * self._initial_stress + return stress + + def get_tangent( + self, eps: t.Union[float, ArrayLike] + ) -> t.Union[float, ArrayLike]: + """Return the tangent for given strain.""" + if not self._strain_compatibility: + return self._wrapped_law.get_tangent(0) * 1e-6 + return self._wrapped_law.get_tangent(eps + self._initial_strain) + + def __marin__( + self, strain: t.Tuple[float, float] + ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]: + """Returns coefficients and strain limits for Marin integration in a + simply formatted way. + + Arguments: + strain (float, float): Tuple defining the strain profile: eps = + strain[0] + strain[1]*y. + + Example: + [(0, -0.002), (-0.002, -0.003)] + [(a0, a1, a2), (a0)] + """ + return self._wrapped_law.__marin__( + strain=[strain[0] + self._initial_strain, strain[1]] + ) + + def __marin_tangent__( + self, strain: t.Tuple[float, float] + ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]: + """Returns coefficients and strain limits for Marin integration of + tangent in a simply formatted way. + + Arguments: + strain (float, float): Tuple defining the strain profile: eps = + strain[0] + strain[1]*y. + + Example: + [(0, -0.002), (-0.002, -0.003)] + [(a0, a1, a2), (a0)] + """ + return self._wrapped_law.__marin_tangent__( + strain=[strain[0] + self._initial_strain, strain[1]] + ) + + def get_ultimate_strain( + self, yielding: bool = False + ) -> t.Tuple[float, float]: + """Return the ultimate strain (negative and positive).""" + ult_strain = self._wrapped_law.get_ultimate_strain(yielding=yielding) + return ( + ult_strain[0] - self._initial_strain, + ult_strain[1] - self._initial_strain, + ) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 262a369f..869ea743 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -17,6 +17,8 @@ class ParabolaRectangle2D(ParabolaRectangle): in tension. """ + __materials__: t.Tuple[str] = ('concrete',) + def __init__( self, fc: float, diff --git a/structuralcodes/materials/reinforcement/__init__.py b/structuralcodes/materials/reinforcement/__init__.py index 907c1559..291346f3 100644 --- a/structuralcodes/materials/reinforcement/__init__.py +++ b/structuralcodes/materials/reinforcement/__init__.py @@ -33,6 +33,7 @@ def create_reinforcement( name: t.Optional[str] = None, density: float = 7850, design_code: t.Optional[str] = None, + **kwargs, ) -> t.Optional[Reinforcement]: """A factory function to create the correct type of reinforcement based on the desired design code. @@ -50,6 +51,10 @@ def create_reinforcement( desired standard. If None (default) the globally used design standard will be adopted. Otherwise the design standard specified will be used for the instance of the material. + **kwargs: Other valid keyword arguments that are collected and passed + to the specific reinforcement material. Please inspect the + documentation of the other reinforcement materials to see valid + arguments. Raises: ValueError: If the design code is not valid or does not cover @@ -80,5 +85,6 @@ def create_reinforcement( ftk=ftk, epsuk=epsuk, gamma_s=gamma_s, + **kwargs, ) return None diff --git a/structuralcodes/materials/reinforcement/_reinforcement.py b/structuralcodes/materials/reinforcement/_reinforcement.py index d5b3a310..1e829e8e 100644 --- a/structuralcodes/materials/reinforcement/_reinforcement.py +++ b/structuralcodes/materials/reinforcement/_reinforcement.py @@ -24,10 +24,19 @@ def __init__( epsuk: float, gamma_s: t.Optional[float] = None, name: t.Optional[str] = None, + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, ) -> None: """Initializes an abstract reinforcement material.""" name = name if name is not None else 'Reinforcement' - super().__init__(density, name) + super().__init__( + density=density, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + name=name, + ) self._fyk = abs(fyk) self._Es = abs(Es) diff --git a/structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py b/structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py index cc2801f3..4e1eb438 100644 --- a/structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +++ b/structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py @@ -31,6 +31,9 @@ def __init__( ConstitutiveLaw, ] ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, ): """Initializes a new instance of Reinforcement for EC2 2004. @@ -53,6 +56,13 @@ def __init__( constitutive law type for reinforcement. (valid options for string: 'elastic', 'elasticplastic', or 'elasticperfectlyplastic'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. Raises: ValueError: If the constitutive law name is not available for the @@ -71,6 +81,9 @@ def __init__( ftk=ftk, epsuk=epsuk, gamma_s=gamma_s, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._gamma_eps = gamma_eps self._constitutive_law = ( @@ -84,6 +97,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for reinforcement.' ) + self._apply_initial_strain() def fyd(self) -> float: """The design yield strength.""" @@ -129,7 +143,7 @@ def __elasticplastic__(self) -> dict: """Returns kwargs for ElasticPlastic constitutive law with strain hardening. """ - Eh = (self.ftd() - self.fyd()) / (self.epsuk - self.epsyd) + Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) return { 'E': self.Es, 'fy': self.fyd(), diff --git a/structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py b/structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py index dc043fc4..f40d9556 100644 --- a/structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +++ b/structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py @@ -30,6 +30,9 @@ def __init__( ConstitutiveLaw, ] ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, ): """Initializes a new instance of Reinforcement for EC2 2023. @@ -50,6 +53,13 @@ def __init__( constitutive law type for reinforcement. (valid options for string: 'elastic', 'elasticplastic', or 'elasticperfectlyplastic'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. Raises: ValueError: If the constitutive law name is not available for the @@ -67,6 +77,9 @@ def __init__( ftk=ftk, epsuk=epsuk, gamma_s=gamma_s, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._constitutive_law = ( constitutive_law @@ -79,6 +92,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for reinforcement.' ) + self._apply_initial_strain() def fyd(self) -> float: """The design yield strength.""" @@ -117,7 +131,7 @@ def __elasticplastic__(self) -> dict: """Returns kwargs for ElasticPlastic constitutive law with strain hardening. """ - Eh = (self.ftd() - self.fyd()) / (self.epsuk - self.epsyd) + Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) return { 'E': self.Es, 'fy': self.fyd(), diff --git a/structuralcodes/materials/reinforcement/_reinforcementMC2010.py b/structuralcodes/materials/reinforcement/_reinforcementMC2010.py index 18213819..ad6cea68 100644 --- a/structuralcodes/materials/reinforcement/_reinforcementMC2010.py +++ b/structuralcodes/materials/reinforcement/_reinforcementMC2010.py @@ -31,6 +31,9 @@ def __init__( ConstitutiveLaw, ] ] = 'elasticplastic', + initial_strain: t.Optional[float] = None, + initial_stress: t.Optional[float] = None, + strain_compatibility: t.Optional[bool] = None, ): """Initializes a new instance of Reinforcement for MC2010. @@ -53,6 +56,13 @@ def __init__( constitutive law type for reinforcement. (valid options for string: 'elastic', 'elasticplastic', or 'elasticperfectlyplastic'). + initial_strain (Optional[float]): Initial strain of the material. + initial_stress (Optional[float]): Initial stress of the material. + strain_compatibility (Optional[bool]): Only relevant if + initial_strain or initial_stress are different from zero. If + True, the material deforms with the geometry. If False, the + stress in the material upon loading is kept constant + corresponding to the initial strain. Raises: ValueError: If the constitutive law name is not available for the @@ -71,6 +81,9 @@ def __init__( ftk=ftk, epsuk=epsuk, gamma_s=gamma_s, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, ) self._gamma_eps = gamma_eps self._constitutive_law = ( @@ -84,6 +97,7 @@ def __init__( raise ValueError( 'The provided constitutive law is not valid for reinforcement.' ) + self._apply_initial_strain() def fyd(self) -> float: """The design yield strength.""" @@ -125,7 +139,7 @@ def __elasticplastic__(self) -> dict: """Returns kwargs for ElasticPlastic constitutive law with strain hardening. """ - Eh = (self.ftd() - self.fyd()) / (self.epsuk - self.epsyd) + Eh = (self.ftd() - self.fyd()) / (self.epsud() - self.epsyd) return { 'E': self.Es, 'fy': self.fyd(), diff --git a/structuralcodes/sections/_generic.py b/structuralcodes/sections/_generic.py index 0153243a..75a0440f 100644 --- a/structuralcodes/sections/_generic.py +++ b/structuralcodes/sections/_generic.py @@ -19,7 +19,7 @@ PointGeometry, SurfaceGeometry, ) -from structuralcodes.materials.constitutive_laws import Elastic +from structuralcodes.materials.basic import ElasticMaterial from .section_integrators import SectionIntegrator, integrator_factory @@ -42,6 +42,8 @@ class GenericSection(Section): strength, moment curvature, etc.). """ + geometry: CompoundGeometry + def __init__( self, geometry: t.Union[SurfaceGeometry, CompoundGeometry], @@ -96,6 +98,7 @@ class GenericSectionCalculator(SectionCalculator): """Calculator class implementing analysis algorithms for code checks.""" integrator: SectionIntegrator + section: GenericSection def __init__( self, @@ -155,13 +158,17 @@ def _calculate_gross_section_properties(self) -> s_res.SectionProperties: # Computation of surface area, reinforcement area, EA (axial rigidity) # and mass: Morten -> problem with units! how do we deal with it? for geo in self.section.geometry.geometries: - gp.ea += geo.area * geo.material.get_tangent(eps=0) + gp.ea += geo.area * geo.material.constitutive_law.get_tangent( + eps=0 + ) if geo.density is not None: # this assumes area in mm2 and density in kg/m3 gp.mass += geo.area * geo.density * 1e-9 for geo in self.section.geometry.point_geometries: - gp.ea += geo.area * geo.material.get_tangent(eps=0) + gp.ea += geo.area * geo.material.constitutive_law.get_tangent( + eps=0 + ) gp.area_reinforcement += geo.area if geo.density is not None: # this assumes area in mm2 and density in kg/m3 @@ -237,7 +244,7 @@ def compute_area_moments(geometry, material=None): # Create a dummy material for integration of area moments # This is used for J, S etc, not for E_J E_S etc - dummy_mat = Elastic(E=1) + dummy_mat = ElasticMaterial(E=1, density=1) # Computation of moments of area (material-independet) # Note: this could be un-meaningfull when many materials # are combined @@ -309,15 +316,40 @@ def get_balanced_failure_strain( chi_min = 1e10 for g in geom.geometries + geom.point_geometries: for other_g in geom.geometries + geom.point_geometries: + # This is left on purpose: even if tempted we should not do + # this check: # if g != other_g: - eps_p = g.material.get_ultimate_strain(yielding=yielding)[1] + eps_p = g.material.constitutive_law.get_ultimate_strain( + yielding=yielding + )[1] if isinstance(g, SurfaceGeometry): y_p = g.polygon.bounds[1] elif isinstance(g, PointGeometry): y_p = g._point.coords[0][1] - eps_n = other_g.material.get_ultimate_strain( - yielding=yielding + # Check if the section is a reinforced concrete section: + # If it is, we need to obtain the "yield" strain of concrete + # (-0.002 for default parabola-rectangle concrete) + # If the geometry is not concrete, don't get the yield strain + # If it is not a reinforced concrete section, return + # the yield strain if asked. + is_rc_section = self.section.geometry.reinforced_concrete + is_concrete_geom = ( + isinstance(other_g, SurfaceGeometry) and other_g.concrete + ) + + use_yielding = ( + yielding + if ( + (is_rc_section and is_concrete_geom) + or (not is_rc_section) + ) + else False + ) + + eps_n = other_g.material.constitutive_law.get_ultimate_strain( + yielding=use_yielding )[0] + if isinstance(other_g, SurfaceGeometry): y_n = other_g.polygon.bounds[3] elif isinstance(other_g, PointGeometry): @@ -583,14 +615,20 @@ def calculate_limit_axial_load(self): @property def n_min(self) -> float: - """Return minimum axial load.""" + """Return minimum axial load. + + In most situations, this is the capacity in compression. + """ if self._n_min is None: self._n_min, self._n_max = self.calculate_limit_axial_load() return self._n_min @property def n_max(self) -> float: - """Return maximum axial load.""" + """Return maximum axial load. + + In most situations, this is the capacity in tension. + """ if self._n_max is None: self._n_min, self._n_max = self.calculate_limit_axial_load() return self._n_max @@ -694,6 +732,9 @@ def calculate_bending_strength( Returns: UltimateBendingMomentResults: The results from the calculation. """ + # Check if the section can carry the axial load + self.check_axial_load(n=n) + # Compute the bending strength with the bisection algorithm # Rotate the section of angle theta rotated_geom = self.section.geometry.rotate(-theta) @@ -701,8 +742,6 @@ def calculate_bending_strength( # Rotate also triangulated data! self._rotate_triangulated_data(-theta) - # Check if the section can carry the axial load - self.check_axial_load(n=n) # Find the strain distribution corresponding to failure and equilibrium # with external axial force strain = self.find_equilibrium_fixed_pivot(rotated_geom, n) @@ -768,6 +807,9 @@ def calculate_moment_curvature( Returns: MomentCurvatureResults: The calculation results. """ + # Check if the section can carry the axial load + self.check_axial_load(n=n) + # Create an empty response object res = s_res.MomentCurvatureResults() res.n = n @@ -777,9 +819,6 @@ def calculate_moment_curvature( # Rotate also triangulated data! self._rotate_triangulated_data(-theta) - # Check if the section can carry the axial load - self.check_axial_load(n=n) - if chi is None: # Find ultimate curvature from the strain distribution # corresponding to failure and equilibrium with external axial diff --git a/structuralcodes/sections/_rc_utils.py b/structuralcodes/sections/_rc_utils.py index 7a90fba5..8267242f 100644 --- a/structuralcodes/sections/_rc_utils.py +++ b/structuralcodes/sections/_rc_utils.py @@ -4,7 +4,8 @@ import structuralcodes.core._section_results as s_res from structuralcodes.geometry import CompoundGeometry, SurfaceGeometry -from structuralcodes.materials.constitutive_laws import Elastic, UserDefined +from structuralcodes.materials.basic import ElasticMaterial, GenericMaterial +from structuralcodes.materials.constitutive_laws import UserDefined from structuralcodes.sections import GenericSection from structuralcodes.sections.section_integrators import FiberIntegrator @@ -57,13 +58,22 @@ def create_surface_geometries(polygons_list, material): rotated_geometry = section.geometry.rotate(-theta) for geo in rotated_geometry.geometries: - Ec = geo.material.get_tangent(eps=0) - elastic_concrete = UserDefined([-100, 0], [-100 * Ec, 0]) + Ec = geo.material.constitutive_law.get_tangent(eps=0) + density = geo.material.density + elastic_concrete_law = UserDefined([-100, 0], [-100 * Ec, 0]) + elastic_concrete = GenericMaterial( + density=density, + constitutive_law=elastic_concrete_law, + name='elastic concrete', + ) geo._material = elastic_concrete for pg in rotated_geometry.point_geometries: - Es = pg.material.get_tangent(eps=0) - elastic_steel = Elastic(Es, 'elastic steel') + Es = pg.material.constitutive_law.get_tangent(eps=0) + density = pg.material.density + elastic_steel = ElasticMaterial( + E=Es, density=density, name='elastic steel' + ) pg._material = elastic_steel curv = -1e-5 # Any curvature should return the same mechanical properties. diff --git a/structuralcodes/sections/section_integrators/_fiber_integrator.py b/structuralcodes/sections/section_integrators/_fiber_integrator.py index 2ffb5493..a78abb05 100644 --- a/structuralcodes/sections/section_integrators/_fiber_integrator.py +++ b/structuralcodes/sections/section_integrators/_fiber_integrator.py @@ -88,7 +88,7 @@ def triangulate( max_area = g.area * mesh_size # triangulate the geometry getting back the mesh mesh = triangle.triangulate(tri, f'pq{30:.1f}Aa{max_area}o1') - mat = g.material + constitutive_law = g.material.constitutive_law # Get x and y coordinates (centroid) and area for each fiber x = [] y = [] @@ -128,7 +128,7 @@ def triangulate( # return back the triangulation data triangulated_data.append( - (np.array(x), np.array(y), np.array(area), mat) + (np.array(x), np.array(y), np.array(area), constitutive_law) ) # For the reinforcement # Tentative proposal for managing reinforcement (PointGeometry) @@ -139,19 +139,27 @@ def triangulate( x = x[0] y = y[0] area = pg.area - mat = pg.material - if reinf_data.get(mat) is None: - reinf_data[mat] = [ + constitutive_law = pg.material.constitutive_law + if reinf_data.get(constitutive_law) is None: + reinf_data[constitutive_law] = [ np.array(x), np.array(y), np.array(area), ] else: - reinf_data[mat][0] = np.hstack((reinf_data[mat][0], x)) - reinf_data[mat][1] = np.hstack((reinf_data[mat][1], y)) - reinf_data[mat][2] = np.hstack((reinf_data[mat][2], area)) - for mat, value in reinf_data.items(): - triangulated_data.append((value[0], value[1], value[2], mat)) + reinf_data[constitutive_law][0] = np.hstack( + (reinf_data[constitutive_law][0], x) + ) + reinf_data[constitutive_law][1] = np.hstack( + (reinf_data[constitutive_law][1], y) + ) + reinf_data[constitutive_law][2] = np.hstack( + (reinf_data[constitutive_law][2], area) + ) + for constitutive_law, value in reinf_data.items(): + triangulated_data.append( + (value[0], value[1], value[2], constitutive_law) + ) return triangulated_data @@ -199,7 +207,7 @@ def prepare_input( prepared_input = [] - triangulated_data = kwargs.get('tri', None) + triangulated_data = kwargs.get('tri') if triangulated_data is None: # No triangulation is provided, triangulate the section # Fiber integrator for generic section uses delaunay triangulation diff --git a/structuralcodes/sections/section_integrators/_marin_integrator.py b/structuralcodes/sections/section_integrators/_marin_integrator.py index 84ccd725..92a84fe0 100644 --- a/structuralcodes/sections/section_integrators/_marin_integrator.py +++ b/structuralcodes/sections/section_integrators/_marin_integrator.py @@ -71,29 +71,32 @@ def _get_coefficcients( ) -> t.Tuple[t.List[t.Tuple], t.List[t.Tuple]]: """Get Marin coefficients.""" if integrate == 'stress': - if hasattr(geo.material, '__marin__'): - strains, coeffs = geo.material.__marin__(strain=strain) + if hasattr(geo.material.constitutive_law, '__marin__'): + strains, coeffs = geo.material.constitutive_law.__marin__( + strain=strain + ) else: raise AttributeError( - f'The material object {geo.material} of geometry {geo} \ - does not have implement the __marin__ function. \ - Please implement the function or use another integrator,\ - like ' - 'Fibre' - '' + 'The constitutive law object ' + f'{geo.material.constitutive_law} of geometry {geo} does ' + 'not have implement the __marin__ function. Please ' + 'implement the function or use another integrator, like ' + 'Fiber.' ) elif integrate == 'modulus': - if hasattr(geo.material, '__marin_tangent__'): - strains, coeffs = geo.material.__marin_tangent__(strain=strain) + if hasattr(geo.material.constitutive_law, '__marin_tangent__'): + strains, coeffs = ( + geo.material.constitutive_law.__marin_tangent__( + strain=strain + ) + ) else: raise AttributeError( - f'The material object {geo.material} of geometry {geo} \ - does not have implement the __marin_tangent__ function\ - . \ - Please implement the function or use another integrato\ - r, like ' - 'Fibre' - '' + 'The constitutive law object ' + f'{geo.material.constitutive_law} of geometry {geo} does ' + 'not have implement the __marin_tangent__ function. Please' + ' implement the function or use another integrator, like ' + 'Fibre.' ) else: raise ValueError(f'Unknown integrate type: {integrate}') @@ -159,9 +162,11 @@ def _process_point_geometries( x.append(xp) y.append(yp) if integrate == 'stress': - IA.append(pg.material.get_stress(strain_) * A) + IA.append(pg.material.constitutive_law.get_stress(strain_) * A) elif integrate == 'modulus': - IA.append(pg.material.get_tangent(strain_) * A) + IA.append( + pg.material.constitutive_law.get_tangent(strain_) * A + ) input.append((1, np.array(x), np.array(y), np.array(IA))) def prepare_input( diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index 0f73492f..5c0aeb99 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -68,9 +68,9 @@ def prepare_input( for z in z_coords: fiber_strain = strain[:3] + z * strain[3:] if integrate == 'stress': - integrand = material.get_stress(fiber_strain) + integrand = material.constitutive_law.get_stress(fiber_strain) elif integrate == 'modulus': - integrand = material.get_secant(fiber_strain) + integrand = material.constitutive_law.get_secant(fiber_strain) else: raise ValueError(f'Unknown integrate type: {integrate}') @@ -86,10 +86,10 @@ def prepare_input( eps_sj = r.T @ fiber_strain if integrate == 'stress': - sig_sj = material.get_stress(eps_sj[0]) + sig_sj = material.constitutive_law.get_stress(eps_sj[0]) integrand = As * r.T.T @ np.array([sig_sj, 0, 0]) elif integrate == 'modulus': - mod = material.get_secant(eps_sj[0]) + mod = material.constitutive_law.get_secant(eps_sj[0]) integrand = r.T.T @ np.diag([mod, 0, 0]) @ r.T * As else: raise ValueError(f'Unknown integrate type: {integrate}') diff --git a/tests/test_core/test_rc_utils.py b/tests/test_core/test_rc_utils.py index a6e8fbe5..e029d8e8 100644 --- a/tests/test_core/test_rc_utils.py +++ b/tests/test_core/test_rc_utils.py @@ -94,27 +94,27 @@ def test_calculate_elastic_cracked_properties_comparison(): cracked_prop2 = section2.gross_properties # Compare specific properties between the two objects - assert cracked_prop1.area == pytest.approx( - cracked_prop2.area, rel=1e-3 - ), 'Areas do not match' - assert cracked_prop1.cy == pytest.approx( - cracked_prop2.cy, rel=1e-3 - ), 'cy do not match' - assert cracked_prop1.cz == pytest.approx( - cracked_prop2.cz, rel=1e-3 - ), 'cz do not match' + assert cracked_prop1.area == pytest.approx(cracked_prop2.area, rel=1e-3), ( + 'Areas do not match' + ) + assert cracked_prop1.cy == pytest.approx(cracked_prop2.cy, rel=1e-3), ( + 'cy do not match' + ) + assert cracked_prop1.cz == pytest.approx(cracked_prop2.cz, rel=1e-3), ( + 'cz do not match' + ) assert cracked_prop1.e_i11 == pytest.approx( cracked_prop2.e_i11, rel=1e-3 ), 'cze_i11 do not match' assert cracked_prop1.e_i22 == pytest.approx( cracked_prop2.e_i22, rel=1e-3 ), 'e_i22 do not match' - assert cracked_prop1.sy == pytest.approx( - cracked_prop2.sy, rel=1e-3 - ), 'sy do not match' - assert cracked_prop1.sz == pytest.approx( - cracked_prop2.sz, rel=1e-3 - ), 'sz do not match' + assert cracked_prop1.sy == pytest.approx(cracked_prop2.sy, rel=1e-3), ( + 'sy do not match' + ) + assert cracked_prop1.sz == pytest.approx(cracked_prop2.sz, rel=1e-3), ( + 'sz do not match' + ) # check if works when geometry is required result = calculate_elastic_cracked_properties( diff --git a/tests/test_ec2_2004/test_ec2_2004_shear.py b/tests/test_ec2_2004/test_ec2_2004_shear.py index 3ba5d046..49654455 100644 --- a/tests/test_ec2_2004/test_ec2_2004_shear.py +++ b/tests/test_ec2_2004/test_ec2_2004_shear.py @@ -221,6 +221,23 @@ def test_VRds(Asw, s, z, theta, fyk, alpha, gamma_s, expected): ) +@pytest.mark.parametrize( + 'Ved, z, theta, fywd, expected', + [ + (100e3, 300, 45, 500 / 1.15, 0.76666), + (150e3, 300, 45, 500 / 1.15, 1.14999), + (200e3, 350, 30, 500 / 1.15, 0.75880), + (250e3, 350, 45, 500 / 1.15, 1.64285), + (120e3, 400, 45, 500 / 1.15, 0.68999), + (180e3, 350, 35, 500 / 1.15, 0.82824), + ], +) +def test_Asw_s_required(Ved, z, theta, fywd, expected): + """Test the Asw_s_required function.""" + result = shear.Asw_s_required(Ved, z, theta, fywd) + assert math.isclose(result, expected, rel_tol=0.01) + + @pytest.mark.parametrize( ( 'bw, z, fck, theta, Ned, Ac, gamma_c, alpha, alpha_cc, limit_fyd, ' diff --git a/tests/test_geometry/test_add.py b/tests/test_geometry/test_add.py new file mode 100644 index 00000000..6d66a52c --- /dev/null +++ b/tests/test_geometry/test_add.py @@ -0,0 +1,133 @@ +"""Tests for adding Geometry objects.""" + +import typing as t + +import pytest +from shapely import Polygon + +from structuralcodes.geometry import ( + CompoundGeometry, + PointGeometry, + SurfaceGeometry, +) +from structuralcodes.materials.basic import ElasticMaterial + + +@pytest.fixture +def two_rectangles() -> t.Tuple[SurfaceGeometry]: + """Fixture for two rectangles.""" + concrete = ElasticMaterial(E=30000, density=2500) + width_web = 250 + width_flange = 1000 + height_web = 650 + height_flange = 150 + polygon_web = Polygon( + ( + (-width_web / 2, 0), + (width_web / 2, 0), + (width_web / 2, height_web), + (-width_web / 2, height_web), + ) + ) + polygon_flange = Polygon( + ( + (-width_flange / 2, height_web), + (width_flange / 2, height_web), + (width_flange / 2, height_web + height_flange), + (-width_flange / 2, height_web + height_flange), + ), + ) + + return SurfaceGeometry( + poly=polygon_web, material=concrete + ), SurfaceGeometry(poly=polygon_flange, material=concrete) + + +@pytest.fixture +def two_points() -> t.Tuple[PointGeometry]: + """Fixture for two points.""" + steel = ElasticMaterial(E=200000, density=7850) + return PointGeometry( + point=(0, 50), diameter=16, material=steel + ), PointGeometry(point=(100, 50), diameter=16, material=steel) + + +def test_add_surface_geometries(two_rectangles): + """Test adding two surface geometries.""" + geom_web, geom_flange = two_rectangles + + compound = geom_web + geom_flange + + assert isinstance(compound, CompoundGeometry) + assert geom_web, geom_flange in compound.geometries + + +def test_add_surface_and_point_geometries(two_rectangles, two_points): + """Test adding a surface geometry and a point geometry.""" + surface, _ = two_rectangles + point, _ = two_points + + # Add surface and point + compound = surface + point + + assert isinstance(compound, CompoundGeometry) + assert surface in compound.geometries + assert point in compound.point_geometries + assert len(compound.geometries) == 1 + assert len(compound.point_geometries) == 1 + + # Add point and surface + compound = point + surface + + assert isinstance(compound, CompoundGeometry) + assert surface in compound.geometries + assert point in compound.point_geometries + assert len(compound.geometries) == 1 + assert len(compound.point_geometries) == 1 + + +def test_add_point_geometries(two_points): + """Test adding two point geometries.""" + point_1, point_2 = two_points + + compound = point_1 + point_2 + + assert isinstance(compound, CompoundGeometry) + assert point_1, point_2 in compound.point_geometries + + +def test_add_to_compound_geometries(two_rectangles, two_points): + """Test adding to compound geometries.""" + geom_web, geom_flange = two_rectangles + point_1, _ = two_points + + compound_1 = geom_web + geom_flange + compound_2 = geom_web + geom_flange + compound_3 = compound_1 + compound_2 + + assert isinstance(compound_1, CompoundGeometry) + assert isinstance(compound_2, CompoundGeometry) + assert isinstance(compound_3, CompoundGeometry) + assert len(compound_1.geometries) == 2 + assert len(compound_2.geometries) == 2 + assert len(compound_3.geometries) == 4 + assert len(compound_3.point_geometries) == 0 + + # Add a point and a surface + compound_4 = compound_3 + point_1 + compound_4 += geom_web + assert isinstance(compound_4, CompoundGeometry) + assert len(compound_4.geometries) == 5 + assert len(compound_4.point_geometries) == 1 + + # Add a compound to a point + compound_5 = point_1 + compound_4 + assert isinstance(compound_5, CompoundGeometry) + assert len(compound_5.geometries) == 5 + assert len(compound_5.point_geometries) == 2 + + # Add a compound to a surface + compound_6 = geom_web + compound_5 + assert isinstance(compound_6, CompoundGeometry) + assert len(compound_6.geometries) == 6 + assert len(compound_6.point_geometries) == 2 diff --git a/tests/test_geometry/test_circular.py b/tests/test_geometry/test_circular.py index d01c7968..4e9b546d 100644 --- a/tests/test_geometry/test_circular.py +++ b/tests/test_geometry/test_circular.py @@ -6,8 +6,8 @@ import pytest from structuralcodes.geometry import CircularGeometry, add_reinforcement_circle +from structuralcodes.materials.basic import ElasticMaterial from structuralcodes.materials.concrete import ConcreteMC2010 -from structuralcodes.materials.constitutive_laws import Elastic from structuralcodes.materials.reinforcement import ReinforcementMC2010 @@ -18,7 +18,7 @@ ) def test_create_circular_geometry(diameter, n_points): """Test creating a CircularGeometry.""" - mat = Elastic(300000) + mat = ElasticMaterial(E=300000, density=2450) circle = CircularGeometry(diameter, mat, n_points) assert len(circle.polygon.exterior.coords) == n_points + 2 @@ -35,7 +35,7 @@ def test_create_circular_geometry(diameter, n_points): @pytest.mark.parametrize('wrong_diameter', [-100, 0]) def test_create_circular_geometry_exception(wrong_diameter): """Test raising exception when inputing wrong value.""" - mat = Elastic(300000) + mat = ElasticMaterial(E=300000, density=2450) with pytest.raises(ValueError): CircularGeometry(wrong_diameter, mat) @@ -48,7 +48,7 @@ def test_create_circular_geometry_exception(wrong_diameter): def test_circle_with_origin(origin): """Test creating a circle with an origin.""" # Arrange - mat = Elastic(300000) + mat = ElasticMaterial(E=300000, density=2450) diameter = 100 # Act and assert diff --git a/tests/test_geometry/test_geometry.py b/tests/test_geometry/test_geometry.py index 63a00fb1..edce767e 100644 --- a/tests/test_geometry/test_geometry.py +++ b/tests/test_geometry/test_geometry.py @@ -17,9 +17,13 @@ add_reinforcement_line, create_line_point_angle, ) +from structuralcodes.materials.basic import ( + ElasticMaterial, + ElasticPlasticMaterial, + GenericMaterial, +) from structuralcodes.materials.concrete import ConcreteMC2010 from structuralcodes.materials.constitutive_laws import ( - Elastic, ElasticPlastic, ParabolaRectangle, ) @@ -63,7 +67,14 @@ def test_point_geometry(): """Test creating a PointGeometry object.""" Geometry.section_counter = 0 # Create a consitutive law to use - steel = ElasticPlastic(210000, 450) + constitutive_law_steel = ElasticPlastic(210000, 450) + steel = ReinforcementMC2010( + fyk=450, + Es=210000, + ftk=450, + epsuk=0.03, + constitutive_law=constitutive_law_steel, + ) # Create two points with default naming (uses global counter) for i in range(2): @@ -87,7 +98,7 @@ def test_point_geometry(): # Create two points with custom label for filtering for i in range(2): p = PointGeometry(np.array([2, 3]), 12, steel, group_label='Bottom') - assert p.name == f'Geometry_{i+2}' + assert p.name == f'Geometry_{i + 2}' assert p.group_label == 'Bottom' assert math.isclose(p.diameter, 12) assert math.isclose(p.point.coords[0][0], 2) @@ -122,7 +133,7 @@ def test_point_geometry(): # Trick for now since we don't hav a steel material C25 = ConcreteMC2010(25) p = PointGeometry(np.array([2, 3]), 12, C25) - assert isinstance(p.material, ParabolaRectangle) + assert isinstance(p.material.constitutive_law, ParabolaRectangle) # Test Surface Geometry @@ -132,18 +143,20 @@ def test_surface_geometry(): # noqa: PLR0915 C25 = ConcreteMC2010(25) # Create a constitutive law to use - C25_const = ParabolaRectangle(25) + C25_const = GenericMaterial( + density=C25.density, constitutive_law=ParabolaRectangle(25) + ) # Create a rectangular geometry poly = Polygon(((0, 0), (200, 0), (200, 400), (0, 400))) for mat in (C25, C25_const): geo = SurfaceGeometry(poly, mat) - assert isinstance(geo.material, ParabolaRectangle) + assert isinstance(geo.material.constitutive_law, ParabolaRectangle) assert geo.area == 200 * 400 assert geo.centroid[0] == 100 assert geo.centroid[1] == 200 geo_t = geo.translate(-100, -200) - assert isinstance(geo_t.material, ParabolaRectangle) + assert isinstance(geo_t.material.constitutive_law, ParabolaRectangle) assert geo_t.area == 200 * 400 assert geo_t.centroid[0] == 0 assert geo_t.centroid[1] == 0 @@ -216,8 +229,7 @@ def test_surface_geometry(): # noqa: PLR0915 SurfaceGeometry(poly=poly, material=1) assert ( str(excinfo.value) - == f'mat should be a valid structuralcodes.base.Material \ - or structuralcodes.base.ConstitutiveLaw object. \ + == f'mat should be a valid structuralcodes.base.Material object. \ {repr(1)}' ) @@ -247,7 +259,12 @@ def test_compound_geometry(): """Test creating a SurfaceGeometry object.""" # Create a material to use C25 = ConcreteMC2010(25) - steel = ElasticPlastic(210000, 450) + steel = ReinforcementMC2010( + fyk=450, + Es=210000, + ftk=450, + epsuk=0.03, + ) # Create a rectangular geometry poly = Polygon(((0, 0), (200, 0), (200, 400), (0, 400))) @@ -309,8 +326,12 @@ def test_compound_geometry(): geo = CompoundGeometry(multi_pol, [C25, steel]) assert_geometries_equal(geo.geometries[0].polygon, web) assert_geometries_equal(geo.geometries[1].polygon, flange) - assert isinstance(geo.geometries[0].material, ParabolaRectangle) - assert isinstance(geo.geometries[1].material, ElasticPlastic) + assert isinstance( + geo.geometries[0].material.constitutive_law, ParabolaRectangle + ) + assert isinstance( + geo.geometries[1].material.constitutive_law, ElasticPlastic + ) # check error is raised when the number of materials is incorrect with pytest.raises(ValueError) as excinfo: CompoundGeometry(multi_pol, [C25, C25, C25]) @@ -341,7 +362,7 @@ def test_add_geometries(): polys.append( Polygon([(-150, -300), (0, -300), (0, -289.3), (-150, -289.3)]) ) - mat = ElasticPlastic(E=206000, fy=300) + mat = ElasticPlasticMaterial(E=206000, fy=300, density=7850) geo1 = SurfaceGeometry(polys[-1], mat) polys.append(Polygon([(-150, 289.3), (0, 289.3), (0, 300), (-150, 300)])) @@ -385,7 +406,7 @@ def test_add_geometries(): def test_sub_geometries(): """Test subtraction between geometries.""" - mat = ElasticPlastic(E=206000, fy=300) + mat = ElasticPlasticMaterial(E=206000, fy=300, density=7850) # Create the exptected polygons poly_1 = Polygon( shell=[(-100, -200), (100, -200), (100, 200), (-100, 200)], @@ -451,7 +472,7 @@ def test_sub_geometries(): ) def test_extents_calculation(w, h): """Test extents calculation for SurfaceGeometry and CompoundGeometry.""" - mat = Elastic(E=206000) + mat = ElasticMaterial(E=206000, density=7850) # Create a rectangle geo_rect = SurfaceGeometry( Polygon( @@ -496,7 +517,7 @@ def test_extents_calculation(w, h): ) def test_property_reinforced_concrete(w, h, c): """Test property reinforced_concrete.""" - mat = Elastic(E=206000) + mat = ElasticMaterial(E=206000, density=7850) # Create a rectangle geo_rect = SurfaceGeometry( Polygon( @@ -509,7 +530,7 @@ def test_property_reinforced_concrete(w, h, c): ), mat, ) - steel = Elastic(E=206000) + steel = ElasticMaterial(E=206000, density=7850) geo_rc = add_reinforcement_line( geo_rect, (-w / 2 + c, -h / 2 + c), diff --git a/tests/test_geometry/test_rectangular.py b/tests/test_geometry/test_rectangular.py index fdc1b7b3..9cd6253d 100644 --- a/tests/test_geometry/test_rectangular.py +++ b/tests/test_geometry/test_rectangular.py @@ -9,8 +9,8 @@ RectangularGeometry, add_reinforcement_line, ) +from structuralcodes.materials.basic import ElasticMaterial from structuralcodes.materials.concrete import ConcreteMC2010 -from structuralcodes.materials.constitutive_laws import Elastic from structuralcodes.materials.reinforcement import ReinforcementMC2010 @@ -21,7 +21,7 @@ ) def test_create_rectangular_geometry(w, h): """Test creating a RectangularGeometry.""" - mat = Elastic(300000) + mat = ElasticMaterial(E=300000, density=2450) rect = RectangularGeometry(w, h, mat) assert math.isclose(rect.width, w) @@ -47,7 +47,7 @@ def test_create_rectangular_geometry(w, h): ) def test_create_rectangular_geometry_exception(wrong_width, wrong_height): """Test raising exception when inputing wrong value.""" - mat = Elastic(30000) + mat = ElasticMaterial(E=30000, density=2450) with pytest.raises(ValueError): RectangularGeometry(wrong_width, wrong_height, mat) @@ -60,7 +60,7 @@ def test_create_rectangular_geometry_exception(wrong_width, wrong_height): def test_rectangle_with_origin(origin): """Test creating a rectangle with an origin.""" # Arrange - mat = Elastic(30000) + mat = ElasticMaterial(E=30000, density=2450) height = 600 width = 200 diff --git a/tests/test_geometry/test_shell.py b/tests/test_geometry/test_shell.py index f96f4b81..60e0c1bf 100644 --- a/tests/test_geometry/test_shell.py +++ b/tests/test_geometry/test_shell.py @@ -8,34 +8,38 @@ ShellGeometry, ShellReinforcement, ) +from structuralcodes.materials.basic import GenericMaterial from structuralcodes.materials.concrete import ConcreteEC2_2004 from structuralcodes.materials.constitutive_laws import ( Elastic2D, - ElasticPlastic, ) +from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 def test_shell_geometry(): """Test the ShellGeometry class.""" - # Create a material to use - concrete = ConcreteEC2_2004(fck=35) # Choose a constitutive law to use const = Elastic2D(E=200000, nu=0.2) - for mat in (concrete, const): - shell = ShellGeometry( - thickness=200, material=mat, name='Shell', group_label='Group1' - ) - assert shell.thickness == 200 - assert isinstance(shell.material, (ConcreteEC2_2004, Elastic2D)) - assert shell.name == 'Shell' - assert shell.group_label == 'Group1' + # Create a material to use + concrete = ConcreteEC2_2004(fck=35, constitutive_law=const) + + shell = ShellGeometry( + thickness=200, material=concrete, name='Shell', group_label='Group1' + ) + assert shell.thickness == 200 + assert isinstance(shell.material, ConcreteEC2_2004) + assert shell.name == 'Shell' + assert shell.group_label == 'Group1' def test_negative_thickness_raises(): """Test that a negative thickness raises a ValueError.""" + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.2) + ) with pytest.raises(ValueError): - ShellGeometry(thickness=-200, material=Elastic2D(E=200000, nu=0.2)) + ShellGeometry(thickness=-200, material=material) def test_shell_reinforcement(): @@ -44,7 +48,9 @@ def test_shell_reinforcement(): n_bars = 4 cc_bars = 500 d = 16 - material = Elastic2D(E=200000, nu=0.3) + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.3) + ) phi = np.pi / 4 shell_reinforcement = ShellReinforcement( z=z, @@ -69,7 +75,13 @@ def test_shell_reinforcement(): def test_add_reinforcement(): """Test the add_reinforcement function.""" # Create a shell geometry - shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.3) + ) + reinforcement = ReinforcementEC2_2004( + fyk=500, Es=200000, ftk=500, epsuk=3e-2 + ) + shell = ShellGeometry(thickness=200, material=material) # Create a reinforcement reinf_1 = ShellReinforcement( @@ -77,7 +89,7 @@ def test_add_reinforcement(): n_bars=4, cc_bars=500, diameter_bar=16, - material=Elastic2D(E=200000, nu=0.3), + material=reinforcement, phi=0, ) @@ -86,7 +98,7 @@ def test_add_reinforcement(): n_bars=6, cc_bars=600, diameter_bar=20, - material=Elastic2D(E=200000, nu=0.3), + material=reinforcement, phi=np.pi / 4, ) @@ -103,12 +115,16 @@ def test_add_reinforcement(): def test_add_reinforcement_invalid_type(): """Test that adding a reinforcement of invalid type raises an error.""" # Create a shell geometry - shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.2) + ) + shell = ShellGeometry(thickness=200, material=material) # Create a reinforcement - reinf_1 = PointGeometry( - np.array([2, 3]), 12, ElasticPlastic(E=200000, fy=500), name='Rebar' + reinforcement = ReinforcementEC2_2004( + fyk=500, ftk=500, Es=200000, epsuk=3e-2 ) + reinf_1 = PointGeometry(np.array([2, 3]), 12, reinforcement, name='Rebar') with pytest.raises(TypeError): shell.add_reinforcement(reinf_1) @@ -122,13 +138,19 @@ def test_add_reinforcement_invalid_type(): ) def test_add_reinforcement_invalid_z_value(invalid_z): """Test that adding a reinforcement outside the shell raises an error.""" - shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.2) + ) + shell = ShellGeometry(thickness=200, material=material) + reinforcement = ReinforcementEC2_2004( + fyk=500, ftk=500, Es=200000, epsuk=3e-2 + ) reinf = ShellReinforcement( z=invalid_z, n_bars=1, cc_bars=100, diameter_bar=12, - material=Elastic2D(E=200000, nu=0.3), + material=reinforcement, phi=0, ) with pytest.raises(ValueError): @@ -138,15 +160,21 @@ def test_add_reinforcement_invalid_z_value(invalid_z): def test_repr_svg(): """Test the SVG representation of the shell geometry.""" # Create a shell geometry - shell = ShellGeometry(thickness=200, material=Elastic2D(E=200000, nu=0.2)) + material = GenericMaterial( + density=7850, constitutive_law=Elastic2D(E=200000, nu=0.2) + ) + shell = ShellGeometry(thickness=200, material=material) # Add a reinforcement + reinforcement = ReinforcementEC2_2004( + fyk=500, ftk=500, Es=200000, epsuk=3e-2 + ) reinf = ShellReinforcement( z=-60, n_bars=4, cc_bars=500, diameter_bar=16, - material=Elastic2D(E=200000, nu=0.3), + material=reinforcement, phi=0, ) shell.add_reinforcement(reinf) diff --git a/tests/test_geometry/test_steel_sections.py b/tests/test_geometry/test_steel_sections.py index 2c42ea10..00be824b 100644 --- a/tests/test_geometry/test_steel_sections.py +++ b/tests/test_geometry/test_steel_sections.py @@ -16,9 +16,12 @@ CompoundGeometry, SurfaceGeometry, ) +from structuralcodes.materials.basic import ( + ElasticMaterial, + ElasticPlasticMaterial, + GenericMaterial, +) from structuralcodes.materials.constitutive_laws import ( - Elastic, - ElasticPlastic, UserDefined, ) from structuralcodes.sections._generic import GenericSection @@ -249,7 +252,7 @@ def test_section_get_polygon(cls, name): Es = 206000 fy = 355 eps_su = fy / Es - steel = ElasticPlastic(E=Es, fy=fy, eps_su=eps_su) + steel = ElasticPlasticMaterial(E=Es, fy=fy, density=7850, eps_su=eps_su) # Create geometry geo = SurfaceGeometry(cls.get_polygon(name), steel) @@ -957,7 +960,7 @@ def test_Isection_elastic_fiber(cls, name): Es = 206000 fy = 355 eps_su = fy / Es - steel = ElasticPlastic(E=Es, fy=fy, eps_su=eps_su) + steel = ElasticPlasticMaterial(E=Es, fy=fy, density=7850, eps_su=eps_su) # Create geometry i_beam = cls(name) @@ -1018,7 +1021,7 @@ def test_Isection_elastic_marin(cls, name): Es = 206000 fy = 355 eps_su = fy / Es - steel = ElasticPlastic(E=Es, fy=fy, eps_su=eps_su) + steel = ElasticPlasticMaterial(E=Es, fy=fy, density=7850, eps_su=eps_su) # Create geometry i_beam = cls(name) @@ -1079,7 +1082,7 @@ def test_Isection_plastic_fiber(cls, name): Es = 206000 fy = 355 eps_su = 0.15 - steel = ElasticPlastic(E=Es, fy=fy, eps_su=eps_su) + steel = ElasticPlasticMaterial(E=Es, fy=fy, density=7850, eps_su=eps_su) # Create geometry i_beam = cls(name) @@ -1140,7 +1143,7 @@ def test_Isection_plastic_marin(cls, name): Es = 206000 fy = 355 eps_su = 0.15 - steel = ElasticPlastic(E=Es, fy=fy, eps_su=eps_su) + steel = ElasticPlasticMaterial(E=Es, fy=fy, density=7850, eps_su=eps_su) # Create geometry i_beam = cls(name) @@ -1200,8 +1203,8 @@ def test_Isection_elastic_material_marin(cls, name): """Test Steel I section elastic strength.""" Es = 206000 fy = 355 - steel = Elastic(E=Es) - steel.set_ultimate_strain(fy / Es) + steel = ElasticMaterial(E=Es, density=7850) + steel.constitutive_law.set_ultimate_strain(fy / Es) # Create geometry i_beam = cls(name) geo = CompoundGeometry([SurfaceGeometry(i_beam.polygon, steel)]) @@ -1261,9 +1264,10 @@ def test_Isection_user_material_marin(cls, name): Es = 206000 fy = 355 eps_su = 7e-2 - steel = UserDefined( + steel_law = UserDefined( x=[-eps_su, -fy / Es, 0, fy / Es, eps_su], y=[-fy, -fy, 0, fy, fy] ) + steel = GenericMaterial(density=7850, constitutive_law=steel_law) # Create geometry i_beam = cls(name) geo = CompoundGeometry([SurfaceGeometry(i_beam.polygon, steel)]) @@ -1421,9 +1425,10 @@ def test_profiles(cls, name, Wyel, Wzel, Wypl, Wzpl): Es = 206000 fy = 355 eps_su = 7e-2 - steel = UserDefined( + steel_law = UserDefined( x=[-eps_su, -fy / Es, 0, fy / Es, eps_su], y=[-fy, -fy, 0, fy, fy] ) + steel = GenericMaterial(density=7850, constitutive_law=steel_law) # Create geometry beam = cls(name) @@ -1437,7 +1442,7 @@ def test_profiles(cls, name, Wyel, Wzel, Wypl, Wzpl): # Create the section with fiber sec = GenericSection(geo) # Elastic strength - steel.set_ultimate_strain(fy / Es) + steel_law.set_ultimate_strain(fy / Es) results = sec.section_calculator.calculate_bending_strength(theta=0, n=0) assert math.isclose(-results.m_y * 1e-6, mye_expected, rel_tol=2.5e-2) results = sec.section_calculator.calculate_bending_strength( @@ -1445,7 +1450,7 @@ def test_profiles(cls, name, Wyel, Wzel, Wypl, Wzpl): ) assert math.isclose(-results.m_z * 1e-6, mze_expected, rel_tol=2.5e-2) # Plastic strength - steel.set_ultimate_strain(0.07) + steel_law.set_ultimate_strain(0.07) results = sec.section_calculator.calculate_bending_strength(theta=0, n=0) assert math.isclose(-results.m_y * 1e-6, myp_expected, rel_tol=2.5e-2) results = sec.section_calculator.calculate_bending_strength( diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 901c753c..8ee5d8cf 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -11,6 +11,7 @@ Elastic, Elastic2D, ElasticPlastic, + InitialStrain, ParabolaRectangle, ParabolaRectangle2D, Popovics, @@ -753,3 +754,106 @@ def test_bilinearcompression(fc, eps_c, eps_cu): eps_min, eps_max = law.get_ultimate_strain(yielding=True) assert math.isclose(eps_min, -eps_c) assert math.isclose(eps_max, 100) + + +@pytest.mark.parametrize( + 'fy, eps_su', + [ + (350, 0.07), + (250, 0.03), + (355, 0.002), + ], +) +@pytest.mark.parametrize( + 'initial_strain', [0.0, 0.001, 0.005, 0.01, -0.001, -0.005] +) +def test_initstrain(fy, eps_su, initial_strain): + """Test InitStrain constitutive law.""" + base_law = ElasticPlastic(fy=fy, E=200000, eps_su=eps_su) + law = InitialStrain( + constitutive_law=base_law, initial_strain=initial_strain + ) + + ult_strain_base = base_law.get_ultimate_strain() + ult_strain = law.get_ultimate_strain() + assert math.isclose( + ult_strain_base[0] - initial_strain, + ult_strain[0], + ) + assert math.isclose( + ult_strain_base[1] - initial_strain, + ult_strain[1], + ) + + strain = np.linspace(-eps_su * 1.1, eps_su * 1.1, 100) + + # Test stresses + sig_expected = base_law.get_stress(strain + initial_strain) + sig_computed = law.get_stress(strain) + + assert_allclose(sig_computed, sig_expected) + + # Test tangents + tan_expected = base_law.get_tangent(strain + initial_strain) + tan_computed = law.get_tangent(strain) + + assert_allclose(tan_computed, tan_expected) + + +def test_initial_strain_not_constitutive_law(): + """Test if TypeError is raised if an InitialStrain law is initialized + without a ConstitutiveLaw. + """ + with pytest.raises(TypeError): + InitialStrain(constitutive_law=None, initial_strain=1e-3) + + +def test_initial_strain_strain_compatibility_property(): + """Test the strain_compatibility property.""" + # Arrange + constitutive_law = Elastic(E=30000) + initial_strain_value = 2e-3 + + initial_strain_no_strain_compatibility = InitialStrain( + constitutive_law=constitutive_law, + initial_strain=initial_strain_value, + strain_compatibility=False, + ) + + initial_strain_strain_compatibility = InitialStrain( + constitutive_law=constitutive_law, + initial_strain=initial_strain_value, + strain_compatibility=True, + ) + + # Act and assert + assert not initial_strain_no_strain_compatibility.strain_compatibility + assert initial_strain_strain_compatibility.strain_compatibility + + +def test_initial_strain_get_stress_tangent_no_strain_compatibility(): + """Test the get_stress and get_tangent methods without strain + compatibility. + """ + # Arrange + constitutive_law = Elastic(E=30000) + initial_strain_value = 2e-3 + + initial_strain = InitialStrain( + constitutive_law=constitutive_law, + initial_strain=initial_strain_value, + strain_compatibility=False, + ) + + initial_stress = constitutive_law.get_stress(initial_strain_value) + initial_tangent = constitutive_law.get_tangent(0) + + strains = np.linspace(-2e-3, 2e-3, 10) + + # Act + stresses = initial_strain.get_stress(strains) + tangents = initial_strain.get_tangent(strains) + + # Assert + assert np.allclose(stresses, initial_stress) + assert np.allclose(tangents, initial_tangent * 1e-6) diff --git a/tests/test_materials/test_marin_coefficiens.py b/tests/test_materials/test_marin_coefficiens.py index 7581c7dd..3cb3a6c9 100644 --- a/tests/test_materials/test_marin_coefficiens.py +++ b/tests/test_materials/test_marin_coefficiens.py @@ -9,6 +9,7 @@ BilinearCompression, Elastic, ElasticPlastic, + InitialStrain, ParabolaRectangle, UserDefined, ) @@ -292,6 +293,65 @@ def test_marin_elasticplastic_uniform_strain(eps_0, E, fy, Eh, eps_su): assert math.isclose(coeffs[0][i], expected[1][i]) +@pytest.mark.parametrize('eps_0', eps0) +@pytest.mark.parametrize('init_strain', [0.0, 0.001]) +@pytest.mark.parametrize( + 'E, fy, Eh, eps_su', + [ + (200000, 450, 0.0, 0.0675), + (200000, 500, 2000.0, 0.0675), + (200000, 500, 0.0, 0.0675), + ], +) +def test_marin_init_strain_uniform_strain( + eps_0, E, fy, Eh, eps_su, init_strain +): + """Test the marin coefficients for a strain plane of pure tension or + compression for InitialStrain constitutive law. + """ + base_law = ElasticPlastic(E, fy, Eh, eps_su) + law = InitialStrain(base_law, init_strain) + + eps_y = base_law._eps_sy + _, eps_su_p = law.get_ultimate_strain() + + delta_sigma = fy * (1 - Eh / E) + + # Check marin coefficients for stress integration + sign = 1 if eps_0 >= 0 else -1 + if abs(eps_0 + init_strain) <= eps_y: + expected = (None, (E * (eps_0 + init_strain), 0.0)) + elif abs(eps_0 + init_strain) <= eps_su_p: + expected = ( + None, + (Eh * (eps_0 + init_strain) + sign * delta_sigma, 0.0), + ) + else: + expected = (None, (0.0,)) + + strain, coeffs = law.__marin__([eps_0, 0]) + + assert strain == expected[0] + assert len(coeffs[0]) == len(expected[1]) + for i in range(len(coeffs[0])): + assert math.isclose(coeffs[0][i], expected[1][i]) + + # Check marin coefficients for modulus integration + if abs(eps_0 + init_strain) <= eps_y: + expected = (None, (E,)) + elif abs(eps_0 + init_strain) <= eps_su_p: + expected = (None, (Eh,)) + else: + expected = (None, (0.0,)) + + strain, coeffs = law.__marin_tangent__([eps_0, 0]) + + assert strain == expected[0] + assert len(coeffs[0]) == len(expected[1]) + for i in range(len(coeffs[0])): + assert math.isclose(coeffs[0][i], expected[1][i]) + + @pytest.mark.parametrize('eps_0', eps0) @pytest.mark.parametrize( 'eps_min', [(-0.01), (-0.005), (-0.004), (-0.003), (-0.002), (-0.001)] diff --git a/tests/test_materials/test_reinforcements.py b/tests/test_materials/test_reinforcements.py index ef48060b..8fc2fb40 100644 --- a/tests/test_materials/test_reinforcements.py +++ b/tests/test_materials/test_reinforcements.py @@ -127,3 +127,91 @@ def test_invalid_constitutive_law(reinforcement_type): epsuk=6e-2, constitutive_law=invalid_constitutive_law, ) + + +@pytest.mark.parametrize( + 'initial_strain, initial_stress, strain_compatibility', + [ + (None, 300, True), + (0.0015, None, None), + (None, 450, None), + (0.0025, None, True), + (0.0025, None, False), + ], +) +def test_initial_strain_and_stress( + initial_strain, initial_stress, strain_compatibility +): + """Test initializing reinforcement with initial strain and stress.""" + # Arrange + fyk = 500 + Es = 200000 + ftk = 1.15 * fyk + epsuk = 7.5e-2 + + # Act + reinf = ReinforcementEC2_2004( + fyk=fyk, + Es=Es, + ftk=ftk, + epsuk=epsuk, + initial_strain=initial_strain, + initial_stress=initial_stress, + strain_compatibility=strain_compatibility, + ) + + # Assert + assert reinf.strain_compatibility == strain_compatibility + assert reinf.fyk == fyk + assert reinf.Es == Es + assert math.isclose(reinf.fyd(), fyk / 1.15) + if initial_strain is not None: + expected_stress = ( + initial_strain * Es + if initial_strain < reinf.epsyd + else reinf.constitutive_law.wrapped_law.get_stress(initial_strain) + ) + # Check that stra_compatibility is not None and is false + if ( + reinf.strain_compatibility is not None + and not reinf._strain_compatibility + ): + expected_stress *= 0 + expected_stress += reinf.initial_stress + assert math.isclose(reinf.initial_strain, initial_strain) + assert math.isclose(reinf.initial_stress, expected_stress) + if initial_stress is not None: + expected_strain = ( + initial_stress / Es + if initial_stress < reinf.fyd() + else (initial_stress - reinf.fyd()) + / reinf.constitutive_law.wrapped_law._Eh + + reinf.epsyd + ) + if ( + reinf.strain_compatibility is not None + and not reinf._strain_compatibility + ): + expected_stress *= 0 + assert math.isclose(reinf.initial_strain, expected_strain) + assert math.isclose(reinf.initial_stress, initial_stress) + + +def test_initial_strain_and_stress_invalid(): + """Test initializing reinforcement with initial strain and stress.""" + # Arrange + fyk = 500 + Es = 200000 + ftk = 1.15 * fyk + epsuk = 7.5e-2 + + # Act + with pytest.raises(ValueError): + ReinforcementEC2_2004( + fyk=fyk, + Es=Es, + ftk=ftk, + epsuk=epsuk, + initial_strain=0.002, + initial_stress=400, + ) diff --git a/tests/test_mc2010/test_mc2010_concrete_punching.py b/tests/test_mc2010/test_mc2010_concrete_punching.py index 7131dcce..54ad777f 100644 --- a/tests/test_mc2010/test_mc2010_concrete_punching.py +++ b/tests/test_mc2010/test_mc2010_concrete_punching.py @@ -7,102 +7,405 @@ from structuralcodes.codes.mc2010 import _concrete_punching +def test_b0(): + """Test the b_0 function.""" + assert math.isclose( + _concrete_punching.b_0(v_ed=100, v_prep_d_max=2.3), + 43.47, # math check + rel_tol=0.001, + ) + assert _concrete_punching.b_0(v_ed=1000, v_prep_d_max=0.001) == 1000000 + assert _concrete_punching.b_0(v_ed=0, v_prep_d_max=1) == 0 + + @pytest.mark.parametrize( - 'Ved, e_u, l_x, l_y, inner, edge_par, edge_per, corner, expected', + 'v_ed, e_u, b_s, inner, edge_par, edge_per, corner, expected', [ - (10e3, 20, 2e3, 2e3, True, False, False, False, 1401), - (10e3, 20, 2e3, 3e3, False, True, False, False, 2500), - (10e3, 20, 2e3, 3e3, False, False, True, False, 1497), - (10e3, 20, 2e3, 3e3, False, False, False, True, 5000), + (10e3, 20, 660, True, False, False, False, 1401), # Inner + (10e3, 20, 660, False, True, False, False, 2500), # Edge par + (10e3, 20, 660, False, False, True, False, 1553.03), # Edge perp + (10e3, 20, 660, False, False, False, True, 5000), # Corner + # Edge cases + (10e3, 0, 660, True, False, False, False, 1250), # No ecc + (10e3, 100, 660, True, False, False, False, 2007.58), # Large ecc ], ) -def test_m_ed(Ved, e_u, l_x, l_y, inner, edge_par, edge_per, corner, expected): +def test_m_ed(v_ed, e_u, b_s, inner, edge_par, edge_per, corner, expected): """Test the m_ed function.""" assert math.isclose( _concrete_punching.m_ed( - Ved, e_u, l_x, l_y, inner, edge_par, edge_per, corner + v_ed, e_u, b_s, inner, edge_par, edge_per, corner ), expected, rel_tol=0.001, ) +def test_m_ed_invalid(): + """Test m_ed with invalid inputs.""" + with pytest.raises(ValueError, match='Placement is not defined'): + _concrete_punching.m_ed(10e3, 20, 660, False, False, False, False) + + @pytest.mark.parametrize( ( - 'l_x, l_y, f_yd, d, e_s, approx_lvl_p, Ved, e_u, inner, edge_par, ' - 'edge_per, corner, m_rd, x_direction, expected' + 'psi_punching_level_one, psi_punching_level_two, ' + 'psi_punching_level_three, approx_lvl_p, expected' ), [ - ( - 2e3, - 3e3, - 434, - 160, - 200e3, - 1, - 50e3, - 20, - True, - False, - False, - False, - 140, - True, - 0.013426875, + (0.013427, 0.47089, 0.47089, 1, 0.013427), # Level 1 + (0.013427, 0.47089, 0.47089, 2, 0.47089), # Level 2 + (0.013427, 0.47089, 0.37671, 3, 0.37671), # Level 3 + ], +) +def test_psi_punching( + psi_punching_level_one, + psi_punching_level_two, + psi_punching_level_three, + approx_lvl_p, + expected, +): + """Test the psi_punching function.""" + assert math.isclose( + _concrete_punching.psi_punching( + psi_punching_level_one, + psi_punching_level_two, + psi_punching_level_three, + approx_lvl_p, ), - ( - 2e3, - 3e3, - 434, - 160, - 200e3, - 2, - 10e3, - 20, - True, - False, - False, - False, - 140, - False, - 0.41269218238, + expected, + rel_tol=0.001, + ) + + +def test_psi_punching_invalid(): + """Test psi_punching with invalid inputs.""" + with pytest.raises(ValueError, match='Approximation level is not defined'): + _concrete_punching.psi_punching(0.01, 0.02, 0.03, 4) + + +@pytest.mark.parametrize( + 'k_dg_val, d_eff, psi_punching_val, expected', + [ + (0.75, 160, 0.015, 0.3205), # Normal case + (0.75, 160, 0.05, 0.145), # Larger rotation + (0.75, 160, 0.001, 0.6), # Small rotation + (1.0, 160, 0.015, 0.273), # Different k_dg + ], +) +def test_k_psi(k_dg_val, d_eff, psi_punching_val, expected): + """Test the k_psi function.""" + assert math.isclose( + _concrete_punching.k_psi(k_dg_val, d_eff, psi_punching_val), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'd_g, expected', + [ + (32, 0.75), # Large aggregate, limited by 0.75 + (16, 1.0), # Medium aggregate + (8, 1.333), # Small aggregate + (0, 2.0), # No aggregate + ], +) +def test_k_dg(d_g, expected): + """Test the k_dg function.""" + assert math.isclose( + _concrete_punching.k_dg(d_g), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'k_psi_val, b_0, d_v, f_ck, gamma_c, expected', + [ + (0.6, 1000, 160, 30, 1.5, 350542.44), # Normal case + (0.3, 1000, 160, 30, 1.5, 175271.22), # Lower k_psi + (0.6, 1000, 160, 90, 1.5, 607157.31), # High strength concrete + (0.6, 1000, 160, 30, 2.0, 262906.83), # Different safety factor + ], +) +def test_v_rdc_punching(k_psi_val, b_0, d_v, f_ck, gamma_c, expected): + """Test the v_rdc_punching function.""" + assert math.isclose( + _concrete_punching.v_rdc_punching(k_psi_val, b_0, d_v, f_ck, gamma_c), + expected, + rel_tol=0.001, + ) + + +def test_v_rds_punching(): + """Test the v_rds_punching function.""" + # Normal case + result = _concrete_punching.v_rds_punching( + f_ywd=434.78, # 500/1.15 + e_u=20, + b_u=1000, + alpha=90, + sigma_swd=400, + a_sw=100, + v_ed=1000, + ) + assert math.isclose(result, 39215.686, rel_tol=0.001) + + # Test warning for insufficient reinforcement + warn_msg = 'Consider increasing punching shear reinforcement' + with pytest.warns(UserWarning, match=warn_msg): + _concrete_punching.v_rds_punching( + f_ywd=434.78, # 500/1.15 + e_u=20, + b_u=1000, + alpha=90, + sigma_swd=400, + a_sw=1, # Very small area + v_ed=1000, + ) + + +@pytest.mark.parametrize( + ( + 'd_v, f_ck, d_head, stirrups_compression, ' + 'b0_val, k_psi_val, gamma_c, expected' + ), + [ + # Normal cases + (160, 30, True, True, 500, 0.6, 1.5, 292118.70), + (160, 30, True, False, 500, 0.6, 1.5, 292118.70), + (160, 30, False, True, 500, 0.6, 1.5, 292118.70), + (160, 30, False, False, 500, 0.6, 1.5, 292118.70), + # tests to trigger "k_sys * k_psi_val * base_resistance" + (160, 30, False, False, 500, 0.1, 1.5, 58423), # k = 2 + (160, 30, True, True, 500, 0.1, 1.5, 81793), # k = 2.8 + (160, 30, False, True, 500, 0.1, 1.5, 70108), # k = 2.4 + (160, 30, True, False, 500, 0.1, 1.5, 81793), # k = 2.8 + ], +) +def test_v_rd_max_punching( + d_v, + f_ck, + d_head, + stirrups_compression, + b0_val, + k_psi_val, + gamma_c, + expected, +): + """Test the v_rd_max_punching function.""" + assert math.isclose( + _concrete_punching.v_rd_max_punching( + d_v, f_ck, d_head, stirrups_compression, b0_val, k_psi_val, gamma_c ), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'v_rd_c, v_rd_s, v_rd_max, expected', + [ + (100, 50, 200, 150), + (150, 100, 200, 200), + (100, 0, 150, 100), + (0, 100, 150, 100), + (120, 80, 200, 200), ], ) -def test_psi_punching( +def test_v_rd_punching(v_rd_c, v_rd_s, v_rd_max, expected): + """Test the v_rd_punching function.""" + assert math.isclose( + _concrete_punching.v_rd_punching(v_rd_c, v_rd_s, v_rd_max), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'l_x, l_y, expected', + [ + (2000, 2000, 660), + (2000, 3000, 808.332), + (1000, 1000, 330.0), + (5000, 500, 500), + ], +) +def test_b_s(l_x, l_y, expected): + """Test the b_s function.""" + assert math.isclose( + _concrete_punching.b_s(l_x=l_x, l_y=l_y), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'l_x, l_y, expected', + [ + (2000, 3000, 2000), + (5000, 500, 500), + ], +) +def test_b_sr(l_x, l_y, expected): + """Test the b_sr function.""" + assert math.isclose( + _concrete_punching.b_sr(l_x=l_x, l_y=l_y), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'l_x, l_y, f_yd, d_eff, e_s, expected', + [ + (2000, 3000, 434, 160, 200e3, 0.013427), # Normal case + (1000, 1000, 434, 160, 200e3, 0.004476), # Equal spans + (2000, 3000, 434, 50, 200e3, 0.042966), # Small effective depth + (2000, 3000, 500, 160, 200e3, 0.015476), # Higher steel strength + ], +) +def test_psi_punching_level_one(l_x, l_y, f_yd, d_eff, e_s, expected): + """Test the psi_punching_level_one function.""" + assert math.isclose( + _concrete_punching.psi_punching_level_one(l_x, l_y, f_yd, d_eff, e_s), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'r_s, f_yd, d_eff, e_s, m_ed, m_rd, m_Pd, expected', + [ + (440, 434, 160, 200e3, 1500, 140, 0, 0.3139), # Normal case + (440, 434, 160, 200e3, 1400, 140, 0, 0.2831), # Lower m_ed + (440, 434, 160, 200e3, 1500, 200, 0, 0.1839), # Higher m_rd + (440, 434, 160, 200e3, 1500, 140, 50, 0.5789), # With prestressing + ], +) +def test_psi_punching_level_two( + r_s, f_yd, d_eff, e_s, m_ed, m_rd, m_Pd, expected +): + """Test the psi_punching_level_two function.""" + assert math.isclose( + _concrete_punching.psi_punching_level_two( + r_s, f_yd, d_eff, e_s, m_ed, m_rd, m_Pd + ), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + ( + 'psi_punching_level_two, is_uncracked_model, ' + 'is_moment_from_uncracked_model, expected' + ), + [ + (0.47089, False, False, 0.47089), # No change + (0.47089, True, True, 0.37671), # Both conditions true + (0.47089, True, False, 0.47089), # Only one condition true + (0.47089, False, True, 0.47089), # Only one condition true + ], +) +def test_psi_punching_level_three( + psi_punching_level_two, + is_uncracked_model, + is_moment_from_uncracked_model, + expected, +): + """Test the psi_punching_level_three function.""" + assert math.isclose( + _concrete_punching.psi_punching_level_three( + psi_punching_level_two, + is_uncracked_model, + is_moment_from_uncracked_model, + ), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + ( + 'l_x, l_y, x_direction, is_level_three_approximation, ' + 'column_edge_or_corner, b_sr_val, expected' + ), + [ + (2000, 3000, True, False, False, -1, 440), # x direction + (2000, 3000, False, False, False, -1, 660), # y direction + (2000, 3000, True, True, True, 2000, 1340), # Level 3, edge/corner, + # x direction + (2000, 3000, False, True, True, 2000, 1340), # Level 3, edge/corner, + # y direction + ], +) +def test_r_s( l_x, l_y, - f_yd, - d, - e_s, - approx_lvl_p, - Ved, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, x_direction, + is_level_three_approximation, + column_edge_or_corner, + b_sr_val, expected, ): - """Test the psi_punching function.""" + """Test the r_s function.""" assert math.isclose( - _concrete_punching.psi_punching( + _concrete_punching.r_s( l_x, l_y, - f_yd, - d, - e_s, - approx_lvl_p, - Ved, - e_u, - inner, - edge_par, - edge_per, - corner, - m_rd, x_direction, + is_level_three_approximation, + column_edge_or_corner, + b_sr_val, + ), + expected, + rel_tol=0.001, + ) + + +def test_r_s_error(): + """Test r_s with invalid inputs.""" + with pytest.raises( + ValueError, match='b_sr is not defined for Level 3 of Approximation' + ): + _concrete_punching.r_s(2000, 3000, True, True, True, None) + + +@pytest.mark.parametrize( + 'f_ywk, gamma_s, expected', + [ + (500, 1.15, 434.78), # Normal case + (600, 1.15, 521.74), # Higher steel strength + (500, 1.0, 500), # No safety factor + ], +) +def test_f_ywd(f_ywk, gamma_s, expected): + """Test the f_ywd function.""" + assert math.isclose( + _concrete_punching.f_ywd(f_ywk, gamma_s), + expected, + rel_tol=0.001, + ) + + +@pytest.mark.parametrize( + 'e_s, psi_punching, alpha, f_bd, d_eff, f_ywd, phi_w, expected', + [ + (200e3, 0.015, 90, 3, 160, 434.78, 8, 434.78), # Limited by f_ywd + (200e3, 0.001, 90, 3, 160, 434.78, 8, 37.9334), # Small rotation + (200e3, 0.015, 45, 3, 160, 434.78, 8, 434.78), # Different angle + (200e3, 0.015, 90, 3, 160, 300, 8, 300), # Lower f_ywd + ], +) +def test_sigma_swd( + e_s, psi_punching, alpha, f_bd, d_eff, f_ywd, phi_w, expected +): + """Test the sigma_swd function.""" + assert math.isclose( + _concrete_punching.sigma_swd( + e_s, psi_punching, alpha, f_bd, d_eff, f_ywd, phi_w ), expected, rel_tol=0.001, diff --git a/tests/test_sections/test_generic_section.py b/tests/test_sections/test_generic_section.py index 23e5c140..b811fde0 100644 --- a/tests/test_sections/test_generic_section.py +++ b/tests/test_sections/test_generic_section.py @@ -15,8 +15,9 @@ add_reinforcement_circle, add_reinforcement_line, ) +from structuralcodes.materials.basic import ElasticMaterial, GenericMaterial from structuralcodes.materials.concrete import ConcreteEC2_2004, ConcreteMC2010 -from structuralcodes.materials.constitutive_laws import Elastic, Sargin +from structuralcodes.materials.constitutive_laws import InitialStrain, Sargin from structuralcodes.materials.reinforcement import ( ReinforcementEC2_2004, ReinforcementMC2010, @@ -145,7 +146,7 @@ def test_rectangular_section(): def test_rectangular_section_tangent_stiffness(b, h, E, integrator): """Test stiffness matrix of elastic rectangular section.""" # Create materials to use - elastic = Elastic(E) + elastic = ElasticMaterial(E=E, density=2450) # The section poly = Polygon( @@ -352,7 +353,7 @@ def test_rectangular_rc_section_tangent_stiffness( # For comparison, let's compare this with a elastic section of only # reacting concrete: - elastic = Elastic(Ec) + elastic = ElasticMaterial(E=Ec, density=concrete.density) geo = SurfaceGeometry( Polygon( ( @@ -412,7 +413,7 @@ def test_rectangular_rc_section_tangent_stiffness( def test_rectangular_section_tangent_stiffness_translated(b, h, E, integrator): """Test stiffness matrix of elastic rectangular section.""" # Create materials to use - elastic = Elastic(E) + elastic = ElasticMaterial(E=E, density=2450) # The section poly = Polygon(((0, 0), (b, 0), (b, h), (0, h))) @@ -870,7 +871,7 @@ def test_strain_plane_calculation_elastic_Nmm(n, my, mz, Ec, b, h): Elastic materials, test many load combinations. """ # Create materials to use - concrete = Elastic(Ec) + concrete = ElasticMaterial(E=Ec, density=2450) # Create the section geom = SurfaceGeometry( @@ -944,7 +945,7 @@ def test_strain_plane_calculation_elastic_kNm(n, my, mz, Ec, b, h): Units in kN, m and kPa """ # Create materials to use - concrete = Elastic(Ec) + concrete = ElasticMaterial(E=Ec, density=2450) # Create the section geom = SurfaceGeometry( @@ -1285,18 +1286,202 @@ def test_moment_curvature_large_circular_section(): -1.06641639e11, -1.20528802e11, -1.34385677e11, - -1.83767156e11, - -1.91178122e11, - -1.95069846e11, - -1.97732022e11, - -2.00008766e11, - -2.02080156e11, - -2.03933914e11, - -2.05666626e11, - -2.07302091e11, - -2.08884969e11, + -1.83835739e11, + -1.91419082e11, + -1.95481133e11, + -1.98304138e11, + -2.00740666e11, + -2.02971492e11, + -2.04990133e11, + -2.06880606e11, + -2.08682290e11, + -2.10425206e11, ] ) assert np.allclose(result.chi_y, exp_result_chi, rtol=1e-5, atol=1e-6) assert np.allclose(result.m_y, exp_result_m, rtol=1e-5, atol=1e-2) + + +def test_rotate_triangulation_data(): + """Test showing a bug in the storing of triangulation data. + + Triangulation data was stored after the rotation of the section leading to + an inconsistency in the results. + """ + # Create materials to use + fc = 40 + fy = 500 + Es = 200000 + concrete = ConcreteEC2_2004(fck=fc) + duct_props = reinforcement_duct_props(fyk=fy, ductility_class='C') + reinforcement = ReinforcementEC2_2004( + fyk=fy, Es=Es, ftk=fy, epsuk=duct_props['epsuk'] + ) + + # The section + width = 1000 + height = 250 + + diameter_bottom = [10] + spacing_bottom = [200] + diameter_top = [12, 16] + spacing_top = [200, 200] + cover_bottom = 35 + cover_top = 35 + + # Create the geometry + rectangle = Polygon( + ( + (-width / 2, -height / 2), + (width / 2, -height / 2), + (width / 2, height / 2), + (-width / 2, height / 2), + ) + ) + + # Create the section + geometry = SurfaceGeometry(rectangle, concrete) + # Base reinforcement + for diameter, spacing in zip(diameter_bottom, spacing_bottom): + geometry = add_reinforcement_line( + geo=geometry, + coords_i=(-width / 2 + cover_bottom, -height / 2 + cover_bottom), + coords_j=(width / 2 - cover_bottom, -height / 2 + cover_bottom), + diameter=diameter, + material=reinforcement, + s=spacing, + ) + for diameter, spacing in zip(diameter_top, spacing_top): + geometry = add_reinforcement_line( + geo=geometry, + coords_i=(-width / 2 + cover_top, height / 2 - cover_top), + coords_j=(width / 2 - cover_top, height / 2 - cover_top), + diameter=diameter, + material=reinforcement, + s=spacing, + ) + + # geometry = geometry.translate(width/2, height/2) + section = GenericSection( + geometry=geometry, + integrator='Fiber', + mesh_size=0.001, + ) + + section.section_calculator.calculate_bending_strength(theta=0) + + res1 = section.section_calculator.calculate_bending_strength(theta=np.pi) + + # geometry = geometry.translate(width/2, height/2) + section = GenericSection( + geometry=geometry, + integrator='Fiber', + mesh_size=0.001, + ) + section.geometry + + res2 = section.section_calculator.calculate_bending_strength(theta=np.pi) + + assert math.isclose(res2.m_y, res1.m_y, rel_tol=1e-3) + + +@pytest.mark.parametrize('fck', [35, 55, 65]) +@pytest.mark.parametrize('fyk', [450, 550]) +@pytest.mark.parametrize( + 'ductility_class', + ['a', 'b', 'c'], +) +def test_rectangular_section_init_strain(fck, fyk, ductility_class): + """Test a rectangular section with different concretes and n. + + This checks that with initial strain set to zero, the results + are the same for both Marin and Fiber integrators, compared + to the default reinforcement material. + """ + # crete the materials to use + concrete = ConcreteEC2_2004(fck=fck) + props = reinforcement_duct_props(fyk=fyk, ductility_class=ductility_class) + + steel = ReinforcementEC2_2004( + fyk=fyk, Es=200000, ftk=props['ftk'], epsuk=props['epsuk'] + ) + + # Create a dummy initStrain material + init_strain = InitialStrain(steel.constitutive_law, 0.0) + init_strain_material = GenericMaterial( + density=7850, constitutive_law=init_strain + ) + + # The section + poly = Polygon(((0, 0), (200, 0), (200, 400), (0, 400))) + geo = SurfaceGeometry(poly, concrete) + geo = add_reinforcement_line( + geo=geo, + coords_i=(40, 40), + coords_j=(160, 40), + diameter=16, + material=steel, + n=4, + ) + geo = add_reinforcement_line( + geo=geo, + coords_i=(40, 360), + coords_j=(160, 360), + diameter=16, + material=steel, + n=4, + ) + geo = geo.translate(-100, -200) + + # Create the section with fiber integrator + sec_fiber = GenericSection(geo, integrator='fiber', mesh_size=0.001) + + # Compute bending strength My- + res_fiber = sec_fiber.section_calculator.calculate_bending_strength() + + # Create the section with default marin integrator + sec_marin = GenericSection(geo) + + # Compute bending strength My- + res_marin = sec_marin.section_calculator.calculate_bending_strength() + + assert math.isclose(res_fiber.m_y, res_marin.m_y, rel_tol=1e-3) + + # Section with init_strain_material: + geo = SurfaceGeometry(poly, concrete) + geo = add_reinforcement_line( + geo=geo, + coords_i=(40, 40), + coords_j=(160, 40), + diameter=16, + material=init_strain_material, + n=4, + ) + geo = add_reinforcement_line( + geo=geo, + coords_i=(40, 360), + coords_j=(160, 360), + diameter=16, + material=init_strain_material, + n=4, + ) + geo = geo.translate(-100, -200) + + # Create the section with fiber integrator + sec_fiber = GenericSection(geo, integrator='fiber', mesh_size=0.001) + + # Compute bending strength My- + res_fiber_i = sec_fiber.section_calculator.calculate_bending_strength() + + # Create the section with default marin integrator + sec_marin = GenericSection(geo) + + # Compute bending strength My- + res_marin_i = sec_marin.section_calculator.calculate_bending_strength() + + assert math.isclose(res_fiber_i.m_y, res_marin_i.m_y, rel_tol=1e-3) + + # Check they are all the same + assert math.isclose(res_fiber.m_y, res_fiber_i.m_y, rel_tol=1e-3) + assert math.isclose(res_marin.m_y, res_fiber_i.m_y, rel_tol=1e-3) diff --git a/tests/test_sections/test_shell_section.py b/tests/test_sections/test_shell_section.py index 33cba480..aa7b51e6 100644 --- a/tests/test_sections/test_shell_section.py +++ b/tests/test_sections/test_shell_section.py @@ -7,11 +7,14 @@ ShellGeometry, ShellReinforcement, ) +from structuralcodes.materials.basic import GenericMaterial +from structuralcodes.materials.concrete import ConcreteEC2_2004 from structuralcodes.materials.constitutive_laws import ( Elastic2D, ElasticPlastic, ParabolaRectangle2D, ) +from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 from structuralcodes.sections import ShellSection # Membrane and bending-strain parameter ranges @@ -37,7 +40,8 @@ def test_integrate_strain_profile( Ec, nu, thickness, eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy ): """Elastic plate: strains → stress-resultants.""" - material = Elastic2D(Ec, nu) + constitutive_law = Elastic2D(Ec, nu) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) shell = ShellSection(ShellGeometry(thickness, material)) # Numerically integrated forces/moments @@ -62,7 +66,9 @@ def test_integrate_strain_profile( def test_integrate_strain_profile_tangent(E=30000, nu=0.20, t=200): """Elastic plate: tangent stiffness matrix.""" - shell = ShellSection(ShellGeometry(t, Elastic2D(E, nu))) + constitutive_law = Elastic2D(E, nu) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) + shell = ShellSection(ShellGeometry(t, material=material)) K = shell.section_calculator.integrate_strain_profile( np.array([1e-3] * 3 + [1e-6] * 3), integrate='modulus' ) @@ -90,7 +96,9 @@ def test_integrate_strain_profile_tangent(E=30000, nu=0.20, t=200): def test_wrong_integrator(): """Unknown keyword must raise ValueError.""" - shell = ShellSection(ShellGeometry(200, Elastic2D(30000, 0.20))) + constitutive_law = Elastic2D(30000, 0.20) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) + shell = ShellSection(ShellGeometry(200, material=material)) with pytest.raises(ValueError): shell.section_calculator.integrate_strain_profile( np.zeros(6), integrate='tangent' @@ -115,7 +123,9 @@ def test_wrong_integrator(): @pytest.mark.parametrize('Ec, nu, t', [(30000, 0.20, 200), (30000, 0.20, 600)]) def test_elastic_strain_profile(Ec, nu, t, nx, ny, nxy, mx, my, mxy): """Loads → strains for an isotropic plate.""" - shell = ShellSection(ShellGeometry(t, Elastic2D(Ec, nu))) + constitutive_law = Elastic2D(Ec, nu) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) + shell = ShellSection(ShellGeometry(t, material=material)) eps = shell.section_calculator.calculate_strain_profile( nx, ny, nxy, mx, my, mxy ) @@ -135,15 +145,16 @@ def test_elastic_strain_profile(Ec, nu, t, nx, ny, nxy, mx, my, mxy): assert np.allclose(eps, expected, rtol=1e-4, atol=1e-3) -def test_default_equals_explicit_mesh_size(t=200): +def test_default_equals_explicit_mesh_size(): """Default mesh_size (0.01) equals explicit 0.01.""" - shell_0 = ShellSection(ShellGeometry(t, Elastic2D(30_000, 0.20))) + t = 200 + constitutive_law = Elastic2D(30000, 0.20) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) + shell_0 = ShellSection(ShellGeometry(t, material=material)) K0 = shell_0.section_calculator.integrate_strain_profile( np.zeros(6), integrate='modulus' ) - shell_1 = ShellSection( - ShellGeometry(t, Elastic2D(30_000, 0.20)), mesh_size=0.01 - ) + shell_1 = ShellSection(ShellGeometry(t, material=material), mesh_size=0.01) K1 = shell_1.section_calculator.integrate_strain_profile( np.zeros(6), integrate='modulus' ) @@ -151,11 +162,12 @@ def test_default_equals_explicit_mesh_size(t=200): @pytest.mark.parametrize('invalid', [-0.2, 0.0, 1.5]) -def test_invalid_mesh_size_raises(invalid, t=200): +def test_invalid_mesh_size_raises(invalid): """mesh_size outside (0,1] → ValueError.""" - shell = ShellSection( - ShellGeometry(t, Elastic2D(30000, 0.20)), mesh_size=invalid - ) + t = 200 + constitutive_law = Elastic2D(30000, 0.20) + material = GenericMaterial(density=2500, constitutive_law=constitutive_law) + shell = ShellSection(ShellGeometry(t, material), mesh_size=invalid) with pytest.raises(ValueError): shell.section_calculator.integrate_strain_profile( np.zeros(6), integrate='stress' @@ -164,9 +176,16 @@ def test_invalid_mesh_size_raises(invalid, t=200): def test_parabola_section(): """ParabolaRectangle2D with mesh_size 0.5.""" - concrete = ParabolaRectangle2D(35, nu=0) - reinforcement = ElasticPlastic(200000, 500) - + parabola_rectangle = ParabolaRectangle2D(35, nu=0) + elastic_plastic = ElasticPlastic(200000, 500) + concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + reinforcement = ReinforcementEC2_2004( + fyk=500, + Es=200000, + ftk=500, + epsuk=3e-2, + constitutive_law=elastic_plastic, + ) Asx1 = ShellReinforcement(-157, 1, 300, 16, reinforcement, 0) geo = ShellGeometry(400, concrete) @@ -196,21 +215,29 @@ def test_parabola_section(): ( -1000, 1000, - np.array([-6.191136e-05, -1.100878e-22, 1.269841e-04, 0, 0, 0]), + np.array([-5.285263e-05, 2.741669e-05, 1.649706e-04, 0, 0, 0]), 0.001, ), ( 1000, 1000, - np.array([6.191136e-05, -1.100878e-22, 1.269841e-04, 0, 0, 0]), + np.array([1.330415e-04, 2.785210e-05, 2.186547e-04, 0, 0, 0]), 0.001, ), ], ) def test_parabola_cracked(nx, nxy, expected, tol): """Test parabola rectangle with nu = 0.""" - concrete = ParabolaRectangle2D(45, nu=0) - reinforcement = ElasticPlastic(200000, 500) + parabola_rectangle = ParabolaRectangle2D(45, nu=0) + elastic_plastic = ElasticPlastic(200000, 500) + concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + reinforcement = ReinforcementEC2_2004( + fyk=500, + Es=200000, + ftk=500, + epsuk=3e-2, + constitutive_law=elastic_plastic, + ) geo = ShellGeometry(350, concrete) Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) @@ -243,21 +270,29 @@ def test_parabola_cracked(nx, nxy, expected, tol): ( -1000, 1000, - np.array([-6.187717e-05, 1.220713e-05, 1.523810e-04, 0, 0, 0]), + np.array([-5.332211e-05, 3.874431e-05, 1.897691e-04, 0, 0, 0]), 0.001, ), ( 1000, 1000, - np.array([6.187717e-05, -1.220713e-05, 1.523810e-04, 0, 0, 0]), + np.array([1.302065e-04, 1.461867e-05, 2.411031e-04, 0, 0, 0]), 0.001, ), ], ) def test_parabola_uncracked(nx, nxy, expected, tol): """Test parabola rectangle with nu = 0.2.""" - concrete = ParabolaRectangle2D(45) - reinforcement = ElasticPlastic(200000, 500) + parabola_rectangle = ParabolaRectangle2D(45) + elastic_plastic = ElasticPlastic(200000, 500) + concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + reinforcement = ReinforcementEC2_2004( + fyk=500, + Es=200000, + ftk=500, + epsuk=3e-2, + constitutive_law=elastic_plastic, + ) geo = ShellGeometry(350, concrete) Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) @@ -280,8 +315,16 @@ def test_parabola_uncracked(nx, nxy, expected, tol): def test_exceed_max_iterations(): """Test that the maximum number of iterations is exceeded.""" - concrete = ParabolaRectangle2D(45) - reinforcement = ElasticPlastic(200000, 500) + parabola_rectangle = ParabolaRectangle2D(45) + elastic_plastic = ElasticPlastic(200000, 500) + concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + reinforcement = ReinforcementEC2_2004( + fyk=500, + Es=200000, + ftk=500, + epsuk=3e-2, + constitutive_law=elastic_plastic, + ) geo = ShellGeometry(350, concrete) Asx1 = ShellReinforcement(-132, 1, 200, 16, reinforcement, 0) Asx2 = ShellReinforcement(132, 1, 200, 16, reinforcement, 0) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..3f2284ff --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +isolated_build = True +envlist = py{39, 310, 311, 312, 313} + +[testenv] +allowlist_externals = make +uv_seed = True +commands = + make deps + make form + make lint + make test \ No newline at end of file From 075dd97e37f7f647e2a80a91a80c60825fb6e39c Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Sun, 31 Aug 2025 21:47:05 +0200 Subject: [PATCH 23/31] Make sure the poisson matrix is calculated only one time (#274) --- .../_parabolarectangle_2d.py | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 869ea743..862e47ba 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -18,6 +18,8 @@ class ParabolaRectangle2D(ParabolaRectangle): """ __materials__: t.Tuple[str] = ('concrete',) + _poisson_matrix_cracked: t.Optional[ArrayLike] = None + _poisson_matrix_uncracked: t.Optional[ArrayLike] = None def __init__( self, @@ -83,23 +85,45 @@ def transform(self, strain: ArrayLike) -> np.ndarray: ) return T, T @ eps - def poisson_matrix(self, nu: float) -> np.ndarray: - """Return the Poisson's ratio matrix.""" - return (1 / (1 - nu**2)) * np.array( - [ - [1, nu], - [nu, 1], - ] - ) + def poisson_matrix(self, cracked: bool) -> np.ndarray: + """Return the Poisson matrix.""" + if cracked: + return self.poisson_matrix_cracked + return self.poisson_matrix_uncracked + + def nu(self, cracked: bool) -> float: + """Return the Poisson ratio.""" + if cracked: + return 0.0 + return self._nu + + @property + def poisson_matrix_uncracked(self) -> ArrayLike: + """Return the uncracked Poisson matrix.""" + if self._poisson_matrix_uncracked is None: + self._poisson_matrix_uncracked = ( + 1 / (1 - self._nu**2) + ) * np.array( + [ + [1, self._nu], + [self._nu, 1], + ] + ) + return self._poisson_matrix_uncracked - def get_effective_principal_strains( - self, eps_p: ArrayLike, nu: float - ) -> np.ndarray: + @property + def poisson_matrix_cracked(self) -> ArrayLike: + """Return the cracked Poisson matrix.""" + if self._poisson_matrix_cracked is None: + self._poisson_matrix_cracked = np.eye(2) + return self._poisson_matrix_cracked + + def get_effective_principal_strains(self, eps_p: ArrayLike) -> np.ndarray: """Compute the effective principal strains to include the influence of Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete Shells' by M. A. Polak and F. J. Vecchio (1993). """ - return self.poisson_matrix(nu) @ eps_p + return self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ eps_p def check_cracked(self, eps_p: ArrayLike) -> bool: """Check if the concrete is cracked. Returns True if any principal @@ -129,13 +153,11 @@ def get_stress(self, strain: ArrayLike) -> np.ndarray: # Neglect shear strain related to the principal strain direction eps_p = eps_p[:2] - nu = 0.0 if self.check_cracked(eps_p) else self._nu - # Compressive-strength reduction factor due to lateral tension. beta = self.strength_reduction_lateral_cracking(eps_p) # Include the influence of Poisson's ratio on the principal strains. - eps_pf = self.get_effective_principal_strains(eps_p, nu) + eps_pf = self.get_effective_principal_strains(eps_p) # Compute the principal stresses from 1D parabola-rectangle law. sig_p = super().get_stress(eps_pf) @@ -150,14 +172,12 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: """Compute the 3x3 secant stiffness matrix C.""" T, eps_p = self.transform(strain) eps_p = eps_p[:2] - nu = 0.0 if self.check_cracked(eps_p) else self._nu # Compressive-strength reduction factor due to lateral tension. beta = self.strength_reduction_lateral_cracking(eps_p) # Poisson correction - P = self.poisson_matrix(nu) - eps_pf = self.get_effective_principal_strains(eps_p, nu) + eps_pf = self.get_effective_principal_strains(eps_p) sig_p = super().get_stress(eps_pf) @@ -175,22 +195,28 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: # Initial 2x2 secant D = np.diag([E_11, E_22]) - C = P @ D + C = self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ D # Ensure symmetry C = 0.5 * (C + C.T) # Shear modulus - G_12 = np.array([[(E_11 + E_22) / (4 * (1 + nu))]]) # Shape (1, 1) + G_12 = np.array( + [ + [ + (E_11 + E_22) + / (4 * (1 + self.nu(self.check_cracked(eps_p=eps_p)))) + ] + ] + ) # Shape (1, 1) Z_21 = np.zeros((2, 1)) # Shape (2, 1) - Z_12 = np.zeros((1, 2)) # Shape (1, 2) # 3x3 secant stiffness matrix Cp = np.block( [ [C, Z_21], - [Z_12, G_12], + [Z_21.T, G_12], ] ) From 72eeabea5d800d0617f857634e168a9cb6bf7492 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:13:38 +0200 Subject: [PATCH 24/31] Split the transform method in two simpler functions (#275) --- .../_parabolarectangle_2d.py | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 862e47ba..1bf0d734 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -69,22 +69,6 @@ def c_2(self) -> float: """ return self._c_2 - def transform(self, strain: ArrayLike) -> np.ndarray: - """Transform the strain vector to principal directions.""" - eps = np.atleast_1d(strain) - # Use arctan2 to obtain the angle in the correct quadrant - theta = 0.5 * np.arctan2(eps[2], eps[0] - eps[1]) - c = np.cos(theta) - s = np.sin(theta) - T = np.array( - [ - [c * c, s * s, s * c], - [s * s, c * c, -s * c], - [-2 * s * c, 2 * s * c, c * c - s * s], - ] - ) - return T, T @ eps - def poisson_matrix(self, cracked: bool) -> np.ndarray: """Return the Poisson matrix.""" if cracked: @@ -148,10 +132,11 @@ def get_stress(self, strain: ArrayLike) -> np.ndarray: """Return a 2D stress vector [sigma_x, sigma_y, tau_xy] given a 2D strain vector [eps_x, epx_y, gamma_xy]. """ - T, eps_p = self.transform(strain) + # Calculate principal strains and principal strain direction + eps_p, phi = calculate_principal_strains(strain=strain) - # Neglect shear strain related to the principal strain direction - eps_p = eps_p[:2] + # Establish strain transformation matrix + T = establish_strain_transformation_matrix(phi=phi) # Compressive-strength reduction factor due to lateral tension. beta = self.strength_reduction_lateral_cracking(eps_p) @@ -170,8 +155,11 @@ def get_stress(self, strain: ArrayLike) -> np.ndarray: def get_secant(self, strain: ArrayLike) -> np.ndarray: """Compute the 3x3 secant stiffness matrix C.""" - T, eps_p = self.transform(strain) - eps_p = eps_p[:2] + # Calculate principal strains and principal strain direction + eps_p, phi = calculate_principal_strains(strain=strain) + + # Establish strain transformation matrix + T = establish_strain_transformation_matrix(phi=phi) # Compressive-strength reduction factor due to lateral tension. beta = self.strength_reduction_lateral_cracking(eps_p) @@ -221,3 +209,35 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: ) return T.T @ Cp @ T + + +def calculate_principal_strains( + strain: ArrayLike, +) -> t.Tuple[ArrayLike, float]: + """Calculate the principal strains in 2D.""" + eps = np.atleast_1d(strain) + # Use arctan2 to obtain the angle in the correct quadrant + phi = 0.5 * np.arctan2(eps[2], eps[0] - eps[1]) + eps_p = np.array( + [ + (eps[0] + eps[1]) / 2 + + (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, + (eps[0] + eps[1]) / 2 + - (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, + ] + ) + + return eps_p, phi + + +def establish_strain_transformation_matrix(phi: float) -> ArrayLike: + """Establish the strain transformation matrix for a given angle.""" + c = np.cos(phi) + s = np.sin(phi) + return np.array( + [ + [c * c, s * s, s * c], + [s * s, c * c, -s * c], + [-2 * s * c, 2 * s * c, c * c - s * s], + ] + ) From f4941efbc533724a3a2a7f9291b9b2f8362caeec Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:50:50 +0200 Subject: [PATCH 25/31] feat: add area and transf matrix as internal attrs and props of shell reinforcement (#278) --- structuralcodes/geometry/_shell_geometry.py | 35 ++++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 975e76e0..648170e0 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -3,6 +3,7 @@ import typing as t import numpy as np +from numpy.typing import ArrayLike from ..core.base import Material from ._geometry import Geometry @@ -17,6 +18,8 @@ class ShellReinforcement(Geometry): _diameter_bar: float _material: Material _phi: float + _area: t.Optional[float] + _T: t.Optional[ArrayLike] def __init__( self, @@ -43,6 +46,8 @@ def __init__( self._diameter_bar = diameter_bar self._material = material self._phi = phi + self._area = None + self._T = None @property def z(self) -> float: @@ -77,14 +82,28 @@ def phi(self) -> float: @property def T(self) -> np.ndarray: """Return the transformation matrix for the reinforcement.""" - c, s = np.cos(self.phi), np.sin(self.phi) - return np.array( - [ - [c * c, s * s, c * s], - [s * s, c * c, -c * s], - [-2 * c * s, 2 * c * s, c * c - s * s], - ] - ) + if self._T is None: + c, s = np.cos(self.phi), np.sin(self.phi) + self._T = np.array( + [ + [c * c, s * s, c * s], + [s * s, c * c, -c * s], + [-2 * c * s, 2 * c * s, c * c - s * s], + ] + ) + return self._T + + @property + def area(self) -> float: + """Return the reinforcement area per unit width.""" + if self._area is None: + self._area = ( + self.n_bars + * np.pi + * (self.diameter_bar / 2) ** 2 + / self.cc_bars + ) + return self._area def _repr_svg_(self) -> str: """Returns the svg representation.""" From 5f35d12251933615e177a0c76666a20e748deef7 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:58:00 +0200 Subject: [PATCH 26/31] feat: improve shell reinf integration (#279) * Add stress and modulus transformation as props of shell reinf * Avoid matrix multiplication during iterations --- structuralcodes/geometry/_shell_geometry.py | 24 +++++++++++++++++++ .../section_integrators/_shell_integrator.py | 5 ++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/structuralcodes/geometry/_shell_geometry.py b/structuralcodes/geometry/_shell_geometry.py index 648170e0..b4ae44ce 100644 --- a/structuralcodes/geometry/_shell_geometry.py +++ b/structuralcodes/geometry/_shell_geometry.py @@ -20,6 +20,8 @@ class ShellReinforcement(Geometry): _phi: float _area: t.Optional[float] _T: t.Optional[ArrayLike] + _local_stress_to_global_force: t.Optional[ArrayLike] + _local_modulus_to_global_stiffness: t.Optional[ArrayLike] def __init__( self, @@ -48,6 +50,8 @@ def __init__( self._phi = phi self._area = None self._T = None + self._local_stress_to_global_force = None + self._local_modulus_to_global_stiffness = None @property def z(self) -> float: @@ -93,6 +97,26 @@ def T(self) -> np.ndarray: ) return self._T + @property + def local_stress_to_global_force(self) -> np.ndarray: + """Return the array to multiply with a local stress to obtain + contribution the global stress resultant. + """ + if self._local_stress_to_global_force is None: + self._local_stress_to_global_force = self.area * self.T.T[:, 0] + return self._local_stress_to_global_force + + @property + def local_modulus_to_global_stiffness(self) -> np.ndarray: + """Return the matrix to multiply with the local modulus to obtain the + contribution to the global stiffness. + """ + if self._local_modulus_to_global_stiffness is None: + self._local_modulus_to_global_stiffness = ( + self.T.T @ np.diag([1, 0, 0]) @ self.T * self.area + ) + return self._local_modulus_to_global_stiffness + @property def area(self) -> float: """Return the reinforcement area per unit width.""" diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index 5c0aeb99..fe42dbfa 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -79,7 +79,6 @@ def prepare_input( for r in geo.reinforcement: z_r = r.z - As = r.n_bars * np.pi * (r.diameter_bar / 2) ** 2 / r.cc_bars material = r.material fiber_strain = strain[:3] + z_r * strain[3:] @@ -87,10 +86,10 @@ def prepare_input( if integrate == 'stress': sig_sj = material.constitutive_law.get_stress(eps_sj[0]) - integrand = As * r.T.T @ np.array([sig_sj, 0, 0]) + integrand = r.local_stress_to_global_force * sig_sj elif integrate == 'modulus': mod = material.constitutive_law.get_secant(eps_sj[0]) - integrand = r.T.T @ np.diag([mod, 0, 0]) @ r.T * As + integrand = r.local_modulus_to_global_stiffness * mod else: raise ValueError(f'Unknown integrate type: {integrate}') From c7b453a873e81d4d59cca7d6be1b8e03b7920198 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:10:15 +0200 Subject: [PATCH 27/31] feat: simplify get_secant of parabolarectangle2d (#280) --- .../_parabolarectangle_2d.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py index 1bf0d734..cea671de 100644 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py @@ -168,34 +168,25 @@ def get_secant(self, strain: ArrayLike) -> np.ndarray: eps_pf = self.get_effective_principal_strains(eps_p) sig_p = super().get_stress(eps_pf) + sig_p[sig_p < 0] *= beta - # Avoid division by zero - tol = 1e-12 - if abs(eps_pf[0]) > tol: - E_11 = (sig_p[0] * (beta if sig_p[0] < 0 else 1.0)) / eps_pf[0] - else: - E_11 = self._fc * self._n / self._eps_0 - - if abs(eps_pf[1]) > tol: - E_22 = (sig_p[1] * (beta if sig_p[1] < 0 else 1.0)) / eps_pf[1] - else: - E_22 = self._fc * self._n / self._eps_0 + # Establish the diagonal of material stiffness matrix + # First set the diagonal terms equal to the initial stiffness + # Then calculate the secant stiffness for components with nonzero + # strains + D = self._fc * self._n / self._eps_0 * np.ones_like(sig_p) + nonzero_strains = np.abs(eps_pf) > 0 + D[nonzero_strains] = sig_p[nonzero_strains] / eps_pf[nonzero_strains] - # Initial 2x2 secant - D = np.diag([E_11, E_22]) - C = self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ D + # Correct for the poisson effect + C = self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ np.diag(D) # Ensure symmetry C = 0.5 * (C + C.T) # Shear modulus G_12 = np.array( - [ - [ - (E_11 + E_22) - / (4 * (1 + self.nu(self.check_cracked(eps_p=eps_p)))) - ] - ] + [[D.mean() / (2 * (1 + self.nu(self.check_cracked(eps_p=eps_p))))]] ) # Shape (1, 1) Z_21 = np.zeros((2, 1)) # Shape (2, 1) From 22be1885f68b5ff756bd7d38fc907c0152c6a019 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:17:16 +0200 Subject: [PATCH 28/31] feat: change convergence criterion for shell section calculator (#281) * Change to an energy based convergence criterion for the shell section * Update tests --- structuralcodes/sections/_shell_section.py | 19 +- .../test_materials/test_constitutive_laws.py | 22 +- tests/test_sections/test_shell_section.py | 378 +++++++++++++----- 3 files changed, 308 insertions(+), 111 deletions(-) diff --git a/structuralcodes/sections/_shell_section.py b/structuralcodes/sections/_shell_section.py index 06c7e252..a0a112a0 100644 --- a/structuralcodes/sections/_shell_section.py +++ b/structuralcodes/sections/_shell_section.py @@ -188,23 +188,22 @@ def calculate_strain_profile( # LU factorization lu, piv = lu_factor(stiffness) + first_norm = None + # Do Newton loops while True: # Check if number of iterations exceeds the maximum if num_iter > max_iter: raise StopIteration('Maximum number of iterations reached.') + # Calculate response and residuals + response = np.array(self.integrate_strain_profile(strain=strain)) + residual = loads - response if initial: # If the initial stiffness is used, we follow a regular # Newton-Raphson scheme where we calculate the strain increment # from the residual and the initial tangent stiffness matrix - # Calculate response and residuals - response = np.array( - self.integrate_strain_profile(strain=strain) - ) - residual = loads - response - # Solve using the decomposed matrix delta_strain = lu_solve((lu, piv), residual) @@ -232,7 +231,13 @@ def calculate_strain_profile( num_iter += 1 # Check for convergence: - if np.linalg.norm(delta_strain) < tol and num_iter > 1: + if first_norm is None: + first_norm = np.dot(residual, delta_strain) + current_norm = first_norm + else: + current_norm = np.dot(residual, delta_strain) + + if abs(current_norm / first_norm) < tol and num_iter > 1: break return strain.tolist() diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 8ee5d8cf..0a8fcc93 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -326,18 +326,34 @@ def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): assert np.allclose(mat.get_stress(strain), stress, atol=1e-3) -def test_get_secant_shape(): +@pytest.mark.parametrize('nu', (0.0, 0.2)) +@pytest.mark.parametrize( + 'strain', + ( + (0.0, 0.0, 0.0), + (-1.5e-3, 0.0, 0.0), + (-1.5e-3, -1.5e-3, 0.0), + (0.0, -1.5e-3, 0.0), + (-1.5e-3, 0.0, 0.75e-3), + (-1.5e-3, -1.5e-3, 0.75e-3), + (0.0, -1.5e-3, 0.75e-3), + ), +) +def test_get_secant_shape(strain, nu): """Test the secant stiffness matrix shape of the parabola-rectangle 2D material. """ mat = ParabolaRectangle2D( - fc=45.0, eps_0=-0.002, eps_u=-0.0035, n=2.0, nu=0.2 + fc=45.0, eps_0=-0.002, eps_u=-0.0035, n=2.0, nu=nu ) - C = mat.get_secant([0.0, 0.0, 0.0]) + C = mat.get_secant(strain) + # Shape assert C.shape == (3, 3) + # Symmetry assert np.allclose(C, C.T) + # Positive diagonal elements assert np.all(np.diag(C) > 0) diff --git a/tests/test_sections/test_shell_section.py b/tests/test_sections/test_shell_section.py index aa7b51e6..992beeea 100644 --- a/tests/test_sections/test_shell_section.py +++ b/tests/test_sections/test_shell_section.py @@ -1,8 +1,14 @@ """Tests for the Shell Section.""" +import math + import numpy as np import pytest +from structuralcodes.geometry import ( + RectangularGeometry, + add_reinforcement_line, +) from structuralcodes.geometry._shell_geometry import ( ShellGeometry, ShellReinforcement, @@ -12,10 +18,11 @@ from structuralcodes.materials.constitutive_laws import ( Elastic2D, ElasticPlastic, + ParabolaRectangle, ParabolaRectangle2D, ) from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 -from structuralcodes.sections import ShellSection +from structuralcodes.sections import GenericSection, ShellSection # Membrane and bending-strain parameter ranges eps_x = np.linspace(0.0, 1.0e-3, 2) @@ -26,23 +33,31 @@ chi_xy = np.linspace(0.0, 1.0e-6, 2) -@pytest.mark.parametrize('eps_x', eps_x) -@pytest.mark.parametrize('eps_y', eps_y) -@pytest.mark.parametrize('eps_xy', eps_xy) -@pytest.mark.parametrize('chi_x', chi_x) -@pytest.mark.parametrize('chi_y', chi_y) @pytest.mark.parametrize('chi_xy', chi_xy) +@pytest.mark.parametrize('chi_y', chi_y) +@pytest.mark.parametrize('chi_x', chi_x) +@pytest.mark.parametrize('eps_xy', eps_xy) +@pytest.mark.parametrize('eps_y', eps_y) +@pytest.mark.parametrize('eps_x', eps_x) @pytest.mark.parametrize( 'Ec, nu, thickness', [(30000, 0.20, 200), (30000, 0.20, 600)], ) def test_integrate_strain_profile( - Ec, nu, thickness, eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy + Ec, + nu, + thickness, + eps_x, + eps_y, + eps_xy, + chi_x, + chi_y, + chi_xy, ): """Elastic plate: strains → stress-resultants.""" constitutive_law = Elastic2D(Ec, nu) material = GenericMaterial(density=2500, constitutive_law=constitutive_law) - shell = ShellSection(ShellGeometry(thickness, material)) + shell = ShellSection(ShellGeometry(thickness, material), mesh_size=1e-3) # Numerically integrated forces/moments R = shell.section_calculator.integrate_strain_profile( @@ -50,25 +65,31 @@ def test_integrate_strain_profile( ) # Analytical forces/moments - A = Ec * thickness / (1 - nu**2) - B = Ec * thickness**3 / 12 / (1 - nu**2) - Nx = A * (eps_x + nu * eps_y) - Ny = A * (eps_y + nu * eps_x) - Nxy = Ec * thickness / (2 * (1 + nu)) * eps_xy - Mx = B * (chi_x + nu * chi_y) - My = B * (chi_y + nu * chi_x) - Mxy = Ec * thickness**3 / 12 / (2 * (1 + nu)) * chi_xy + A_membrane = Ec * thickness / (1 - nu**2) # Coefficient in Hooke's law + A_bending = Ec * thickness**3 / 12 / (1 - nu**2) # Plate bending stiffness + A_membrane_shear = A_membrane * (1 - nu) / 2 + A_bending_shear = A_bending * (1 - nu) / 2 + + Nx = A_membrane * (eps_x + nu * eps_y) + Ny = A_membrane * (eps_y + nu * eps_x) + Nxy = A_membrane_shear * eps_xy + Mx = A_bending * (chi_x + nu * chi_y) + My = A_bending * (chi_y + nu * chi_x) + Mxy = A_bending_shear * chi_xy expected = np.array([Nx, Ny, Nxy, Mx, My, Mxy]) - assert np.allclose(R, expected, rtol=1e-2, atol=1e-2) + assert np.allclose(R, expected) -def test_integrate_strain_profile_tangent(E=30000, nu=0.20, t=200): - """Elastic plate: tangent stiffness matrix.""" +@pytest.mark.parametrize('nu', ((0, 0.2))) +def test_integrate_strain_profile_stiffness_matrix(nu): + """Elastic plate: stiffness matrix.""" + E = 30000 + t = 200 constitutive_law = Elastic2D(E, nu) material = GenericMaterial(density=2500, constitutive_law=constitutive_law) - shell = ShellSection(ShellGeometry(t, material=material)) + shell = ShellSection(ShellGeometry(t, material=material), mesh_size=1e-3) K = shell.section_calculator.integrate_strain_profile( np.array([1e-3] * 3 + [1e-6] * 3), integrate='modulus' ) @@ -91,7 +112,7 @@ def test_integrate_strain_profile_tangent(E=30000, nu=0.20, t=200): ] ) - assert np.allclose(K, A, rtol=1e-2) + assert np.allclose(K, A) def test_wrong_integrator(): @@ -114,18 +135,18 @@ def test_wrong_integrator(): mxy = np.linspace(-1e8, 1e8, 2) -@pytest.mark.parametrize('nx', nx) -@pytest.mark.parametrize('ny', ny) -@pytest.mark.parametrize('nxy', nxy) -@pytest.mark.parametrize('mx', mx) -@pytest.mark.parametrize('my', my) @pytest.mark.parametrize('mxy', mxy) +@pytest.mark.parametrize('my', my) +@pytest.mark.parametrize('mx', mx) +@pytest.mark.parametrize('nxy', nxy) +@pytest.mark.parametrize('ny', ny) +@pytest.mark.parametrize('nx', nx) @pytest.mark.parametrize('Ec, nu, t', [(30000, 0.20, 200), (30000, 0.20, 600)]) def test_elastic_strain_profile(Ec, nu, t, nx, ny, nxy, mx, my, mxy): """Loads → strains for an isotropic plate.""" constitutive_law = Elastic2D(Ec, nu) material = GenericMaterial(density=2500, constitutive_law=constitutive_law) - shell = ShellSection(ShellGeometry(t, material=material)) + shell = ShellSection(ShellGeometry(t, material=material), mesh_size=1e-3) eps = shell.section_calculator.calculate_strain_profile( nx, ny, nxy, mx, my, mxy ) @@ -142,7 +163,7 @@ def test_elastic_strain_profile(Ec, nu, t, nx, ny, nxy, mx, my, mxy): chi_xy = 2 * (1 + nu) * mxy / D expected = np.array([eps_x, eps_y, eps_xy, chi_x, chi_y, chi_xy]) - assert np.allclose(eps, expected, rtol=1e-4, atol=1e-3) + assert np.allclose(eps, expected) def test_default_equals_explicit_mesh_size(): @@ -174,59 +195,27 @@ def test_invalid_mesh_size_raises(invalid): ) -def test_parabola_section(): - """ParabolaRectangle2D with mesh_size 0.5.""" - parabola_rectangle = ParabolaRectangle2D(35, nu=0) - elastic_plastic = ElasticPlastic(200000, 500) - concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) - reinforcement = ReinforcementEC2_2004( - fyk=500, - Es=200000, - ftk=500, - epsuk=3e-2, - constitutive_law=elastic_plastic, - ) - Asx1 = ShellReinforcement(-157, 1, 300, 16, reinforcement, 0) - - geo = ShellGeometry(400, concrete) - geo.add_reinforcement([Asx1]) - - section = ShellSection(geo, mesh_size=0.1) - calculator = section.section_calculator - strain = calculator.calculate_strain_profile(-1000, 0, 0, 0, 0, 0) - - assert np.allclose( - strain, - np.array([-7.20105642e-05, 0, 0, 1.07581081e-08, 0, 0]), - rtol=1e-6, - atol=1e-7, - ) - - @pytest.mark.parametrize( - 'nx,nxy,expected,tol', + 'nx,nxy,expected', [ ( 0, - 1000, - np.array([3.046432e-05, 3.079979e-05, 1.914840e-04, 0, 0, 0]), - 1e-4, + 500, + np.array([1.43163e-3, 1.919842e-3, 3.466593e-3, 0, 0, 0]), ), ( - -1000, - 1000, - np.array([-5.285263e-05, 2.741669e-05, 1.649706e-04, 0, 0, 0]), - 0.001, + -500, + 500, + np.array([0.618194e-3, 1.476430e-3, 2.065709e-3, 0, 0, 0]), ), ( - 1000, - 1000, - np.array([1.330415e-04, 2.785210e-05, 2.186547e-04, 0, 0, 0]), - 0.001, + 500, + 500, + np.array([2.446863e-3, 2.283832e-3, 4.894237e-3, 0, 0, 0]), ), ], ) -def test_parabola_cracked(nx, nxy, expected, tol): +def test_parabola_zero_initial_nu(nx, nxy, expected): """Test parabola rectangle with nu = 0.""" parabola_rectangle = ParabolaRectangle2D(45, nu=0) elastic_plastic = ElasticPlastic(200000, 500) @@ -244,44 +233,36 @@ def test_parabola_cracked(nx, nxy, expected, tol): Asy1 = ShellReinforcement(-118, 1, 200, 12, reinforcement, np.pi / 2) Asy2 = ShellReinforcement(118, 1, 200, 12, reinforcement, np.pi / 2) geo.add_reinforcement([Asx1, Asx2, Asy1, Asy2]) - section = ShellSection(geo) + section = ShellSection(geo, mesh_size=0.5) calculator = section.section_calculator strain = calculator.calculate_strain_profile( - nx, 0, nxy, 0, 0, 0, initial=True, tol=tol + nx, 0, nxy, 0, 0, 0, max_iter=150 ) - assert np.allclose( - strain, - expected, - rtol=1e-6, - atol=1e-7, - ) + assert np.allclose(strain, expected) @pytest.mark.parametrize( - 'nx,nxy,expected,tol', + 'nx,nxy,expected', [ ( 0, - 1000, - np.array([2.923437e-05, 2.961959e-05, 2.150748e-04, 0, 0, 0]), - 1e-4, + 500, + np.array([1.431675e-3, 1.919788e-3, 3.466592e-3, 0, 0, 0]), ), ( - -1000, - 1000, - np.array([-5.332211e-05, 3.874431e-05, 1.897691e-04, 0, 0, 0]), - 0.001, + -500, + 500, + np.array([0.618204e-3, 1.476423e-3, 2.065719e-3, 0, 0, 0]), ), ( - 1000, - 1000, - np.array([1.302065e-04, 1.461867e-05, 2.411031e-04, 0, 0, 0]), - 0.001, + 500, + 500, + np.array([2.446899e-3, 2.283762e-3, 4.894200e-3, 0, 0, 0]), ), ], ) -def test_parabola_uncracked(nx, nxy, expected, tol): +def test_parabola_initial_nu(nx, nxy, expected): """Test parabola rectangle with nu = 0.2.""" parabola_rectangle = ParabolaRectangle2D(45) elastic_plastic = ElasticPlastic(200000, 500) @@ -299,18 +280,18 @@ def test_parabola_uncracked(nx, nxy, expected, tol): Asy1 = ShellReinforcement(-118, 1, 200, 12, reinforcement, np.pi / 2) Asy2 = ShellReinforcement(118, 1, 200, 12, reinforcement, np.pi / 2) geo.add_reinforcement([Asx1, Asx2, Asy1, Asy2]) - section = ShellSection(geo) - calculator = section.section_calculator - strain = calculator.calculate_strain_profile( - nx, 0, nxy, 0, 0, 0, initial=True, tol=tol + section = ShellSection(geo, mesh_size=0.5) + strain = section.section_calculator.calculate_strain_profile( + nx, + 0, + nxy, + 0, + 0, + 0, + max_iter=200, ) - assert np.allclose( - strain, - expected, - rtol=1e-6, - atol=1e-7, - ) + assert np.allclose(strain, expected) def test_exceed_max_iterations(): @@ -344,3 +325,198 @@ def test_exceed_max_iterations(): 0, 0, ) + + +@pytest.mark.parametrize( + 'axial_force, with_reinforcement', + ( + (-1e6, False), + (-1e6, True), + (1e5, True), + ), +) +def test_compare_uniaxial_with_generic_section_reinforcement( # noqa: PLR0915 + axial_force: float, with_reinforcement: bool +): + """Compare the uniaxial response of the shell section with the generic + section, with reinforcement. + """ + # Arrange + # Geometry + width = 1000 + height = 350 + cover = 35 + diameter = 16 + spacing = 200 + + # Material parameters + fc = 45 # Concrete compressive strength + nu = 0.0 # Poisson's ratio + fyk = 500 # Steel yield strength + Es = 200000 # Steel modulus of elasticity + + # Create a GenericSection + reinforcement = ReinforcementEC2_2004( + fyk=fyk, + Es=Es, + epsuk=3e-2, + ftk=fyk, + constitutive_law='elasticperfectlyplastic', + ) + parabola_rectangle = ParabolaRectangle(fc=fc) + concrete_for_generic = ConcreteEC2_2004( + fck=fc, constitutive_law=parabola_rectangle + ) + generic_geo = RectangularGeometry( + height=height, width=width, material=concrete_for_generic + ) + + if with_reinforcement: + # Bottom reinforcement + generic_geo = add_reinforcement_line( + generic_geo, + ( + -width / 2 + cover + diameter / 2, + -height / 2 + cover + diameter / 2, + ), + ( + width / 2 - cover - diameter / 2, + -height / 2 + cover + diameter / 2, + ), + diameter=diameter, + material=reinforcement, + s=spacing, + ) + + # Top reinforcement + generic_geo = add_reinforcement_line( + generic_geo, + ( + -width / 2 + cover + diameter / 2, + height / 2 - cover - diameter / 2, + ), + ( + width / 2 - cover - diameter / 2, + height / 2 - cover - diameter / 2, + ), + diameter=diameter, + material=reinforcement, + s=spacing, + ) + + generic_sec = GenericSection(geometry=generic_geo, integrator='fiber') + + # Create a ShellSection + parabola_rectangle_2d = ParabolaRectangle2D(fc=fc, nu=nu) + concrete_for_shell = ConcreteEC2_2004( + fck=fc, constitutive_law=parabola_rectangle_2d + ) + shell_geo = ShellGeometry(material=concrete_for_shell, thickness=height) + + if with_reinforcement: + # Create reinforcement for the shell section + z_btm = -height / 2 + cover + diameter / 2 # -132 + z_top = height / 2 - cover - diameter / 2 # 132 + + # Bottom reinforcement + shell_rein_btm = ShellReinforcement( + z_btm, 1, spacing, diameter, reinforcement, 0 + ) + + # Top reinforcement + shell_rein_top = ShellReinforcement( + z_top, 1, spacing, diameter, reinforcement, 0 + ) + + # Add reinforcement to the shell geometry + shell_geo.add_reinforcement([shell_rein_btm, shell_rein_top]) + + shell_sec = ShellSection(geometry=shell_geo, mesh_size=0.5) + + # Calculate "exact" solution + if axial_force > 0: + exact_longitudinal_strain = axial_force / ( + generic_sec.gross_properties.area_reinforcement * Es + ) + else: + exact_longitudinal_strain = 0 + num_iter = 0 + area = width * height + while True: + num_iter += 1 + if num_iter >= 40: + break + exact_longitudinal_strain = axial_force / ( + area + * concrete_for_generic.constitutive_law.get_secant( + exact_longitudinal_strain + ) + + generic_sec.gross_properties.area_reinforcement * Es + ) + + # Act + # Calculate strains + generic_strain = generic_sec.section_calculator.calculate_strain_profile( + n=axial_force, my=0.0, mz=0.0 + ) + shell_strain = shell_sec.section_calculator.calculate_strain_profile( + nx=axial_force / width, + ny=0.0, + nxy=0.0, + mx=0.0, + my=0.0, + mxy=0.0, + ) + + # Compare with exact solution + # Note that the tolerance in isclose is set rather loose because the + # iterative solver of the shell section converges slower than the generic + # section. + assert math.isclose( + shell_strain[0], + exact_longitudinal_strain, + rel_tol=1e-4, + ) + + # Compare GenericSection and ShellSection + assert math.isclose( + generic_strain[0], + shell_strain[0], + rel_tol=1e-4, + ) + + +@pytest.mark.parametrize('nu', [0.0, 0.2]) +@pytest.mark.parametrize( + 'strain', + [ + (-2e-3, 0.0, 0.0), + (-2e-3, -2e-3, 0.0), + (0.0, 0.0, 0.75e-3), + (-2e-3, 0.0, 0.75e-3), + (-2e-3, -2e-3, 0.75e-3), + ], +) +def test_compare_constitutive_law_and_section(strain, nu): + """Compare the stress calculated with a constitutive law with the stress + resultant from the section. + """ + # Arrange + fck = 45 + thickness = 450 + constitutive_law = ParabolaRectangle2D(fc=fck, nu=nu) + concrete = ConcreteEC2_2004(fck=fck, constitutive_law=constitutive_law) + shell_geometry = ShellGeometry(thickness=thickness, material=concrete) + shell_section = ShellSection(geometry=shell_geometry) + + # Act + stress = constitutive_law.get_stress(strain=strain) + stress_resultant = ( + shell_section.section_calculator.integrate_strain_profile( + strain=[*strain, 0.0, 0.0, 0.0], + integrate='stress', + ) + ) + + # Assert + assert np.allclose(stress * thickness, stress_resultant[:3]) From 0610a0dddd1110822f9dd53f718103c6502b0156 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:32:46 +0200 Subject: [PATCH 29/31] Create constitutive law for concrete smeared cracking (#287) --- .../materials/constitutive_laws/__init__.py | 17 +- .../_concrete_smeared_cracking/__init__.py | 25 ++ .../_concrete_smeared_cracking/_core.py | 186 ++++++++++++++ .../_cracking_criterion.py | 24 ++ .../_poisson_reduction.py | 79 ++++++ .../_strength_reduction_lateral_cracking.py | 59 +++++ .../_concrete_smeared_cracking/_utils.py | 40 +++ .../_parabolarectangle_2d.py | 234 ------------------ .../test_materials/test_constitutive_laws.py | 43 +++- tests/test_sections/test_shell_section.py | 69 +++++- 10 files changed, 515 insertions(+), 261 deletions(-) create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/__init__.py create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_cracking_criterion.py create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_poisson_reduction.py create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_strength_reduction_lateral_cracking.py create mode 100644 structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_utils.py delete mode 100644 structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py diff --git a/structuralcodes/materials/constitutive_laws/__init__.py b/structuralcodes/materials/constitutive_laws/__init__.py index 244e8926..4b018984 100644 --- a/structuralcodes/materials/constitutive_laws/__init__.py +++ b/structuralcodes/materials/constitutive_laws/__init__.py @@ -4,12 +4,19 @@ from ...core.base import ConstitutiveLaw, Material from ._bilinearcompression import BilinearCompression +from ._concrete_smeared_cracking import ( + ConcreteSmearedCracking, + ConstantPoissonReduction, + GeneralVecchioCollins, + NoTension, + calculate_principal_strains, + establish_strain_transformation_matrix, +) from ._elastic import Elastic from ._elastic_2d import Elastic2D from ._elasticplastic import ElasticPlastic from ._initial_strain import InitialStrain from ._parabolarectangle import ParabolaRectangle -from ._parabolarectangle_2d import ParabolaRectangle2D from ._popovics import Popovics from ._sargin import Sargin from ._userdefined import UserDefined @@ -19,7 +26,6 @@ 'Elastic2D', 'ElasticPlastic', 'ParabolaRectangle', - 'ParabolaRectangle2D', 'BilinearCompression', 'Popovics', 'Sargin', @@ -27,6 +33,12 @@ 'InitialStrain', 'get_constitutive_laws_list', 'create_constitutive_law', + 'ConcreteSmearedCracking', + 'NoTension', + 'ConstantPoissonReduction', + 'GeneralVecchioCollins', + 'calculate_principal_strains', + 'establish_strain_transformation_matrix', ] CONSTITUTIVE_LAWS: t.Dict[str, ConstitutiveLaw] = { @@ -36,7 +48,6 @@ 'elasticperfectlyplastic': ElasticPlastic, 'bilinearcompression': BilinearCompression, 'parabolarectangle': ParabolaRectangle, - 'parabolarectangle2d': ParabolaRectangle2D, 'popovics': Popovics, 'sargin': Sargin, 'initialstrain': InitialStrain, diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/__init__.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/__init__.py new file mode 100644 index 00000000..92a3adf0 --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/__init__.py @@ -0,0 +1,25 @@ +"""Classes for concrete smeared cracking.""" + +from ._core import ConcreteSmearedCracking +from ._cracking_criterion import CrackingCriterion, NoTension +from ._poisson_reduction import ConstantPoissonReduction, PoissonReduction +from ._strength_reduction_lateral_cracking import ( + GeneralVecchioCollins, + StrengthReductionLateralCracking, +) +from ._utils import ( + calculate_principal_strains, + establish_strain_transformation_matrix, +) + +__all__ = [ + 'ConcreteSmearedCracking', + 'CrackingCriterion', + 'NoTension', + 'PoissonReduction', + 'ConstantPoissonReduction', + 'StrengthReductionLateralCracking', + 'GeneralVecchioCollins', + 'calculate_principal_strains', + 'establish_strain_transformation_matrix', +] diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py new file mode 100644 index 00000000..f2534754 --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py @@ -0,0 +1,186 @@ +"""The core class for representing concrete smeared cracking.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +import typing as t + +import numpy as np +from numpy.typing import ArrayLike + +from structuralcodes.core.base import ConstitutiveLaw + +from ._cracking_criterion import CrackingCriterion, NoTension +from ._poisson_reduction import PoissonReduction +from ._strength_reduction_lateral_cracking import ( + StrengthReductionLateralCracking, +) +from ._utils import ( + calculate_principal_strains, + establish_strain_transformation_matrix, +) + + +class ConcreteSmearedCracking: + """A plane stress model representing smeared cracking in reinforced + concrete. + """ + + _initial_modulus_compression: t.Optional[float] = None + _initial_modulus_tension: t.Optional[float] = None + _uniaxial_compression: ConstitutiveLaw + _strength_reduction_lateral_cracking: StrengthReductionLateralCracking + _poisson_reduction: PoissonReduction + _cracking_criterion: CrackingCriterion + + def __init__( + self, + uniaxial_compression: ConstitutiveLaw, + strength_reduction_lateral_cracking: StrengthReductionLateralCracking, + poisson_reduction: PoissonReduction, + cracking_criterion: t.Optional[CrackingCriterion] = None, + ): + """Initialize the model.""" + self._uniaxial_compression = uniaxial_compression + self._strength_reduction_lateral_cracking = ( + strength_reduction_lateral_cracking + ) + self._poisson_reduction = poisson_reduction + self._cracking_criterion = cracking_criterion or NoTension() + + def get_stress( + self, eps: ArrayLike, return_global: bool = True + ) -> ArrayLike: + """Calculate the stresses based on strains. + + Arguments: + eps (ArrayLike): The strains epsilon_x, epsilon_y and gamma_xy. + + """ + # Calculate principal strains and principal strain direction + eps_p, phi = calculate_principal_strains(strain=eps) + + # Establish strain transformation matrix + T = establish_strain_transformation_matrix(phi=phi) + + # Compressive-strength reduction factor due to lateral tension. + beta = self.strength_reduction_lateral_cracking.reduction(eps_p) + + # Include the influence of Poisson's ratio on the principal strains. + eps_pf = self.calculate_effective_principal_strains(eps_p) + + # Compute the principal stresses from the uniaxial compression law. + sig_p = self.uniaxial_compression.get_stress(eps_pf) + + # Apply the compressive-strength reduction factor + sig_p[sig_p < 0] *= beta + + if not return_global: + return np.array([*sig_p, 0]) + + # Transform back to global coords + return T.T @ np.array([*sig_p, 0]) + + def get_secant( + self, eps: ArrayLike, return_global: bool = True + ) -> np.ndarray: + # Calculate principal strains and principal strain direction + eps_p, phi = calculate_principal_strains(strain=eps) + + # Establish strain transformation matrix + T = establish_strain_transformation_matrix(phi=phi) + + # Compressive-strength reduction factor due to lateral tension. + beta = self.strength_reduction_lateral_cracking.reduction(eps_p) + + # Poisson correction + eps_pf = self.calculate_effective_principal_strains(eps_p) + + # Establish the diagonal of material stiffness matrix + D = self.uniaxial_compression.get_secant(eps_pf) + + # Scale down compressive stiffness with strength reduction factor + D[eps_pf < 0] *= beta + + # Correct for the poisson effect + C = self.poisson_reduction.poisson_matrix( + self.cracking_criterion.cracked(eps_p=eps_p) + ) @ np.diag(D) + + # Ensure symmetry + C = 0.5 * (C + C.T) + + # Shear modulus + G_value = D.mean() / ( + 2 + * ( + 1 + + self.poisson_reduction.nu( + self.cracking_criterion.cracked(eps_p=eps_p) + ) + ) + ) + G_value = max(G_value, 1e-4 * self.initial_modulus_compression) + G_12 = np.array([[G_value]]) # Shape (1, 1) + + Z_21 = np.zeros((2, 1)) # Shape (2, 1) + + # 3x3 secant stiffness matrix + Cp = np.block( + [ + [C, Z_21], + [Z_21.T, G_12], + ] + ) + if not return_global: + return Cp + + return T.T @ Cp @ T + + def get_tangent(): + pass + + def calculate_effective_principal_strains( + self, eps_p: ArrayLike + ) -> ArrayLike: + """Compute the effective principal strains to include the influence of + Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete + Shells' by M. A. Polak and F. J. Vecchio (1993). + """ + return ( + self.poisson_reduction.poisson_matrix( + cracked=self.cracking_criterion.cracked(eps_p=eps_p) + ) + @ eps_p + ) + + @property + def uniaxial_compression(self) -> ConstitutiveLaw: + """Return the constitutive law for uniaxial compression.""" + return self._uniaxial_compression + + @property + def strength_reduction_lateral_cracking( + self, + ) -> StrengthReductionLateralCracking: + """Return the model for strength reduction due to lateral cracking.""" + return self._strength_reduction_lateral_cracking + + @property + def poisson_reduction(self) -> PoissonReduction: + """Return the model for reduction of Poisson ratio due to cracking.""" + return self._poisson_reduction + + @property + def cracking_criterion(self) -> CrackingCriterion: + """Return the cracking criterion.""" + return self._cracking_criterion + + @property + def initial_modulus_compression(self) -> float: + """Return the initial modulus in uniaxial compression.""" + if self._initial_modulus_compression is None: + self._initial_modulus_compression = ( + self.uniaxial_compression.get_tangent(0.0) + ) + + return self._initial_modulus_compression diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_cracking_criterion.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_cracking_criterion.py new file mode 100644 index 00000000..567cdd09 --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_cracking_criterion.py @@ -0,0 +1,24 @@ +"""Cracking criteria for concrete smeared cracking.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +from abc import ABC, abstractmethod + +from numpy.typing import ArrayLike + + +class CrackingCriterion(ABC): + """An abstract based class for modelling a cracking criterion.""" + + @abstractmethod + def cracked(self, eps_p: ArrayLike) -> bool: + """Check if the material is cracked based on principal strains.""" + raise NotImplementedError + + +class NoTension(CrackingCriterion): + """Cracking criterion to represent a material with no tensile strength.""" + + def cracked(self, eps_p: ArrayLike) -> bool: + """Check if the material is cracked based on the principal strains.""" + return max(eps_p) > 0 diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_poisson_reduction.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_poisson_reduction.py new file mode 100644 index 00000000..1eec7e5b --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_poisson_reduction.py @@ -0,0 +1,79 @@ +"""Reduction of Poisson ratio in concrete smeared cracking.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +import typing as t +from abc import ABC, abstractmethod + +import numpy as np +from numpy.typing import ArrayLike + + +class PoissonReduction(ABC): + """An abstract base class for modelling reduction of the Poisson ratio due + to cracking. + """ + + @abstractmethod + def nu( + self, + cracked: bool, + eps: ArrayLike, + eps_p: ArrayLike, + eps_pf: ArrayLike, + ): + raise NotImplementedError + + @abstractmethod + def poisson_matrix( + self, + cracked: bool, + eps: ArrayLike, + eps_p: ArrayLike, + eps_pf: ArrayLike, + ): + raise NotImplementedError + + +class ConstantPoissonReduction(PoissonReduction): + """A material with a constant reduction in Poisson ratio due to + cracking. + """ + + _initial_nu: float + _cracked_nu: float + _poisson_matrix_cracked: t.Optional[np.ndarray] + _poisson_matrix_uncracked: t.Optional[np.ndarray] + + def __init__(self, initial_nu: float, cracked_nu: float = 0.0): + """Initialize the model.""" + self._initial_nu = initial_nu + self._cracked_nu = cracked_nu + self._poisson_matrix_cracked = None + self._poisson_matrix_uncracked = None + + def nu(self, cracked: bool, *args, **kwargs): + """Return the Poisson ratio.""" + del args, kwargs + return self._initial_nu if not cracked else self._cracked_nu + + def poisson_matrix(self, cracked: bool, *args, **kwargs) -> np.ndarray: + """Return the 2x2 Poisson matrix.""" + del args, kwargs + + # Return Poisson matrix + if cracked: + if self._poisson_matrix_cracked is None: + self._poisson_matrix_cracked = np.eye(2) + return self._poisson_matrix_cracked + + if self._poisson_matrix_uncracked is None: + self._poisson_matrix_uncracked = ( + 1 / (1 - self._initial_nu**2) + ) * np.array( + [ + [1, self._initial_nu], + [self._initial_nu, 1], + ] + ) + return self._poisson_matrix_uncracked diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_strength_reduction_lateral_cracking.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_strength_reduction_lateral_cracking.py new file mode 100644 index 00000000..4a47f69a --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_strength_reduction_lateral_cracking.py @@ -0,0 +1,59 @@ +"""Strength reduction due to lateral cracking for concrete smeared cracking.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +from abc import ABC, abstractmethod + +from numpy.typing import ArrayLike + + +class StrengthReductionLateralCracking(ABC): + """Base class for models for accounting for strength reduction due to + lateral cracking. + """ + + @abstractmethod + def reduction(self, strains: ArrayLike) -> float: + """Calculate the strength reduction based on a set of strains.""" + raise NotImplementedError + + +class GeneralVecchioCollins(StrengthReductionLateralCracking): + """General model for accounting for strength reduction due to lateral + cracking based on the work of Vecchio and Collins. + """ + + _c_1: float + _c_2: float + _minimum_value: float + + def __init__(self, c_1: float, c_2: float, minimum_value: float = 0.0): + """Initialize the model.""" + self._c_1 = c_1 + self._c_2 = c_2 + self._minimum_value = minimum_value + + @property + def c_1(self) -> float: + """Return the coefficient c_1.""" + return self._c_1 + + @property + def c_2(self) -> float: + """Return the coefficient c_2.""" + return self._c_2 + + @property + def minimum_value(self) -> float: + """Return the minimum value.""" + return self._minimum_value + + def reduction(self, strains: ArrayLike) -> float: + """Calculate the strength reduction based on a set of principal + strains. + + beta = 1 / (c_1 + c_2 * eps_1) + """ + beta = 1 / (self.c_1 + self.c_2 * max(strains)) + + return max(min(beta, 1), self.minimum_value) diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_utils.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_utils.py new file mode 100644 index 00000000..e059f9fe --- /dev/null +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_utils.py @@ -0,0 +1,40 @@ +"""Utility functions for concrete smeared cracking.""" + +from __future__ import annotations # To have clean hints of ArrayLike in docs + +import typing as t + +import numpy as np +from numpy.typing import ArrayLike + + +def calculate_principal_strains( + strain: ArrayLike, +) -> t.Tuple[ArrayLike, float]: + """Calculate the principal strains in 2D.""" + eps = np.atleast_1d(strain) + # Use arctan2 to obtain the angle in the correct quadrant + phi = 0.5 * np.arctan2(eps[2], eps[0] - eps[1]) + eps_p = np.array( + [ + (eps[0] + eps[1]) / 2 + + (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, + (eps[0] + eps[1]) / 2 + - (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, + ] + ) + + return eps_p, phi + + +def establish_strain_transformation_matrix(phi: float) -> ArrayLike: + """Establish the strain transformation matrix for a given angle.""" + c = np.cos(phi) + s = np.sin(phi) + return np.array( + [ + [c * c, s * s, s * c], + [s * s, c * c, -s * c], + [-2 * s * c, 2 * s * c, c * c - s * s], + ] + ) diff --git a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py b/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py deleted file mode 100644 index cea671de..00000000 --- a/structuralcodes/materials/constitutive_laws/_parabolarectangle_2d.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Parabola-Rectangle 2D constitutive law.""" - -from __future__ import annotations # To have clean hints of ArrayLike in docs - -import typing as t - -import numpy as np -from numpy.typing import ArrayLike - -from ._parabolarectangle import ParabolaRectangle - - -class ParabolaRectangle2D(ParabolaRectangle): - """Class for parabola rectangle constitutive law in 2D. - - The stresses and strains are assumed negative in compression and positive - in tension. - """ - - __materials__: t.Tuple[str] = ('concrete',) - _poisson_matrix_cracked: t.Optional[ArrayLike] = None - _poisson_matrix_uncracked: t.Optional[ArrayLike] = None - - def __init__( - self, - fc: float, - eps_0: float = -0.002, - eps_u: float = -0.0035, - n: float = 2.0, - nu: float = 0.2, - c_1: float = 0.8, - c_2: float = 100, - name: t.Optional[str] = None, - ) -> None: - """Initialize a Parabola-Rectangle 2D Material. - - Arguments: - fc (float): The strength of concrete in compression. - - Keyword Arguments: - eps_0 (float): Peak strain of concrete in compression. Default - value = -0.002. - eps_u (float): Ultimate strain of concrete in compression. Default - value = -0.0035. - n (float): Exponent for the pre-peak branch. Default value = 2. - name (str): A name for the constitutive law. - nu (float): Poisson's ratio. Default value = 0.2. - c_1 (float): First coefficient for the compressive-strength - reduction factor due to lateral tension. Default value = 0.8. - c_2 (float): Second coefficient for the compressive-strength - reduction factor due to lateral tension. Default value = 100. - """ - super().__init__(fc=fc, eps_0=eps_0, eps_u=eps_u, n=n, name=name) - self._nu = nu - self._c_1 = c_1 - self._c_2 = c_2 - - @property - def c_1(self) -> float: - """Return the first coefficient for the compressive-strength reduction - factor due to lateral tension. Default value = 0.8. - """ - return self._c_1 - - @property - def c_2(self) -> float: - """Return the second coefficient for the compressive-strength reduction - factor due to lateral tension. Default value = 100. - """ - return self._c_2 - - def poisson_matrix(self, cracked: bool) -> np.ndarray: - """Return the Poisson matrix.""" - if cracked: - return self.poisson_matrix_cracked - return self.poisson_matrix_uncracked - - def nu(self, cracked: bool) -> float: - """Return the Poisson ratio.""" - if cracked: - return 0.0 - return self._nu - - @property - def poisson_matrix_uncracked(self) -> ArrayLike: - """Return the uncracked Poisson matrix.""" - if self._poisson_matrix_uncracked is None: - self._poisson_matrix_uncracked = ( - 1 / (1 - self._nu**2) - ) * np.array( - [ - [1, self._nu], - [self._nu, 1], - ] - ) - return self._poisson_matrix_uncracked - - @property - def poisson_matrix_cracked(self) -> ArrayLike: - """Return the cracked Poisson matrix.""" - if self._poisson_matrix_cracked is None: - self._poisson_matrix_cracked = np.eye(2) - return self._poisson_matrix_cracked - - def get_effective_principal_strains(self, eps_p: ArrayLike) -> np.ndarray: - """Compute the effective principal strains to include the influence of - Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete - Shells' by M. A. Polak and F. J. Vecchio (1993). - """ - return self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ eps_p - - def check_cracked(self, eps_p: ArrayLike) -> bool: - """Check if the concrete is cracked. Returns True if any principal - strain (eps_p1, eps_p2) is greater than zero. - """ - return np.any(eps_p > 0) - - def strength_reduction_lateral_cracking(self, eps_p: ArrayLike) -> float: - """Return the compressive-strength reduction factor due to lateral - tension. This relation comes from Vecchio & Collins (1986), "The - Modified Compression-Field Theory for Reinforced Concrete Elements - Subjected to Shear. - """ - if not self.check_cracked(eps_p): - return 1 - - beta = 1 / (self.c_1 + self.c_2 * max(eps_p)) - - return min(beta, 1) - - def get_stress(self, strain: ArrayLike) -> np.ndarray: - """Return a 2D stress vector [sigma_x, sigma_y, tau_xy] - given a 2D strain vector [eps_x, epx_y, gamma_xy]. - """ - # Calculate principal strains and principal strain direction - eps_p, phi = calculate_principal_strains(strain=strain) - - # Establish strain transformation matrix - T = establish_strain_transformation_matrix(phi=phi) - - # Compressive-strength reduction factor due to lateral tension. - beta = self.strength_reduction_lateral_cracking(eps_p) - - # Include the influence of Poisson's ratio on the principal strains. - eps_pf = self.get_effective_principal_strains(eps_p) - - # Compute the principal stresses from 1D parabola-rectangle law. - sig_p = super().get_stress(eps_pf) - - # Apply the compressive-strength reduction factor - sig_p[sig_p < 0] *= beta - - # Transform back to global coords - return T.T @ np.array([*sig_p, 0]) - - def get_secant(self, strain: ArrayLike) -> np.ndarray: - """Compute the 3x3 secant stiffness matrix C.""" - # Calculate principal strains and principal strain direction - eps_p, phi = calculate_principal_strains(strain=strain) - - # Establish strain transformation matrix - T = establish_strain_transformation_matrix(phi=phi) - - # Compressive-strength reduction factor due to lateral tension. - beta = self.strength_reduction_lateral_cracking(eps_p) - - # Poisson correction - eps_pf = self.get_effective_principal_strains(eps_p) - - sig_p = super().get_stress(eps_pf) - sig_p[sig_p < 0] *= beta - - # Establish the diagonal of material stiffness matrix - # First set the diagonal terms equal to the initial stiffness - # Then calculate the secant stiffness for components with nonzero - # strains - D = self._fc * self._n / self._eps_0 * np.ones_like(sig_p) - nonzero_strains = np.abs(eps_pf) > 0 - D[nonzero_strains] = sig_p[nonzero_strains] / eps_pf[nonzero_strains] - - # Correct for the poisson effect - C = self.poisson_matrix(self.check_cracked(eps_p=eps_p)) @ np.diag(D) - - # Ensure symmetry - C = 0.5 * (C + C.T) - - # Shear modulus - G_12 = np.array( - [[D.mean() / (2 * (1 + self.nu(self.check_cracked(eps_p=eps_p))))]] - ) # Shape (1, 1) - - Z_21 = np.zeros((2, 1)) # Shape (2, 1) - - # 3x3 secant stiffness matrix - Cp = np.block( - [ - [C, Z_21], - [Z_21.T, G_12], - ] - ) - - return T.T @ Cp @ T - - -def calculate_principal_strains( - strain: ArrayLike, -) -> t.Tuple[ArrayLike, float]: - """Calculate the principal strains in 2D.""" - eps = np.atleast_1d(strain) - # Use arctan2 to obtain the angle in the correct quadrant - phi = 0.5 * np.arctan2(eps[2], eps[0] - eps[1]) - eps_p = np.array( - [ - (eps[0] + eps[1]) / 2 - + (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, - (eps[0] + eps[1]) / 2 - - (((eps[0] - eps[1]) / 2) ** 2 + 0.25 * eps[2] ** 2) ** 0.5, - ] - ) - - return eps_p, phi - - -def establish_strain_transformation_matrix(phi: float) -> ArrayLike: - """Establish the strain transformation matrix for a given angle.""" - c = np.cos(phi) - s = np.sin(phi) - return np.array( - [ - [c * c, s * s, s * c], - [s * s, c * c, -s * c], - [-2 * s * c, 2 * s * c, c * c - s * s], - ] - ) diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 52f4e672..02ac8e80 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -8,12 +8,14 @@ from structuralcodes.materials.constitutive_laws import ( BilinearCompression, + ConcreteSmearedCracking, + ConstantPoissonReduction, Elastic, Elastic2D, ElasticPlastic, + GeneralVecchioCollins, InitialStrain, ParabolaRectangle, - ParabolaRectangle2D, Popovics, Sargin, UserDefined, @@ -320,9 +322,16 @@ def test_parabola_rectangle_floats(fc, eps_0, eps_u, strain, stress, tangent): ), ], ) -def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): - """Test the parabola-rectangle 2D material.""" - mat = ParabolaRectangle2D(fc, eps_0, eps_u) +def test_concrete_smeared_cracking(fc, eps_0, eps_u, strain, stress): + """Test the concrete smeared cracking material.""" + uniaxial_compression = ParabolaRectangle(fc=fc, eps_0=eps_0, eps_u=eps_u) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=0.2) + mat = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) assert np.allclose(mat.get_stress(strain), stress, atol=1e-3) @@ -340,11 +349,16 @@ def test_parabola_rectangle_2d(fc, eps_0, eps_u, strain, stress): ), ) def test_get_secant_shape(strain, nu): - """Test the secant stiffness matrix shape of the parabola-rectangle - 2D material. + """Test the secant stiffness matrix shape of the concrete smeared cracking + material. """ - mat = ParabolaRectangle2D( - fc=45.0, eps_0=-0.002, eps_u=-0.0035, n=2.0, nu=nu + uniaxial_compression = ParabolaRectangle(fc=45.0) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=nu) + mat = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, ) C = mat.get_secant(strain) @@ -392,10 +406,17 @@ def test_get_secant_shape(strain, nu): ], ) def test_get_secant(fc, eps_0, eps_u, nu, strain, expected): - """Test the secant stiffness matrix of the parabola-rectangle - 2D material. + """Test the secant stiffness matrix of the concrete smeared cracking + material. """ - mat = ParabolaRectangle2D(fc, eps_0, eps_u, nu=nu) + uniaxial_compression = ParabolaRectangle(fc=fc, eps_0=eps_0, eps_u=eps_u) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=nu) + mat = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) assert np.allclose(mat.get_secant(strain), expected) diff --git a/tests/test_sections/test_shell_section.py b/tests/test_sections/test_shell_section.py index 992beeea..89ecdfae 100644 --- a/tests/test_sections/test_shell_section.py +++ b/tests/test_sections/test_shell_section.py @@ -16,10 +16,12 @@ from structuralcodes.materials.basic import GenericMaterial from structuralcodes.materials.concrete import ConcreteEC2_2004 from structuralcodes.materials.constitutive_laws import ( + ConcreteSmearedCracking, + ConstantPoissonReduction, Elastic2D, ElasticPlastic, + GeneralVecchioCollins, ParabolaRectangle, - ParabolaRectangle2D, ) from structuralcodes.materials.reinforcement import ReinforcementEC2_2004 from structuralcodes.sections import GenericSection, ShellSection @@ -217,9 +219,18 @@ def test_invalid_mesh_size_raises(invalid): ) def test_parabola_zero_initial_nu(nx, nxy, expected): """Test parabola rectangle with nu = 0.""" - parabola_rectangle = ParabolaRectangle2D(45, nu=0) + uniaxial_compression = ParabolaRectangle(fc=45) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=0) + parabola_rectangle = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) elastic_plastic = ElasticPlastic(200000, 500) - concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + concrete = GenericMaterial( + density=2500, constitutive_law=parabola_rectangle + ) reinforcement = ReinforcementEC2_2004( fyk=500, Es=200000, @@ -264,9 +275,18 @@ def test_parabola_zero_initial_nu(nx, nxy, expected): ) def test_parabola_initial_nu(nx, nxy, expected): """Test parabola rectangle with nu = 0.2.""" - parabola_rectangle = ParabolaRectangle2D(45) + uniaxial_compression = ParabolaRectangle(fc=45) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=0.2) + parabola_rectangle = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) elastic_plastic = ElasticPlastic(200000, 500) - concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + concrete = GenericMaterial( + density=2500, constitutive_law=parabola_rectangle + ) reinforcement = ReinforcementEC2_2004( fyk=500, Es=200000, @@ -296,9 +316,18 @@ def test_parabola_initial_nu(nx, nxy, expected): def test_exceed_max_iterations(): """Test that the maximum number of iterations is exceeded.""" - parabola_rectangle = ParabolaRectangle2D(45) + uniaxial_compression = ParabolaRectangle(fc=45) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=0) + parabola_rectangle = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) elastic_plastic = ElasticPlastic(200000, 500) - concrete = ConcreteEC2_2004(fck=45, constitutive_law=parabola_rectangle) + concrete = GenericMaterial( + density=2500, constitutive_law=parabola_rectangle + ) reinforcement = ReinforcementEC2_2004( fyk=500, Es=200000, @@ -407,9 +436,16 @@ def test_compare_uniaxial_with_generic_section_reinforcement( # noqa: PLR0915 generic_sec = GenericSection(geometry=generic_geo, integrator='fiber') # Create a ShellSection - parabola_rectangle_2d = ParabolaRectangle2D(fc=fc, nu=nu) - concrete_for_shell = ConcreteEC2_2004( - fck=fc, constitutive_law=parabola_rectangle_2d + uniaxial_compression = ParabolaRectangle(fc=fc) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=nu) + smeared_cracking = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) + concrete_for_shell = GenericMaterial( + density=2500, constitutive_law=smeared_cracking ) shell_geo = ShellGeometry(material=concrete_for_shell, thickness=height) @@ -504,13 +540,20 @@ def test_compare_constitutive_law_and_section(strain, nu): # Arrange fck = 45 thickness = 450 - constitutive_law = ParabolaRectangle2D(fc=fck, nu=nu) - concrete = ConcreteEC2_2004(fck=fck, constitutive_law=constitutive_law) + uniaxial_compression = ParabolaRectangle(fc=fck) + strength_reduction = GeneralVecchioCollins(c_1=0.8, c_2=100) + poisson_reduction = ConstantPoissonReduction(initial_nu=nu) + constitutive_law = ConcreteSmearedCracking( + uniaxial_compression=uniaxial_compression, + strength_reduction_lateral_cracking=strength_reduction, + poisson_reduction=poisson_reduction, + ) + concrete = GenericMaterial(density=2500, constitutive_law=constitutive_law) shell_geometry = ShellGeometry(thickness=thickness, material=concrete) shell_section = ShellSection(geometry=shell_geometry) # Act - stress = constitutive_law.get_stress(strain=strain) + stress = constitutive_law.get_stress(eps=strain) stress_resultant = ( shell_section.section_calculator.integrate_strain_profile( strain=[*strain, 0.0, 0.0, 0.0], From cff31449b3032c6e3504350bc382a961c0cf2f03 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:12:26 +0100 Subject: [PATCH 30/31] Check cracking based on effective principal strains (#304) --- .../_concrete_smeared_cracking/_core.py | 41 +++++++++---------- .../test_materials/test_constitutive_laws.py | 2 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py index f2534754..104bc820 100644 --- a/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py +++ b/structuralcodes/materials/constitutive_laws/_concrete_smeared_cracking/_core.py @@ -62,12 +62,12 @@ def get_stress( # Establish strain transformation matrix T = establish_strain_transformation_matrix(phi=phi) - # Compressive-strength reduction factor due to lateral tension. - beta = self.strength_reduction_lateral_cracking.reduction(eps_p) - # Include the influence of Poisson's ratio on the principal strains. eps_pf = self.calculate_effective_principal_strains(eps_p) + # Compressive-strength reduction factor due to lateral tension. + beta = self.strength_reduction_lateral_cracking.reduction(eps_pf) + # Compute the principal stresses from the uniaxial compression law. sig_p = self.uniaxial_compression.get_stress(eps_pf) @@ -89,35 +89,28 @@ def get_secant( # Establish strain transformation matrix T = establish_strain_transformation_matrix(phi=phi) - # Compressive-strength reduction factor due to lateral tension. - beta = self.strength_reduction_lateral_cracking.reduction(eps_p) - # Poisson correction eps_pf = self.calculate_effective_principal_strains(eps_p) + cracked = self.cracking_criterion.cracked(eps_p=eps_pf) # Establish the diagonal of material stiffness matrix D = self.uniaxial_compression.get_secant(eps_pf) + # Compressive-strength reduction factor due to lateral tension. + beta = self.strength_reduction_lateral_cracking.reduction(eps_pf) + # Scale down compressive stiffness with strength reduction factor D[eps_pf < 0] *= beta # Correct for the poisson effect - C = self.poisson_reduction.poisson_matrix( - self.cracking_criterion.cracked(eps_p=eps_p) - ) @ np.diag(D) + C = self.poisson_reduction.poisson_matrix(cracked=cracked) @ np.diag(D) # Ensure symmetry C = 0.5 * (C + C.T) # Shear modulus G_value = D.mean() / ( - 2 - * ( - 1 - + self.poisson_reduction.nu( - self.cracking_criterion.cracked(eps_p=eps_p) - ) - ) + 2 * (1 + self.poisson_reduction.nu(cracked=cracked)) ) G_value = max(G_value, 1e-4 * self.initial_modulus_compression) G_12 = np.array([[G_value]]) # Shape (1, 1) @@ -146,13 +139,19 @@ def calculate_effective_principal_strains( Poisson's ratio. Taken from 'Nonlinear Analysis of Reinforced-Concrete Shells' by M. A. Polak and F. J. Vecchio (1993). """ - return ( - self.poisson_reduction.poisson_matrix( - cracked=self.cracking_criterion.cracked(eps_p=eps_p) - ) - @ eps_p + # Calculate the effective principal strains assuming uncracked + eps_pf_uncracked = ( + self.poisson_reduction.poisson_matrix(cracked=False) @ eps_p ) + # If the assumption of uncracked holds after calculating effective + # strains, return the effective uncracked principal strains + if not self.cracking_criterion.cracked(eps_p=eps_pf_uncracked): + return eps_pf_uncracked + + # Else, return the effective principal strains assuming cracked + return self.poisson_reduction.poisson_matrix(cracked=True) @ eps_p + @property def uniaxial_compression(self) -> ConstitutiveLaw: """Return the constitutive law for uniaxial compression.""" diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py index 02ac8e80..851a468d 100644 --- a/tests/test_materials/test_constitutive_laws.py +++ b/tests/test_materials/test_constitutive_laws.py @@ -290,7 +290,7 @@ def test_parabola_rectangle_floats(fc, eps_0, eps_u, strain, stress, tangent): -0.002, -0.0035, [-0.0015, 0.0, 0.001], - [-26.64581728, -2.44270432, 8.06770432], + [-27.26236979, -7.34049479, 6.640625], ), ( -45.0, From d1be013eb23b7cc60b6f13fc86d37eb460c93c13 Mon Sep 17 00:00:00 2001 From: Morten Engen <58786786+mortenengen@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:22:14 +0100 Subject: [PATCH 31/31] Correct the sign of calculated strain, stress resultant, and modulus (#310) --- .../section_integrators/_shell_integrator.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/structuralcodes/sections/section_integrators/_shell_integrator.py b/structuralcodes/sections/section_integrators/_shell_integrator.py index fe42dbfa..b619bc30 100644 --- a/structuralcodes/sections/section_integrators/_shell_integrator.py +++ b/structuralcodes/sections/section_integrators/_shell_integrator.py @@ -65,8 +65,11 @@ def prepare_input( prepared_input = [] IA = [] z_list = [] + + # A positive in-plane strain gives tension + # A positive curvature gives a negative strain for a positive z-value for z in z_coords: - fiber_strain = strain[:3] + z * strain[3:] + fiber_strain = strain[:3] - z * strain[3:] if integrate == 'stress': integrand = material.constitutive_law.get_stress(fiber_strain) elif integrate == 'modulus': @@ -81,7 +84,7 @@ def prepare_input( z_r = r.z material = r.material - fiber_strain = strain[:3] + z_r * strain[3:] + fiber_strain = strain[:3] - z_r * strain[3:] eps_sj = r.T @ fiber_strain if integrate == 'stress': @@ -118,9 +121,12 @@ def integrate_stress( Nx = np.sum(fiber_stress[:, 0]) Ny = np.sum(fiber_stress[:, 1]) Nxy = np.sum(fiber_stress[:, 2]) - Mx = np.sum(fiber_stress[:, 0] * z) - My = np.sum(fiber_stress[:, 1] * z) - Mxy = np.sum(fiber_stress[:, 2] * z) + + # A positive stress at a positive z-value gives a negative moment + # contribution + Mx = -np.sum(fiber_stress[:, 0] * z) + My = -np.sum(fiber_stress[:, 1] * z) + Mxy = -np.sum(fiber_stress[:, 2] * z) return Nx, Ny, Nxy, Mx, My, Mxy def integrate_modulus( @@ -142,7 +148,7 @@ def integrate_modulus( D = np.zeros((3, 3)) for C_layer, z_i in zip(MA, z): A += C_layer - B += z_i * C_layer + B -= z_i * C_layer D += z_i**2 * C_layer return np.block([[A, B], [B, D]])