structuralcodes 0.0.1__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.

Files changed (50) hide show
  1. structuralcodes/__init__.py +17 -0
  2. structuralcodes/codes/__init__.py +79 -0
  3. structuralcodes/codes/ec2_2004/__init__.py +133 -0
  4. structuralcodes/codes/ec2_2004/_concrete_material_properties.py +239 -0
  5. structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +104 -0
  6. structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +941 -0
  7. structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +257 -0
  8. structuralcodes/codes/ec2_2004/shear.py +506 -0
  9. structuralcodes/codes/ec2_2023/__init__.py +104 -0
  10. structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +17 -0
  11. structuralcodes/codes/ec2_2023/_section5_materials.py +1160 -0
  12. structuralcodes/codes/ec2_2023/_section9_sls.py +325 -0
  13. structuralcodes/codes/mc2010/__init__.py +169 -0
  14. structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +704 -0
  15. structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +104 -0
  16. structuralcodes/codes/mc2010/_concrete_material_properties.py +463 -0
  17. structuralcodes/codes/mc2010/_concrete_punching.py +543 -0
  18. structuralcodes/codes/mc2010/_concrete_shear.py +749 -0
  19. structuralcodes/codes/mc2010/_concrete_torsion.py +164 -0
  20. structuralcodes/codes/mc2010/_reinforcement_material_properties.py +105 -0
  21. structuralcodes/core/__init__.py +1 -0
  22. structuralcodes/core/_section_results.py +211 -0
  23. structuralcodes/core/base.py +260 -0
  24. structuralcodes/geometry/__init__.py +25 -0
  25. structuralcodes/geometry/_geometry.py +875 -0
  26. structuralcodes/geometry/_steel_sections.py +2155 -0
  27. structuralcodes/materials/__init__.py +9 -0
  28. structuralcodes/materials/concrete/__init__.py +82 -0
  29. structuralcodes/materials/concrete/_concrete.py +114 -0
  30. structuralcodes/materials/concrete/_concreteEC2_2004.py +477 -0
  31. structuralcodes/materials/concrete/_concreteEC2_2023.py +435 -0
  32. structuralcodes/materials/concrete/_concreteMC2010.py +494 -0
  33. structuralcodes/materials/constitutive_laws.py +979 -0
  34. structuralcodes/materials/reinforcement/__init__.py +84 -0
  35. structuralcodes/materials/reinforcement/_reinforcement.py +172 -0
  36. structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +103 -0
  37. structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +93 -0
  38. structuralcodes/materials/reinforcement/_reinforcementMC2010.py +98 -0
  39. structuralcodes/sections/__init__.py +23 -0
  40. structuralcodes/sections/_generic.py +1249 -0
  41. structuralcodes/sections/_reinforcement.py +115 -0
  42. structuralcodes/sections/section_integrators/__init__.py +14 -0
  43. structuralcodes/sections/section_integrators/_factory.py +41 -0
  44. structuralcodes/sections/section_integrators/_fiber_integrator.py +238 -0
  45. structuralcodes/sections/section_integrators/_marin_integration.py +47 -0
  46. structuralcodes/sections/section_integrators/_marin_integrator.py +222 -0
  47. structuralcodes/sections/section_integrators/_section_integrator.py +49 -0
  48. structuralcodes-0.0.1.dist-info/METADATA +40 -0
  49. structuralcodes-0.0.1.dist-info/RECORD +50 -0
  50. structuralcodes-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,1249 @@
