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,314 @@
|
|
|
1
|
+
"""Plasticity and nonlinear tangent qualification metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import platform
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .elements import BeamElement, ShellElement
|
|
14
|
+
from .fe_core import FEModel
|
|
15
|
+
from .material_curves import DNVC208MaterialCurve, FiberSectionPlasticityConfig, dnv_c208_steel_curve
|
|
16
|
+
from .plasticity import plane_stress_elastic_matrix, plane_stress_return_map
|
|
17
|
+
|
|
18
|
+
DEFAULT_PLASTICITY_QUALIFICATION_PATH = Path("reports/plasticity_qualification/plasticity_qualification_report.json")
|
|
19
|
+
|
|
20
|
+
E_STEEL = 210.0e9
|
|
21
|
+
NU_STEEL = 0.3
|
|
22
|
+
|
|
23
|
+
_P_MATRIX = np.array(
|
|
24
|
+
[[2.0, -1.0, 0.0], [-1.0, 2.0, 0.0], [0.0, 0.0, 6.0]],
|
|
25
|
+
dtype=float,
|
|
26
|
+
) / 3.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def reference_plastic_curve() -> DNVC208MaterialCurve:
|
|
30
|
+
"""Nearly elastic-perfectly plastic reference curve used by local checks."""
|
|
31
|
+
return DNVC208MaterialCurve(
|
|
32
|
+
sigma_prop=354.0e6,
|
|
33
|
+
sigma_yield=355.0e6,
|
|
34
|
+
sigma_yield_2=355.5e6,
|
|
35
|
+
eps_p_y1=0.004,
|
|
36
|
+
eps_p_y2=0.1,
|
|
37
|
+
K=400.0e6,
|
|
38
|
+
n=0.2,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def yield_function_residual(stress: np.ndarray, alpha: float, curve: DNVC208MaterialCurve) -> float:
|
|
43
|
+
"""Scaled plane-stress J2 yield residual."""
|
|
44
|
+
sigma = np.asarray(stress, dtype=float).reshape(3)
|
|
45
|
+
sy = float(curve.flow_stress(np.array([float(alpha)], dtype=float))[0])
|
|
46
|
+
residual = 0.5 * float(sigma @ _P_MATRIX @ sigma) - sy**2 / 3.0
|
|
47
|
+
return float(residual / max(sy**2, 1.0))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _material_tangent_fd_error(
|
|
51
|
+
strain: np.ndarray,
|
|
52
|
+
curve: DNVC208MaterialCurve,
|
|
53
|
+
plastic: np.ndarray | None = None,
|
|
54
|
+
alpha: np.ndarray | None = None,
|
|
55
|
+
step: float = 1.0e-7,
|
|
56
|
+
) -> Dict[str, Any]:
|
|
57
|
+
strain = np.asarray(strain, dtype=float).reshape(1, 3)
|
|
58
|
+
plastic = np.zeros_like(strain) if plastic is None else np.asarray(plastic, dtype=float).reshape(1, 3)
|
|
59
|
+
alpha = np.zeros(1, dtype=float) if alpha is None else np.asarray(alpha, dtype=float).reshape(1)
|
|
60
|
+
stress, tangent, plastic_new, alpha_new = plane_stress_return_map(
|
|
61
|
+
strain, plastic, alpha, E_STEEL, NU_STEEL, curve
|
|
62
|
+
)
|
|
63
|
+
fd = np.zeros((3, 3), dtype=float)
|
|
64
|
+
for col in range(3):
|
|
65
|
+
perturb = np.zeros_like(strain)
|
|
66
|
+
perturb[0, col] = step
|
|
67
|
+
sp = plane_stress_return_map(strain + perturb, plastic, alpha, E_STEEL, NU_STEEL, curve)[0][0]
|
|
68
|
+
sm = plane_stress_return_map(strain - perturb, plastic, alpha, E_STEEL, NU_STEEL, curve)[0][0]
|
|
69
|
+
fd[:, col] = (sp - sm) / (2.0 * step)
|
|
70
|
+
error = float(np.linalg.norm(tangent[0] - fd) / max(np.linalg.norm(fd), 1.0))
|
|
71
|
+
return {
|
|
72
|
+
"stress": stress[0].tolist(),
|
|
73
|
+
"alpha": float(alpha_new[0]),
|
|
74
|
+
"max_plastic_strain_component": float(np.max(np.abs(plastic_new))),
|
|
75
|
+
"yield_residual": yield_function_residual(stress[0], float(alpha_new[0]), curve),
|
|
76
|
+
"tangent_fd_relative_error": error,
|
|
77
|
+
"tangent_status": "tight" if error < 1.0e-3 else "diagnostic_current_continuum_tangent",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def material_point_path_metrics() -> Dict[str, Any]:
|
|
82
|
+
"""Qualify return-map yield consistency over representative strain paths."""
|
|
83
|
+
curve = reference_plastic_curve()
|
|
84
|
+
elastic_curve = None
|
|
85
|
+
elastic_strain = np.array([[1.0e-4, -0.5e-4, 0.25e-4]], dtype=float)
|
|
86
|
+
elastic_stress, elastic_tangent, _, _ = plane_stress_return_map(
|
|
87
|
+
elastic_strain, np.zeros_like(elastic_strain), np.zeros(1), E_STEEL, NU_STEEL, elastic_curve
|
|
88
|
+
)
|
|
89
|
+
elastic_expected = elastic_strain @ plane_stress_elastic_matrix(E_STEEL, NU_STEEL).T
|
|
90
|
+
|
|
91
|
+
paths = {
|
|
92
|
+
"uniaxial": _material_tangent_fd_error(np.array([0.002, 0.0, 0.0]), curve),
|
|
93
|
+
"biaxial_shear": _material_tangent_fd_error(np.array([0.003, 0.001, 0.0005]), curve),
|
|
94
|
+
"pure_shear": _material_tangent_fd_error(np.array([0.0, 0.0, 0.004]), curve),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
first = _material_tangent_fd_error(np.array([0.003, 0.0, 0.0]), curve)
|
|
98
|
+
plastic_old = np.asarray(first["stress"], dtype=float).reshape(1, 3) * 0.0
|
|
99
|
+
stress, _tangent, plastic_new, alpha_new = plane_stress_return_map(
|
|
100
|
+
np.array([[0.003, 0.0, 0.0]], dtype=float),
|
|
101
|
+
np.zeros((1, 3), dtype=float),
|
|
102
|
+
np.zeros(1, dtype=float),
|
|
103
|
+
E_STEEL,
|
|
104
|
+
NU_STEEL,
|
|
105
|
+
curve,
|
|
106
|
+
)
|
|
107
|
+
unload_stress, _, _, unload_alpha = plane_stress_return_map(
|
|
108
|
+
np.array([[0.001, 0.0, 0.0]], dtype=float),
|
|
109
|
+
plastic_new,
|
|
110
|
+
alpha_new,
|
|
111
|
+
E_STEEL,
|
|
112
|
+
NU_STEEL,
|
|
113
|
+
curve,
|
|
114
|
+
)
|
|
115
|
+
paths["unload_from_plastic"] = {
|
|
116
|
+
"stress": unload_stress[0].tolist(),
|
|
117
|
+
"alpha": float(unload_alpha[0]),
|
|
118
|
+
"alpha_change": float(unload_alpha[0] - alpha_new[0]),
|
|
119
|
+
"yield_residual": yield_function_residual(stress[0], float(alpha_new[0]), curve),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"elastic": {
|
|
124
|
+
"stress_relative_error": float(np.linalg.norm(elastic_stress - elastic_expected) / max(np.linalg.norm(elastic_expected), 1.0)),
|
|
125
|
+
"tangent_relative_error": float(
|
|
126
|
+
np.linalg.norm(elastic_tangent[0] - plane_stress_elastic_matrix(E_STEEL, NU_STEEL))
|
|
127
|
+
/ max(np.linalg.norm(plane_stress_elastic_matrix(E_STEEL, NU_STEEL)), 1.0)
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
"plastic_paths": paths,
|
|
131
|
+
"max_abs_yield_residual": max(abs(path.get("yield_residual", 0.0)) for path in paths.values()),
|
|
132
|
+
"max_material_tangent_fd_error": max(
|
|
133
|
+
path.get("tangent_fd_relative_error", 0.0) for path in paths.values()
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _finite_difference_element_tangent(
|
|
139
|
+
element: Any,
|
|
140
|
+
model: FEModel,
|
|
141
|
+
u_elem: np.ndarray,
|
|
142
|
+
state: Any = None,
|
|
143
|
+
num_layers: int = 5,
|
|
144
|
+
step: float = 1.0e-7,
|
|
145
|
+
) -> Dict[str, Any]:
|
|
146
|
+
material = model.get_material(element.material_name)
|
|
147
|
+
f, K, trial_state = element.compute_nonlinear_response(
|
|
148
|
+
model.mesh, material, u_elem, state, num_layers=num_layers, tangent=True
|
|
149
|
+
)
|
|
150
|
+
fd = np.zeros_like(K)
|
|
151
|
+
for col in range(K.shape[1]):
|
|
152
|
+
perturb = np.zeros_like(u_elem)
|
|
153
|
+
perturb[col] = step
|
|
154
|
+
fp = element.compute_nonlinear_response(
|
|
155
|
+
model.mesh, material, u_elem + perturb, state, num_layers=num_layers, tangent=False
|
|
156
|
+
)[0]
|
|
157
|
+
fm = element.compute_nonlinear_response(
|
|
158
|
+
model.mesh, material, u_elem - perturb, state, num_layers=num_layers, tangent=False
|
|
159
|
+
)[0]
|
|
160
|
+
fd[:, col] = (fp - fm) / (2.0 * step)
|
|
161
|
+
error = float(np.linalg.norm(K - fd) / max(np.linalg.norm(fd), 1.0))
|
|
162
|
+
return {
|
|
163
|
+
"tangent_fd_relative_error": error,
|
|
164
|
+
"force_norm": float(np.linalg.norm(f)),
|
|
165
|
+
"tangent_norm": float(np.linalg.norm(K)),
|
|
166
|
+
"fd_tangent_norm": float(np.linalg.norm(fd)),
|
|
167
|
+
"state_summary": _state_summary(trial_state),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _state_summary(state: Any) -> Dict[str, Any]:
|
|
172
|
+
if not isinstance(state, dict):
|
|
173
|
+
return {}
|
|
174
|
+
summary: Dict[str, Any] = {}
|
|
175
|
+
for key in ("alpha", "plastic_strain", "layer_strain", "fiber_strain"):
|
|
176
|
+
value = np.asarray(state.get(key, []), dtype=float)
|
|
177
|
+
if value.size:
|
|
178
|
+
summary[f"{key}_max"] = float(np.max(value))
|
|
179
|
+
summary[f"{key}_min"] = float(np.min(value))
|
|
180
|
+
summary[f"{key}_max_abs"] = float(np.max(np.abs(value)))
|
|
181
|
+
if "axial_force" in state:
|
|
182
|
+
summary["axial_force"] = float(state["axial_force"])
|
|
183
|
+
return summary
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _beam_model(curve: DNVC208MaterialCurve | None = None, fiber: bool = False) -> Tuple[FEModel, BeamElement]:
|
|
187
|
+
model = FEModel("beam_tangent_metric")
|
|
188
|
+
model.add_material("steel", E_STEEL, NU_STEEL, hardening_curve=curve)
|
|
189
|
+
model.add_node(1, 0.0, 0.0, 0.0)
|
|
190
|
+
model.add_node(2, 1.0, 0.0, 0.0)
|
|
191
|
+
section = {"area": 0.01, "Iy": 1.0e-5, "Iz": 1.0e-5, "J": 1.0e-5}
|
|
192
|
+
if fiber:
|
|
193
|
+
section["fiber_plasticity"] = FiberSectionPlasticityConfig(5, 5)
|
|
194
|
+
element = BeamElement(1, [1, 2], "steel", section)
|
|
195
|
+
model.add_element(1, element)
|
|
196
|
+
return model, element
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _shell_model(curve: DNVC208MaterialCurve | None = None) -> Tuple[FEModel, ShellElement]:
|
|
200
|
+
model = FEModel("shell_tangent_metric")
|
|
201
|
+
model.add_material("steel", E_STEEL, NU_STEEL, hardening_curve=curve)
|
|
202
|
+
for node_id, coord in enumerate(((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)), start=1):
|
|
203
|
+
model.add_node(node_id, *coord)
|
|
204
|
+
element = ShellElement(1, [1, 2, 3, 4], "steel", 0.01)
|
|
205
|
+
model.add_element(1, element)
|
|
206
|
+
return model, element
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def element_tangent_metrics() -> Dict[str, Any]:
|
|
210
|
+
curve = reference_plastic_curve()
|
|
211
|
+
|
|
212
|
+
beam_elastic_model, beam_elastic = _beam_model()
|
|
213
|
+
u_beam_elastic = np.zeros(12, dtype=float)
|
|
214
|
+
u_beam_elastic[6] = 0.001
|
|
215
|
+
u_beam_elastic[7] = 0.0003
|
|
216
|
+
beam_elastic_metric = _finite_difference_element_tangent(beam_elastic, beam_elastic_model, u_beam_elastic)
|
|
217
|
+
|
|
218
|
+
beam_plastic_model, beam_plastic = _beam_model(curve, fiber=True)
|
|
219
|
+
u_beam_plastic = np.zeros(12, dtype=float)
|
|
220
|
+
u_beam_plastic[6] = 0.006
|
|
221
|
+
u_beam_plastic[7] = 0.0003
|
|
222
|
+
beam_plastic_metric = _finite_difference_element_tangent(beam_plastic, beam_plastic_model, u_beam_plastic)
|
|
223
|
+
|
|
224
|
+
shell_elastic_model, shell_elastic = _shell_model()
|
|
225
|
+
u_shell_elastic = np.zeros(24, dtype=float)
|
|
226
|
+
u_shell_elastic[2::6] = [0.0, 0.001, 0.001, 0.0]
|
|
227
|
+
u_shell_elastic[4::6] = [0.001, 0.001, 0.001, 0.001]
|
|
228
|
+
shell_elastic_metric = _finite_difference_element_tangent(
|
|
229
|
+
shell_elastic, shell_elastic_model, u_shell_elastic, step=1.0e-8
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
shell_plastic_model, shell_plastic = _shell_model(curve)
|
|
233
|
+
u_shell_plastic = np.zeros(24, dtype=float)
|
|
234
|
+
coords = shell_plastic.get_node_coordinates(shell_plastic_model.mesh)
|
|
235
|
+
for local, coord in enumerate(coords):
|
|
236
|
+
x, y, _ = coord
|
|
237
|
+
base = local * 6
|
|
238
|
+
u_shell_plastic[base + 0] = 0.003 * x
|
|
239
|
+
u_shell_plastic[base + 1] = -0.0008 * y
|
|
240
|
+
u_shell_plastic[base + 4] = 0.002 * x
|
|
241
|
+
shell_plastic_metric = _finite_difference_element_tangent(
|
|
242
|
+
shell_plastic, shell_plastic_model, u_shell_plastic, num_layers=5
|
|
243
|
+
)
|
|
244
|
+
shell_plastic_metric["tangent_status"] = (
|
|
245
|
+
"tight" if shell_plastic_metric["tangent_fd_relative_error"] < 1.0e-4 else "diagnostic_high_tangent_error"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
max_algorithmic_error = max(
|
|
249
|
+
beam_elastic_metric["tangent_fd_relative_error"],
|
|
250
|
+
beam_plastic_metric["tangent_fd_relative_error"],
|
|
251
|
+
shell_elastic_metric["tangent_fd_relative_error"],
|
|
252
|
+
shell_plastic_metric["tangent_fd_relative_error"],
|
|
253
|
+
)
|
|
254
|
+
return {
|
|
255
|
+
"beam_elastic": beam_elastic_metric,
|
|
256
|
+
"beam_fiber_plastic": beam_plastic_metric,
|
|
257
|
+
"shell_elastic": shell_elastic_metric,
|
|
258
|
+
"shell_layered_plastic": shell_plastic_metric,
|
|
259
|
+
"max_tight_tangent_error": max(
|
|
260
|
+
beam_elastic_metric["tangent_fd_relative_error"],
|
|
261
|
+
beam_plastic_metric["tangent_fd_relative_error"],
|
|
262
|
+
shell_elastic_metric["tangent_fd_relative_error"],
|
|
263
|
+
),
|
|
264
|
+
"max_algorithmic_tangent_error": max_algorithmic_error,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def dnv_curve_metric() -> Dict[str, Any]:
|
|
269
|
+
curves = {}
|
|
270
|
+
for grade, thickness in (("S355", 0.010), ("S420", 0.020), ("S460", 0.050)):
|
|
271
|
+
curve = dnv_c208_steel_curve(grade, thickness)
|
|
272
|
+
curves[f"{grade}_{thickness:g}"] = {
|
|
273
|
+
"sigma_prop": curve.sigma_prop,
|
|
274
|
+
"sigma_yield": curve.sigma_yield,
|
|
275
|
+
"sigma_yield_2": curve.sigma_yield_2,
|
|
276
|
+
"eps_p_y1": curve.eps_p_y1,
|
|
277
|
+
"eps_p_y2": curve.eps_p_y2,
|
|
278
|
+
"K": curve.K,
|
|
279
|
+
"n": curve.n,
|
|
280
|
+
}
|
|
281
|
+
return curves
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def generate_plasticity_qualification_report() -> Dict[str, Any]:
|
|
285
|
+
material = material_point_path_metrics()
|
|
286
|
+
element = element_tangent_metrics()
|
|
287
|
+
return {
|
|
288
|
+
"schema_version": 1,
|
|
289
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
290
|
+
"environment": {
|
|
291
|
+
"platform": platform.platform(),
|
|
292
|
+
"python": platform.python_version(),
|
|
293
|
+
"numpy": np.__version__,
|
|
294
|
+
},
|
|
295
|
+
"dnv_curves": dnv_curve_metric(),
|
|
296
|
+
"material_point": material,
|
|
297
|
+
"element_tangents": element,
|
|
298
|
+
"status": "passed"
|
|
299
|
+
if material["max_abs_yield_residual"] < 1.0e-8 and element["max_algorithmic_tangent_error"] < 1.0e-4
|
|
300
|
+
else "diagnostic",
|
|
301
|
+
"known_limitations": [
|
|
302
|
+
"Plastic material tangents are now consistent numerical algorithmic tangents of the discrete return map.",
|
|
303
|
+
"The numerical tangent is intentionally correctness-first and more expensive than a closed-form analytical algorithmic tangent.",
|
|
304
|
+
"A future speed batch should replace the numerical tangent with an analytical derivative after preserving these finite-difference checks.",
|
|
305
|
+
],
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def write_plasticity_qualification_report(path: Path | str = DEFAULT_PLASTICITY_QUALIFICATION_PATH) -> Dict[str, Any]:
|
|
310
|
+
report = generate_plasticity_qualification_report()
|
|
311
|
+
output = Path(path)
|
|
312
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
314
|
+
return report
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Production-readiness artifacts tied to verification release gates.
|
|
2
|
+
|
|
3
|
+
These helpers implement the safe part of the post-verification production plan:
|
|
4
|
+
capability and scope reporting. They deliberately do not create a qualification
|
|
5
|
+
tag or claim a production release while verification gates remain blocked.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
14
|
+
|
|
15
|
+
from .beam_shell_verification import run_beam_shell_verification
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_PRODUCTION_READINESS_DIR = Path("reports/production_readiness/current")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class CapabilityEntry:
|
|
23
|
+
feature: str
|
|
24
|
+
status: str
|
|
25
|
+
release_gate: str
|
|
26
|
+
verification_cases: List[str]
|
|
27
|
+
limits: Dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
limitations: List[str] = field(default_factory=list)
|
|
29
|
+
required_solver_configuration: Dict[str, Any] = field(default_factory=dict)
|
|
30
|
+
gate_blockers: List[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _gate(report: Mapping[str, Any], name: str) -> Mapping[str, Any]:
|
|
37
|
+
gates = report.get("release_gates") or {}
|
|
38
|
+
value = gates.get(name) or {}
|
|
39
|
+
return value if isinstance(value, Mapping) else {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _gate_blockers(report: Mapping[str, Any], name: str) -> List[str]:
|
|
43
|
+
return [str(item.get("case_id")) for item in (_gate(report, name).get("blockers") or []) if item.get("case_id")]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _status_from_gate(report: Mapping[str, Any], name: str) -> str:
|
|
47
|
+
status = str(_gate(report, name).get("status", "not_evaluated"))
|
|
48
|
+
if status == "passed":
|
|
49
|
+
return "qualified"
|
|
50
|
+
if status == "blocked":
|
|
51
|
+
return "not_qualified"
|
|
52
|
+
return "not_evaluated"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_capability_matrix(report: Optional[Mapping[str, Any]] = None) -> List[CapabilityEntry]:
|
|
56
|
+
"""Return production capability entries derived from verification gates."""
|
|
57
|
+
|
|
58
|
+
report = report or run_beam_shell_verification()
|
|
59
|
+
flat_shell = _status_from_gate(report, "flat_thin_shell")
|
|
60
|
+
flat_stiffened = _status_from_gate(report, "flat_thin_stiffened_shell")
|
|
61
|
+
curved_stiffened = _status_from_gate(report, "curved_thin_stiffened_shell")
|
|
62
|
+
nonlinear = _status_from_gate(report, "nonlinear_capacity")
|
|
63
|
+
contact = _status_from_gate(report, "contact")
|
|
64
|
+
fracture = _status_from_gate(report, "simplified_fracture")
|
|
65
|
+
full_release = _status_from_gate(report, "fully_documented_verified_release")
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
CapabilityEntry(
|
|
69
|
+
feature="flat_thin_shell_linear_static_modal_buckling",
|
|
70
|
+
status=flat_shell,
|
|
71
|
+
release_gate="flat_thin_shell",
|
|
72
|
+
verification_cases=["SHELL-009", "SHELL-010", "SHELL-011"],
|
|
73
|
+
limits={
|
|
74
|
+
"shell_formulations": ["Q4", "Q8"],
|
|
75
|
+
"units": "SI",
|
|
76
|
+
"geometry": "flat thin plates within verified mesh and thickness ranges",
|
|
77
|
+
},
|
|
78
|
+
limitations=[
|
|
79
|
+
"Q8R is experimental pending thickness-robust hourglass and inertia qualification.",
|
|
80
|
+
"Follower pressure, arbitrary contact and unverified material laws are unsupported.",
|
|
81
|
+
],
|
|
82
|
+
required_solver_configuration={"analysis": ["linear_static", "modal", "linear_buckling"]},
|
|
83
|
+
gate_blockers=_gate_blockers(report, "flat_thin_shell"),
|
|
84
|
+
),
|
|
85
|
+
CapabilityEntry(
|
|
86
|
+
feature="flat_thin_stiffened_shell_beam_shell_mpc",
|
|
87
|
+
status=flat_stiffened,
|
|
88
|
+
release_gate="flat_thin_stiffened_shell",
|
|
89
|
+
verification_cases=["COUP-012", "COUP-013", "COUP-014", "COUP-015", "COUP-016", "COUP-017"],
|
|
90
|
+
limits={
|
|
91
|
+
"shell_formulations": ["Q4", "Q8"],
|
|
92
|
+
"beam_formulations": ["B2"],
|
|
93
|
+
"coupling": "interpolated eccentric MPC",
|
|
94
|
+
"stiffeners": "beam stiffeners inside verified eccentricity and mesh-ratio ranges",
|
|
95
|
+
},
|
|
96
|
+
limitations=[
|
|
97
|
+
"Q8R is experimental pending thickness-robust hourglass and inertia qualification.",
|
|
98
|
+
"Equivalent all-shell model-pair checks must pass before production qualification.",
|
|
99
|
+
],
|
|
100
|
+
required_solver_configuration={"member_model": "plates as shell, stiffeners/girders as beams"},
|
|
101
|
+
gate_blockers=_gate_blockers(report, "flat_thin_stiffened_shell"),
|
|
102
|
+
),
|
|
103
|
+
CapabilityEntry(
|
|
104
|
+
feature="curved_thin_stiffened_shell",
|
|
105
|
+
status=curved_stiffened,
|
|
106
|
+
release_gate="curved_thin_stiffened_shell",
|
|
107
|
+
verification_cases=["SHELL-008", "COUP-020", "COUP-021", "CYL-001", "CYL-002", "CYL-003"],
|
|
108
|
+
limits={"geometry": "cylindrical and ring-stiffened shells within verified curvature ranges"},
|
|
109
|
+
limitations=[
|
|
110
|
+
"Curved scope is qualified only for the V3 cylindrical/curved-panel fixtures and documented mesh/curvature ranges."
|
|
111
|
+
],
|
|
112
|
+
required_solver_configuration={"geometry": ["cylinder", "curved_panel"]},
|
|
113
|
+
gate_blockers=_gate_blockers(report, "curved_thin_stiffened_shell"),
|
|
114
|
+
),
|
|
115
|
+
CapabilityEntry(
|
|
116
|
+
feature="nonlinear_capacity_and_buckling_stop",
|
|
117
|
+
status=nonlinear,
|
|
118
|
+
release_gate="nonlinear_capacity",
|
|
119
|
+
verification_cases=["NLG-006", "NLG-007", "NLG-008", "MAT-008", "DYN-001"],
|
|
120
|
+
limits={"nonlinear_scope": "monotonic ultimate capacity with verified buckling-stop diagnostics"},
|
|
121
|
+
limitations=[
|
|
122
|
+
"Unrestricted post-buckling continuation is not a production target.",
|
|
123
|
+
"Follower pressure is unsupported unless NLG-008 passes.",
|
|
124
|
+
],
|
|
125
|
+
required_solver_configuration={"buckling_stop": "required", "post_buckling_target": False},
|
|
126
|
+
gate_blockers=_gate_blockers(report, "nonlinear_capacity"),
|
|
127
|
+
),
|
|
128
|
+
CapabilityEntry(
|
|
129
|
+
feature="limited_rigid_sphere_shell_contact",
|
|
130
|
+
status=contact,
|
|
131
|
+
release_gate="contact",
|
|
132
|
+
verification_cases=["CONTACT-001", "CONTACT-002", "CONTACT-003", "CONTACT-004", "CONTACT-005", "CONTACT-006", "CONTACT-007", "CONTACT-008", "CONTACT-009", "CONTACT-010", "CONTACT-011", "CONTACT-012"],
|
|
133
|
+
limits={
|
|
134
|
+
"contact_scope": "single rigid sphere to shell midsurface or shell-thickness offset surface",
|
|
135
|
+
"contact_law": "frictionless normal penalty",
|
|
136
|
+
"beam_contact": "not direct; beams respond through existing shell-beam coupling",
|
|
137
|
+
"impact_damage": "optional engineering shell damage/erosion; capacity-based mode is gated by FRACT-007..012",
|
|
138
|
+
},
|
|
139
|
+
limitations=[
|
|
140
|
+
"Arbitrary contact, shell-shell contact, direct beam contact, friction, spin, rolling, crack propagation, material nonlinear impact fracture and FSI are unsupported.",
|
|
141
|
+
"Contact remains linear structural transient dynamics plus nonlinear contact-force iteration.",
|
|
142
|
+
],
|
|
143
|
+
required_solver_configuration={"analysis": "sphere_impact_transient", "contact_gate": "CONTACT-001..012"},
|
|
144
|
+
gate_blockers=_gate_blockers(report, "contact"),
|
|
145
|
+
),
|
|
146
|
+
CapabilityEntry(
|
|
147
|
+
feature="simplified_element_erosion_and_impact_damage",
|
|
148
|
+
status=fracture,
|
|
149
|
+
release_gate="simplified_fracture",
|
|
150
|
+
verification_cases=[
|
|
151
|
+
"FRACT-001",
|
|
152
|
+
"FRACT-002",
|
|
153
|
+
"FRACT-003",
|
|
154
|
+
"FRACT-004",
|
|
155
|
+
"FRACT-005",
|
|
156
|
+
"FRACT-006",
|
|
157
|
+
"FRACT-007",
|
|
158
|
+
"FRACT-008",
|
|
159
|
+
"FRACT-009",
|
|
160
|
+
"FRACT-010",
|
|
161
|
+
"FRACT-011",
|
|
162
|
+
"FRACT-012",
|
|
163
|
+
],
|
|
164
|
+
limits={
|
|
165
|
+
"static_fracture_scope": "nonlinear static equivalent-plastic-strain erosion only",
|
|
166
|
+
"impact_damage_scope": "linear sphere-shell transient with engineering capacity-based shell softening/erosion",
|
|
167
|
+
"erosion": "post-converged-increment or post-converged-contact-substep residual-stiffness element erosion",
|
|
168
|
+
},
|
|
169
|
+
limitations=[
|
|
170
|
+
"Not crack propagation, cohesive fracture, remeshing or validated fracture mechanics.",
|
|
171
|
+
"Direct member separation, node deletion, MPC deletion and material nonlinear impact fracture are unsupported.",
|
|
172
|
+
],
|
|
173
|
+
required_solver_configuration={"analysis": ["nonlinear_static", "sphere_impact_transient"], "fracture_gate": "FRACT-001..012"},
|
|
174
|
+
gate_blockers=_gate_blockers(report, "simplified_fracture"),
|
|
175
|
+
),
|
|
176
|
+
CapabilityEntry(
|
|
177
|
+
feature="fully_documented_verified_release",
|
|
178
|
+
status=full_release,
|
|
179
|
+
release_gate="fully_documented_verified_release",
|
|
180
|
+
verification_cases=["V1:*", "V2:*", "V3:*", "V4:*", "V5:*", "MLBC:*", "CONTACT:*", "FRACTURE:*"],
|
|
181
|
+
limits={"claim": "documented ANYsolver beam-shell analysis scope only"},
|
|
182
|
+
limitations=[
|
|
183
|
+
"General-purpose commercial-solver replacement claims are prohibited.",
|
|
184
|
+
"External-reference decks are reproducible handoff artifacts unless executed external-solver result files are supplied.",
|
|
185
|
+
],
|
|
186
|
+
required_solver_configuration={"qualification_evidence": "complete immutable release package"},
|
|
187
|
+
gate_blockers=_gate_blockers(report, "fully_documented_verified_release"),
|
|
188
|
+
),
|
|
189
|
+
CapabilityEntry(
|
|
190
|
+
feature="unsupported_general_purpose_fe",
|
|
191
|
+
status="unsupported",
|
|
192
|
+
release_gate="none",
|
|
193
|
+
verification_cases=[],
|
|
194
|
+
limits={},
|
|
195
|
+
limitations=[
|
|
196
|
+
"Arbitrary CAD topology, arbitrary contact beyond limited sphere-shell impact, fluid-structure interaction, unsupported residual-stress fields and unverified element distortion levels are outside scope.",
|
|
197
|
+
],
|
|
198
|
+
required_solver_configuration={"expert_override": "not sufficient for production qualification"},
|
|
199
|
+
),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def build_verification_scope_statement(report: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]:
|
|
204
|
+
"""Return a concise production scope statement from the live capability matrix."""
|
|
205
|
+
|
|
206
|
+
report = report or run_beam_shell_verification()
|
|
207
|
+
matrix = build_capability_matrix(report)
|
|
208
|
+
by_status: Dict[str, List[str]] = {}
|
|
209
|
+
for entry in matrix:
|
|
210
|
+
by_status.setdefault(entry.status, []).append(entry.feature)
|
|
211
|
+
return {
|
|
212
|
+
"schema_version": 1,
|
|
213
|
+
"production_release_status": "qualified"
|
|
214
|
+
if _status_from_gate(report, "fully_documented_verified_release") == "qualified"
|
|
215
|
+
else "not_qualified",
|
|
216
|
+
"verified": by_status.get("qualified", []),
|
|
217
|
+
"conditionally_supported": by_status.get("not_qualified", []) + by_status.get("not_evaluated", []),
|
|
218
|
+
"experimental": [],
|
|
219
|
+
"unsupported": by_status.get("unsupported", []),
|
|
220
|
+
"explicit_limitations": [
|
|
221
|
+
"Follower pressure unless implemented and verified.",
|
|
222
|
+
"Arbitrary contact beyond limited rigid-sphere-to-shell contact.",
|
|
223
|
+
"Fracture outside simplified nonlinear-static erosion and limited sphere-impact shell damage.",
|
|
224
|
+
"Fluid-structure interaction.",
|
|
225
|
+
"Unrestricted post-buckling continuation.",
|
|
226
|
+
"Arbitrary general-purpose CAD topology.",
|
|
227
|
+
"Unverified material laws.",
|
|
228
|
+
"Unsupported residual-stress fields.",
|
|
229
|
+
"Unsupported element distortion levels.",
|
|
230
|
+
"External-reference decks without executed external-solver result comparisons.",
|
|
231
|
+
],
|
|
232
|
+
"release_gates": {
|
|
233
|
+
name: {
|
|
234
|
+
"status": gate.get("status"),
|
|
235
|
+
"blockers": [item.get("case_id") for item in (gate.get("blockers") or [])],
|
|
236
|
+
}
|
|
237
|
+
for name, gate in (report.get("release_gates") or {}).items()
|
|
238
|
+
if name != "thin_stiffened_shell"
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _markdown_list(items: Iterable[str]) -> List[str]:
|
|
244
|
+
values = [str(item) for item in items]
|
|
245
|
+
return [f"- {item}" for item in values] if values else ["- None"]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def scope_statement_markdown(scope: Mapping[str, Any]) -> str:
|
|
249
|
+
lines = [
|
|
250
|
+
"# ANYsolver Production Scope Statement",
|
|
251
|
+
"",
|
|
252
|
+
f"- Production release status: {scope.get('production_release_status')}",
|
|
253
|
+
"",
|
|
254
|
+
"## Verified",
|
|
255
|
+
"",
|
|
256
|
+
*_markdown_list(scope.get("verified") or []),
|
|
257
|
+
"",
|
|
258
|
+
"## Conditionally Supported",
|
|
259
|
+
"",
|
|
260
|
+
*_markdown_list(scope.get("conditionally_supported") or []),
|
|
261
|
+
"",
|
|
262
|
+
"## Experimental",
|
|
263
|
+
"",
|
|
264
|
+
*_markdown_list(scope.get("experimental") or []),
|
|
265
|
+
"",
|
|
266
|
+
"## Unsupported",
|
|
267
|
+
"",
|
|
268
|
+
*_markdown_list(scope.get("unsupported") or []),
|
|
269
|
+
"",
|
|
270
|
+
"## Explicit Limitations",
|
|
271
|
+
"",
|
|
272
|
+
*_markdown_list(scope.get("explicit_limitations") or []),
|
|
273
|
+
"",
|
|
274
|
+
]
|
|
275
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def write_production_readiness_artifacts(
|
|
279
|
+
output_dir: Path | str = DEFAULT_PRODUCTION_READINESS_DIR,
|
|
280
|
+
*,
|
|
281
|
+
report: Optional[Mapping[str, Any]] = None,
|
|
282
|
+
) -> Dict[str, Any]:
|
|
283
|
+
"""Write capability matrix and scope-statement artifacts."""
|
|
284
|
+
|
|
285
|
+
report = report or run_beam_shell_verification()
|
|
286
|
+
output_path = Path(output_dir)
|
|
287
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
288
|
+
matrix = [entry.to_dict() for entry in build_capability_matrix(report)]
|
|
289
|
+
scope = build_verification_scope_statement(report)
|
|
290
|
+
|
|
291
|
+
matrix_path = output_path / "capability_matrix.json"
|
|
292
|
+
scope_json_path = output_path / "verification_scope.json"
|
|
293
|
+
scope_md_path = output_path / "verification_scope.md"
|
|
294
|
+
matrix_path.write_text(json.dumps(matrix, indent=2, sort_keys=True), encoding="utf-8")
|
|
295
|
+
scope_json_path.write_text(json.dumps(scope, indent=2, sort_keys=True), encoding="utf-8")
|
|
296
|
+
scope_md_path.write_text(scope_statement_markdown(scope), encoding="utf-8")
|
|
297
|
+
return {
|
|
298
|
+
"output_dir": str(output_path),
|
|
299
|
+
"capability_matrix": str(matrix_path),
|
|
300
|
+
"verification_scope_json": str(scope_json_path),
|
|
301
|
+
"verification_scope_markdown": str(scope_md_path),
|
|
302
|
+
"production_release_status": scope["production_release_status"],
|
|
303
|
+
"capability_count": len(matrix),
|
|
304
|
+
}
|