poly-mpm 1.0.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 (57) hide show
  1. MPM.py +101 -0
  2. constit.py +469 -0
  3. diagnostics.py +210 -0
  4. grid.py +259 -0
  5. initialise.py +187 -0
  6. inputs/__init__.py +0 -0
  7. inputs/anisotropy.py +119 -0
  8. inputs/avalanche.py +108 -0
  9. inputs/ball.py +98 -0
  10. inputs/bi_conveyor.py +116 -0
  11. inputs/bi_dam_break.py +167 -0
  12. inputs/bi_double_insets.py +128 -0
  13. inputs/bi_drum.py +159 -0
  14. inputs/bi_seg_test.py +166 -0
  15. inputs/bi_square_drum.py +146 -0
  16. inputs/bi_vertical_chute.py +145 -0
  17. inputs/bingham_unit_test.py +127 -0
  18. inputs/brazillian.py +124 -0
  19. inputs/chuteflow.py +117 -0
  20. inputs/dam_break.py +107 -0
  21. inputs/dp_rate_unit_test.py +117 -0
  22. inputs/dp_unit_test.py +120 -0
  23. inputs/flow.py +159 -0
  24. inputs/fluid.py +137 -0
  25. inputs/francois.py +153 -0
  26. inputs/gsd.py +142 -0
  27. inputs/incline.py +128 -0
  28. inputs/macro.py +117 -0
  29. inputs/modern_ball.py +15 -0
  30. inputs/pierre.py +137 -0
  31. inputs/poly_avalanche.py +109 -0
  32. inputs/poly_seg_test.py +118 -0
  33. inputs/pouliquen_unit_test.py +167 -0
  34. inputs/seg_unit_test.py +107 -0
  35. inputs/silo.py +131 -0
  36. inputs/slope_stab.py +111 -0
  37. inputs/stokes.py +161 -0
  38. inputs/tri_seg_test.py +127 -0
  39. inputs/triaxial.py +100 -0
  40. inputs/tunnel.py +105 -0
  41. inputs/vicky.py +105 -0
  42. inputs/vicky_latest.py +113 -0
  43. inputs/viscous_unit_test.py +120 -0
  44. inputs/vm_unit_test.py +92 -0
  45. integrators.py +260 -0
  46. interpolation.py +71 -0
  47. kinematics.py +20 -0
  48. particles.py +529 -0
  49. plotting.py +217 -0
  50. poly_mpm-1.0.0.dist-info/METADATA +157 -0
  51. poly_mpm-1.0.0.dist-info/RECORD +57 -0
  52. poly_mpm-1.0.0.dist-info/WHEEL +5 -0
  53. poly_mpm-1.0.0.dist-info/entry_points.txt +2 -0
  54. poly_mpm-1.0.0.dist-info/licenses/LICENSE +674 -0
  55. poly_mpm-1.0.0.dist-info/top_level.txt +13 -0
  56. stability.py +66 -0
  57. transfers.py +36 -0