1
+ """Generic class section implemenetation."""
2
+
3
+ from __future__ import annotations # To have clean hints of ArrayLike in docs
4
+
5
+ import typing as t
6
+ import warnings
7
+ from math import cos, sin
8
+
9
+ import numpy as np
10
+ from numpy.typing import ArrayLike
11
+ from shapely import MultiPolygon
12
+ from shapely.ops import unary_union
13
+
14
+ import structuralcodes.core._section_results as s_res
15
+ from structuralcodes.core.base import Section, SectionCalculator
16
+ from structuralcodes.geometry import (
17
+ CompoundGeometry,
18
+ PointGeometry,
19
+ SurfaceGeometry,
20
+ )
21
+ from structuralcodes.materials.constitutive_laws import Elastic
22
+
23
+ from .section_integrators import integrator_factory
24
+
25
+
26
+ class GenericSection(Section):
27
+ """This is the implementation of the generic class section.
28
+
29
+ The section is a 2D geometry where Y axis is horizontal while Z axis is
30
+ vertical.
31
+
32
+ The moments and curvatures around Y and Z axes are assumed positive
33
+ according to RHR.
34
+
35
+ Attributes:
36
+ geometry (Union(SurfaceGeometry, CompoundGeometry)): The geometry of
37
+ the section.
38
+ name (str): The name of the section.
39
+ section_calculator (GenericSectionCalculator): The object responsible
40
+ for performing different calculations on the section (e.g. bending
41
+ strength, moment curvature, etc.).
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ geometry: t.Union[SurfaceGeometry, CompoundGeometry],
47
+ name: t.Optional[str] = None,
48
+ integrator: t.Literal['marin', 'fiber'] = 'marin',
49
+ **kwargs,
50
+ ) -> None:
51
+ """Initialize a GenericSection.
52
+
53
+ Arguments:
54
+ geometry (Union(SurfaceGeometry, CompoundGeometry)): The geometry
55
+ of the section.
56
+ name (str): The name of the section.
57
+ integrator (str): The name of the SectionIntegrator to use.
58
+ """
59
+ if name is None:
60
+ name = 'GenericSection'
61
+ super().__init__(name)
62
+ # Since only CompoundGeometry has the attribute geometries,
63
+ # if a SurfaceGeometry is input, we create a CompoundGeometry
64
+ # with only that geometry contained. After that all algorithms
65
+ # work as usal.
66
+ if isinstance(geometry, SurfaceGeometry):
67
+ geometry = CompoundGeometry([geometry])
68
+ self.geometry = geometry
69
+ self.section_calculator = GenericSectionCalculator(
70
+ sec=self, integrator=integrator, **kwargs
71
+ )
72
+ self._gross_properties = None
73
+
74
+ @property
75
+ def gross_properties(self) -> s_res.GrossProperties:
76
+ """Return the gross properties of the section."""
77
+ if self._gross_properties is None:
78
+ self._gross_properties = (
79
+ self.section_calculator._calculate_gross_section_properties()
80
+ )
81
+ return self._gross_properties
82
+
83
+
84
+ class GenericSectionCalculator(SectionCalculator):
85
+ """Calculator class implementing analysis algorithms for code checks."""
86
+
87
+ def __init__(
88
+ self,
89
+ sec: GenericSection,
90
+ integrator: t.Literal['marin', 'fiber'] = 'marin',
91
+ **kwargs,
92
+ ) -> None:
93
+ """Initialize the GenericSectionCalculator.
94
+
95
+ Arguments:
96
+ section (GenericSection): The section object.
97
+ integrator (str): The SectionIntegrator to be used for computations
98
+ (default = 'marin').
99
+
100
+ Note:
101
+ When using 'fiber' integrator the kwarg 'mesh_size' can be used to
102
+ specify a dimensionless number (between 0 and 1) specifying the
103
+ size of the resulting mesh.
104
+ """
105
+ super().__init__(section=sec)
106
+ # Select the integrator if specified
107
+ self.integrator = integrator_factory(integrator)()
108
+ # Mesh size used for Fibre integrator
109
+ self.mesh_size = kwargs.get('mesh_size', 0.01)
110
+ # triangulated_data used for Fibre integrator
111
+ self.triangulated_data = None
112
+ # Maximum and minimum axial load
113
+ self._n_max = None
114
+ self._n_min = None
115
+
116
+ def _calculate_gross_section_properties(self) -> s_res.GrossProperties:
117
+ """Calculates the gross section properties of the GenericSection.
118
+
119
+ This function is private and called when the section is created.
120
+ It stores the result into the result object.
121
+
122
+ Returns:
123
+ GrossProperties: The gross properties of the section.
124
+ """
125
+ # It will use the algorithms for generic sections
126
+ gp = s_res.GrossProperties()
127
+
128
+ # Computation of perimeter using shapely
129
+ polygon = unary_union(
130
+ [geo.polygon for geo in self.section.geometry.geometries]
131
+ )
132
+ if isinstance(polygon, MultiPolygon):
133
+ gp.perimeter = 0.0
134
+ warnings.warn(
135
+ 'Perimiter computation for a multi polygon is not defined.'
136
+ )
137
+
138
+ gp.perimeter = polygon.exterior.length
139
+
140
+ # Computation of area: this is taken directly from shapely
141
+ gp.area = self.section.geometry.area
142
+ # Computation of surface area, reinforcement area, EA (axial rigidity)
143
+ # and mass: Morten -> problem with units! how do we deal with it?
144
+ for geo in self.section.geometry.geometries:
145
+ gp.ea += geo.area * geo.material.get_tangent(eps=0)[0]
146
+ if geo.density is not None:
147
+ # this assumes area in mm2 and density in kg/m3
148
+ gp.mass += geo.area * geo.density * 1e-9
149
+
150
+ for geo in self.section.geometry.point_geometries:
151
+ gp.ea += geo.area * geo.material.get_tangent(eps=0)[0]
152
+ gp.area_reinforcement += geo.area
153
+ if geo.density is not None:
154
+ # this assumes area in mm2 and density in kg/m3
155
+ gp.mass += geo.area * geo.density * 1e-9
156
+
157
+ # Computation of area moments
158
+ #
159
+ # Implementation idea:
160
+ # Using integrator: we need to compute the following integrals:
161
+ # E Sy = integr(E*z*dA)
162
+ # E Sz = integr(E*y*dA)
163
+ # E Iyy = integr(E*z*z*dA)
164
+ # E Izz = integr(E*y*y*dA)
165
+ # E Iyz = integr(E*y*z*dA)
166
+ #
167
+ # The first can be imagined as computing axial force
168
+ # by integration of a E*z stress;
169
+ # since eps = eps_a + ky * z - kz * y
170
+ # E*z is the stress corresponding to an elastic material
171
+ # with stiffness E and strain equal to z (i.e. eps_a and kz = 0,
172
+ # ky = 1 )
173
+ #
174
+ # With the same idea we can integrate the other quantities.
175
+
176
+ def compute_area_moments(geometry, material=None):
177
+ # create a new dummy geometry from the original one
178
+ # with dummy material
179
+ geometry = geometry.from_geometry(
180
+ geo=geometry, new_material=material
181
+ )
182
+ # Integrate a dummy strain profile for getting first and
183
+ # second moment respect y axis and product moment
184
+ (
185
+ sy,
186
+ iyy,
187
+ iyz,
188
+ tri,
189
+ ) = self.integrator.integrate_strain_response_on_geometry(
190
+ geometry,
191
+ [0, 1, 0],
192
+ mesh_size=self.mesh_size,
193
+ )
194
+ # Change sign due to moment sign convention
195
+ iyz *= -1
196
+ # Integrate a dummy strain profile for getting first
197
+ # and second moment respect z axis and product moment
198
+ (
199
+ sz,
200
+ izy,
201
+ izz,
202
+ _,
203
+ ) = self.integrator.integrate_strain_response_on_geometry(
204
+ geometry,
205
+ [0, 0, -1],
206
+ tri=tri,
207
+ mesh_size=self.mesh_size,
208
+ )
209
+ # Change sign due to moment sign convention
210
+ izz *= -1
211
+ if abs(abs(izy) - abs(iyz)) > 10:
212
+ error_str = 'Something went wrong with computation of '
213
+ error_str += f'moments of area: iyz = {iyz}, izy = {izy}.\n'
214
+ error_str += 'They should be equal but are not!'
215
+ raise RuntimeError(error_str)
216
+
217
+ return sy, sz, iyy, izz, iyz
218
+
219
+ # Create a dummy material for integration of area moments
220
+ # This is used for J, S etc, not for E_J E_S etc
221
+ dummy_mat = Elastic(E=1)
222
+ # Computation of moments of area (material-independet)
223
+ # Note: this could be un-meaningfull when many materials
224
+ # are combined
225
+ gp.sy, gp.sz, gp.iyy, gp.izz, gp.iyz = compute_area_moments(
226
+ geometry=self.section.geometry, material=dummy_mat
227
+ )
228
+
229
+ # Computation of moments of area times E
230
+ gp.e_sy, gp.e_sz, gp.e_iyy, gp.e_izz, gp.e_iyz = compute_area_moments(
231
+ geometry=self.section.geometry
232
+ )
233
+
234
+ # Compute Centroid coordinates
235
+ gp.cy = gp.e_sz / gp.ea
236
+ gp.cz = gp.e_sy / gp.ea
237
+
238
+ # Compute of moments of area relative to yz centroidal axes
239
+ translated_geometry = self.section.geometry.translate(
240
+ dx=-gp.cy, dy=-gp.cz
241
+ )
242
+ _, _, gp.iyy_c, gp.izz_c, gp.iyz_c = compute_area_moments(
243
+ geometry=translated_geometry, material=dummy_mat
244
+ )
245
+
246
+ # Computation of moments of area times E
247
+ _, _, gp.e_iyy_c, gp.e_izz_c, gp.e_iyz_c = compute_area_moments(
248
+ geometry=translated_geometry
249
+ )
250
+
251
+ # Compute principal axes of inertia and principal inertia
252
+ def find_principal_axes_moments(iyy, izz, iyz):
253
+ eigres = np.linalg.eig(np.array([[iyy, iyz], [iyz, izz]]))
254
+ max_idx = np.argmax(eigres[0])
255
+ min_idx = 0 if max_idx == 1 else 1
256
+ i11 = eigres[0][max_idx]
257
+ i22 = eigres[0][min_idx]
258
+ theta = np.arccos(np.dot(np.array([1, 0]), eigres[1][:, max_idx]))
259
+ return i11, i22, theta
260
+
261
+ gp.i11, gp.i22, gp.theta = find_principal_axes_moments(
262
+ gp.iyy_c, gp.izz_c, gp.iyz_c
263
+ )
264
+ gp.e_i11, gp.e_i22, gp.e_theta = find_principal_axes_moments(
265
+ gp.e_iyy_c, gp.e_izz_c, gp.e_iyz_c
266
+ )
267
+
268
+ return gp
269
+
270
+ def get_balanced_failure_strain(
271
+ self, geom: CompoundGeometry, yielding: bool = False
272
+ ) -> t.Tuple[float, float, float]:
273
+ """Returns the strain profile corresponding to balanced failure.
274
+
275
+ This is found from all ultimate strains for all materials, checking
276
+ the minimum value of curvature.
277
+
278
+ Arguments:
279
+ geom (CompoundGeometry): The compund geometry.
280
+ yielding (bool): consider yielding instead of ultimate strain,
281
+ default = False.
282
+
283
+ Returns:
284
+ Tuple(float, float, List): It returns a tuple with, 1) Value of y
285
+ coordinate for negative failure, 2) Value of y coordinate for
286
+ positive failure, 3) Strain profile as a list with three values:
287
+ axial strain, curvature y*, curvature z* (assumed zero since in the
288
+ rotated frame y*z* it is a case of uniaxial bending).
289
+ """
290
+ chi_min = 1e10
291
+ for g in geom.geometries + geom.point_geometries:
292
+ for other_g in geom.geometries + geom.point_geometries:
293
+ # if g != other_g:
294
+ eps_p = g.material.get_ultimate_strain(yielding=yielding)[0]
295
+ if isinstance(g, SurfaceGeometry):
296
+ y_p = g.polygon.bounds[1]
297
+ elif isinstance(g, PointGeometry):
298
+ y_p = g._point.coords[0][1]
299
+ eps_n = other_g.material.get_ultimate_strain(
300
+ yielding=yielding
301
+ )[1]
302
+ if isinstance(other_g, SurfaceGeometry):
303
+ y_n = other_g.polygon.bounds[3]
304
+ elif isinstance(other_g, PointGeometry):
305
+ y_n = other_g._point.coords[0][1]
306
+ if y_p >= y_n:
307
+ continue
308
+ chi = -(eps_p - eps_n) / (y_p - y_n)
309
+ # print(y_p,eps_p,y_n,eps_n,chi)
310
+ if chi < chi_min:
311
+ chi_min = chi
312
+ eps_0 = eps_n + chi_min * y_n
313
+ y_n_min = y_n
314
+ y_p_min = y_p
315
+ y_p, y_n = y_p_min, y_n_min
316
+ # In standard CRS negative curvature stretches bottom fiber
317
+ strain = [eps_0, -chi_min, 0]
318
+ return (y_n, y_p, strain)
319
+
320
+ def find_equilibrium_fixed_pivot(
321
+ self, geom: CompoundGeometry, n: float, yielding: bool = False
322
+ ) -> t.Tuple[float, float, float]:
323
+ """Find the equilibrium changing curvature fixed a pivot.
324
+ The algorithm uses bisection algorithm between curvature
325
+ of balanced failure and 0. Selected the pivot point as
326
+ the top or the bottom one, the neutral axis is lowered or
327
+ raised respectively.
328
+
329
+ Arguments:
330
+ geom (CompoundGeometry): A geometry in the rotated reference
331
+ system.
332
+ n (float): Value of external axial force needed to be equilibrated.
333
+ yielding (bool): ...
334
+
335
+ Returns:
336
+ Tuple(float, float, float): 3 floats: Axial strain at (0,0), and
337
+ curvatures of y* and z* axes. Note that being uniaxial bending,
338
+ curvature along z* is 0.0.
339
+ """
340
+ # Number of maximum iteration for the bisection algorithm
341
+ ITMAX = 100
342
+ # 1. Start with a balanced failure: this is found from all ultimate
343
+ # strains for all materials, checking the minimum curvature value
344
+ y_n, y_p, strain = self.get_balanced_failure_strain(geom, yielding)
345
+ eps_p = strain[0] + strain[1] * y_p
346
+ eps_n = strain[0] + strain[1] * y_n
347
+ # Integrate this strain profile corresponding to balanced failure
348
+ (
349
+ n_int,
350
+ _,
351
+ _,
352
+ tri,
353
+ ) = self.integrator.integrate_strain_response_on_geometry(
354
+ geom, strain, tri=self.triangulated_data, mesh_size=self.mesh_size
355
+ )
356
+ # Check if there is equilibrium with this strain distribution
357
+ chi_a = strain[1]
358
+ dn_a = n_int - n
359
+ # It may occur that dn_a is already almost zero (in equilibrium)
360
+ if abs(dn_a) <= 1e-2:
361
+ # return the equilibrium position
362
+ return [strain[0], chi_a, 0]
363
+ chi_b = -1e-13
364
+ if n_int < n:
365
+ # Too much compression, raise NA
366
+ pivot = y_p
367
+ strain_pivot = eps_p
368
+ else:
369
+ # Too much tension, lower NA
370
+ pivot = y_n
371
+ strain_pivot = eps_n
372
+ eps_0 = strain_pivot - chi_b * pivot
373
+ n_int, _, _, _ = self.integrator.integrate_strain_response_on_geometry(
374
+ geom, [eps_0, chi_b, 0], tri=self.triangulated_data
375
+ )
376
+ dn_b = n_int - n
377
+ it = 0
378
+ while (abs(dn_a - dn_b) > 1e-2) and (it < ITMAX):
379
+ chi_c = (chi_a + chi_b) / 2.0
380
+ eps_0 = strain_pivot - chi_c * pivot
381
+ (
382
+ n_int,
383
+ _,
384
+ _,
385
+ _,
386
+ ) = self.integrator.integrate_strain_response_on_geometry(
387
+ geom, [eps_0, chi_c, 0], tri=self.triangulated_data
388
+ )
389
+ dn_c = n_int - n
390
+ if dn_c * dn_a < 0:
391
+ chi_b = chi_c
392
+ dn_b = dn_c
393
+ else:
394
+ chi_a = chi_c
395
+ dn_a = dn_c
396
+ it += 1
397
+ if it >= ITMAX:
398
+ s = f'Last iteration reached a unbalance of {dn_c}'
399
+ raise ValueError(f'Maximum number of iterations reached.\n{s}')
400
+ # Found equilibrium
401
+ # save the triangulation data
402
+ if self.triangulated_data is None:
403
+ self.triangulated_data = tri
404
+ # Return the strain distribution
405
+ return [eps_0, chi_c, 0]
406
+
407
+ def _prefind_range_curvature_equilibrium(
408
+ self,
409
+ geom: CompoundGeometry,
410
+ n: float,
411
+ curv: float,
412
+ eps_0_a: float,
413
+ dn_a: float,
414
+ ):
415
+ """Perfind range where the curvature equilibrium is located.
416
+
417
+ This algorithms quickly finds a position of NA that guaranteed the
418
+ existence of at least one zero in the function dn vs. curv in order to
419
+ apply the bisection algorithm.
420
+ """
421
+ ITMAX = 20
422
+ sign = -1 if dn_a > 0 else 1
423
+ found = False
424
+ it = 0
425
+ delta = 1e-3
426
+ while not found and it < ITMAX:
427
+ eps_0_b = eps_0_a + sign * delta * (it + 1)
428
+ (
429
+ n_int,
430
+ _,
431
+ _,
432
+ _,
433
+ ) = self.integrator.integrate_strain_response_on_geometry(
434
+ geom, [eps_0_b, curv, 0], tri=self.triangulated_data
435
+ )
436
+ dn_b = n_int - n
437
+ if dn_a * dn_b < 0:
438
+ found = True
439
+ elif abs(dn_b) > abs(dn_a):
440
+ # we are driving aay from the solution, probably due
441
+ # to failure of a material
442
+ delta /= 2
443
+ it -= 1
444
+ it += 1
445
+ if it >= ITMAX and not found:
446
+ s = f'Last iteration reached a unbalance of: \
447
+ dn_a = {dn_a} dn_b = {dn_b})'
448
+ raise ValueError(f'Maximum number of iterations reached.\n{s}')
449
+ return (eps_0_b, dn_b)
450
+
451
+ def find_equilibrium_fixed_curvature(
452
+ self, geom: CompoundGeometry, n: float, curv: float, eps_0: float
453
+ ) -> t.Tuple[float, float, float]:
454
+ """Find strain profile with equilibrium with fixed curvature.
455
+
456
+ Given curvature and external axial force, find the strain profile that
457
+ makes internal and external axial force in equilibrium.
458
+
459
+ Arguments:
460
+ geom (CompounGeometry): The geometry.
461
+ n (float): The external axial load.
462
+ curv (float): The value of curvature.
463
+ eps_0 (float): A first attempt for neutral axis position.
464
+
465
+ Returns:
466
+ Tuple(float, float, float): The axial strain and the two
467
+ curvatures.
468
+ """
469
+ # Useful for Moment Curvature Analysis
470
+ # Number of maximum iteration for the bisection algorithm
471
+ ITMAX = 100
472
+ # Start from previous position of N.A.
473
+ eps_0_a = eps_0
474
+ # find internal axial force by integration
475
+ (
476
+ n_int,
477
+ _,
478
+ _,
479
+ tri,
480
+ ) = self.integrator.integrate_strain_response_on_geometry(
481
+ geom, [eps_0, curv, 0], tri=self.triangulated_data
482
+ )
483
+ if self.triangulated_data is None:
484
+ self.triangulated_data = tri
485
+ dn_a = n_int - n
486
+ # It may occur that dn_a is already almost zero (in eqiulibrium)
487
+ if abs(dn_a) <= 1e-2:
488
+ # return the equilibrium position
489
+ return [eps_0_a, curv, 0]
490
+ eps_0_b, dn_b = self._prefind_range_curvature_equilibrium(
491
+ geom, n, curv, eps_0_a, dn_a
492
+ )
493
+ # Found a range within there is the solution, apply bisection
494
+ it = 0
495
+ while (abs(dn_a - dn_b) > 1e-2) and (it < ITMAX):
496
+ eps_0_c = (eps_0_a + eps_0_b) / 2
497
+ (
498
+ n_int,
499
+ _,
500
+ _,
501
+ _,
502
+ ) = self.integrator.integrate_strain_response_on_geometry(
503
+ geom, [eps_0_c, curv, 0], tri=self.triangulated_data
504
+ )
505
+ dn_c = n_int - n
506
+ if dn_a * dn_c < 0:
507
+ dn_b = dn_c
508
+ eps_0_b = eps_0_c
509
+ else:
510
+ dn_a = dn_c
511
+ eps_0_a = eps_0_c
512
+ it += 1
513
+ if it >= ITMAX:
514
+ s = f'Last iteration reached a unbalance of: \
515
+ dn_c = {dn_c}'
516
+ raise ValueError(f'Maximum number of iterations reached.\n{s}')
517
+ return eps_0_c, curv, 0
518
+
519
+ def calculate_limit_axial_load(self):
520
+ """Compute maximum and minimum axial load.
521
+
522
+ Returns:
523
+ Tuple(float, float): Minimum and Maximum axial load.
524
+ """
525
+ # Find balanced failure to get strain limits
526
+ y_n, y_p, strain = self.get_balanced_failure_strain(
527
+ geom=self.section.geometry, yielding=False
528
+ )
529
+ eps_p = strain[0] + strain[1] * y_p
530
+ eps_n = strain[0] + strain[1] * y_n
531
+
532
+ n_min, _, _, tri = (
533
+ self.integrator.integrate_strain_response_on_geometry(
534
+ self.section.geometry,
535
+ [eps_n, 0, 0],
536
+ tri=self.triangulated_data,
537
+ mesh_size=self.mesh_size,
538
+ )
539
+ )
540
+ n_max, _, _, _ = self.integrator.integrate_strain_response_on_geometry(
541
+ self.section.geometry, [eps_p, 0, 0], tri=tri
542
+ )
543
+
544
+ if self.triangulated_data is None:
545
+ self.triangulated_data = tri
546
+ return n_min, n_max
547
+
548
+ @property
549
+ def n_min(self) -> float:
550
+ """Return minimum axial load."""
551
+ if self._n_min is None:
552
+ self._n_min, self._n_max = self.calculate_limit_axial_load()
553
+ return self._n_min
554
+
555
+ @property
556
+ def n_max(self) -> float:
557
+ """Return maximum axial load."""
558
+ if self._n_max is None:
559
+ self._n_min, self._n_max = self.calculate_limit_axial_load()
560
+ return self._n_max
561
+
562
+ def check_axial_load(self, n: float):
563
+ """Check if axial load n is within section limits.
564
+
565
+ Raises:
566
+ ValueError: If axial load cannot be carried by the section.
567
+ """
568
+ if n < self.n_min or n > self.n_max:
569
+ error_str = f'Axial load {n} cannot be taken by section.\n'
570
+ error_str += f'n_min = {self.n_min} / n_max = {self.n_max}'
571
+ raise ValueError(error_str)
572
+
573
+ def _rotate_triangulated_data(self, theta: float):
574
+ """Rotate triangulated data of angle theta."""
575
+ rotated_triangulated_data = []
576
+ for tr in self.triangulated_data:
577
+ T = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
578
+ coords = np.vstack((tr[0], tr[1]))
579
+ coords_r = T @ coords
580
+ rotated_triangulated_data.append(
581
+ (coords_r[0, :], coords_r[1, :], tr[2], tr[3])
582
+ )
583
+ self.triangulated_data = rotated_triangulated_data
584
+
585
+ def integrate_strain_profile(
586
+ self, strain: ArrayLike
587
+ ) -> t.Tuple[float, float, float]:
588
+ """Integrate a strain profile returning internal forces.
589
+
590
+ Arguments:
591
+ strain (ArrayLike): Represents the deformation plane. The strain
592
+ should have three entries representing respectively: axial
593
+ strain (At 0,0 coordinates), curv_y, curv_z.
594
+
595
+ Returns:
596
+ Tuple(float, float, float): N, My and Mz.
597
+ """
598
+ N, My, Mz, _ = self.integrator.integrate_strain_response_on_geometry(
599
+ geo=self.section.geometry,
600
+ strain=strain,
601
+ tri=self.triangulated_data,
602
+ mesh_size=self.mesh_size,
603
+ )
604
+ return N, My, Mz
605
+
606
+ def calculate_bending_strength(
607
+ self, theta=0, n=0
608
+ ) -> s_res.UltimateBendingMomentResults:
609
+ """Calculates the bending strength for given inclination of n.a. and
610
+ axial load.
611
+
612
+ Arguments:
613
+ theta (float): Inclination of n.a. respect to section y axis,
614
+ default = 0.
615
+ n (float): Axial load applied to the section (+: tension, -:
616
+ compression), default = 0.
617
+
618
+ Returns:
619
+ UltimateBendingMomentResults: The results from the calculation.
620
+ """
621
+ # Compute the bending strength with the bisection algorithm
622
+ # Rotate the section of angle theta
623
+ rotated_geom = self.section.geometry.rotate(-theta)
624
+ if self.triangulated_data is not None:
625
+ # Rotate also triangulated data!
626
+ self._rotate_triangulated_data(-theta)
627
+
628
+ # Check if the section can carry the axial load
629
+ self.check_axial_load(n=n)
630
+ # Find the strain distribution corresponding to failure and equilibrium
631
+ # with external axial force
632
+ strain = self.find_equilibrium_fixed_pivot(rotated_geom, n)
633
+ # Compute the internal forces with this strain distribution
634
+ N, My, Mz, _ = self.integrator.integrate_strain_response_on_geometry(
635
+ geo=rotated_geom, strain=strain, tri=self.triangulated_data
636
+ )
637
+
638
+ # Rotate back to section CRS TODO Check
639
+ T = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
640
+ M = T @ np.array([[My], [Mz]])
641
+ if self.triangulated_data is not None:
642
+ # Rotate back also triangulated data!
643
+ self._rotate_triangulated_data(theta)
644
+
645
+ # Create result object
646
+ res = s_res.UltimateBendingMomentResults()
647
+ res.theta = theta
648
+ res.n = N
649
+ res.chi_y = strain[1]
650
+ res.chi_z = strain[2]
651
+ res.eps_a = strain[0]
652
+ res.m_y = M[0, 0]
653
+ res.m_z = M[1, 0]
654
+
655
+ return res
656
+
657
+ def calculate_moment_curvature(
658
+ self,
659
+ theta: float = 0.0,
660
+ n: float = 0.0,
661
+ chi_first: float = 1e-8,
662
+ num_pre_yield: int = 10,
663
+ num_post_yield: int = 10,
664
+ chi: t.Optional[ArrayLike] = None,
665
+ ) -> s_res.MomentCurvatureResults:
666
+ """Calculates the moment-curvature relation for given inclination of
667
+ n.a. and axial load.
668
+
669
+ Arguments:
670
+ theta (float): Inclination of n.a. respect to y axis, default = 0.
671
+ n (float): Axial load applied to the section (+: tension, -:
672
+ compression), default = 0.
673
+ chi_first (float): The first value of the curvature, default =
674
+ 1e-8.
675
+ num_pre_yield (int): Number of points before yielding. Note that
676
+ the yield curvature will be at the num_pre_yield-th point in
677
+ the result array, default = 10.
678
+ num_post_yield (int): Number of points after yielding, default =
679
+ 10.
680
+ chi (Optional[ArrayLike]): An ArrayLike with curvatures to
681
+ calculate the moment response for. If chi is None, the array is
682
+ constructed from chi_first, num_pre_yield and num_post_yield.
683
+ If chi is not None, chi_first, num_pre_yield and num_post_yield
684
+ are disregarded, and the provided chi is used directly in the
685
+ calculations.
686
+
687
+ Returns:
688
+ MomentCurvatureResults: The calculation results.
689
+ """
690
+ # Create an empty response object
691
+ res = s_res.MomentCurvatureResults()
692
+ res.n = n
693
+ # Rotate the section of angle theta
694
+ rotated_geom = self.section.geometry.rotate(-theta)
695
+ if self.triangulated_data is not None:
696
+ # Rotate also triangulated data!
697
+ self._rotate_triangulated_data(-theta)
698
+
699
+ # Check if the section can carry the axial load
700
+ self.check_axial_load(n=n)
701
+
702
+ if chi is None:
703
+ # Find ultimate curvature from the strain distribution
704
+ # corresponding to failure and equilibrium with external axial
705
+ # force
706
+ strain = self.find_equilibrium_fixed_pivot(rotated_geom, n)
707
+ chi_ultimate = strain[1]
708
+ # Find the yielding curvature
709
+ strain = self.find_equilibrium_fixed_pivot(
710
+ rotated_geom, n, yielding=True
711
+ )
712
+ chi_yield = strain[1]
713
+ if chi_ultimate * chi_yield < 0:
714
+ # They cannot have opposite signs!
715
+ raise ValueError(
716
+ 'curvature at yield and ultimate cannot have opposite '
717
+ 'signs!'
718
+ )
719
+
720
+ # Make sure the sign of the first curvature matches the sign of the
721
+ # yield curvature
722
+ chi_first *= -1.0 if chi_first * chi_yield < 0 else 1.0
723
+
724
+ # The first curvature should be less than the yield curvature
725
+ if abs(chi_first) >= abs(chi_yield):
726
+ chi_first = chi_yield / num_pre_yield
727
+
728
+ # Define the array of curvatures
729
+ if abs(chi_ultimate) <= abs(chi_yield) + 1e-8:
730
+ # We don't want a plastic branch in the analysis
731
+ # this is done to speed up analysis
732
+ chi = np.linspace(chi_first, chi_yield, num_pre_yield)
733
+ else:
734
+ chi = np.concatenate(
735
+ (
736
+ np.linspace(
737
+ chi_first,
738
+ chi_yield,
739
+ num_pre_yield - 1,
740
+ endpoint=False,
741
+ ),
742
+ np.linspace(
743
+ chi_yield, chi_ultimate, num_post_yield + 1
744
+ ),
745
+ )
746
+ )
747
+
748
+ # prepare results
749
+ eps_a = np.zeros_like(chi)
750
+ my = np.zeros_like(chi)
751
+ mz = np.zeros_like(chi)
752
+ chi_y = np.zeros_like(chi)
753
+ chi_z = np.zeros_like(chi)
754
+ # Previous position of strain at (0,0)
755
+ strain = [0, 0, 0]
756
+ # For each value of curvature
757
+ for i, curv in enumerate(chi):
758
+ # find the new position of neutral axis for mantaining equilibrium
759
+ # store the information in the results object for the current
760
+ # value of curvature
761
+ strain = self.find_equilibrium_fixed_curvature(
762
+ rotated_geom, n, curv, strain[0]
763
+ )
764
+ (
765
+ _,
766
+ My,
767
+ Mz,
768
+ _,
769
+ ) = self.integrator.integrate_strain_response_on_geometry(
770
+ geo=rotated_geom, strain=strain, tri=self.triangulated_data
771
+ )
772
+ # Rotate back to section CRS
773
+ T = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
774
+ M = T @ np.array([[My], [Mz]])
775
+ eps_a[i] = strain[0]
776
+ my[i] = M[0, 0]
777
+ mz[i] = M[1, 0]
778
+ chi_mat = T @ np.array([[curv], [0]])
779
+ chi_y[i] = chi_mat[0, 0]
780
+ chi_z[i] = chi_mat[1, 0]
781
+
782
+ if self.triangulated_data is not None:
783
+ # Rotate back also triangulated data!
784
+ self._rotate_triangulated_data(theta)
785
+ res.chi_y = chi_y
786
+ res.chi_z = chi_z
787
+ res.eps_axial = eps_a
788
+ res.m_y = my
789
+ res.m_z = mz
790
+
791
+ return res
792
+
793
+ def _process_num_strain_profiles(
794
+ self,
795
+ num: int = 35,
796
+ min_1: int = 1,
797
+ min_2: int = 2,
798
+ min_3: int = 15,
799
+ min_4: int = 10,
800
+ min_5: int = 3,
801
+ min_6: int = 4,
802
+ ):
803
+ """Return number of strain profiles for each field given the total
804
+ number of strain profiles.
805
+
806
+ If the total number of strain profiles is given by user, divide this
807
+ for every field according to a default pre-defined discretization
808
+ for each field (1, 2, 15, 10, 3, 4 for fields 1 to 6). For each field
809
+ if the user set a desired minimum number of strain profiles, guarantee
810
+ to create a number of strain profiles greater or equal to the desired
811
+ one. Therefore the function never return less strain profiles than
812
+ desired.
813
+
814
+ Arguments:
815
+ num (int): Total number of strain profiles (Optional, default =
816
+ 35). If specified num and num_1, ..., num_6 the total number of
817
+ num may be different.
818
+ min_1 (int): Minimum number of strain profiles in field 1
819
+ (Optional, default = 1).
820
+ min_2 (int): Minimum number of strain profiles in field 2
821
+ (Optional, default = 2).
822
+ min_3 (int): Minimum number of strain profiles in field 3
823
+ (Optional, default = 15).
824
+ min_4 (int): Minimum number of strain profiles in field 4
825
+ (Optional, default = 10).
826
+ min_5 (int): Minimum number of strain profiles in field 5
827
+ (Optional, default = 3).
828
+ min_6 (int): Minimum number of strain profiles in field 6
829
+ (Optional, default = 4).
830
+
831
+ Return:
832
+ (int, int, int, int, int, int): 6-tuple of int number representing
833
+ number of strain profiles for each field.
834
+ """
835
+ n1_attempt = int(num / 35 * 1)
836
+ n2_attempt = int(num / 35 * 2)
837
+ n3_attempt = int(num / 35 * 15)
838
+ n4_attempt = int(num / 35 * 10)
839
+ n5_attempt = int(num / 35 * 3)
840
+ n6_attempt = int(num / 35 * 4)
841
+ num_1 = max(n1_attempt, min_1)
842
+ num_2 = max(n2_attempt, min_2)
843
+ num_3 = max(n3_attempt, min_3)
844
+ num_4 = max(n4_attempt, min_4)
845
+ num_5 = max(n5_attempt, min_5)
846
+ num_6 = max(n6_attempt, min_6)
847
+ return (num_1, num_2, num_3, num_4, num_5, num_6)
848
+
849
+ def calculate_nm_interaction_domain(
850
+ self,
851
+ theta: float = 0,
852
+ num_1: int = 1,
853
+ num_2: int = 2,
854
+ num_3: int = 15,
855
+ num_4: int = 10,
856
+ num_5: int = 3,
857
+ num_6: int = 4,
858
+ num: t.Optional[int] = None,
859
+ type_1: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
860
+ type_2: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
861
+ type_3: t.Literal['linear', 'geometric', 'quadratic'] = 'geometric',
862
+ type_4: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
863
+ type_5: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
864
+ type_6: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
865
+ ) -> s_res.NMInteractionDomain:
866
+ """Calculate the NM interaction domain.
867
+
868
+ Arguments:
869
+ theta (float): Inclination of n.a. respect to y axis
870
+ (Optional, default = 0).
871
+ num_1 (int): Number of strain profiles in field 1
872
+ (Optional, default = 1).
873
+ num_2 (int): Number of strain profiles in field 2
874
+ (Optional, default = 2).
875
+ num_3 (int): Number of strain profiles in field 3
876
+ (Optional, default = 15).
877
+ num_4 (int): Number of strain profiles in field 4
878
+ (Optional, default = 10).
879
+ num_5 (int): Number of strain profiles in field 5
880
+ (Optional, default = 3).
881
+ num_6 (int): Number of strain profiles in field 6
882
+ (Optional, default = 4).
883
+ num (int): Total number of strain profiles (Optional, default =
884
+ None). If specified num and num_1, ..., num_6 the total number
885
+ of num may be different.
886
+ type_1 (str): Type of spacing for field 1. 'linear' for a
887
+ linear spacing, 'geometric' for a geometric spacing 'quadratic'
888
+ for a quadratic spacing (default = 'linear').
889
+ type_2 (str): Type of spacing for field 2 (default = 'linear'). See
890
+ type_1 for options.
891
+ type_3 (str): Type of spacing for field 3 (default = 'geometric').
892
+ See type_1 for options.
893
+ type_4 (str): Type of spacing for field 4 (default = 'linear'). See
894
+ type_1 for options.
895
+ type_5 (str): Type of spacing for field 5 (default = 'linear'). See
896
+ type_1 for options.
897
+ type_6 (str): Type of spacing for field 6 (default = 'linear'). See
898
+ type_1 for options.
899
+
900
+ Returns:
901
+ NMInteractionDomain: The calculation results.
902
+ """
903
+ # Prepare the results
904
+ res = s_res.NMInteractionDomain()
905
+ res.theta = theta
906
+
907
+ # Process num if given.
908
+ if num is not None:
909
+ num_1, num_2, num_3, num_4, num_5, num_6 = (
910
+ self._process_num_strain_profiles(
911
+ num, num_1, num_2, num_3, num_4, num_5, num_6
912
+ )
913
+ )
914
+
915
+ # Get ultimate strain profiles for theta angle
916
+ strains = self._compute_ultimate_strain_profiles(
917
+ theta=theta,
918
+ num_1=num_1,
919
+ num_2=num_2,
920
+ num_3=num_3,
921
+ num_4=num_4,
922
+ num_5=num_5,
923
+ num_6=num_6,
924
+ type_1=type_1,
925
+ type_2=type_2,
926
+ type_3=type_3,
927
+ type_4=type_4,
928
+ type_5=type_5,
929
+ type_6=type_6,
930
+ )
931
+
932
+ # integrate all strain profiles
933
+ forces = np.zeros_like(strains)
934
+ for i, strain in enumerate(strains):
935
+ N, My, Mz, tri = (
936
+ self.integrator.integrate_strain_response_on_geometry(
937
+ geo=self.section.geometry,
938
+ strain=strain,
939
+ tri=self.triangulated_data,
940
+ mesh_size=self.mesh_size,
941
+ )
942
+ )
943
+ if self.triangulated_data is None:
944
+ self.triangulated_data = tri
945
+ forces[i, 0] = N
946
+ forces[i, 1] = My
947
+ forces[i, 2] = Mz
948
+
949
+ # Save to results
950
+ res.strains = strains
951
+ res.m_z = forces[:, 2]
952
+ res.m_y = forces[:, 1]
953
+ res.n = forces[:, 0]
954
+
955
+ return res
956
+
957
+ def _compute_ultimate_strain_profiles(
958
+ self,
959
+ theta: float = 0,
960
+ num_1: int = 1,
961
+ num_2: int = 2,
962
+ num_3: int = 15,
963
+ num_4: int = 10,
964
+ num_5: int = 3,
965
+ num_6: int = 4,
966
+ type_1: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
967
+ type_2: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
968
+ type_3: t.Literal['linear', 'geometric', 'quadratic'] = 'geometric',
969
+ type_4: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
970
+ type_5: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
971
+ type_6: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
972
+ ):
973
+ """Return an array of ultimate strain profiles.
974
+
975
+ Arguments:
976
+ theta (float): The angle of neutral axis.
977
+ num_1 (int): Number of strain profiles in field 1
978
+ (Optional, default = 1).
979
+ num_2 (int): Number of strain profiles in field 2
980
+ (Optional, default = 2).
981
+ num_3 (int): Number of strain profiles in field 3
982
+ (Optional, default = 15).
983
+ num_4 (int): Number of strain profiles in field 4
984
+ (Optional, default = 10).
985
+ num_5 (int): Number of strain profiles in field 5
986
+ (Optional, default = 3).
987
+ num_6 (int): Number of strain profiles in field 6
988
+ (Optional, default = 4).
989
+ type_1 (literal): Type of spacing for field 1. 'linear' for a
990
+ linear spacing, 'geometric' for a geometric spacing 'quadratic'
991
+ for a quadratic spacing (Optional default = 'linear').
992
+ type_2 (literal): Type of spacing for field 2 (default = 'linear').
993
+ See type_1 for options.
994
+ type_3 (literal): Type of spacing for field 3 (default =
995
+ 'geometric'). See type_1 for options.
996
+ type_4 (literal): Type of spacing for field 4 (default = 'linear').
997
+ See type_1 for options.
998
+ type_5 (literal): Type of spacing for field 5 (default = 'linear').
999
+ See type_1 for options.
1000
+ type_6 (literal): Type of spacing for field 6 (default = 'linear').
1001
+ See type_1 for options.
1002
+ """
1003
+ rotated_geom = self.section.geometry.rotate(-theta)
1004
+
1005
+ # Find yield failure
1006
+ y_n, y_p, strain = self.get_balanced_failure_strain(
1007
+ geom=rotated_geom, yielding=True
1008
+ )
1009
+ eps_p_y = strain[0] + strain[1] * y_p
1010
+ eps_n_y = strain[0] + strain[1] * y_n
1011
+ # Find balanced failure: this defines the transition
1012
+ # between fields 2 and 3
1013
+ y_n, y_p, strain = self.get_balanced_failure_strain(
1014
+ geom=rotated_geom, yielding=False
1015
+ )
1016
+ eps_p_b = strain[0] + strain[1] * y_p
1017
+ eps_n_b = strain[0] + strain[1] * y_n
1018
+
1019
+ # get h of the rotated geometry
1020
+ _, _, min_y, _ = rotated_geom.calculate_extents()
1021
+ h = y_n - min_y
1022
+
1023
+ def _np_space(a, b, n, type, endpoint):
1024
+ if n < 0:
1025
+ raise ValueError(
1026
+ 'Number of discretizations cannot be negative!'
1027
+ )
1028
+ if type.lower() == 'linear':
1029
+ return np.linspace(a, b, n, endpoint=endpoint)
1030
+ if type.lower() == 'geometric':
1031
+ if b != 0 and a != 0:
1032
+ return np.geomspace(a, b, n, endpoint=endpoint)
1033
+ small_value = 1e-10
1034
+ if b == 0:
1035
+ b = small_value * np.sign(a)
1036
+ return np.append(
1037
+ np.geomspace(a, b, n - 1, endpoint=endpoint), 0
1038
+ )
1039
+ if a == 0:
1040
+ a = small_value * np.sign(b)
1041
+ return np.insert(
1042
+ np.geomspace(a, b, n - 1, endpoint=endpoint), 0, 0
1043
+ )
1044
+ if type.lower() == 'quadratic':
1045
+ quadratic_spaced = (np.linspace(0, 1, n) ** 2) * (a - b) + b
1046
+ return quadratic_spaced[::-1]
1047
+ raise ValueError(f'Type of spacing not known: {type}')
1048
+
1049
+ # For generation of fields 1 and 2 pivot on positive strain
1050
+ # Field 1: pivot on positive strain
1051
+ eps_n = _np_space(eps_p_b, 0, num_1, type_1, endpoint=False)
1052
+ eps_p = np.zeros_like(eps_n) + eps_p_b
1053
+ # Field 2: pivot on positive strain
1054
+ eps_n = np.append(
1055
+ eps_n, _np_space(0, eps_n_b, num_2, type_2, endpoint=False)
1056
+ )
1057
+ eps_p = np.append(eps_p, np.zeros(num_2) + eps_p_b)
1058
+ # For fields 3-4-5 pivot on negative strain
1059
+ # Field 3: pivot on negative strain
1060
+ eps_n = np.append(eps_n, np.zeros(num_3) + eps_n_b)
1061
+ eps_p = np.append(
1062
+ eps_p, _np_space(eps_p_b, eps_p_y, num_3, type_3, endpoint=False)
1063
+ )
1064
+ # Field 4: pivot on negative strain
1065
+ eps_n = np.append(eps_n, np.zeros(num_4) + eps_n_b)
1066
+ eps_p = np.append(
1067
+ eps_p, _np_space(eps_p_y, 0, num_4, type_4, endpoint=False)
1068
+ )
1069
+ # Field 5: pivot on negative strain
1070
+ eps_p_lim = eps_n_b * (h - (y_n - y_p)) / h
1071
+ eps_n = np.append(eps_n, np.zeros(num_5) + eps_n_b)
1072
+ eps_p = np.append(
1073
+ eps_p, _np_space(0, eps_p_lim, num_5, type_5, endpoint=False)
1074
+ )
1075
+ # Field 6: pivot on eps_n_y point or eps_n_b
1076
+ # If reinforced concrete section pivot on eps_n_y (default -0.002)
1077
+ # otherwise pivot on eps_n_b (in top chord)
1078
+ if self.section.geometry.reinforced_concrete:
1079
+ z_pivot = y_n - (1 - eps_n_y / eps_n_b) * h
1080
+ eps_p_6 = np.append(
1081
+ _np_space(
1082
+ eps_p_lim, eps_n_y, num_6 - 1, type_6, endpoint=False
1083
+ ),
1084
+ eps_n_y,
1085
+ )
1086
+ eps_n_6 = (
1087
+ -(eps_n_y - eps_p_6) * (z_pivot - y_n) / (z_pivot - y_p)
1088
+ + eps_n_y
1089
+ )
1090
+ else:
1091
+ eps_n_6 = np.zeros(num_6) + eps_n_b
1092
+ eps_p_6 = np.append(
1093
+ _np_space(
1094
+ eps_p_lim, eps_n_b, num_6 - 1, type_6, endpoint=False
1095
+ ),
1096
+ eps_n_b,
1097
+ )
1098
+ eps_n = np.append(eps_n, eps_n_6)
1099
+ eps_p = np.append(eps_p, eps_p_6)
1100
+
1101
+ # rotate them
1102
+ kappa_y = (eps_n - eps_p) / (y_n - y_p)
1103
+ eps_a = eps_n - kappa_y * y_n
1104
+ kappa_z = np.zeros_like(kappa_y)
1105
+
1106
+ # rotate back components to work in section CRS
1107
+ T = np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])
1108
+ components = np.vstack((kappa_y, kappa_z))
1109
+ rotated_components = T @ components
1110
+ return np.column_stack((eps_a, rotated_components.T))
1111
+
1112
+ def calculate_nmm_interaction_domain(
1113
+ self,
1114
+ num_theta: int = 32,
1115
+ num_1: int = 1,
1116
+ num_2: int = 2,
1117
+ num_3: int = 15,
1118
+ num_4: int = 10,
1119
+ num_5: int = 3,
1120
+ num_6: int = 4,
1121
+ num: t.Optional[int] = None,
1122
+ type_1: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
1123
+ type_2: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
1124
+ type_3: t.Literal['linear', 'geometric', 'quadratic'] = 'geometric',
1125
+ type_4: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
1126
+ type_5: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
1127
+ type_6: t.Literal['linear', 'geometric', 'quadratic'] = 'linear',
1128
+ ) -> s_res.NMMInteractionDomain:
1129
+ """Calculates the NMM interaction domain.
1130
+
1131
+ Arguments:
1132
+ num_theta (int): Number of discretization of angle of neutral axis
1133
+ (Optional, Default = 32).
1134
+ num_1 (int): Number of strain profiles in field 1
1135
+ (Optional, default = 1).
1136
+ num_2 (int): Number of strain profiles in field 2
1137
+ (Optional, default = 2).
1138
+ num_3 (int): Number of strain profiles in field 3
1139
+ (Optional, default = 15).
1140
+ num_4 (int): Number of strain profiles in field 4
1141
+ (Optional, default = 10).
1142
+ num_5 (int): Number of strain profiles in field 5
1143
+ (Optional, default = 3).
1144
+ num_6 (int): Number of strain profiles in field 6
1145
+ (Optional, default = 4).
1146
+ num (int): Total number of strain profiles (Optional, default =
1147
+ None). If specified num and num_1, ..., num_6 the total number
1148
+ of num may be different.
1149
+ type_1 (literal): Type of spacing for field 1. 'linear' for a
1150
+ linear spacing, 'geometric' for a geometric spacing 'quadratic'
1151
+ for a quadratic spacing (Optional default = 'linear').
1152
+ type_2 (literal): Type of spacing for field 2 (default = 'linear').
1153
+ See type_1 for options.
1154
+ type_3 (literal): Type of spacing for field 3 (default =
1155
+ 'geometric'). See type_1 for options.
1156
+ type_4 (literal): Type of spacing for field 4 (default = 'linear').
1157
+ See type_1 for options.
1158
+ type_5 (literal): Type of spacing for field 5 (default = 'linear').
1159
+ See type_1 for options.
1160
+ type_6 (literal): Type of spacing for field 6 (default = 'linear').
1161
+ See type_1 for options.
1162
+
1163
+ Returns:
1164
+ NMInteractionDomain: The calculation results.
1165
+ """
1166
+ res = s_res.NMMInteractionDomain()
1167
+ res.num_theta = num_theta
1168
+
1169
+ # Process num if given.
1170
+ if num is not None:
1171
+ num_1, num_2, num_3, num_4, num_5, num_6 = (
1172
+ self._process_num_strain_profiles(
1173
+ num, num_1, num_2, num_3, num_4, num_5, num_6
1174
+ )
1175
+ )
1176
+
1177
+ # cycle for all n_thetas
1178
+ thetas = np.linspace(0, np.pi * 2, num_theta)
1179
+ # Initialize an empty array with the correct shape
1180
+ strains = np.empty((0, 3))
1181
+ for theta in thetas:
1182
+ # Get ultimate strain profiles for theta angle
1183
+ strain = self._compute_ultimate_strain_profiles(
1184
+ theta=theta,
1185
+ num_1=num_1,
1186
+ num_2=num_2,
1187
+ num_3=num_3,
1188
+ num_4=num_4,
1189
+ num_5=num_5,
1190
+ num_6=num_6,
1191
+ type_1=type_1,
1192
+ type_2=type_2,
1193
+ type_3=type_3,
1194
+ type_4=type_4,
1195
+ type_5=type_5,
1196
+ type_6=type_6,
1197
+ )
1198
+ strains = np.vstack((strains, strain))
1199
+
1200
+ # integrate all strain profiles
1201
+ forces = np.zeros_like(strains)
1202
+ for i, strain in enumerate(strains):
1203
+ N, My, Mz, tri = (
1204
+ self.integrator.integrate_strain_response_on_geometry(
1205
+ geo=self.section.geometry,
1206
+ strain=strain,
1207
+ tri=self.triangulated_data,
1208
+ )
1209
+ )
1210
+ if self.triangulated_data is None:
1211
+ self.triangulated_data = tri
1212
+ forces[i, 0] = N
1213
+ forces[i, 1] = My
1214
+ forces[i, 2] = Mz
1215
+
1216
+ # Save to results
1217
+ res.strains = strains
1218
+ res.forces = forces
1219
+
1220
+ return res
1221
+
1222
+ def calculate_mm_interaction_domain(
1223
+ self, n: float = 0, num_theta: int = 32
1224
+ ) -> s_res.MMInteractionDomain:
1225
+ """Calculate the My-Mz interaction domain.
1226
+
1227
+ Arguments:
1228
+ n (float): Axial force, default = 0.
1229
+ n_theta (int): Number of discretization for theta, default = 32.
1230
+
1231
+ Return:
1232
+ MMInteractionDomain: The calculation results.
1233
+ """
1234
+ # Prepare the results
1235
+ res = s_res.MMInteractionDomain()
1236
+ res.num_theta = num_theta
1237
+ res.n = n
1238
+ # Create array of thetas
1239
+ res.theta = np.linspace(0, np.pi * 2, num_theta)
1240
+ # Initialize the result's arrays
1241
+ res.m_y = np.zeros_like(res.theta)
1242
+ res.m_z = np.zeros_like(res.theta)
1243
+ # Compute strength for given angle of NA
1244
+ for i, th in enumerate(res.theta):
1245
+ res_bend_strength = self.calculate_bending_strength(theta=th, n=n)
1246
+ res.m_y[i] = res_bend_strength.m_y
1247
+ res.m_z[i] = res_bend_strength.m_z
1248
+
1249
+ return res