structuralcodes 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of structuralcodes might be problematic. Click here for more details.

@@ -7,7 +7,8 @@ import typing as t
7
7
  import warnings
8
8
 
9
9
  import numpy as np
10
- from numpy.typing import ArrayLike
10
+ from numpy.typing import ArrayLike, NDArray
11
+ from scipy.linalg import lu_factor, lu_solve
11
12
  from shapely import MultiPolygon
12
13
  from shapely.ops import unary_union
13
14
 
@@ -20,7 +21,7 @@ from structuralcodes.geometry import (
20
21
  )
21
22
  from structuralcodes.materials.constitutive_laws import Elastic
22
23
 
23
- from .section_integrators import integrator_factory
24
+ from .section_integrators import SectionIntegrator, integrator_factory
24
25
 
25
26
 
26
27
  class GenericSection(Section):
@@ -82,7 +83,7 @@ class GenericSection(Section):
82
83
  self._gross_properties = None
83
84
 
84
85
  @property
85
- def gross_properties(self) -> s_res.GrossProperties:
86
+ def gross_properties(self) -> s_res.SectionProperties:
86
87
  """Return the gross properties of the section."""
87
88
  if self._gross_properties is None:
88
89
  self._gross_properties = (
@@ -94,6 +95,8 @@ class GenericSection(Section):
94
95
  class GenericSectionCalculator(SectionCalculator):
95
96
  """Calculator class implementing analysis algorithms for code checks."""
96
97
 
98
+ integrator: SectionIntegrator
99
+
97
100
  def __init__(
98
101
  self,
99
102
  sec: GenericSection,
@@ -123,17 +126,17 @@ class GenericSectionCalculator(SectionCalculator):
123
126
  self._n_max = None
124
127
  self._n_min = None
125
128
 
126
- def _calculate_gross_section_properties(self) -> s_res.GrossProperties:
129
+ def _calculate_gross_section_properties(self) -> s_res.SectionProperties:
127
130
  """Calculates the gross section properties of the GenericSection.
128
131
 
129
132
  This function is private and called when the section is created.
130
133
  It stores the result into the result object.
131
134
 
132
135
  Returns:
133
- GrossProperties: The gross properties of the section.
136
+ SectionProperties: The gross properties of the section.
134
137
  """
135
138
  # It will use the algorithms for generic sections
136
- gp = s_res.GrossProperties()
139
+ gp = s_res.SectionProperties()
137
140
 
138
141
  # Computation of perimeter using shapely
139
142
  polygon = unary_union(
@@ -335,7 +338,7 @@ class GenericSectionCalculator(SectionCalculator):
335
338
 
336
339
  def find_equilibrium_fixed_pivot(
337
340
  self, geom: CompoundGeometry, n: float, yielding: bool = False
338
- ) -> t.Tuple[float, float, float]:
341
+ ) -> t.List[float]:
339
342
  """Find the equilibrium changing curvature fixed a pivot.
340
343
  The algorithm uses bisection algorithm between curvature
341
344
  of balanced failure and 0. Selected the pivot point as
@@ -349,8 +352,8 @@ class GenericSectionCalculator(SectionCalculator):
349
352
  yielding (bool): ...
350
353
 
351
354
  Returns:
352
- Tuple(float, float, float): 3 floats: Axial strain at (0,0), and
353
- curvatures of y* and z* axes. Note that being uniaxial bending,
355
+ List(float): 3 floats: Axial strain at (0,0), and curvatures of y*
356
+ and z* axes. Note that being uniaxial bending,
354
357
  curvature along z* is 0.0.
355
358
  """
356
359
  # Number of maximum iteration for the bisection algorithm
@@ -369,6 +372,10 @@ class GenericSectionCalculator(SectionCalculator):
369
372
  ) = self.integrator.integrate_strain_response_on_geometry(
370
373
  geom, strain, tri=self.triangulated_data, mesh_size=self.mesh_size
371
374
  )
375
+ # save the triangulation data
376
+ if self.triangulated_data is None and tri is not None:
377
+ self.triangulated_data = tri
378
+
372
379
  # Check if there is equilibrium with this strain distribution
373
380
  chi_a = strain[1]
374
381
  dn_a = n_int - n
@@ -414,9 +421,6 @@ class GenericSectionCalculator(SectionCalculator):
414
421
  s = f'Last iteration reached a unbalance of {dn_c}'
415
422
  raise ValueError(f'Maximum number of iterations reached.\n{s}')
416
423
  # Found equilibrium
417
- # save the triangulation data
418
- if self.triangulated_data is None:
419
- self.triangulated_data = tri
420
424
  # Return the strain distribution
421
425
  return [eps_0, chi_c, 0]
422
426
 
@@ -496,7 +500,7 @@ class GenericSectionCalculator(SectionCalculator):
496
500
  ) = self.integrator.integrate_strain_response_on_geometry(
497
501
  geom, [eps_0, curv, 0], tri=self.triangulated_data
498
502
  )
