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.
- MPM.py +101 -0
- constit.py +469 -0
- diagnostics.py +210 -0
- grid.py +259 -0
- initialise.py +187 -0
- inputs/__init__.py +0 -0
- inputs/anisotropy.py +119 -0
- inputs/avalanche.py +108 -0
- inputs/ball.py +98 -0
- inputs/bi_conveyor.py +116 -0
- inputs/bi_dam_break.py +167 -0
- inputs/bi_double_insets.py +128 -0
- inputs/bi_drum.py +159 -0
- inputs/bi_seg_test.py +166 -0
- inputs/bi_square_drum.py +146 -0
- inputs/bi_vertical_chute.py +145 -0
- inputs/bingham_unit_test.py +127 -0
- inputs/brazillian.py +124 -0
- inputs/chuteflow.py +117 -0
- inputs/dam_break.py +107 -0
- inputs/dp_rate_unit_test.py +117 -0
- inputs/dp_unit_test.py +120 -0
- inputs/flow.py +159 -0
- inputs/fluid.py +137 -0
- inputs/francois.py +153 -0
- inputs/gsd.py +142 -0
- inputs/incline.py +128 -0
- inputs/macro.py +117 -0
- inputs/modern_ball.py +15 -0
- inputs/pierre.py +137 -0
- inputs/poly_avalanche.py +109 -0
- inputs/poly_seg_test.py +118 -0
- inputs/pouliquen_unit_test.py +167 -0
- inputs/seg_unit_test.py +107 -0
- inputs/silo.py +131 -0
- inputs/slope_stab.py +111 -0
- inputs/stokes.py +161 -0
- inputs/tri_seg_test.py +127 -0
- inputs/triaxial.py +100 -0
- inputs/tunnel.py +105 -0
- inputs/vicky.py +105 -0
- inputs/vicky_latest.py +113 -0
- inputs/viscous_unit_test.py +120 -0
- inputs/vm_unit_test.py +92 -0
- integrators.py +260 -0
- interpolation.py +71 -0
- kinematics.py +20 -0
- particles.py +529 -0
- plotting.py +217 -0
- poly_mpm-1.0.0.dist-info/METADATA +157 -0
- poly_mpm-1.0.0.dist-info/RECORD +57 -0
- poly_mpm-1.0.0.dist-info/WHEEL +5 -0
- poly_mpm-1.0.0.dist-info/entry_points.txt +2 -0
- poly_mpm-1.0.0.dist-info/licenses/LICENSE +674 -0
- poly_mpm-1.0.0.dist-info/top_level.txt +13 -0
- stability.py +66 -0
- 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
|