MPM.py ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """Array MPM timestep and command-line entry point."""
3
+
4
+ import sys
5
+ import numpy as np
6
+ import initialise
7
+
8
+
9
+ def step(P, G, L):
10
+ """Advance one complete timestep, without output or changing the clock."""
11
+ if not np.isfinite(P.dt) or P.dt <= 0:
12
+ raise ValueError("dt must be finite and positive")
13
+ from stability import limits
14
+
15
+ if P.mechanics == "updated":
16
+ P.timestep_limits = limits(P, G, L)
17
+ safe = min(P.timestep_limits.values(), default=P.dt)
18
+ if P.timestep_policy == "enforce" and P.dt > safe * (1 + 1e-12):
19
+ raise ValueError(
20
+ f"dt={P.dt:g} exceeds estimated stable dt={safe:g}; use dynamic stepping or reduce dt"
21
+ )
22
+ if L.diagnostics is not None:
23
+ L.diagnostics.begin(P, L)
24
+ G.wipe(P)
25
+ P.update_forces()
26
+ L.get_reference_nodes(P, G)
27
+ L.get_basis_functions(P, G)
28
+ if P.O.check_positions:
29
+ L.recover_position(P, G)
30
+ L.get_nodal_mass_momentum(P, G)
31
+ L.update_stress_strain(P, G)
32
+ L.get_nodal_forces(P, G)
33
+ G.BCs(P)
34
+ G.update_momentum(P)
35
+ G.calculate_gammadot(P, G, P.smooth_gamma_dot)
36
+ if P.segregate:
37
+ G.update_pk(P, G)
38
+ G.calculate_phi_increment(P, L)
39
+ L.move_grainsize_on_grid(P, G)
40
+ L.move_material_points(P, G)
41
+ if L.diagnostics is not None:
42
+ from diagnostics import inventory
43
+
44
+ boundary_before = inventory(P, L)
45
+ L.process_boundaries(P, G)
46
+ if L.diagnostics is not None:
47
+ L.diagnostics.finish(P, G, L, boundary_before)
48
+
49
+
50
+ def run(P, G, L, *, max_steps=None, output=True, progress=False):
51
+ """Run prepared state. max_steps permits bounded smoke tests and previews."""
52
+ if output:
53
+ P.O.at_start(L, P, G)
54
+ count = 0
55
+ next_save = P.t + P.savetime
56
+ while P.t < P.t_f and (max_steps is None or count < max_steps):
57
+ nominal_dt = P.dt
58
+ remaining = P.t_f - P.t
59
+ if remaining <= 16 * np.finfo(float).eps * max(abs(P.t_f), nominal_dt):
60
+ P.t = P.t_f
61
+ break
62
+ P.dt = min(P.dt, remaining)
63
+ if P.time_stepping == "dynamic" and P.mechanics == "updated":
64
+ from stability import limits
65
+
66
+ restrictions = limits(P, G, L)
67
+ if restrictions:
68
+ P.dt = min(P.dt, min(restrictions.values()))
69
+ used_dt = P.dt
70
+ step(P, G, L)
71
+ P.t += used_dt
72
+ P.tstep += 1
73
+ count += 1
74
+ if output:
75
+ P.O.after_every_timestep(L, P, G)
76
+ if P.t >= next_save:
77
+ P.O.after_every_savetime(L, P, G)
78
+ next_save += (np.floor((P.t - next_save) / P.savetime) + 1) * P.savetime
79
+ if progress:
80
+ print(f"{P.t/P.t_f:.1%} complete, t = {P.t:.6g}", end="\r")
81
+ P.dt = nominal_dt
82
+ if P.time_stepping == "dynamic" and P.t < P.t_f:
83
+ P.update_timestep(P, G)
84
+ if L.diagnostics is not None:
85
+ L.diagnostics.complete()
86
+ if output:
87
+ P.O.at_end(L, P, G)
88
+ if L.diagnostics is not None and P.O.save_diagnostics:
89
+ L.diagnostics.save(P.save_dir)
90
+ return P, G, L
91
+
92
+
93
+ def main(params=None):
94
+ state = initialise.get_parameters(sys.argv[1:] if params is None else params)
95
+ run(*state, progress=True)
96
+ print()
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ sys.exit(main())
constit.py ADDED
@@ -0,0 +1,469 @@
1
+ """Array constitutive kernels. Cauchy stress uses tension-positive signs.
2
+
3
+ Experimental scalar implementations are retained in legacy/constit_scalar.py.
4
+ Kernels update history and dstress; update_stress selects the integration scheme.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+ EYE = np.eye(3)
10
+ SUPPORTED = ("rigid", "elastic", "von_mises", "viscous", "pouliquen", "bingham", "dp", "dp_rate", "linear_mu")
11
+ ALGEBRAIC = ("bingham", "linear_mu")
12
+ FLUIDS = ("viscous", "bingham", "pouliquen", "linear_mu")
13
+ COROTATIONAL = ("dp", "dp_rate")
14
+ RETIRED = {
15
+ "elastic_time": "Retired: arbitrary per-step mass deletion is not a constitutive law; use elastic for constant stiffness.",
16
+ "ken_kamrin": "Retired: signed-pressure/viscosity errors in an unfinished duplicate; use pouliquen.",
17
+ "ken_simple": "Retired: stale shear rate and hardcoded density cutoff; use linear_mu with b=0 for constant friction.",
18
+ }
19
+
20
+
21
+ def validate_material(material, mechanics="updated"):
22
+ """Physical parameter domain for the newly restored laws (scalar SI inputs)."""
23
+ name = material.law
24
+ if name == "rigid" or (mechanics == "legacy" and name not in ALGEBRAIC + COROTATIONAL):
25
+ return
26
+
27
+ def scalar(key, lower=0.0, strict=False, default=None):
28
+ value = getattr(material, key, default)
29
+ if (
30
+ value is None
31
+ or np.ndim(value) != 0
32
+ or not np.isfinite(value)
33
+ or (value <= lower if strict else value < lower)
34
+ ):
35
+ comparison = ">" if strict else ">="
36
+ raise ValueError(f"{name}: {key} must be a finite scalar {comparison} {lower}")
37
+ return value
38
+
39
+ scalar("K", strict=name in COROTATIONAL + ("elastic", "von_mises"))
40
+ if name in ("elastic", "von_mises"):
41
+ scalar("G", strict=True)
42
+ if name == "von_mises":
43
+ if hasattr(material, "yield_stress"):
44
+ scalar("yield_stress", strict=True)
45
+ else:
46
+ scalar("k", strict=True)
47
+ return
48
+ if name == "viscous":
49
+ scalar("mu_s")
50
+ scalar("mu_v")
51
+ return
52
+ if name == "pouliquen":
53
+ scalar("rho_s", strict=True)
54
+ scalar("mu_0")
55
+ scalar("delta_mu")
56
+ scalar("I_0", strict=True)
57
+ scalar("eta_max", strict=True)
58
+ return
59
+ if name in COROTATIONAL:
60
+ scalar("G", strict=True)
61
+ mu = scalar("mu", strict=True)
62
+ if scalar("beta") > mu:
63
+ raise ValueError(f"{name}: beta must satisfy 0 <= beta <= mu for nonnegative plastic dissipation")
64
+ if name == "dp_rate":
65
+ scalar("t_star")
66
+ elif name == "bingham":
67
+ mu_s = scalar("mu_s")
68
+ tau = scalar("tau_0")
69
+ mu_0 = scalar("mu_0", lower=mu_s, strict=tau > 0)
70
+ scalar("mu_v", default=0.0)
71
+ if hasattr(material, "gamma_c"):
72
+ expected = tau / (mu_0 - mu_s) if tau else 0.0
73
+ if not np.isclose(scalar("gamma_c"), expected, rtol=1e-10, atol=1e-14):
74
+ raise ValueError(
75
+ "bingham: gamma_c must equal tau_0/(mu_0-mu_s); omit it to derive the transition"
76
+ )
77
+ else:
78
+ scalar("mu_0")
79
+ scalar("b")
80
+ scalar("rho_s", strict=True)
81
+ scalar("eta_max", strict=True)
82
+
83
+
84
+ def invariants(a):
85
+ trace = np.trace(a, axis1=-2, axis2=-1)
86
+ return trace, a - trace[:, None, None] * EYE / 3
87
+
88
+
89
+ def rigid(mp, material, params):
90
+ mp.dstress.fill(0)
91
+
92
+
93
+ def elastic(mp, material, params):
94
+ trace, dev = invariants(mp.dstrain)
95
+ mp.dstress = material.K * trace[:, None, None] * EYE + 2 * material.G * dev
96
+ mp.gammadot = np.linalg.norm(dev, axis=(1, 2)) / params.dt
97
+ mp.pressure = np.trace(mp.stress, axis1=1, axis2=2) / 3
98
+
99
+
100
+ def viscous(mp, material, params):
101
+ trace, dev = invariants(mp.dstrain)
102
+ mp.de_kk = trace / 3
103
+ mp.de_ij = dev
104
+ mp.dp = material.K * mp.de_kk
105
+ mp.pressure += mp.dp
106
+ mp.dev_stress = 2 * material.mu_s * dev / params.dt
107
+ volumetric = material.mu_v * mp.de_kk[:, None, None] * EYE / params.dt
108
+ mp.dstress = mp.pressure[:, None, None] * EYE + mp.dev_stress - volumetric - mp.stress
109
+ mp.gammadot = np.linalg.norm(dev, axis=(1, 2)) / params.dt
110
+
111
+
112
+ def von_mises(mp, material, params):
113
+ trace, dev = invariants(mp.dstrain)
114
+ work = np.abs(np.sum(mp.dev_stress * dev, axis=(1, 2)))
115
+ norm = np.linalg.norm(mp.dev_stress, axis=(1, 2))
116
+ # For s < 2 the legacy expression is singular at zero stress. Its product
117
+ # with the zero work/stress is defined here by its zero-increment limit.
118
+ factor = np.zeros_like(norm)
119
+ active = norm > 0
120
+ factor[active] = (
121
+ work[active] / (2 * material.k**2) * (norm[active] / (np.sqrt(2) * material.k)) ** (material.s - 2)
122
+ )
123
+ mp.dev_dstress = 2 * material.G * (dev - factor[:, None, None] * mp.dev_stress)
124
+ mp.sigma_kk += 3 * material.K * trace
125
+ mp.dev_stress += mp.dev_dstress
126
+ mp.dstress = mp.dev_stress + mp.sigma_kk[:, None, None] * EYE / 3 - mp.stress
127
+ mp.yieldfunction = norm / (np.sqrt(2) * material.k) - 1
128
+ mp.gammadot = np.linalg.norm(dev, axis=(1, 2)) / params.dt
129
+ mp.pressure = np.trace(mp.stress, axis1=1, axis2=2) / 3
130
+
131
+
132
+ def pouliquen(mp, material, params):
133
+ trace, dev = invariants(mp.dstrain)
134
+ mp.de_kk = trace / 3
135
+ mp.de_ij = dev
136
+ mp.gammadot = np.linalg.norm(2 * dev / params.dt, axis=(1, 2))
137
+ size = mp.phi @ params.G.s
138
+ pressure = np.abs(mp.pressure)
139
+ mp.I = np.zeros_like(pressure)
140
+ positive = pressure > 0
141
+ mp.I[positive] = mp.gammadot[positive] * size[positive] * np.sqrt(material.rho_s / pressure[positive])
142
+ # I is undefined at zero pressure: store zero for that diagnostic and
143
+ # evaluate friction using its infinite-I limit without a huge sentinel.
144
+ ratio = np.divide(mp.I, material.I_0 + mp.I, out=np.zeros_like(mp.I), where=mp.I > 0)
145
+ ratio[(~positive) & (mp.gammadot > 0)] = 1
146
+ mp.mu = material.mu_0 + material.delta_mu * ratio
147
+ numerator = 2 * np.sqrt(2) * mp.mu * pressure
148
+ mp.eta = np.divide(numerator, mp.gammadot, out=np.zeros_like(pressure), where=mp.gammadot > 0)
149
+ mp.eta[(mp.gammadot == 0) & (numerator > 0)] = np.finfo(float).max
150
+ mp.eta_limited = np.minimum(mp.eta, material.eta_max)
151
+ mp.dev_stress = mp.eta_limited[:, None, None] * dev / params.dt
152
+ mp.dp = material.K * mp.de_kk
153
+ mp.pressure += mp.dp
154
+ cutoff = (mp.pressure > 0) | (mp.rho < mp.initial_rho * 0.8)
155
+ mp.dp[cutoff] -= mp.pressure[cutoff]
156
+ mp.pressure[cutoff] = 0
157
+ mp.dstress = mp.pressure[:, None, None] * EYE + mp.dev_stress - mp.stress
158
+
159
+
160
+ def _flow_increment(mp, params):
161
+ trace, dev = invariants(mp.dstrain)
162
+ mp.de_kk = trace / 3
163
+ mp.de_ij = dev
164
+ mp.gammadot = np.sqrt(2.0) * np.linalg.norm(dev, axis=(1, 2)) / params.dt
165
+ return trace, dev / params.dt
166
+
167
+
168
+ def bingham(mp, material, params):
169
+ """Bi-viscosity Bingham fluid; gamma = sqrt(2 D':D'), viscosities in Pa s."""
170
+ trace, rate = _flow_increment(mp, params)
171
+ mp.dp = material.K * trace
172
+ mp.pressure += mp.dp
173
+ mp.eta = np.full_like(mp.gammadot, material.mu_s)
174
+ if material.tau_0:
175
+ mp.eta.fill(material.mu_0)
176
+ flowing = mp.gammadot > material.tau_0 / (material.mu_0 - material.mu_s)
177
+ mp.eta[flowing] = material.mu_s + material.tau_0 / mp.gammadot[flowing]
178
+ mp.eta_limited = mp.eta.copy()
179
+ mp.dev_stress = 2 * mp.eta[:, None, None] * rate
180
+ bulk = getattr(material, "mu_v", 0.0) * trace / params.dt
181
+ mp.dstress = (mp.pressure + bulk)[:, None, None] * EYE + mp.dev_stress - mp.stress
182
+
183
+
184
+ def linear_mu(mp, material, params):
185
+ """Dense flowing linear mu(I) law, with finite effective-viscosity cap."""
186
+ trace, rate = _flow_increment(mp, params)
187
+ old_pressure = mp.pressure.copy()
188
+ mp.pressure = np.minimum(old_pressure + material.K * trace, 0.0)
189
+ mp.dp = mp.pressure - old_pressure
190
+ p = -mp.pressure
191
+ size = mp.phi @ params.G.s
192
+ mp.I = np.zeros_like(p)
193
+ positive = p > 0
194
+ mp.I[positive] = mp.gammadot[positive] * size[positive] * np.sqrt(material.rho_s) / np.sqrt(p[positive])
195
+ mp.mu = material.mu_0 + material.b * mp.I
196
+ # Evaluate mu*p/gamma as a sum to avoid zero-pressure and zero-rate singularities.
197
+ mp.eta = np.divide(
198
+ material.mu_0 * p,
199
+ mp.gammadot,
200
+ out=np.where(material.mu_0 * p > 0, material.eta_max, 0.0),
201
+ where=mp.gammadot > 0,
202
+ )
203
+ mp.eta += material.b * size * np.sqrt(material.rho_s) * np.sqrt(p)
204
+ mp.eta = np.minimum(mp.eta, material.eta_max)
205
+ mp.eta[~positive] = 0.0
206
+ mp.eta_limited = mp.eta.copy()
207
+ mp.dev_stress = 2 * mp.eta[:, None, None] * rate
208
+ mp.dstress = mp.pressure[:, None, None] * EYE + mp.dev_stress - mp.stress
209
+
210
+
211
+ def _drucker_prager(mp, material, params, t_star):
212
+ """Backward Euler for f=q-mu*p and g=q-beta*p; no cohesion or tension.
213
+
214
+ p=-tr(sigma)/3, q=sqrt(3/2 s:s). Perzyna viscosity is 3 G t_star.
215
+ A separate associated mean-tension cutoff handles the cone apex without
216
+ dividing by p or q. The rate model permits viscous shear overstress at p=0.
217
+ """
218
+ trace, dev = invariants(mp.dstrain)
219
+ mp.de_kk = trace / 3
220
+ mp.de_ij = dev
221
+ trial = mp.stress + material.K * trace[:, None, None] * EYE + 2 * material.G * dev
222
+ tr, shear = invariants(trial)
223
+ p_trial = -tr / 3
224
+ q_trial = np.sqrt(1.5) * np.linalg.norm(shear, axis=(1, 2))
225
+ viscous = 3 * material.G * t_star / params.dt
226
+ multiplier = np.maximum(q_trial - material.mu * p_trial, 0.0) / (
227
+ 3 * material.G + material.K * material.mu * material.beta + viscous
228
+ )
229
+ p = p_trial + material.K * material.beta * multiplier
230
+ tension = p < 0
231
+ multiplier[tension] = q_trial[tension] / (3 * material.G + viscous)
232
+ p[tension] = 0.0
233
+ q = np.maximum(q_trial - 3 * material.G * multiplier, 0.0)
234
+ scale = np.divide(q, q_trial, out=np.zeros_like(q), where=q_trial > 0)
235
+ mp.dev_stress = shear * scale[:, None, None]
236
+ mp.pressure = -p
237
+ mp.dp = mp.pressure - np.trace(mp.stress, axis1=1, axis2=2) / 3
238
+ mp.sigma_kk = -3 * p
239
+ mp.mu.fill(material.mu)
240
+ mp.gammadot = np.sqrt(2.0) * np.linalg.norm(dev, axis=(1, 2)) / params.dt
241
+ mp.yieldfunction = q - material.mu * p
242
+ mp.dstress = -p[:, None, None] * EYE + mp.dev_stress - mp.stress
243
+
244
+
245
+ def dp(mp, material, params):
246
+ """Cohesionless, perfectly plastic Drucker-Prager solid."""
247
+ _drucker_prager(mp, material, params, 0.0)
248
+
249
+
250
+ def dp_rate(mp, material, params):
251
+ """Linear Perzyna Drucker-Prager solid; t_star is a relaxation time (s)."""
252
+ _drucker_prager(mp, material, params, material.t_star)
253
+
254
+
255
+ def _half_rotation(mp, dt):
256
+ """Batched Rodrigues exponential of half the incremental spin."""
257
+ spin = (mp.L - mp.L_T) * dt / 4
258
+ angle = np.linalg.norm(spin, axis=(1, 2)) / np.sqrt(2.0)
259
+ a = np.sinc(angle / np.pi)
260
+ b = 0.5 * np.sinc(angle / (2 * np.pi)) ** 2
261
+ return EYE + a[:, None, None] * spin + b[:, None, None] * (spin @ spin)
262
+
263
+
264
+ def update_stress(mp, material, params):
265
+ """Integrate restored laws consistently with their stress/rate definition.
266
+
267
+ Keep the original five models' legacy integration for compatibility. New
268
+ fluids prescribe current stress directly. New solids use a symmetric spin
269
+ split for a corotational hypoelastic/plastic update (exact for pure spin).
270
+ """
271
+ if material.law not in ALGEBRAIC + COROTATIONAL:
272
+ mp.law(mp, material, params)
273
+ mp.stress += mp.dstress - (mp.L @ mp.stress + mp.stress @ mp.L_T) * params.dt
274
+ return
275
+ old = mp.stress.copy()
276
+ old_dev = old - np.trace(old, axis1=1, axis2=2)[:, None, None] * EYE / 3
277
+ if material.law in COROTATIONAL:
278
+ rotation = _half_rotation(mp, params.dt)
279
+ mp.stress = rotation @ old @ rotation.swapaxes(1, 2)
280
+ mp.law(mp, material, params)
281
+ mp.stress = rotation @ (mp.stress + mp.dstress) @ rotation.swapaxes(1, 2)
282
+ elif material.law in ALGEBRAIC:
283
+ mp.law(mp, material, params)
284
+ mp.stress += mp.dstress
285
+ mp.dstress = mp.stress - old
286
+ # Fluid pressure is the elastic mean part: bulk viscosity is not history.
287
+ if material.law in COROTATIONAL:
288
+ mp.pressure = np.trace(mp.stress, axis1=1, axis2=2) / 3
289
+ mean = np.trace(mp.stress, axis1=1, axis2=2) / 3
290
+ mp.sigma_kk = 3 * mean
291
+ mp.dev_stress = mp.stress - mean[:, None, None] * EYE
292
+ mp.dev_dstress = mp.dev_stress - old_dev
293
+
294
+
295
+ LEGACY_REGISTRY = {name: globals()[name] for name in SUPPORTED}
296
+ _legacy_update_stress = update_stress
297
+
298
+
299
+ def elastic(mp, material, params):
300
+ """Compressible neo-Hookean Cauchy stress, with reference prestress.
301
+
302
+ Reference energy density::
303
+
304
+ W0 = G/2 (J^(-2/3) tr(F F^T)-3) + K/2 (ln J)^2
305
+ W0 += S0:(F^T F-I)/2
306
+
307
+ The reference prestress S0 is the input stress at F=I. The law recovers
308
+ isotropic linear elasticity for small increments around an unstressed state.
309
+ """
310
+ J = mp.J
311
+ logJ = np.log(J)
312
+ b = mp.F @ mp.F.swapaxes(1, 2)
313
+ trace_b = np.trace(b, axis1=1, axis2=2)
314
+ scale = J ** (-2 / 3)
315
+ tau = (
316
+ material.G * scale[:, None, None] * (b - trace_b[:, None, None] * EYE / 3)
317
+ + material.K * logJ[:, None, None] * EYE
318
+ )
319
+ tau += mp.F @ mp.reference_stress @ mp.F.swapaxes(1, 2)
320
+ stress = tau / J[:, None, None]
321
+ mp.dstress = stress - mp.stress
322
+ green = (mp.F.swapaxes(1, 2) @ mp.F - EYE) / 2
323
+ energy = (
324
+ material.G / 2 * (scale * trace_b - 3)
325
+ + material.K / 2 * logJ**2
326
+ + np.sum(mp.reference_stress * green, axis=(1, 2))
327
+ )
328
+ mp.elastic_energy = mp.V0 * energy
329
+ _, dev = invariants(mp.dstrain)
330
+ mp.gammadot = np.sqrt(2) * np.linalg.norm(dev, axis=(1, 2)) / params.dt
331
+
332
+
333
+ def viscous(mp, material, params):
334
+ """Newtonian fluid with positive bulk dissipation and standard bulk modulus."""
335
+ trace, rate = _flow_increment(mp, params)
336
+ mp.dp = material.K * trace
337
+ mp.pressure += mp.dp
338
+ mp.eta.fill(material.mu_s)
339
+ mp.eta_limited = mp.eta.copy()
340
+ mp.dev_stress = 2 * material.mu_s * rate
341
+ bulk = material.mu_v * trace / params.dt
342
+ mp.dstress = (mp.pressure + bulk)[:, None, None] * EYE + mp.dev_stress - mp.stress
343
+
344
+
345
+ def pouliquen(mp, material, params):
346
+ """Bounded mu(I) flowing law using engineering shear rate and true density.
347
+
348
+ No grid-occupancy density cutoff. The finite cap regularises the zero-rate
349
+ limit. At zero confinement, I is undefined (stored as zero), mu uses its
350
+ infinite-I limit for nonzero flow, and frictional stress vanishes.
351
+ """
352
+ trace, rate = _flow_increment(mp, params)
353
+ old_pressure = mp.pressure.copy()
354
+ mp.pressure = np.minimum(old_pressure + material.K * trace, 0.0)
355
+ mp.dp = mp.pressure - old_pressure
356
+ p = -mp.pressure
357
+ size = mp.phi @ params.G.s
358
+ mp.I.fill(0.0)
359
+ positive = p > 0
360
+ mp.I[positive] = mp.gammadot[positive] * size[positive] * np.sqrt(material.rho_s) / np.sqrt(p[positive])
361
+ ratio = np.divide(mp.I, material.I_0 + mp.I, out=np.zeros_like(p), where=mp.I > 0)
362
+ ratio[(~positive) & (mp.gammadot > 0)] = 1.0
363
+ mp.mu = material.mu_0 + material.delta_mu * ratio
364
+ mp.eta = np.divide(
365
+ mp.mu * p, mp.gammadot, out=np.where(mp.mu * p > 0, material.eta_max, 0.0), where=mp.gammadot > 0
366
+ )
367
+ mp.eta = np.minimum(mp.eta, material.eta_max)
368
+ mp.eta_limited = mp.eta.copy()
369
+ mp.dev_stress = 2 * mp.eta[:, None, None] * rate
370
+ mp.dstress = mp.pressure[:, None, None] * EYE + mp.dev_stress - mp.stress
371
+
372
+
373
+ def von_mises(mp, material, params):
374
+ """Perfect J2 plasticity; yield_stress (Pa), or sqrt(3)*legacy k."""
375
+ trace, dev = invariants(mp.dstrain)
376
+ trial = mp.stress + material.K * trace[:, None, None] * EYE + 2 * material.G * dev
377
+ tr, shear = invariants(trial)
378
+ q = np.sqrt(1.5) * np.linalg.norm(shear, axis=(1, 2))
379
+ strength = getattr(material, "yield_stress", np.sqrt(3) * getattr(material, "k", 0.0))
380
+ multiplier = np.maximum(q - strength, 0) / (3 * material.G)
381
+ scale = np.divide(np.minimum(q, strength), q, out=np.zeros_like(q), where=q > 0)
382
+ stress = tr[:, None, None] * EYE / 3 + scale[:, None, None] * shear
383
+ mp.dstress = stress - mp.stress
384
+ mp.yieldfunction = np.minimum(q, strength) - strength
385
+ mp.gammadot = np.sqrt(2) * np.linalg.norm(dev, axis=(1, 2)) / params.dt
386
+
387
+
388
+ def _compliance(stress, material):
389
+ trace, dev = invariants(stress)
390
+ return trace[:, None, None] * EYE / (9 * material.K) + dev / (2 * material.G)
391
+
392
+
393
+ def update_stress(mp, material, params):
394
+ """Objective updated mechanics; historical integration is opt-in legacy mode."""
395
+ if params.mechanics == "legacy":
396
+ _legacy_update_stress(mp, material, params)
397
+ return
398
+ old = mp.stress.copy()
399
+ old_bulk_pressure = mp.pressure.copy()
400
+ _, old_dev = invariants(old)
401
+ plastic = material.law in ("dp", "dp_rate", "von_mises")
402
+ if plastic:
403
+ R = _half_rotation(mp, params.dt)
404
+ mp.stress = R @ old @ R.swapaxes(1, 2)
405
+ mp.plastic_strain = R @ mp.plastic_strain @ R.swapaxes(1, 2)
406
+ before = mp.stress.copy()
407
+ mp.law(mp, material, params)
408
+ result = mp.stress + mp.dstress
409
+ if plastic:
410
+ dep = mp.dstrain - _compliance(result - before, material)
411
+ dissipation = np.sum(result * dep, axis=(1, 2)) * mp.V
412
+ tolerance = 1e-10 * np.maximum(1.0, np.linalg.norm(result, axis=(1, 2)) * mp.V)
413
+ if np.any(dissipation < -tolerance):
414
+ raise FloatingPointError("negative plastic dissipation")
415
+ mp.plastic_work += np.maximum(dissipation, 0)
416
+ mp.plastic_strain += dep
417
+ mp.plastic_strain = R @ mp.plastic_strain @ R.swapaxes(1, 2)
418
+ result = R @ result @ R.swapaxes(1, 2)
419
+ mp.elastic_strain = _compliance(result, material)
420
+ mp.elastic_energy = 0.5 * mp.V * np.sum(result * mp.elastic_strain, axis=(1, 2))
421
+ elif material.law != "elastic":
422
+ trace, dev = invariants(mp.dstrain)
423
+ _, shear = invariants(result)
424
+ bulk_viscosity = getattr(material, "mu_v", 0.0) if material.law in ("viscous", "bingham") else 0.0
425
+ mp.viscous_work += mp.V * (np.sum(shear * dev, axis=(1, 2)) + bulk_viscosity * trace**2 / params.dt)
426
+ # Integrate elastic bulk work p(J) dV exactly along this exponential
427
+ # volume increment. No-tension flow laws have a clipped pressure path.
428
+ old_volume = mp.V * np.exp(-trace)
429
+ if material.K:
430
+ start = np.zeros_like(trace)
431
+ stop = trace.copy()
432
+ if material.law in ("pouliquen", "linear_mu"):
433
+ cutoff = -old_bulk_pressure / material.K
434
+ start = np.minimum(start, cutoff)
435
+ stop = np.minimum(stop, cutoff)
436
+
437
+ def potential(t):
438
+ remainder = np.where(
439
+ np.abs(t) < 1e-4,
440
+ t**2 / 2 + t**3 / 3 + t**4 / 8 + t**5 / 30 + t**6 / 144,
441
+ t * np.exp(t) - np.expm1(t),
442
+ )
443
+ return old_bulk_pressure * np.expm1(t) + material.K * remainder
444
+
445
+ mp.elastic_energy += old_volume * (potential(stop) - potential(start))
446
+ else:
447
+ mp.elastic_energy += mp.pressure * (mp.V - old_volume)
448
+ mp.stress = result
449
+ mp.dstress = result - old
450
+ mean, dev = invariants(result)
451
+ if material.law in ("elastic", "dp", "dp_rate", "von_mises"):
452
+ mp.pressure = mean / 3
453
+ mp.sigma_kk = mean
454
+ mp.dev_stress = dev
455
+ mp.dev_dstress = dev - old_dev
456
+ mp.material_work += np.sum(0.5 * (old + result) * mp.dstrain, axis=(1, 2)) * mp.V
457
+
458
+
459
+ REGISTRY = {name: globals()[name] for name in SUPPORTED}
460
+
461
+
462
+ def get_law(name, mechanics="updated"):
463
+ try:
464
+ return (LEGACY_REGISTRY if mechanics == "legacy" else REGISTRY)[name]
465
+ except KeyError:
466
+ reason = RETIRED.get(name, "No implemented and tested constitutive law with this name.")
467
+ raise ValueError(
468
+ f"Unsupported material law {name!r}. {reason} Supported: {', '.join(SUPPORTED)}."
469
+ ) from None