-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsolver.jl
More file actions
177 lines (148 loc) · 8.14 KB
/
Copy pathsolver.jl
File metadata and controls
177 lines (148 loc) · 8.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# ===========================================================================
# PhaseFieldFracture — a readable, pure-Julia phase-field fracture solver
#
# Author: Yang Bai — Materials Mechanics Laboratory (MMLab)
# Contact: yangbai90@outlook.com
# Copyright: © 2026 Materials Mechanics Laboratory (MMLab). All rights reserved.
# ===========================================================================
# ===========================================================================
# Staggered solver: monotonic loading + alternate minimization
# ===========================================================================
#
# The coupled problem "find u and φ that minimize the energy" is nonlinear
# because of the g(φ)ψ coupling. We solve it the standard robust way:
# hold one field fixed and solve the (now linear) problem for the other, and
# alternate. Per load step:
#
# ┌─ solve displacement u with φ fixed (Newton; nonlinear once
# │ the tension/compression
# │ split is active)
# │ update history H(u) (irreversible driving force)
# └─ solve damage φ with H fixed (linear AT2 phase field)
#
# repeated until neither field changes, then the history is committed and the
# imposed displacement is increased for the next step.
# ---------------------------------------------------------------------------
# Settings and problem definition
# ---------------------------------------------------------------------------
"Numerical settings for the load stepping and the staggered iteration."
Base.@kwdef struct Settings
nsteps::Int = 50 # number of load steps
umax::Float64 = 8e-3 # final imposed top displacement u_y (reached at the last step)
max_iter::Int = 50 # cap on staggered iterations within one load step
tol::Float64 = 1e-6 # convergence tolerance on the relative change of u and φ
out_every::Int = 5 # save a .vtu snapshot every `out_every` steps
end
"Everything needed to run one simulation: geometry, material, settings, notch, output paths."
struct Problem
mesh::Mesh # the finite element mesh
material::Material # elastic + fracture properties
settings::Settings # solver controls
crack_nodes::Vector{Int} # nodes of the prescribed initial crack (held at φ = 1)
outdir::String # folder for the .vtu / .pvd files
histfile::String # path of the CSV load-history file
end
# ---------------------------------------------------------------------------
# Small numerical helpers
# ---------------------------------------------------------------------------
"Relative change between two successive iterates — the staggered convergence measure."
rel_change(new, old) = norm(new - old) / max(norm(new), 1.0)
"""
solve_dirichlet(K, f, bc) -> x
Solve the linear system `K x = f` subject to Dirichlet (prescribed-value)
constraints `bc`, a dictionary `dof => value`.
Splitting the dofs into *free* (unknown) and *fixed* (prescribed):
x_fixed = the prescribed values
K_ff x_free = f_free − K_fc x_fixed (the reduced system actually solved)
Here `K[free, fixed] * x[fixed]` moves the known columns to the right-hand side.
"""
function solve_dirichlet(K, f, bc::Dict{Int,Float64})
ndof = length(f)
isfixed = falses(ndof) # boolean mask: is this dof prescribed?
for d in keys(bc)
isfixed[d] = true
end
fixed = findall(isfixed) # indices of prescribed dofs
free = findall(.!isfixed) # indices of free (unknown) dofs
x = zeros(ndof)
for (d, v) in bc # write the prescribed values into x
x[d] = v
end
if !isempty(free) # solve the reduced system for the free dofs
x[free] = K[free, free] \ (f[free] - K[free, fixed] * x[fixed])
end
return x
end
# ---------------------------------------------------------------------------
# The main driver
# ---------------------------------------------------------------------------
"""
run_simulation(problem) -> (u, φ, H)
Run the full staggered simulation. Returns the final displacement `u`, damage
`φ`, and history field `H`. Along the way it writes:
* `problem.histfile` — CSV load history (one row per step),
* `problem.outdir/field_XXXX.vtu` — nodal fields at saved steps,
* `problem.outdir/timeseries.pvd` — ParaView collection over those steps.
"""
function run_simulation(problem::Problem)
mesh, mat, set = problem.mesh, problem.material, problem.settings
mkpath(problem.outdir)
write_history_header(problem.histfile)
# State vectors. `u` holds the two displacement dofs per node; `φ` holds one
# damage value per node. The pre-crack nodes start fully broken.
u = zeros(2 * nnodes(mesh))
φ = zeros(nnodes(mesh))
for n in problem.crack_nodes
φ[n] = 1.0
end
# History field H: the maximum elastic energy density seen so far at every
# Gauss point. It is the irreversible driving force for damage. `H` is the
# committed value (carried between load steps); `H_trial` is a scratch copy
# recomputed inside the staggered loop from the current displacement.
H = zeros(length(GAUSS), ncells(mesh))
H_trial = similar(H)
# Regularize the notch: with zero history, one phase-field solve spreads the
# sharp φ = 1 crack line into the smooth AT2 damage profile of width ~ℓ.
φ = solve_phase(mesh, mat, H, φ, problem.crack_nodes)
# Snapshot 0 — the cracked body before any load is applied. `pvd` accumulates
# (time, filename) pairs for the ParaView collection written at the very end.
f_int = zeros(2 * nnodes(mesh)) # internal force (= 0 at u = 0)
write_vtu(joinpath(problem.outdir, "field_0000.vtu"), mesh, u, φ)
pvd = Tuple{Float64,String}[(0.0, "field_0000.vtu")]
log_step(problem, 0, 0.0, u, φ, f_int, 0, true)
for step in 1:set.nsteps
top_uy = set.umax * step / set.nsteps # ramp the imposed displacement linearly
φ_committed = copy(φ) # damage floor for this step (irreversibility)
converged = false
iters = set.max_iter
# ---- staggered (alternate-minimization) loop ----
for it in 1:set.max_iter
u_prev, φ_prev = copy(u), copy(φ) # previous iterates, for convergence test
u, f_int = solve_displacement(mesh, mat, φ, top_uy, u) # (1) elasticity (Newton)
update_history!(H_trial, H, mesh, mat, u) # (2) H ← max(H, ψ⁺)
φ = solve_phase(mesh, mat, H_trial, φ_committed, problem.crack_nodes) # (3) damage
# Stop once neither displacement nor damage changes appreciably.
if max(rel_change(u, u_prev), rel_change(φ, φ_prev)) < set.tol
converged = true
iters = it
break
end
end
converged || @printf(" (step %d did not converge in %d iterations)\n", step, set.max_iter)
# Commit the history field once the two fields have settled at this load.
update_history!(H_trial, H, mesh, mat, u)
H .= H_trial
# Save a snapshot on the chosen cadence (and always on the final step).
if step % set.out_every == 0 || step == set.nsteps
fname = @sprintf("field_%04d.vtu", step)
write_vtu(joinpath(problem.outdir, fname), mesh, u, φ)
push!(pvd, (top_uy, fname)) # use the imposed load as the "time"
end
log_step(problem, step, top_uy, u, φ, f_int, iters, converged)
@printf("step %3d/%d | u_y = %.4e | max φ (off-crack) = %.4f | iters = %d\n",
step, set.nsteps, top_uy, max_free_phi(φ, problem.crack_nodes), iters)
end
# Tie all snapshots into one ParaView time series.
write_pvd(joinpath(problem.outdir, "timeseries.pvd"), pvd)
return u, φ, H
end