499
- if self.triangulated_data is None:
503
+ if self.triangulated_data is None and tri is not None:
500
504
  self.triangulated_data = tri
501
505
  dn_a = n_int - n
502
506
  # It may occur that dn_a is already almost zero (in eqiulibrium)
@@ -557,7 +561,7 @@ class GenericSectionCalculator(SectionCalculator):
557
561
  self.section.geometry, [eps_p, 0, 0], tri=tri
558
562
  )
559
563
 
560
- if self.triangulated_data is None:
564
+ if self.triangulated_data is None and tri is not None:
561
565
  self.triangulated_data = tri
562
566
  return n_min, n_max
563
567
 
@@ -604,25 +608,60 @@ class GenericSectionCalculator(SectionCalculator):
604
608
  self.triangulated_data = rotated_triangulated_data
605
609
 
606
610
  def integrate_strain_profile(
607
- self, strain: ArrayLike
608
- ) -> t.Tuple[float, float, float]:
609
- """Integrate a strain profile returning internal forces.
611
+ self,
612
+ strain: ArrayLike,
613
+ integrate: t.Literal['stress', 'modulus'] = 'stress',
614
+ ) -> t.Union[t.Tuple[float, float, float], NDArray]:
615
+ """Integrate a strain profile returning stress resultants or tangent
616
+ section stiffness matrix.
610
617
 
611
618
  Arguments:
612
619
  strain (ArrayLike): Represents the deformation plane. The strain
613
620
  should have three entries representing respectively: axial
614
621
  strain (At 0,0 coordinates), curv_y, curv_z.
622
+ integrate (str): a string indicating the quantity to integrate over
623
+ the section. It can be 'stress' or 'modulus'. When 'stress'
624
+ is selected, the return value will be the stress resultants N,
625
+ My, Mz, while if 'modulus' is selected, the return will be the
626
+ tangent section stiffness matrix (default is 'stress').
615
627
 
616
628
  Returns:
