ANYsolver 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anysolver/__init__.py +631 -0
- anysolver/anystructure_fem_mode.py +942 -0
- anysolver/arc_length.py +758 -0
- anysolver/assembly.py +950 -0
- anysolver/baselines.py +303 -0
- anysolver/beam_shell_verification.py +4276 -0
- anysolver/beam_validity.py +110 -0
- anysolver/benchmarks.py +495 -0
- anysolver/boundary.py +476 -0
- anysolver/buckling.py +442 -0
- anysolver/buckling_validity.py +91 -0
- anysolver/capacity_workflow.py +350 -0
- anysolver/cases.py +173 -0
- anysolver/composite_strip_verification.py +297 -0
- anysolver/contact.py +3461 -0
- anysolver/corotational.py +371 -0
- anysolver/cylinder_benchmarks.py +364 -0
- anysolver/dynamics.py +723 -0
- anysolver/element_qualification.py +432 -0
- anysolver/elements.py +2724 -0
- anysolver/external_references.py +369 -0
- anysolver/fe_core.py +327 -0
- anysolver/fracture.py +551 -0
- anysolver/imperfections.py +390 -0
- anysolver/jit_compiler.py +108 -0
- anysolver/kernel_warmup.py +180 -0
- anysolver/linalg.py +760 -0
- anysolver/mass_properties.py +179 -0
- anysolver/material_curves.py +231 -0
- anysolver/matrix_assembly.py +558 -0
- anysolver/mesh_gen.py +1065 -0
- anysolver/mesh_load_bc_verification.py +609 -0
- anysolver/modal.py +282 -0
- anysolver/nonlinear.py +300 -0
- anysolver/nonlinear_performance.py +920 -0
- anysolver/nonlinear_performance_batch_b.py +592 -0
- anysolver/nonlinear_performance_batch_c.py +506 -0
- anysolver/nonlinear_performance_bootstrap.py +120 -0
- anysolver/nonlinear_reduced_assembly.py +760 -0
- anysolver/nonlinear_static.py +1518 -0
- anysolver/plasticity.py +419 -0
- anysolver/plasticity_qualification.py +314 -0
- anysolver/production_readiness.py +304 -0
- anysolver/quality_control.py +1497 -0
- anysolver/recovery.py +505 -0
- anysolver/recovery_policy.py +205 -0
- anysolver/reference_cases.py +425 -0
- anysolver/results.py +503 -0
- anysolver/runtime.py +9030 -0
- anysolver/s4_validity.py +326 -0
- anysolver/sesam_fem/__init__.py +55 -0
- anysolver/sesam_fem/__main__.py +80 -0
- anysolver/sesam_fem/diagnostics.py +65 -0
- anysolver/sesam_fem/document.py +814 -0
- anysolver/sesam_fem/exporter.py +84 -0
- anysolver/sesam_fem/importer.py +365 -0
- anysolver/sesam_fem/records.py +257 -0
- anysolver/sesam_fem/schema.py +107 -0
- anysolver/sesam_fem/sif_importer.py +397 -0
- anysolver/sesam_fem/validation.py +62 -0
- anysolver/shell_benchmarks.py +367 -0
- anysolver/test_cases.py +610 -0
- anysolver/validation.py +416 -0
- anysolver/vectorized_nonlinear.py +334 -0
- anysolver/vectorized_stiffness.py +571 -0
- anysolver-0.1.0.dist-info/METADATA +165 -0
- anysolver-0.1.0.dist-info/RECORD +70 -0
- anysolver-0.1.0.dist-info/WHEEL +5 -0
- anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
- anysolver-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Model mass properties and rigid-body inertia diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .elements import BeamElement, QuadraticBeamElement, ShellElement
|
|
11
|
+
from .matrix_assembly import assemble_mass_matrix
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .fe_core import FEModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MassProperties:
|
|
19
|
+
"""Integrated and assembled mass diagnostics for an FE model."""
|
|
20
|
+
|
|
21
|
+
total_mass: float
|
|
22
|
+
center_of_mass: np.ndarray
|
|
23
|
+
first_moment: np.ndarray
|
|
24
|
+
inertia_tensor_origin: np.ndarray
|
|
25
|
+
inertia_tensor_center_of_mass: np.ndarray
|
|
26
|
+
rigid_body_mass_matrix: np.ndarray
|
|
27
|
+
assembled_translation_masses: Dict[str, float]
|
|
28
|
+
num_mass_points: int
|
|
29
|
+
skipped_elements: List[int]
|
|
30
|
+
assembly_info: Dict[str, Any]
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"total_mass": float(self.total_mass),
|
|
35
|
+
"center_of_mass": self.center_of_mass.tolist(),
|
|
36
|
+
"first_moment": self.first_moment.tolist(),
|
|
37
|
+
"inertia_tensor_origin": self.inertia_tensor_origin.tolist(),
|
|
38
|
+
"inertia_tensor_center_of_mass": self.inertia_tensor_center_of_mass.tolist(),
|
|
39
|
+
"rigid_body_mass_matrix": self.rigid_body_mass_matrix.tolist(),
|
|
40
|
+
"assembled_translation_masses": dict(self.assembled_translation_masses),
|
|
41
|
+
"num_mass_points": int(self.num_mass_points),
|
|
42
|
+
"skipped_elements": [int(eid) for eid in self.skipped_elements],
|
|
43
|
+
"assembly_info": self.assembly_info,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _point_inertia(points: List[Tuple[float, np.ndarray]], reference: np.ndarray) -> np.ndarray:
|
|
48
|
+
inertia = np.zeros((3, 3), dtype=float)
|
|
49
|
+
eye = np.eye(3)
|
|
50
|
+
for mass, coords in points:
|
|
51
|
+
r = np.asarray(coords, dtype=float) - reference
|
|
52
|
+
inertia += float(mass) * ((float(r @ r) * eye) - np.outer(r, r))
|
|
53
|
+
return inertia
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _rigid_body_modes(model: "FEModel", reference: np.ndarray) -> np.ndarray:
|
|
57
|
+
total_dofs = model.mesh.dof_manager.total_dofs
|
|
58
|
+
modes = np.zeros((total_dofs, 6), dtype=float)
|
|
59
|
+
for node in model.mesh.nodes.values():
|
|
60
|
+
x, y, z = node.coords() - reference
|
|
61
|
+
ux, uy, uz, rx, ry, rz = node.dofs[:6]
|
|
62
|
+
modes[ux, 0] = 1.0
|
|
63
|
+
modes[uy, 1] = 1.0
|
|
64
|
+
modes[uz, 2] = 1.0
|
|
65
|
+
|
|
66
|
+
modes[uy, 3] = -z
|
|
67
|
+
modes[uz, 3] = y
|
|
68
|
+
modes[rx, 3] = 1.0
|
|
69
|
+
|
|
70
|
+
modes[ux, 4] = z
|
|
71
|
+
modes[uz, 4] = -x
|
|
72
|
+
modes[ry, 4] = 1.0
|
|
73
|
+
|
|
74
|
+
modes[ux, 5] = -y
|
|
75
|
+
modes[uy, 5] = x
|
|
76
|
+
modes[rz, 5] = 1.0
|
|
77
|
+
return modes
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _shell_mass_points(model: "FEModel", element: ShellElement) -> List[Tuple[float, np.ndarray]]:
|
|
81
|
+
material = model.get_material(element.material_name)
|
|
82
|
+
coords = element.get_node_coordinates(model.mesh)
|
|
83
|
+
points: List[Tuple[float, np.ndarray]] = []
|
|
84
|
+
for (xi, eta), weight in zip(element.gauss_points, element.gauss_weights):
|
|
85
|
+
N, dN_dxi, dN_deta = element.compute_shape_functions(float(xi), float(eta))
|
|
86
|
+
_, _, _, det_j = element._local_frame_and_derivatives(coords, dN_dxi, dN_deta)
|
|
87
|
+
mass = float(material.density) * float(element.thickness) * float(det_j) * float(weight)
|
|
88
|
+
points.append((mass, np.asarray(N @ coords, dtype=float)))
|
|
89
|
+
return points
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _beam_mass_points(model: "FEModel", element: BeamElement) -> List[Tuple[float, np.ndarray]]:
|
|
93
|
+
material = model.get_material(element.material_name)
|
|
94
|
+
coords = element.get_node_coordinates(model.mesh)
|
|
95
|
+
if isinstance(element, QuadraticBeamElement):
|
|
96
|
+
length = float(np.linalg.norm(coords[2] - coords[0]))
|
|
97
|
+
points = []
|
|
98
|
+
for xi, weight in zip(element.GAUSS_POINTS, element.GAUSS_WEIGHTS):
|
|
99
|
+
N, _ = element.compute_shape_functions(float(xi))
|
|
100
|
+
mass = float(material.density) * float(element._A) * length / 2.0 * float(weight)
|
|
101
|
+
points.append((mass, np.asarray(N @ coords, dtype=float)))
|
|
102
|
+
return points
|
|
103
|
+
length = float(np.linalg.norm(coords[1] - coords[0]))
|
|
104
|
+
xi_values = (-1.0 / np.sqrt(3.0), 1.0 / np.sqrt(3.0))
|
|
105
|
+
points = []
|
|
106
|
+
for xi in xi_values:
|
|
107
|
+
N = np.array([(1.0 - xi) / 2.0, (1.0 + xi) / 2.0], dtype=float)
|
|
108
|
+
mass = float(material.density) * float(element._A) * length / 2.0
|
|
109
|
+
points.append((mass, np.asarray(N @ coords, dtype=float)))
|
|
110
|
+
return points
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def element_mass_points(model: "FEModel") -> Tuple[List[Tuple[float, np.ndarray]], List[int]]:
|
|
114
|
+
"""Return quadrature mass points for elements with physical density."""
|
|
115
|
+
points: List[Tuple[float, np.ndarray]] = []
|
|
116
|
+
skipped: List[int] = []
|
|
117
|
+
for elem_id, element in model.mesh.elements.items():
|
|
118
|
+
try:
|
|
119
|
+
if isinstance(element, ShellElement):
|
|
120
|
+
points.extend(_shell_mass_points(model, element))
|
|
121
|
+
elif isinstance(element, BeamElement):
|
|
122
|
+
points.extend(_beam_mass_points(model, element))
|
|
123
|
+
else:
|
|
124
|
+
skipped.append(int(elem_id))
|
|
125
|
+
except Exception:
|
|
126
|
+
skipped.append(int(elem_id))
|
|
127
|
+
return points, skipped
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def calculate_mass_properties(model: "FEModel", reference_point: Any = None) -> MassProperties:
|
|
131
|
+
"""Calculate integrated mass properties and assembled rigid-body mass.
|
|
132
|
+
|
|
133
|
+
The scalar mass, first moment and inertia tensors are obtained from
|
|
134
|
+
element quadrature plus model point masses. The 6x6 rigid-body mass matrix
|
|
135
|
+
is additionally formed from the assembled mass matrix using rigid
|
|
136
|
+
translation/rotation fields, so shell and beam rotary inertia terms are
|
|
137
|
+
visible in diagnostics.
|
|
138
|
+
"""
|
|
139
|
+
points, skipped = element_mass_points(model)
|
|
140
|
+
for node_id, mass in getattr(model.mesh, "point_masses", {}).items():
|
|
141
|
+
node = model.mesh.get_node(int(node_id))
|
|
142
|
+
if node is None:
|
|
143
|
+
raise ValueError(f"Point mass references missing node {node_id}")
|
|
144
|
+
mass_value = float(mass)
|
|
145
|
+
if not np.isfinite(mass_value) or mass_value < 0.0:
|
|
146
|
+
raise ValueError(f"Point mass at node {node_id} must be finite and non-negative")
|
|
147
|
+
if mass_value > 0.0:
|
|
148
|
+
points.append((mass_value, node.coords()))
|
|
149
|
+
total_mass = float(sum(mass for mass, _ in points))
|
|
150
|
+
first_moment = np.zeros(3, dtype=float)
|
|
151
|
+
for mass, coords in points:
|
|
152
|
+
first_moment += float(mass) * np.asarray(coords, dtype=float)
|
|
153
|
+
center = first_moment / total_mass if total_mass > 0.0 else np.zeros(3, dtype=float)
|
|
154
|
+
reference = center if reference_point is None else np.asarray(reference_point, dtype=float).reshape(3)
|
|
155
|
+
|
|
156
|
+
M, assembly_info = assemble_mass_matrix(model)
|
|
157
|
+
modes = _rigid_body_modes(model, reference)
|
|
158
|
+
rbm = np.asarray(modes.T @ (M @ modes), dtype=float)
|
|
159
|
+
tx = modes[:, 0]
|
|
160
|
+
ty = modes[:, 1]
|
|
161
|
+
tz = modes[:, 2]
|
|
162
|
+
assembled_translation_masses = {
|
|
163
|
+
"x": float(tx @ (M @ tx)),
|
|
164
|
+
"y": float(ty @ (M @ ty)),
|
|
165
|
+
"z": float(tz @ (M @ tz)),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return MassProperties(
|
|
169
|
+
total_mass=total_mass,
|
|
170
|
+
center_of_mass=center,
|
|
171
|
+
first_moment=first_moment,
|
|
172
|
+
inertia_tensor_origin=_point_inertia(points, np.zeros(3, dtype=float)),
|
|
173
|
+
inertia_tensor_center_of_mass=_point_inertia(points, center),
|
|
174
|
+
rigid_body_mass_matrix=rbm,
|
|
175
|
+
assembled_translation_masses=assembled_translation_masses,
|
|
176
|
+
num_mass_points=len(points),
|
|
177
|
+
skipped_elements=skipped,
|
|
178
|
+
assembly_info=assembly_info,
|
|
179
|
+
)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Nonlinear material hardening curves.
|
|
2
|
+
|
|
3
|
+
Implements the DNV-RP-C208 (September 2019, amended October 2022) section
|
|
4
|
+
4.6.6 recommendation: the flow stress is a function of true plastic strain
|
|
5
|
+
built from a stepwise linear part with a yield plateau and a power-law part:
|
|
6
|
+
|
|
7
|
+
Part 1: sigma_prop -> sigma_yield over 0 .. eps_p_y1
|
|
8
|
+
Part 2: sigma_yield -> sigma_yield_2 over eps_p_y1 .. eps_p_y2
|
|
9
|
+
Part 3: sigma = K * (eps_p + (sigma_yield_2 / K)**(1/n) - eps_p_y2)**n
|
|
10
|
+
|
|
11
|
+
All evaluations are vectorized over numpy arrays of equivalent plastic
|
|
12
|
+
strain, because the return mapping calls them for every integration point and
|
|
13
|
+
thickness layer at once.
|
|
14
|
+
|
|
15
|
+
The curve is expressed in true stress / true plastic strain exactly as
|
|
16
|
+
tabulated by the RP; the solver consumes it directly as the J2 flow stress.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Dict, Mapping, Optional
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_MPA = 1.0e6
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class FiberSectionPlasticityConfig:
|
|
32
|
+
"""Opt-in beam fiber plasticity configuration.
|
|
33
|
+
|
|
34
|
+
The beam fiber model uses an equivalent rectangular fiber grid whose
|
|
35
|
+
coordinates are scaled to match the supplied section ``A``, ``Iy`` and
|
|
36
|
+
``Iz``. It is intended for monotonic capacity checks of beam/stiffener
|
|
37
|
+
idealizations; shear and torsion remain elastic in v1.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
num_y: int = 5
|
|
41
|
+
num_z: int = 5
|
|
42
|
+
material_curve: Optional["DNVC208MaterialCurve"] = None
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if self.num_y <= 0 or self.num_z <= 0:
|
|
46
|
+
raise ValueError("num_y and num_z must be positive")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class DNVC208MaterialCurve:
|
|
51
|
+
"""DNV-RP-C208 stepwise-linear plus power-law flow curve."""
|
|
52
|
+
|
|
53
|
+
sigma_prop: float
|
|
54
|
+
sigma_yield: float
|
|
55
|
+
sigma_yield_2: float
|
|
56
|
+
eps_p_y1: float
|
|
57
|
+
eps_p_y2: float
|
|
58
|
+
K: float
|
|
59
|
+
n: float
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
if self.sigma_prop <= 0.0:
|
|
63
|
+
raise ValueError("sigma_prop must be positive")
|
|
64
|
+
if self.sigma_yield < self.sigma_prop:
|
|
65
|
+
raise ValueError("sigma_yield must be >= sigma_prop")
|
|
66
|
+
if self.sigma_yield_2 < self.sigma_yield:
|
|
67
|
+
raise ValueError("sigma_yield_2 must be >= sigma_yield")
|
|
68
|
+
if not (0.0 < self.eps_p_y1 < self.eps_p_y2):
|
|
69
|
+
raise ValueError("require 0 < eps_p_y1 < eps_p_y2")
|
|
70
|
+
if self.K <= 0.0 or not (0.0 < self.n < 1.0):
|
|
71
|
+
raise ValueError("require K > 0 and 0 < n < 1")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def _power_offset(self) -> float:
|
|
75
|
+
"""Plastic-strain offset making Part 3 continuous at eps_p_y2."""
|
|
76
|
+
return (self.sigma_yield_2 / self.K) ** (1.0 / self.n) - self.eps_p_y2
|
|
77
|
+
|
|
78
|
+
def flow_stress(self, eps_p: np.ndarray) -> np.ndarray:
|
|
79
|
+
"""Flow (yield) stress as a function of equivalent plastic strain."""
|
|
80
|
+
eps_p = np.maximum(np.asarray(eps_p, dtype=float), 0.0)
|
|
81
|
+
slope_1 = (self.sigma_yield - self.sigma_prop) / self.eps_p_y1
|
|
82
|
+
slope_2 = (self.sigma_yield_2 - self.sigma_yield) / (self.eps_p_y2 - self.eps_p_y1)
|
|
83
|
+
part_1 = self.sigma_prop + slope_1 * eps_p
|
|
84
|
+
part_2 = self.sigma_yield + slope_2 * (eps_p - self.eps_p_y1)
|
|
85
|
+
part_3 = self.K * np.power(np.maximum(eps_p + self._power_offset, 1.0e-12), self.n)
|
|
86
|
+
return np.where(
|
|
87
|
+
eps_p <= self.eps_p_y1,
|
|
88
|
+
part_1,
|
|
89
|
+
np.where(eps_p <= self.eps_p_y2, part_2, part_3),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def hardening_modulus(self, eps_p: np.ndarray) -> np.ndarray:
|
|
93
|
+
"""d(flow stress)/d(equivalent plastic strain)."""
|
|
94
|
+
eps_p = np.maximum(np.asarray(eps_p, dtype=float), 0.0)
|
|
95
|
+
slope_1 = (self.sigma_yield - self.sigma_prop) / self.eps_p_y1
|
|
96
|
+
slope_2 = (self.sigma_yield_2 - self.sigma_yield) / (self.eps_p_y2 - self.eps_p_y1)
|
|
97
|
+
base = np.maximum(eps_p + self._power_offset, 1.0e-12)
|
|
98
|
+
slope_3 = self.K * self.n * np.power(base, self.n - 1.0)
|
|
99
|
+
return np.where(
|
|
100
|
+
eps_p <= self.eps_p_y1,
|
|
101
|
+
slope_1,
|
|
102
|
+
np.where(eps_p <= self.eps_p_y2, slope_2, slope_3),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def curve_from_properties(properties: dict) -> DNVC208MaterialCurve:
|
|
107
|
+
"""Build a curve from an RP-C208 table row (e.g. Table 4-2 .. 4-6)."""
|
|
108
|
+
return DNVC208MaterialCurve(
|
|
109
|
+
sigma_prop=float(properties["sigma_prop"]),
|
|
110
|
+
sigma_yield=float(properties["sigma_yield"]),
|
|
111
|
+
sigma_yield_2=float(properties["sigma_yield_2"]),
|
|
112
|
+
eps_p_y1=float(properties.get("eps_p_y1", 0.004)),
|
|
113
|
+
eps_p_y2=float(properties["eps_p_y2"]),
|
|
114
|
+
K=float(properties["K"]),
|
|
115
|
+
n=float(properties["n"]),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_DNV_C208_LOW_FRACTILE_STEEL_MPA: Dict[str, tuple[tuple[float, float, Mapping[str, float]], ...]] = {
|
|
120
|
+
"S235": (
|
|
121
|
+
(0.0, 16.0, {"sigma_prop": 211.7, "sigma_yield": 236.2, "sigma_yield_2": 243.4, "eps_p_y2": 0.020, "K": 520.0, "n": 0.166}),
|
|
122
|
+
(16.0, 40.0, {"sigma_prop": 202.7, "sigma_yield": 226.1, "sigma_yield_2": 233.2, "eps_p_y2": 0.020, "K": 520.0, "n": 0.166}),
|
|
123
|
+
(40.0, 63.0, {"sigma_prop": 193.7, "sigma_yield": 216.1, "sigma_yield_2": 223.0, "eps_p_y2": 0.020, "K": 520.0, "n": 0.166}),
|
|
124
|
+
(63.0, 100.0, {"sigma_prop": 193.7, "sigma_yield": 216.1, "sigma_yield_2": 223.0, "eps_p_y2": 0.020, "K": 520.0, "n": 0.166}),
|
|
125
|
+
),
|
|
126
|
+
"S275": (
|
|
127
|
+
(0.0, 16.0, {"sigma_prop": 247.8, "sigma_yield": 276.5, "sigma_yield_2": 282.8, "eps_p_y2": 0.017, "K": 620.0, "n": 0.166}),
|
|
128
|
+
(16.0, 40.0, {"sigma_prop": 238.8, "sigma_yield": 266.4, "sigma_yield_2": 272.6, "eps_p_y2": 0.017, "K": 620.0, "n": 0.166}),
|
|
129
|
+
(40.0, 63.0, {"sigma_prop": 229.8, "sigma_yield": 256.3, "sigma_yield_2": 262.4, "eps_p_y2": 0.017, "K": 620.0, "n": 0.166}),
|
|
130
|
+
),
|
|
131
|
+
"S355": (
|
|
132
|
+
(0.0, 16.0, {"sigma_prop": 320.0, "sigma_yield": 357.0, "sigma_yield_2": 363.3, "eps_p_y2": 0.015, "K": 740.0, "n": 0.166}),
|
|
133
|
+
(16.0, 40.0, {"sigma_prop": 311.0, "sigma_yield": 346.9, "sigma_yield_2": 353.1, "eps_p_y2": 0.015, "K": 740.0, "n": 0.166}),
|
|
134
|
+
(40.0, 63.0, {"sigma_prop": 301.9, "sigma_yield": 336.9, "sigma_yield_2": 342.9, "eps_p_y2": 0.015, "K": 725.0, "n": 0.166}),
|
|
135
|
+
(63.0, 100.0, {"sigma_prop": 283.9, "sigma_yield": 316.7, "sigma_yield_2": 322.5, "eps_p_y2": 0.015, "K": 725.0, "n": 0.166}),
|
|
136
|
+
),
|
|
137
|
+
"S420": (
|
|
138
|
+
(0.0, 16.0, {"sigma_prop": 378.7, "sigma_yield": 422.5, "sigma_yield_2": 427.6, "eps_p_y2": 0.012, "K": 738.0, "n": 0.140}),
|
|
139
|
+
(16.0, 40.0, {"sigma_prop": 360.6, "sigma_yield": 402.4, "sigma_yield_2": 407.3, "eps_p_y2": 0.012, "K": 703.0, "n": 0.140}),
|
|
140
|
+
(40.0, 63.0, {"sigma_prop": 351.6, "sigma_yield": 392.3, "sigma_yield_2": 397.1, "eps_p_y2": 0.012, "K": 686.0, "n": 0.140}),
|
|
141
|
+
),
|
|
142
|
+
"S460": (
|
|
143
|
+
(0.0, 16.0, {"sigma_prop": 414.8, "sigma_yield": 462.8, "sigma_yield_2": 466.9, "eps_p_y2": 0.010, "K": 772.0, "n": 0.120}),
|
|
144
|
+
(16.0, 40.0, {"sigma_prop": 396.7, "sigma_yield": 442.7, "sigma_yield_2": 446.6, "eps_p_y2": 0.010, "K": 745.0, "n": 0.120}),
|
|
145
|
+
(40.0, 63.0, {"sigma_prop": 374.2, "sigma_yield": 417.5, "sigma_yield_2": 421.2, "eps_p_y2": 0.010, "K": 703.0, "n": 0.120}),
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _thickness_class_label(lower_mm: float, upper_mm: float) -> str:
|
|
151
|
+
if lower_mm <= 0.0:
|
|
152
|
+
return f"t <= {upper_mm:g}"
|
|
153
|
+
return f"{lower_mm:g} < t <= {upper_mm:g}"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _normalized_thickness_class(value: str) -> str:
|
|
157
|
+
return str(value or "auto").strip().lower().replace(" ", "")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def dnv_c208_steel_properties(
|
|
161
|
+
grade: str,
|
|
162
|
+
thickness: float,
|
|
163
|
+
thickness_class: str = "auto",
|
|
164
|
+
fractile: str = "low",
|
|
165
|
+
) -> Dict[str, float | str]:
|
|
166
|
+
"""Return one validated RP-C208 low-fractile steel table row in SI units.
|
|
167
|
+
|
|
168
|
+
``thickness`` is in metres, matching solver SI units. The built-in table
|
|
169
|
+
covers the RP-C208 low-fractile curves from section 4.6.6. Mean curves are
|
|
170
|
+
intentionally not guessed; pass explicit properties through
|
|
171
|
+
``curve_from_properties`` if mean data is required.
|
|
172
|
+
|
|
173
|
+
Automatic selection fails closed when the thickness is outside the table.
|
|
174
|
+
An explicit ``thickness_class`` may select a table row intentionally.
|
|
175
|
+
"""
|
|
176
|
+
if fractile.lower() not in {"low", "low_fractile", "5%", "5_percent"}:
|
|
177
|
+
raise NotImplementedError("Built-in RP-C208 mean curves are not available; supply explicit curve properties")
|
|
178
|
+
grade_key = grade.upper().replace(" ", "")
|
|
179
|
+
if grade_key not in _DNV_C208_LOW_FRACTILE_STEEL_MPA:
|
|
180
|
+
raise ValueError(f"Unsupported RP-C208 steel grade {grade!r}; use one of {sorted(_DNV_C208_LOW_FRACTILE_STEEL_MPA)}")
|
|
181
|
+
thickness_mm = float(thickness) * 1000.0
|
|
182
|
+
if thickness_mm <= 0.0:
|
|
183
|
+
raise ValueError("thickness must be positive")
|
|
184
|
+
rows = _DNV_C208_LOW_FRACTILE_STEEL_MPA[grade_key]
|
|
185
|
+
selected: Optional[tuple[float, float, Mapping[str, float]]] = None
|
|
186
|
+
class_key = _normalized_thickness_class(thickness_class)
|
|
187
|
+
automatic = class_key in {"auto", "automatic", "bythickness", "autobyplatethickness"}
|
|
188
|
+
if automatic:
|
|
189
|
+
for row in rows:
|
|
190
|
+
lower, upper, _properties_mpa = row
|
|
191
|
+
if lower < thickness_mm <= upper:
|
|
192
|
+
selected = row
|
|
193
|
+
break
|
|
194
|
+
if selected is None:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Thickness {thickness_mm:g} mm is outside the built-in RP-C208 range "
|
|
197
|
+
f"for {grade_key} (maximum {rows[-1][1]:g} mm)"
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
for row in rows:
|
|
201
|
+
lower, upper, _properties_mpa = row
|
|
202
|
+
if class_key == _normalized_thickness_class(_thickness_class_label(lower, upper)):
|
|
203
|
+
selected = row
|
|
204
|
+
break
|
|
205
|
+
if selected is None:
|
|
206
|
+
labels = [_thickness_class_label(lower, upper) for lower, upper, _properties in rows]
|
|
207
|
+
raise ValueError(f"Unsupported thickness_class {thickness_class!r} for {grade_key}; use one of {labels}")
|
|
208
|
+
|
|
209
|
+
lower, upper, selected_mpa = selected
|
|
210
|
+
properties = {
|
|
211
|
+
"grade": grade_key,
|
|
212
|
+
"thickness_class": _thickness_class_label(lower, upper),
|
|
213
|
+
"thickness_mm": thickness_mm,
|
|
214
|
+
"source": "DNV-RP-C208 section 4.6.6 low-fractile true stress-strain values",
|
|
215
|
+
"E_pa": 210_000.0 * _MPA,
|
|
216
|
+
"sigma_prop": float(selected_mpa["sigma_prop"]) * _MPA,
|
|
217
|
+
"sigma_yield": float(selected_mpa["sigma_yield"]) * _MPA,
|
|
218
|
+
"sigma_yield_2": float(selected_mpa["sigma_yield_2"]) * _MPA,
|
|
219
|
+
"eps_p_y1": float(selected_mpa.get("eps_p_y1", 0.004)),
|
|
220
|
+
"eps_p_y2": float(selected_mpa["eps_p_y2"]),
|
|
221
|
+
"K": float(selected_mpa["K"]) * _MPA,
|
|
222
|
+
"n": float(selected_mpa["n"]),
|
|
223
|
+
}
|
|
224
|
+
return properties
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def dnv_c208_steel_curve(grade: str, thickness: float, fractile: str = "low") -> DNVC208MaterialCurve:
|
|
228
|
+
"""Return a validated RP-C208 steel curve for a grade and plate thickness."""
|
|
229
|
+
|
|
230
|
+
properties = dnv_c208_steel_properties(grade, thickness, fractile=fractile)
|
|
231
|
+
return curve_from_properties(properties)
|