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.
- structuralcodes/__init__.py +17 -0
- structuralcodes/codes/__init__.py +79 -0
- structuralcodes/codes/ec2_2004/__init__.py +133 -0
- structuralcodes/codes/ec2_2004/_concrete_material_properties.py +239 -0
- structuralcodes/codes/ec2_2004/_reinforcement_material_properties.py +104 -0
- structuralcodes/codes/ec2_2004/_section_7_3_crack_control.py +941 -0
- structuralcodes/codes/ec2_2004/annex_b_shrink_and_creep.py +257 -0
- structuralcodes/codes/ec2_2004/shear.py +506 -0
- structuralcodes/codes/ec2_2023/__init__.py +104 -0
- structuralcodes/codes/ec2_2023/_annexB_time_dependent.py +17 -0
- structuralcodes/codes/ec2_2023/_section5_materials.py +1160 -0
- structuralcodes/codes/ec2_2023/_section9_sls.py +325 -0
- structuralcodes/codes/mc2010/__init__.py +169 -0
- structuralcodes/codes/mc2010/_concrete_creep_and_shrinkage.py +704 -0
- structuralcodes/codes/mc2010/_concrete_interface_different_casting_times.py +104 -0
- structuralcodes/codes/mc2010/_concrete_material_properties.py +463 -0
- structuralcodes/codes/mc2010/_concrete_punching.py +543 -0
- structuralcodes/codes/mc2010/_concrete_shear.py +749 -0
- structuralcodes/codes/mc2010/_concrete_torsion.py +164 -0
- structuralcodes/codes/mc2010/_reinforcement_material_properties.py +105 -0
- structuralcodes/core/__init__.py +1 -0
- structuralcodes/core/_section_results.py +211 -0
- structuralcodes/core/base.py +260 -0
- structuralcodes/geometry/__init__.py +25 -0
- structuralcodes/geometry/_geometry.py +875 -0
- structuralcodes/geometry/_steel_sections.py +2155 -0
- structuralcodes/materials/__init__.py +9 -0
- structuralcodes/materials/concrete/__init__.py +82 -0
- structuralcodes/materials/concrete/_concrete.py +114 -0
- structuralcodes/materials/concrete/_concreteEC2_2004.py +477 -0
- structuralcodes/materials/concrete/_concreteEC2_2023.py +435 -0
- structuralcodes/materials/concrete/_concreteMC2010.py +494 -0
- structuralcodes/materials/constitutive_laws.py +979 -0
- structuralcodes/materials/reinforcement/__init__.py +84 -0
- structuralcodes/materials/reinforcement/_reinforcement.py +172 -0
- structuralcodes/materials/reinforcement/_reinforcementEC2_2004.py +103 -0
- structuralcodes/materials/reinforcement/_reinforcementEC2_2023.py +93 -0
- structuralcodes/materials/reinforcement/_reinforcementMC2010.py +98 -0
- structuralcodes/sections/__init__.py +23 -0
- structuralcodes/sections/_generic.py +1249 -0
- structuralcodes/sections/_reinforcement.py +115 -0
- structuralcodes/sections/section_integrators/__init__.py +14 -0
- structuralcodes/sections/section_integrators/_factory.py +41 -0
- structuralcodes/sections/section_integrators/_fiber_integrator.py +238 -0
- structuralcodes/sections/section_integrators/_marin_integration.py +47 -0
- structuralcodes/sections/section_integrators/_marin_integrator.py +222 -0
- structuralcodes/sections/section_integrators/_section_integrator.py +49 -0
- structuralcodes-0.0.1.dist-info/METADATA +40 -0
- structuralcodes-0.0.1.dist-info/RECORD +50 -0
- 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
|