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.
Files changed (70) hide show
  1. anysolver/__init__.py +631 -0
  2. anysolver/anystructure_fem_mode.py +942 -0
  3. anysolver/arc_length.py +758 -0
  4. anysolver/assembly.py +950 -0
  5. anysolver/baselines.py +303 -0
  6. anysolver/beam_shell_verification.py +4276 -0
  7. anysolver/beam_validity.py +110 -0
  8. anysolver/benchmarks.py +495 -0
  9. anysolver/boundary.py +476 -0
  10. anysolver/buckling.py +442 -0
  11. anysolver/buckling_validity.py +91 -0
  12. anysolver/capacity_workflow.py +350 -0
  13. anysolver/cases.py +173 -0
  14. anysolver/composite_strip_verification.py +297 -0
  15. anysolver/contact.py +3461 -0
  16. anysolver/corotational.py +371 -0
  17. anysolver/cylinder_benchmarks.py +364 -0
  18. anysolver/dynamics.py +723 -0
  19. anysolver/element_qualification.py +432 -0
  20. anysolver/elements.py +2724 -0
  21. anysolver/external_references.py +369 -0
  22. anysolver/fe_core.py +327 -0
  23. anysolver/fracture.py +551 -0
  24. anysolver/imperfections.py +390 -0
  25. anysolver/jit_compiler.py +108 -0
  26. anysolver/kernel_warmup.py +180 -0
  27. anysolver/linalg.py +760 -0
  28. anysolver/mass_properties.py +179 -0
  29. anysolver/material_curves.py +231 -0
  30. anysolver/matrix_assembly.py +558 -0
  31. anysolver/mesh_gen.py +1065 -0
  32. anysolver/mesh_load_bc_verification.py +609 -0
  33. anysolver/modal.py +282 -0
  34. anysolver/nonlinear.py +300 -0
  35. anysolver/nonlinear_performance.py +920 -0
  36. anysolver/nonlinear_performance_batch_b.py +592 -0
  37. anysolver/nonlinear_performance_batch_c.py +506 -0
  38. anysolver/nonlinear_performance_bootstrap.py +120 -0
  39. anysolver/nonlinear_reduced_assembly.py +760 -0
  40. anysolver/nonlinear_static.py +1518 -0
  41. anysolver/plasticity.py +419 -0
  42. anysolver/plasticity_qualification.py +314 -0
  43. anysolver/production_readiness.py +304 -0
  44. anysolver/quality_control.py +1497 -0
  45. anysolver/recovery.py +505 -0
  46. anysolver/recovery_policy.py +205 -0
  47. anysolver/reference_cases.py +425 -0
  48. anysolver/results.py +503 -0
  49. anysolver/runtime.py +9030 -0
  50. anysolver/s4_validity.py +326 -0
  51. anysolver/sesam_fem/__init__.py +55 -0
  52. anysolver/sesam_fem/__main__.py +80 -0
  53. anysolver/sesam_fem/diagnostics.py +65 -0
  54. anysolver/sesam_fem/document.py +814 -0
  55. anysolver/sesam_fem/exporter.py +84 -0
  56. anysolver/sesam_fem/importer.py +365 -0
  57. anysolver/sesam_fem/records.py +257 -0
  58. anysolver/sesam_fem/schema.py +107 -0
  59. anysolver/sesam_fem/sif_importer.py +397 -0
  60. anysolver/sesam_fem/validation.py +62 -0
  61. anysolver/shell_benchmarks.py +367 -0
  62. anysolver/test_cases.py +610 -0
  63. anysolver/validation.py +416 -0
  64. anysolver/vectorized_nonlinear.py +334 -0
  65. anysolver/vectorized_stiffness.py +571 -0
  66. anysolver-0.1.0.dist-info/METADATA +165 -0
  67. anysolver-0.1.0.dist-info/RECORD +70 -0
  68. anysolver-0.1.0.dist-info/WHEEL +5 -0
  69. anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. 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
+ }