617
- Tuple(float, float, float): N, My and Mz.
629
+ Union(Tuple(float, float, float),NDArray): N, My and Mz when
630
+ `integrate='stress'`, or a numpy array representing the stiffness
631
+ matrix then `integrate='modulus'`.
632
+
633
+ Examples:
634
+ result = self.integrate_strain_profile(strain,integrate='tangent')
635
+ # `result` will be the tangent stiffness matrix (a 3x3 numpy array)
636
+
637
+ result = self.integrate_strain_profile(strain)
638
+ # `result` will be a tuple containing section forces (N, My, Mz)
639
+
640
+ Raises:
641
+ ValueError: If a unkown value is passed to the `integrate`
642
+ parameter.
618
643
  """
619
- N, My, Mz, _ = self.integrator.integrate_strain_response_on_geometry(
644
+ result = self.integrator.integrate_strain_response_on_geometry(
620
645
  geo=self.section.geometry,
621
646
  strain=strain,
647
+ integrate=integrate,
622
648
  tri=self.triangulated_data,
623
649
  mesh_size=self.mesh_size,
624
650
  )
625
- return N, My, Mz
651
+
652
+ # Save triangulation data for future use
653
+ if self.triangulated_data is None and result[-1] is not None:
654
+ self.triangulated_data = result[-1]
655
+
656
+ # manage the returning from integrate_strain_response_on_geometry:
657
+ # this function returns one of the two:
658
+ # a. float, float, float, tri (i.e. N, My, Mz, tri)
659
+ # b. (NDArray, tri) (i.e. section stiff matrix, tri)
660
+ # We need to return only forces or stifness
661
+ # (without triangultion data)
662
+ if len(result) == 2:
663
+ return result[0]
664
+ return result[:-1]
626
665
 
627
666
  def calculate_bending_strength(
628
667
  self, theta=0, n=0
@@ -664,13 +703,15 @@ class GenericSectionCalculator(SectionCalculator):
664
703
  if self.triangulated_data is not None:
665
704
  # Rotate back also triangulated data!
666
705
  self._rotate_triangulated_data(theta)
706
+ # Rotate also curvature!
707
+ strain_rotated = T @ np.array([[strain[1]], [strain[2]]])
667
708
 
668
709
  # Create result object
669
710
  res = s_res.UltimateBendingMomentResults()
670
711
  res.theta = theta
671
712
  res.n = N
672
- res.chi_y = strain[1]
673
- res.chi_z = strain[2]
713
+ res.chi_y = strain_rotated[0, 0]
714
+ res.chi_z = strain_rotated[1, 0]
674
715
  res.eps_a = strain[0]
675
716
  res.m_y = M[0, 0]
676
717
  res.m_z = M[1, 0]
@@ -969,7 +1010,7 @@ class GenericSectionCalculator(SectionCalculator):
969
1010
  mesh_size=self.mesh_size,
970
1011
  )
971
1012
  )
972
- if self.triangulated_data is None:
1013
+ if self.triangulated_data is None and tri is not None:
973
1014
  self.triangulated_data = tri
974
1015
  forces[i, 0] = N
975
1016
  forces[i, 1] = My
@@ -1239,7 +1280,7 @@ class GenericSectionCalculator(SectionCalculator):
1239
1280
  tri=self.triangulated_data,
1240
1281
  )
1241
1282
  )
1242
- if self.triangulated_data is None:
1283
+ if self.triangulated_data is None and tri is not None:
1243
1284
  self.triangulated_data = tri
1244
1285
  forces[i, 0] = N
1245
1286
  forces[i, 1] = My
@@ -1285,3 +1326,99 @@ class GenericSectionCalculator(SectionCalculator):
1285
1326
  res.strains[i, 2] = res_bend_strength.chi_z
1286
1327
 
1287
1328
  return res
1329
+
1330
+ def calculate_strain_profile(
1331
+ self,
1332
+ n,
1333
+ my,
1334
+ mz,
1335
+ initial: bool = False,
1336
+ max_iter: int = 10,
1337
+ tol: float = 1e-6,
1338
+ ) -> t.List[float]:
1339
+ """Get the strain plane for a given axial force and biaxial bending.
1340
+
1341
+ Args:
1342
+ n (float): Axial load.
1343
+ my (float): Bending moment around y-axis.
1344
+ mz (float): Bending moment around z-axis.
1345
+ initial (bool): If True the modified newton with initial tanget is
1346
+ used (default = False).
1347
+ max_iter (int): the maximum number of iterations in the iterative
1348
+ process (default = 10).
1349
+ tol (float): the tolerance for convergence test in terms of strain
1350
+ increment.
1351
+
1352
+ Returns:
1353
+ List(float): 3 floats: Axial strain at (0,0), and curvatures of the
1354
+ section around y and z axes.
1355
+ """
1356
+ # Get the gometry
1357
+ geom = self.section.geometry
1358
+
1359
+ # Collect loads in a numpy array
1360
+ loads = np.array([n, my, mz])
1361
+
1362
+ # Compute initial tangent stiffness matrix
1363
+ stiffness_tangent, tri = (
1364
+ self.integrator.integrate_strain_response_on_geometry(
1365
+ geom,
1366
+ [0, 0, 0],
1367
+ integrate='modulus',
1368
+ tri=self.triangulated_data,
1369
+ )
1370
+ )
1371
+ # eventually save the triangulation data
1372
+ if self.triangulated_data is None and tri is not None:
1373
+ self.triangulated_data = tri
1374
+
1375
+ # Calculate strain plane with Newton Rhapson Iterative method
1376
+ num_iter = 0
1377
+ strain = np.zeros(3)
1378
+
1379
+ # Factorize once the stiffness matrix if using initial
1380
+ if initial:
1381
+ # LU factorization
1382
+ # (maybe also Choelesky could work if matrix always SPD?)
1383
+ lu, piv = lu_factor(stiffness_tangent)
1384
+
1385
+ # Do Newton loops
1386
+ while True:
1387
+ # Check if number of iterations exceeds the maximum
1388
+ if num_iter > max_iter:
1389
+ break
1390
+
1391
+ # Calculate response and residuals
1392
+ response = np.array(self.integrate_strain_profile(strain=strain))
1393
+ residual = loads - response
1394
+
1395
+ if initial:
1396
+ # Solve using the decomposed matrix
1397
+ delta_strain = lu_solve((lu, piv), residual)
1398
+ else:
1399
+ # Calculate the current tangent stiffness
1400
+ stiffness_tangent, _ = (
1401
+ self.integrator.integrate_strain_response_on_geometry(
1402
+ geom,
1403
+ strain,
1404
+ integrate='modulus',
1405
+ tri=self.triangulated_data,
1406
+ )
1407
+ )
1408
+
1409
+ # Solve using the current tangent stiffness
1410
+ delta_strain = np.linalg.solve(stiffness_tangent, residual)
1411
+
1412
+ # Update the strain
1413
+ strain += delta_strain
1414
+
1415
+ num_iter += 1
1416
+
1417
+ # Check for convergence:
1418
+ if np.linalg.norm(delta_strain) < tol:
1419
+ break
1420
+
1421
+ if num_iter >= max_iter:
1422
+ raise StopIteration('Maximum number of iterations reached.')
1423
+
1424
+ return strain.tolist()
@@ -0,0 +1,114 @@
1
+ """A collection of functions related to reinforced concrete sections."""
2
+
3
+ import typing as t
4
+
5
+ import structuralcodes.core._section_results as s_res
6
+ from structuralcodes.geometry import CompoundGeometry, SurfaceGeometry
7
+ from structuralcodes.materials.constitutive_laws import Elastic, UserDefined
8
+ from structuralcodes.sections import GenericSection
9
+ from structuralcodes.sections.section_integrators import FiberIntegrator
10
+
11
+
12
+ def calculate_elastic_cracked_properties(
13
+ section: GenericSection,
14
+ theta: float = 0,
15
+ return_cracked_section: bool = False,
16
+ ) -> t.Union[
17
+ s_res.SectionProperties, t.Tuple[s_res.SectionProperties, CompoundGeometry]
18
+ ]:
19
+ """Calculates the cracked section properties of a reinforced concrete
20
+ section. (GenericSection). Materials in surface geometries and point
21
+ geometries are elastic-linear in order to make the cracking properties
22
+ independent of the stress state. Tension in all surface geometries is
23
+ neglected.
24
+
25
+ Args:
26
+ section (GenericSection): The section to use as basis for the
27
+ calculation.
28
+ theta (float): Angle of the neutral axis to the horizontal in radians.
29
+ theta=0 implies upper compression block.
30
+ return_cracked_section (bool): If true, returns also the cracked
31
+ section.
32
+
33
+ Returns:
34
+ t.Union[SectionProperties, t.Tuple[SectionProperties, GenericSection]]:
35
+ SectionProperties data of a cracked section (i.e cracked section
36
+ properties) and (if return_cracked_section is True) the cracked
37
+ section.
38
+ """
39
+
40
+ def create_surface_geometries(polygons_list, material):
41
+ """Process shapely polygons to SurfaceGeometries."""
42
+ # Create an empty list to store SurfaceGeometry objects
43
+ surface_geometries = []
44
+
45
+ # Iterate over the list of polygons and create SurfaceGeometry for each
46
+ for polygon in polygons_list:
47
+ # Create a new SurfaceGeometry for the current polygon
48
+ surface_geometry = SurfaceGeometry(polygon, material)
49
+ # Add the new SurfaceGeometry to the list
50
+ surface_geometries.append(surface_geometry)
51
+
52
+ return CompoundGeometry(surface_geometries)
53
+
54
+ if not section.geometry.reinforced_concrete:
55
+ return None
56
+
57
+ rotated_geometry = section.geometry.rotate(-theta)
58
+
59
+ for geo in rotated_geometry.geometries:
60
+ Ec = geo.material.get_tangent(eps=0)
61
+ elastic_concrete = UserDefined([-100, 0], [-100 * Ec, 0])
62
+ geo._material = elastic_concrete
63
+
64
+ for pg in rotated_geometry.point_geometries:
65
+ Es = pg.material.get_tangent(eps=0)
66
+ elastic_steel = Elastic(Es, 'elastic steel')
67
+ pg._material = elastic_steel
68
+
69
+ curv = -1e-5 # Any curvature should return the same mechanical properties.
70
+ # Find the equilibrium with fixed curvature
71
+ eps = section.section_calculator.find_equilibrium_fixed_curvature(
72
+ rotated_geometry, 0, curv, 0
73
+ )[0]
74
+ z_na = -eps / curv # distance to neutral fibre
75
+
76
+ # Cutting concrete geometries and retaining the compressed block
77
+ cut_geom = None
78
+ for part in rotated_geometry.geometries:
79
+ upper_div, lower_div = part.split(((0, z_na), 0))
80
+ # Convert to SurfaceGeometry
81
+ subpart = create_surface_geometries(upper_div, part.material)
82
+ if cut_geom is None:
83
+ cut_geom = subpart
84
+ else:
85
+ cut_geom += subpart
86
+
87
+ # Add reinforcement geometries
88
+ for reinf in rotated_geometry.point_geometries:
89
+ cut_geom += reinf
90
+
91
+ # return geoemtry to original rotation
92
+ cracked_geom = cut_geom.rotate(theta)
93
+
94
+ # Define the cracked section
95
+ if isinstance(section.section_calculator.integrator, FiberIntegrator):
96
+ mesh_size = getattr(
97
+ section.section_calculator.integrator, 'mesh_size', 0.01
98
+ )
99
+ cracked_sec = GenericSection(
100
+ cracked_geom,
101
+ integrator='fiber',
102
+ mesh_size=mesh_size,
103
+ )
104
+ else:
105
+ cracked_sec = GenericSection(cracked_geom, integrator='marin')
106
+
107
+ cracked_prop = cracked_sec.gross_properties
108
+
109
+ if return_cracked_section:
110
+ result = (cracked_prop, cracked_geom)
111
+ else:
112
+ result = cracked_prop
113
+
114
+ return result