Skip to content

Commit 9a1ce86

Browse files
committed
Now make it public, since it is very useful
1 parent 53c0d54 commit 9a1ce86

File tree

5 files changed

+207
-36
lines changed

5 files changed

+207
-36
lines changed

docs/src/private_api_reference.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,4 @@ Peridynamics.displacement_bc!
7171
Peridynamics.velocity_databc!
7272
Peridynamics.forcedensity_databc!
7373
Peridynamics.NewtonRaphson
74-
Peridynamics.Study
75-
Peridynamics.submit!
76-
Peridynamics.process_each_job
7774
```

docs/src/public_api_reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,14 @@ reset_mpi_progress_bars!
7878
VelocityVerlet
7979
DynamicRelaxation
8080
Job
81+
Study
8182
submit
83+
submit!
8284
```
8385

8486
## Postprocessing
8587
```@docs
8688
read_vtk
8789
process_each_export
90+
process_each_job
8891
```

src/Peridynamics.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ export Body, point_set!, point_sets, no_failure!, material!, velocity_bc!, veloc
3030
uniform_cylinder, round_sphere, round_cylinder, n_points
3131

3232
# Running simulations
33-
export VelocityVerlet, DynamicRelaxation, Job, submit
33+
export VelocityVerlet, DynamicRelaxation, Job, Study, submit, submit!
3434

3535
# Pre processing
3636
export read_inp
3737

3838
# Post processing and helpers
39-
export read_vtk, process_each_export, mpi_isroot, force_mpi_run!, force_threads_run!,
40-
enable_mpi_timers!, disable_mpi_timers!, enable_mpi_progress_bars!,
41-
reset_mpi_progress_bars!, @mpitime, @mpiroot
39+
export read_vtk, process_each_export, process_each_job, mpi_isroot, force_mpi_run!,
40+
force_threads_run!, enable_mpi_timers!, disable_mpi_timers!,
41+
enable_mpi_progress_bars!, reset_mpi_progress_bars!, @mpitime, @mpiroot
4242

4343
function __init__()
4444
init_mpi()

src/core/study.jl

