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,419 @@
1
+ """Vectorized J2 plane-stress plasticity with isotropic hardening.
2
+
3
+ The return mapping follows the classical plane-stress projected algorithm
4
+ (Simo & Hughes). In the eigenbasis of C*P the update decouples into two
5
+ scalar modes, so the plastic multiplier is found by a scalar Newton iteration
6
+ that runs simultaneously for every yielding integration point / thickness
7
+ layer (numpy arrays, no Python-level point loops).
8
+
9
+ When a tangent is requested for a nonlinear global solve, the public wrapper
10
+ returns a consistent algorithmic tangent obtained by differentiating the same
11
+ discrete stress update with central finite differences. This is deliberately
12
+ more expensive than the older continuum tangent, but it keeps the shell
13
+ layered-plastic tangent consistent while a closed-form analytical algorithmic
14
+ tangent is still pending.
15
+
16
+ Conventions
17
+ -----------
18
+ Stress/strain vectors are [xx, yy, xy] with engineering shear strain.
19
+ The yield function is f = 1/2 sigma^T P sigma - 1/3 sigma_y(alpha)^2 with
20
+
21
+ P = 1/3 [[ 2, -1, 0],
22
+ [-1, 2, 0],
23
+ [ 0, 0, 6]]
24
+
25
+ and alpha the equivalent plastic strain with rate
26
+ alpha_dot = lambda_dot * sqrt(2/3 sigma^T P sigma).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import TYPE_CHECKING, Tuple
32
+
33
+ import numpy as np
34
+
35
+ from .jit_compiler import njit
36
+
37
+ if TYPE_CHECKING:
38
+ from .material_curves import DNVC208MaterialCurve
39
+
40
+ _P_MATRIX = np.array(
41
+ [[2.0, -1.0, 0.0], [-1.0, 2.0, 0.0], [0.0, 0.0, 6.0]],
42
+ dtype=float,
43
+ ) / 3.0
44
+
45
+
46
+ @njit
47
+ def plane_stress_elastic_matrix(E: float, nu: float) -> np.ndarray:
48
+ return E / (1.0 - nu**2) * np.array(
49
+ [[1.0, nu, 0.0], [nu, 1.0, 0.0], [0.0, 0.0, (1.0 - nu) / 2.0]],
50
+ )
51
+
52
+
53
+ @njit
54
+ def _jit_flow_stress(
55
+ eps_p: np.ndarray,
56
+ sigma_prop: float,
57
+ sigma_yield: float,
58
+ sigma_yield_2: float,
59
+ eps_p_y1: float,
60
+ eps_p_y2: float,
61
+ K: float,
62
+ n: float,
63
+ power_offset: float,
64
+ ) -> np.ndarray:
65
+ eps_p = np.maximum(eps_p, 0.0)
66
+ slope_1 = (sigma_yield - sigma_prop) / eps_p_y1
67
+ slope_2 = (sigma_yield_2 - sigma_yield) / (eps_p_y2 - eps_p_y1)
68
+ part_1 = sigma_prop + slope_1 * eps_p
69
+ part_2 = sigma_yield + slope_2 * (eps_p - eps_p_y1)
70
+
71
+ res = np.zeros(eps_p.shape[0])
72
+ for i in range(eps_p.shape[0]):
73
+ val = eps_p[i]
74
+ if val <= eps_p_y1:
75
+ res[i] = part_1[i]
76
+ elif val <= eps_p_y2:
77
+ res[i] = part_2[i]
78
+ else:
79
+ res[i] = K * np.power(max(val + power_offset, 1.0e-12), n)
80
+ return res
81
+
82
+
83
+ @njit
84
+ def _jit_hardening_modulus(
85
+ eps_p: np.ndarray,
86
+ sigma_prop: float,
87
+ sigma_yield: float,
88
+ sigma_yield_2: float,
89
+ eps_p_y1: float,
90
+ eps_p_y2: float,
91
+ K: float,
92
+ n: float,
93
+ power_offset: float,
94
+ ) -> np.ndarray:
95
+ eps_p = np.maximum(eps_p, 0.0)
96
+ slope_1 = (sigma_yield - sigma_prop) / eps_p_y1
97
+ slope_2 = (sigma_yield_2 - sigma_yield) / (eps_p_y2 - eps_p_y1)
98
+
99
+ res = np.zeros(eps_p.shape[0])
100
+ for i in range(eps_p.shape[0]):
101
+ val = eps_p[i]
102
+ if val <= eps_p_y1:
103
+ res[i] = slope_1
104
+ elif val <= eps_p_y2:
105
+ res[i] = slope_2
106
+ else:
107
+ base = max(val + power_offset, 1.0e-12)
108
+ res[i] = K * n * np.power(base, n - 1.0)
109
+ return res
110
+
111
+
112
+ @njit
113
+ def _jit_plane_stress_return_map(
114
+ strain: np.ndarray,
115
+ plastic_strain: np.ndarray,
116
+ alpha: np.ndarray,
117
+ E: float,
118
+ nu: float,
119
+ sigma_prop: float,
120
+ sigma_yield: float,
121
+ sigma_yield_2: float,
122
+ eps_p_y1: float,
123
+ eps_p_y2: float,
124
+ K: float,
125
+ n: float,
126
+ power_offset: float,
127
+ max_iterations: int = 30,
128
+ tolerance: float = 1.0e-10,
129
+ compute_tangent: bool = True,
130
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
131
+ n_points = strain.shape[0]
132
+ C = plane_stress_elastic_matrix(E, nu)
133
+ C_inv = np.linalg.inv(C)
134
+ G = E / (2.0 * (1.0 + nu))
135
+ c_a = E / (3.0 * (1.0 - nu))
136
+
137
+ sigma = (strain - plastic_strain) @ C.T
138
+ sy_n = _jit_flow_stress(
139
+ alpha, sigma_prop, sigma_yield, sigma_yield_2, eps_p_y1, eps_p_y2, K, n, power_offset
140
+ )
141
+ a1 = sigma[:, 0] + sigma[:, 1]
142
+ a2 = sigma[:, 0] - sigma[:, 1]
143
+ a3 = sigma[:, 2]
144
+ phi2_trial = a1**2 / 12.0 + a2**2 / 4.0 + a3**2
145
+ f_trial = phi2_trial - sy_n**2 / 3.0
146
+
147
+ yielding = f_trial > tolerance * np.maximum(sy_n**2, 1.0)
148
+ C_ep = np.zeros((n_points, 3, 3))
149
+ if compute_tangent:
150
+ for i in range(n_points):
151
+ C_ep[i] = C
152
+ new_plastic = plastic_strain.copy()
153
+ new_alpha = alpha.copy()
154
+
155
+ if not np.any(yielding):
156
+ return sigma, C_ep, new_plastic, new_alpha
157
+
158
+ # Identify yielding indices
159
+ yielding_indices = np.where(yielding)[0]
160
+ n_yielding = yielding_indices.shape[0]
161
+
162
+ b1 = a1[yielding]
163
+ b23 = a2[yielding] ** 2 / 4.0 + a3[yielding] ** 2
164
+ alpha_y = alpha[yielding]
165
+ dl = np.zeros(n_yielding)
166
+
167
+ for _ in range(max_iterations):
168
+ dA = 1.0 + c_a * dl
169
+ dB = 1.0 + 2.0 * G * dl
170
+ phi2 = b1**2 / (12.0 * dA**2) + b23 / dB**2
171
+ phi = np.sqrt(np.maximum(phi2, 1.0e-30))
172
+ g = 2.0 * np.sqrt(np.maximum(phi2 / 3.0, 1.0e-30))
173
+ alpha_new = alpha_y + dl * g
174
+ sy = _jit_flow_stress(
175
+ alpha_new, sigma_prop, sigma_yield, sigma_yield_2, eps_p_y1, eps_p_y2, K, n, power_offset
176
+ )
177
+ H = _jit_hardening_modulus(
178
+ alpha_new, sigma_prop, sigma_yield, sigma_yield_2, eps_p_y1, eps_p_y2, K, n, power_offset
179
+ )
180
+ f = phi2 - sy**2 / 3.0
181
+
182
+ all_scaled = True
183
+ for i in range(n_yielding):
184
+ if np.abs(f[i]) > tolerance * max(sy[i]**2, 1.0):
185
+ all_scaled = False
186
+ break
187
+ if all_scaled:
188
+ break
189
+
190
+ d_phi2 = -2.0 * (b1**2 * c_a / (12.0 * dA**3) + 2.0 * G * b23 / dB**3)
191
+ d_g = d_phi2 / (3.0 * np.maximum(np.sqrt(phi2 / 3.0), 1.0e-30))
192
+ d_alpha = g + dl * d_g
193
+ d_f = d_phi2 - (2.0 / 3.0) * sy * H * d_alpha
194
+
195
+ # Safe division step
196
+ for i in range(n_yielding):
197
+ df_val = d_f[i]
198
+ if np.abs(df_val) <= 1.0e-30:
199
+ df_val = -1.0e-30 if df_val < 0.0 else 1.0e-30
200
+ step = f[i] / df_val
201
+ dl[i] = max(dl[i] - step, 0.0)
202
+
203
+ dA = 1.0 + c_a * dl
204
+ dB = 1.0 + 2.0 * G * dl
205
+ sig_a = b1 / dA
206
+ sig_b = a2[yielding] / dB
207
+ tau = a3[yielding] / dB
208
+
209
+ sigma_y_pts = np.zeros((n_yielding, 3))
210
+ sigma_y_pts[:, 0] = (sig_a + sig_b) / 2.0
211
+ sigma_y_pts[:, 1] = (sig_a - sig_b) / 2.0
212
+ sigma_y_pts[:, 2] = tau
213
+
214
+ for idx, i in enumerate(yielding_indices):
215
+ sigma[i] = sigma_y_pts[idx]
216
+
217
+ phi2 = sig_a**2 / 12.0 + sig_b**2 / 4.0 + tau**2
218
+ new_alpha[yielding] = alpha_y + dl * 2.0 * np.sqrt(np.maximum(phi2 / 3.0, 1.0e-30))
219
+
220
+ p_strain_yielding = strain[yielding] - sigma_y_pts @ C_inv.T
221
+ for idx, i in enumerate(yielding_indices):
222
+ new_plastic[i] = p_strain_yielding[idx]
223
+
224
+ if not compute_tangent:
225
+ return sigma, C_ep, new_plastic, new_alpha
226
+
227
+ # Continuum elastoplastic tangent: C_ep = C - (C m)(C m)^T / (m^T C m + 4/9 sy^2 H)
228
+ _P_MATRIX_local = np.array(
229
+ [[2.0, -1.0, 0.0], [-1.0, 2.0, 0.0], [0.0, 0.0, 6.0]],
230
+ ) / 3.0
231
+ m = sigma_y_pts @ _P_MATRIX_local.T
232
+ Cm = m @ C.T
233
+ sy_final = _jit_flow_stress(
234
+ new_alpha[yielding], sigma_prop, sigma_yield, sigma_yield_2, eps_p_y1, eps_p_y2, K, n, power_offset
235
+ )
236
+ H_final = _jit_hardening_modulus(
237
+ new_alpha[yielding], sigma_prop, sigma_yield, sigma_yield_2, eps_p_y1, eps_p_y2, K, n, power_offset
238
+ )
239
+
240
+ denom = np.zeros(n_yielding)
241
+ for idx in range(n_yielding):
242
+ denom[idx] = m[idx, 0] * Cm[idx, 0] + m[idx, 1] * Cm[idx, 1] + m[idx, 2] * Cm[idx, 2]
243
+ denom += (4.0 / 9.0) * sy_final**2 * H_final
244
+
245
+ for idx, i in enumerate(yielding_indices):
246
+ denom_val = max(denom[idx], 1.0e-30)
247
+ for r in range(3):
248
+ for c in range(3):
249
+ C_ep[i, r, c] = C[r, c] - Cm[idx, r] * Cm[idx, c] / denom_val
250
+
251
+ return sigma, C_ep, new_plastic, new_alpha
252
+
253
+
254
+ def plane_stress_return_map(
255
+ strain: np.ndarray,
256
+ plastic_strain: np.ndarray,
257
+ alpha: np.ndarray,
258
+ E: float,
259
+ nu: float,
260
+ curve: "DNVC208MaterialCurve",
261
+ max_iterations: int = 30,
262
+ tolerance: float = 1.0e-10,
263
+ compute_tangent: bool = True,
264
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
265
+ """Map total strains to stresses, tangents and updated plastic state."""
266
+ strain = np.asarray(strain, dtype=float)
267
+ plastic_strain = np.asarray(plastic_strain, dtype=float)
268
+ alpha = np.asarray(alpha, dtype=float)
269
+
270
+ if curve is None:
271
+ C = plane_stress_elastic_matrix(E, nu)
272
+ n_points = strain.shape[0]
273
+ C_ep = np.broadcast_to(C, (n_points, 3, 3)).copy() if compute_tangent else np.zeros((n_points, 3, 3))
274
+ sigma = (strain - plastic_strain) @ C.T
275
+ return sigma, C_ep, plastic_strain.copy(), alpha.copy()
276
+
277
+ sigma, _continuum_tangent, new_plastic, new_alpha = _jit_plane_stress_return_map(
278
+ strain,
279
+ plastic_strain,
280
+ alpha,
281
+ E,
282
+ nu,
283
+ float(curve.sigma_prop),
284
+ float(curve.sigma_yield),
285
+ float(curve.sigma_yield_2),
286
+ float(curve.eps_p_y1),
287
+ float(curve.eps_p_y2),
288
+ float(curve.K),
289
+ float(curve.n),
290
+ float(curve._power_offset),
291
+ max_iterations,
292
+ tolerance,
293
+ False,
294
+ )
295
+ if not compute_tangent:
296
+ return sigma, _continuum_tangent, new_plastic, new_alpha
297
+
298
+ C_alg = _finite_difference_algorithmic_tangent(
299
+ strain,
300
+ plastic_strain,
301
+ alpha,
302
+ E,
303
+ nu,
304
+ curve,
305
+ max_iterations=max_iterations,
306
+ tolerance=tolerance,
307
+ )
308
+ return sigma, C_alg, new_plastic, new_alpha
309
+
310
+
311
+ def _finite_difference_algorithmic_tangent(
312
+ strain: np.ndarray,
313
+ plastic_strain: np.ndarray,
314
+ alpha: np.ndarray,
315
+ E: float,
316
+ nu: float,
317
+ curve: "DNVC208MaterialCurve",
318
+ max_iterations: int = 30,
319
+ tolerance: float = 1.0e-10,
320
+ step: float = 1.0e-7,
321
+ ) -> np.ndarray:
322
+ """Central-difference tangent of the exact discrete stress update."""
323
+ n_points = int(strain.shape[0])
324
+ tangent = np.zeros((n_points, 3, 3), dtype=float)
325
+ for col in range(3):
326
+ perturb = np.zeros_like(strain)
327
+ perturb[:, col] = step
328
+ sigma_plus, _, _, _ = _jit_plane_stress_return_map(
329
+ strain + perturb,
330
+ plastic_strain,
331
+ alpha,
332
+ E,
333
+ nu,
334
+ float(curve.sigma_prop),
335
+ float(curve.sigma_yield),
336
+ float(curve.sigma_yield_2),
337
+ float(curve.eps_p_y1),
338
+ float(curve.eps_p_y2),
339
+ float(curve.K),
340
+ float(curve.n),
341
+ float(curve._power_offset),
342
+ max_iterations,
343
+ tolerance,
344
+ False,
345
+ )
346
+ sigma_minus, _, _, _ = _jit_plane_stress_return_map(
347
+ strain - perturb,
348
+ plastic_strain,
349
+ alpha,
350
+ E,
351
+ nu,
352
+ float(curve.sigma_prop),
353
+ float(curve.sigma_yield),
354
+ float(curve.sigma_yield_2),
355
+ float(curve.eps_p_y1),
356
+ float(curve.eps_p_y2),
357
+ float(curve.K),
358
+ float(curve.n),
359
+ float(curve._power_offset),
360
+ max_iterations,
361
+ tolerance,
362
+ False,
363
+ )
364
+ tangent[:, :, col] = (sigma_plus - sigma_minus) / (2.0 * step)
365
+ return tangent
366
+
367
+
368
+ _LOBATTO_RULES = {
369
+ 3: (np.array([-1.0, 0.0, 1.0]), np.array([1.0, 4.0, 1.0]) / 3.0),
370
+ 5: (
371
+ np.array([-1.0, -np.sqrt(3.0 / 7.0), 0.0, np.sqrt(3.0 / 7.0), 1.0]),
372
+ np.array([1.0 / 10.0, 49.0 / 90.0, 32.0 / 45.0, 49.0 / 90.0, 1.0 / 10.0]),
373
+ ),
374
+ 7: (
375
+ np.array(
376
+ [-1.0, -0.830223896278567, -0.468848793470714, 0.0,
377
+ 0.468848793470714, 0.830223896278567, 1.0]
378
+ ),
379
+ np.array(
380
+ [0.047619047619048, 0.276826047361566, 0.431745381209863, 0.487619047619048,
381
+ 0.431745381209863, 0.276826047361566, 0.047619047619048]
382
+ ),
383
+ ),
384
+ 9: (
385
+ np.array(
386
+ [-1.0, -0.899757995411460, -0.677186279510738, -0.363117463826178, 0.0,
387
+ 0.363117463826178, 0.677186279510738, 0.899757995411460, 1.0]
388
+ ),
389
+ np.array(
390
+ [0.027777777777778, 0.165495361560806, 0.274538712500162, 0.346428510973046,
391
+ 0.371519274376417, 0.346428510973046, 0.274538712500162, 0.165495361560806,
392
+ 0.027777777777778]
393
+ ),
394
+ ),
395
+ 11: (
396
+ np.array(
397
+ [-1.0, -0.934001430408059, -0.784483473663144, -0.565235326996205,
398
+ -0.295758135586939, 0.0, 0.295758135586939, 0.565235326996205,
399
+ 0.784483473663144, 0.934001430408059, 1.0]
400
+ ),
401
+ np.array(
402
+ [0.018181818181818, 0.109612273266995, 0.187169881780305, 0.248048104264028,
403
+ 0.286879124779008, 0.300217595455691, 0.286879124779008, 0.248048104264028,
404
+ 0.187169881780305, 0.109612273266995, 0.018181818181818]
405
+ ),
406
+ ),
407
+ }
408
+
409
+
410
+ def lobatto_layers(num_layers: int, thickness: float) -> Tuple[np.ndarray, np.ndarray]:
411
+ """Through-thickness Gauss-Lobatto coordinates and weights.
412
+
413
+ Lobatto rules include the surface points, where yielding starts first.
414
+ Returns (z, w) with z in [-h/2, h/2] and sum(w) = h.
415
+ """
416
+ if num_layers not in _LOBATTO_RULES:
417
+ raise ValueError(f"num_layers must be one of {sorted(_LOBATTO_RULES)}")
418
+ points, weights = _LOBATTO_RULES[num_layers]
419
+ return 0.5 * thickness * points, 0.5 * thickness * weights