Lines changed: 118 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
"""
22
Study(jobcreator::Function, setups::Vector{<:NamedTuple}; root::String)
33
4-
$(internal_api_warning())
5-
$(experimental_api_warning())
6-
74
A structure for managing parameter studies with multiple peridynamic simulations. The
85
`Study` type coordinates the execution of multiple simulation jobs with different parameter
96
configurations, tracks their status, and logs all results to a central logfile.
@@ -38,11 +35,13 @@ configurations, tracks their status, and logs all results to a central logfile.
3835
3936
# Example
4037
```julia
38+
# some function that creates a Job from a parameter setup
4139
function create_job(setup::NamedTuple, root::String)
4240
body = Body(BBMaterial(), uniform_box(1.0, 1.0, 1.0, 0.1))
4341
material!(body, horizon=0.3, E=setup.E, rho=1000, Gc=100)
4442
velocity_ic!(body, :all_points, :x, setup.velocity)
4543
solver = VelocityVerlet(steps=1000)
44+
# create a unique path for this job based on parameters
4645
path = joinpath(root, "sim_E\$(setup.E)_v\$(setup.velocity)")
4746
return Job(body, solver; path=path, freq=10)
4847
end
@@ -75,7 +74,17 @@ struct Study{F,S,J}
7574
check_jobpaths_unique(jobpaths)
7675
sim_success = fill(false, length(jobs))
7776
logfile = joinpath(root, "study_log.log")
78-
new{F,S,J}(jobcreator, setups, jobs, jobpaths, root, logfile, sim_success)
77+
st = new{F,S,J}(jobcreator, setups, jobs, jobpaths, root, logfile, sim_success)
78+
# If a logfile already exists from a previous run, initialize sim_success
79+
# from the logfile so processing or resuming works across interrupted runs.
80+
if isfile(st.logfile)
81+
try
82+
update_sim_success_from_log!(st)
83+
catch err
84+
@warn "failed to refresh study status from logfile" error=err
85+
end
86+
end
87+
return st
7988
end
8089
end
8190

@@ -119,9 +128,6 @@ end
119128
"""
120129
submit!(study::Study; kwargs...)
121130
122-
$(internal_api_warning())
123-
$(experimental_api_warning())
124-
125131
Submit and execute all simulation jobs in a parameter study. Jobs are run sequentially,
126132
and each job can utilize MPI or multithreading as configured. If a job fails, the error
127133
is logged and execution continues with the remaining jobs.
@@ -166,16 +172,45 @@ See also: [`Study`](@ref), [`submit`](@ref)
166172
"""
167173
function submit!(study::Study; kwargs...)
168174
# Create root directory if it doesn't exist
169-
if !isdir(study.root)
170-
mkpath(study.root)
175+
isdir(study.root) || mkpath(study.root)
176+
177+
# If logfile exists, refresh sim_success and append a resume marker. Otherwise
178+
# create a new logfile with header.
179+
if isfile(study.logfile)
180+
try
181+
update_sim_success_from_log!(study)
182+
catch err
183+
@warn "failed to refresh study status from existing logfile" error=err
184+
end
185+
open(study.logfile, "a") do io
186+
datetime = Dates.format(Dates.now(), "yyyy-mm-dd, HH:MM:SS")
187+
write(io, "\n--- RESUMED: $datetime ---\n\n")
188+
end
189+
else
190+
open(study.logfile, "w+") do io
191+
write(io, get_logfile_head())
192+
write(io, peridynamics_banner(color=false))
193+
write(io, "\nSIMULATION STUDY LOGFILE\n\n")
194+
end
171195
end
172196

173-
open(study.logfile, "w+") do io
174-
write(io, get_logfile_head())
175-
write(io, peridynamics_banner(color=false))
176-
write(io, "\nSIMULATION STUDY LOGFILE\n\n")
177-
end
197+
# Now submit each job
178198
for (i, job) in enumerate(study.jobs)
199+
# If this job already completed in a previous run, skip execution
200+
open(study.logfile, "a") do io
201+
msg = "Simulation `$(study.jobpaths[i])`:\n"
202+
for (key, value) in pairs(study.setups[i])
203+
msg *= " $(key): $(value)\n"
204+
end
205+
write(io, msg)
206+
end
207+
if study.sim_success[i]
208+
# Write a short note about skipping to the study logfile
209+
open(study.logfile, "a") do io
210+
write(io, " status: skipped (already completed)\n\n")
211+
end
212+
continue
213+
end
179214
success = false
180215
simtime = @elapsed begin
181216
try
@@ -188,23 +223,18 @@ function submit!(study::Study; kwargs...)
188223
log_it(job.options, "\nERROR: Simulation failed with error!\n")
189224
log_it(job.options, sprint(showerror, err, catch_backtrace()))
190225
log_it(job.options, "\n")
191-
catch log_err
192-
# If logging fails, just continue - error will be recorded in study log
226+
catch
227+
# If logging fails, just continue - error will be recorded in job log
193228
end
194229
end
195230
end
196231
study.sim_success[i] = success
197232
open(study.logfile, "a") do io
198-
msg = "Simulation `$(study.jobpaths[i])`:\n"
199-
for (key, value) in pairs(study.setups[i])
200-
msg *= " $(key): $(value)\n"
201-
end
202233
if success
203-
msg *= @sprintf(" status: completed ✓ (%.2f seconds)\n", simtime)
234+
msg *= @sprintf(" status: completed ✓ (%.2f seconds)\n\n", simtime)
204235
else
205-
msg *= " status: failed ✗\n"
236+
msg *= " status: failed ✗\n\n"
206237
end
207-
msg *= "\n"
208238
write(io, msg)
209239
end
210240
end
@@ -216,10 +246,62 @@ function submit!(study::Study; kwargs...)
216246
end
217247

218248
"""
219-
process_each_job(f::Function, study::Study, default_result::NamedTuple)
249+
update_sim_success_from_log!(study::Study)
220250
221251
$(internal_api_warning())
222-
$(experimental_api_warning())
252+
253+
Read the `study.logfile` and update `study.sim_success` flags according to the
254+
last recorded status for each job. This allows resuming processing or submission
255+
after an interrupted run.
256+
"""
257+
function update_sim_success_from_log!(study::Study)
258+
isfile(study.logfile) || return false
259+
260+
# Read logfile line by line
261+
lines = readlines(study.logfile)
262+
263+
for (i, path) in enumerate(study.jobpaths)
264+
# Search for the Simulation line for this job path
265+
sim_line_pattern = "Simulation `$(path)`:"
266+
sim_line_idx = findfirst(line -> occursin(sim_line_pattern, line), lines)
267+
268+
if sim_line_idx === nothing
269+
# No record for this job in logfile
270+
study.sim_success[i] = false
271+
continue
272+
end
273+
274+
# Look for status line in the next few lines after the simulation line
275+
found_status = false
276+
for j in (sim_line_idx + 1):length(lines)
277+
if occursin("status: completed", lines[j])
278+
study.sim_success[i] = true
279+
found_status = true
280+
break
281+
elseif occursin("status: failed", lines[j])
282+
study.sim_success[i] = false
283+
found_status = true
284+
break
285+
elseif occursin("status: skipped", lines[j])
286+
study.sim_success[i] = true
287+
found_status = true
288+
break
289+
elseif occursin("Simulation `", lines[j])
290+
# Reached next simulation entry without finding status
291+
break
292+
end
293+
end
294+
295+
if !found_status
296+
# No status found after simulation line
297+
study.sim_success[i] = false
298+
end
299+
end
300+
return true
301+
end
302+
303+
"""
304+
process_each_job(f::Function, study::Study, default_result::NamedTuple)
223305
224306
Apply a processing function to each successfully completed job in a parameter study.
225307
This function iterates through all jobs in the study and applies the user-defined
@@ -255,10 +337,8 @@ submit!(study)
255337
256338
# Define a function to extract maximum displacement from results
257339
function extract_max_displacement(job::Job, setup::NamedTuple)
258-
# Read results from job output directory
259-
results_file = joinpath(job.options.root, "results_step_0010.jld2")
260-
data = load_results(results_file)
261-
max_u = maximum(norm, eachcol(data.displacement))
340+
# Calculate maximum displacement
341+
max_u = ...
262342
return (; E=setup.E, velocity=setup.velocity, max_displacement=max_u)
263343
end
264344
@@ -272,6 +352,15 @@ successful_results = [r for r in results if !isnan(r.max_displacement)]
272352
See also: [`Study`](@ref), [`submit!`](@ref)
273353
"""
274354
function process_each_job(f::F, study::Study, default_result::NamedTuple) where {F}
355+
# Refresh sim_success from logfile if it exists (in case study was interrupted/resumed)
356+
if isfile(study.logfile)
357+
try
358+
update_sim_success_from_log!(study)
359+
catch err
360+
@warn "failed to refresh study status from logfile before processing" error=err
361+
end
362+
end
363+
275364
results = fill(default_result, length(study.jobs))
276365
for (i, job) in enumerate(study.jobs)
277366
if study.sim_success[i]

test/core/test_study.jl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,88 @@
3737
Peridynamics.MPI_RUN[] = mpi_run_current_value
3838
end
3939

40+
@testitem "resume processing from existing logfile" tags=[:skipci] begin
41+
mpi_run_current_value = Peridynamics.MPI_RUN[]
42+
Peridynamics.MPI_RUN[] = false
43+
44+
mktempdir() do tmpdir
45+
function create_job(setup::NamedTuple, root::String)
46+
body = Body(BBMaterial(), rand(3, 10), rand(10))
47+
material!(body, horizon=1, E=setup.E, rho=1, Gc=1)
48+
velocity_ic!(body, :all_points, :x, 1.0)
49+
vv = VelocityVerlet(steps=setup.n_steps)
50+
path = joinpath(root, "sim_$(setup.E)")
51+
job = Job(body, vv; path=path, freq=5)
52+
return job
53+
end
54+
55+
setups = [
56+
(; E=1.0, n_steps=1),
57+
(; E=2.0, n_steps=1),
58+
]
59+
60+
studyroot = joinpath(tmpdir, "study")
61+
study = Peridynamics.Study(create_job, setups; root=studyroot)
62+
63+
# Simulate an interrupted run: create study root and mark first job completed
64+
mkpath(study.root)
65+
mkpath(study.jobpaths[1])
66+
open(study.logfile, "w") do io
67+
write(io, "SIMULATION STUDY LOGFILE\n\n")
68+
write(io, "Simulation `$(study.jobpaths[1])`:\n status: completed ✓ (0.00 seconds)\n\n")
69+
end
70+
71+
# Now processing should detect that first job succeeded and only process that one
72+
processed = Peridynamics.process_each_job((job, setup) -> (; E=setup.E), study, (; E=0.0))
73+
74+
@test processed[1].E == 1.0
75+
@test processed[2].E == 0.0
76+
end
77+
78+
Peridynamics.MPI_RUN[] = mpi_run_current_value
79+
end
80+
81+
@testitem "resume submit! only runs remaining jobs" tags=[:skipci] begin
82+
mpi_run_current_value = Peridynamics.MPI_RUN[]
83+
Peridynamics.MPI_RUN[] = false
84+
85+
mktempdir() do tmpdir
86+
function create_job(setup::NamedTuple, root::String)
87+
body = Body(BBMaterial(), rand(3, 10), rand(10))
88+
material!(body, horizon=1, E=setup.E, rho=1, Gc=1)
89+
velocity_ic!(body, :all_points, :x, 1.0)
90+
vv = VelocityVerlet(steps=setup.n_steps)
91+
path = joinpath(root, "sim_$(setup.E)")
92+
job = Job(body, vv; path=path, freq=5)
93+
return job
94+
end
95+
96+
setups = [
97+
(; E=1.0, n_steps=1),
98+
(; E=2.0, n_steps=1),
99+
]
100+
101+
studyroot = joinpath(tmpdir, "study")
102+
study = Peridynamics.Study(create_job, setups; root=studyroot)
103+
104+
# Simulate an interrupted run: create study root and mark first job completed
105+
mkpath(study.root)
106+
mkpath(study.jobpaths[1])
107+
open(study.logfile, "w") do io
108+
write(io, "SIMULATION STUDY LOGFILE\n\n")
109+
write(io, "Simulation `$(study.jobpaths[1])`:\n status: completed ✓ (0.00 seconds)\n\n")
110+
end
111+
112+
# Now resuming submit! should only run the remaining job (E=2)
113+
Peridynamics.submit!(study; quiet=true)
114+
115+
@test study.sim_success == [true, true]
116+
@test isdir(study.jobpaths[2])
117+
end
118+
119+
Peridynamics.MPI_RUN[] = mpi_run_current_value
120+
end
121+
40122
@testitem "Study constructor with empty setups" begin
41123
mktempdir() do tmpdir
42124
function create_job(setup::NamedTuple, root::String)

0 commit comments

Comments
 (0)