• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

trixi-framework / Trixi.jl / 21639639306

03 Feb 2026 05:01PM UTC coverage: 14.281% (-82.8%) from 97.034%
21639639306

Pull #2601

github

web-flow
Merge fd2e0cdf8 into ead0db32a
Pull Request #2601: Adaptive Volume Integral

28 of 281 new or added lines in 41 files covered. (9.96%)

36284 existing lines in 548 files now uncovered.

6317 of 44235 relevant lines covered (14.28%)

100377.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.67
/src/callbacks_step/save_solution.jl
1
# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
2
# Since these FMAs can increase the performance of many numerical algorithms,
3
# we need to opt-in explicitly.
4
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
5
@muladd begin
6
#! format: noindent
7

8
"""
9
    SaveSolutionCallback(; interval::Integer=0,
10
                           dt=nothing,
11
                           save_initial_solution=true,
12
                           save_final_solution=true,
13
                           output_directory="out",
14
                           solution_variables=cons2prim,
15
                           extra_node_variables=())
16

17
Save the current numerical solution in regular intervals. Either pass `interval` to save
18
every `interval` time steps or pass `dt` to save in intervals of `dt` in terms
19
of integration time by adding additional (shortened) time steps where necessary (note that this may change the solution).
20
`solution_variables` can be any callable that converts the conservative variables
21
at a single point to a set of solution variables. The first parameter passed
22
to `solution_variables` will be the set of conservative variables
23
and the second parameter is the equation struct.
24

25
Additional nodal variables such as vorticity or the Mach number can be saved by passing a tuple of symbols
26
to `extra_node_variables`, e.g., `extra_node_variables = (:vorticity, :mach)`.
27
In that case the function `get_node_variable` must be defined for each symbol in the tuple.
28
The expected signature of the function for (purely) hyperbolic equations is:
29
```julia
30
function get_node_variable(::Val{symbol}, u, mesh, equations, dg, cache)
31
    # Implementation goes here
32
end
33
```
34
and must return an array of dimension
35
`(ntuple(_ -> n_nodes, ndims(mesh))..., n_elements)`.
36

37
For parabolic-hyperbolic equations `equations_parabolic` and `cache_parabolic` must be added:
38
```julia
39
function get_node_variable(::Val{symbol}, u, mesh, equations, dg, cache,
40
                           equations_parabolic, cache_parabolic)
41
    # Implementation goes here
42
end
43
```
44
"""
45
struct SaveSolutionCallback{IntervalType, SolutionVariablesType}
46
    interval_or_dt::IntervalType
12✔
47
    save_initial_solution::Bool
48
    save_final_solution::Bool
49
    output_directory::String
50
    solution_variables::SolutionVariablesType
51
    node_variables::Dict{Symbol, Any}
52
end
53

UNCOV
54
function Base.show(io::IO, cb::DiscreteCallback{<:Any, <:SaveSolutionCallback})
×
UNCOV
55
    @nospecialize cb # reduce precompilation time
×
56

UNCOV
57
    save_solution_callback = cb.affect!
×
UNCOV
58
    print(io, "SaveSolutionCallback(interval=", save_solution_callback.interval_or_dt,
×
59
          ")")
UNCOV
60
    return nothing
×
61
end
62

63
function Base.show(io::IO,
×
64
                   cb::DiscreteCallback{<:Any,
65
                                        <:PeriodicCallbackAffect{<:SaveSolutionCallback}})
66
    @nospecialize cb # reduce precompilation time
×
67

68
    save_solution_callback = cb.affect!.affect!
×
69
    print(io, "SaveSolutionCallback(dt=", save_solution_callback.interval_or_dt, ")")
×
70
    return nothing
×
71
end
72

73
function Base.show(io::IO, ::MIME"text/plain",
12✔
74
                   cb::DiscreteCallback{<:Any, <:SaveSolutionCallback})
75
    @nospecialize cb # reduce precompilation time
12✔
76

77
    if get(io, :compact, false)
48✔
78
        show(io, cb)
×
79
    else
80
        save_solution_callback = cb.affect!
12✔
81

82
        setup = [
24✔
83
            "interval" => save_solution_callback.interval_or_dt,
84
            "solution variables" => save_solution_callback.solution_variables,
85
            "save initial solution" => save_solution_callback.save_initial_solution ?
86
                                       "yes" : "no",
87
            "save final solution" => save_solution_callback.save_final_solution ?
88
                                     "yes" : "no",
89
            "output directory" => abspath(normpath(save_solution_callback.output_directory))
90
        ]
91
        summary_box(io, "SaveSolutionCallback", setup)
12✔
92
    end
93
end
94

UNCOV
95
function Base.show(io::IO, ::MIME"text/plain",
×
96
                   cb::DiscreteCallback{<:Any,
97
                                        <:PeriodicCallbackAffect{<:SaveSolutionCallback}})
UNCOV
98
    @nospecialize cb # reduce precompilation time
×
99

UNCOV
100
    if get(io, :compact, false)
×
101
        show(io, cb)
×
102
    else
UNCOV
103
        save_solution_callback = cb.affect!.affect!
×
104

UNCOV
105
        setup = [
×
106
            "dt" => save_solution_callback.interval_or_dt,
107
            "solution variables" => save_solution_callback.solution_variables,
108
            "save initial solution" => save_solution_callback.save_initial_solution ?
109
                                       "yes" : "no",
110
            "save final solution" => save_solution_callback.save_final_solution ?
111
                                     "yes" : "no",
112
            "output directory" => abspath(normpath(save_solution_callback.output_directory))
113
        ]
UNCOV
114
        summary_box(io, "SaveSolutionCallback", setup)
×
115
    end
116
end
117

118
function SaveSolutionCallback(; interval::Integer = 0,
24✔
119
                              dt = nothing,
120
                              save_initial_solution = true,
121
                              save_final_solution = true,
122
                              output_directory = "out",
123
                              solution_variables = cons2prim,
124
                              extra_node_variables = ())
125
    if !isnothing(dt) && interval > 0
12✔
126
        throw(ArgumentError("You can either set the number of steps between output (using `interval`) or the time between outputs (using `dt`) but not both simultaneously"))
×
127
    end
128

129
    # Expected most frequent behavior comes first
130
    if isnothing(dt)
12✔
131
        interval_or_dt = interval
12✔
132
    else # !isnothing(dt)
UNCOV
133
        interval_or_dt = dt
×
134
    end
135

136
    node_variables = Dict{Symbol, Any}(var => nothing for var in extra_node_variables)
12✔
137
    solution_callback = SaveSolutionCallback(interval_or_dt,
12✔
138
                                             save_initial_solution, save_final_solution,
139
                                             output_directory, solution_variables,
140
                                             node_variables)
141

142
    # Expected most frequent behavior comes first
143
    if isnothing(dt)
12✔
144
        # Save every `interval` (accepted) time steps
145
        # The first one is the condition, the second the affect!
146
        return DiscreteCallback(solution_callback, solution_callback,
12✔
147
                                save_positions = (false, false),
148
                                initialize = initialize_save_cb!)
149
    else
150
        # Add a `tstop` every `dt`, and save the final solution.
UNCOV
151
        return PeriodicCallback(solution_callback, dt,
×
152
                                save_positions = (false, false),
153
                                initialize = initialize_save_cb!,
154
                                final_affect = save_final_solution)
155
    end
156
end
157

158
function initialize_save_cb!(cb, u, t, integrator)
12✔
159
    # The SaveSolutionCallback is either cb.affect! (with DiscreteCallback)
160
    # or cb.affect!.affect! (with PeriodicCallback).
161
    # Let recursive dispatch handle this.
162
    return initialize_save_cb!(cb.affect!, u, t, integrator)
12✔
163
end
164

165
function initialize_save_cb!(solution_callback::SaveSolutionCallback, u, t, integrator)
12✔
166
    mpi_isroot() && mkpath(solution_callback.output_directory)
12✔
167

168
    semi = integrator.p
12✔
169
    @trixi_timeit timer() "I/O" save_mesh(semi, solution_callback.output_directory)
12✔
170

171
    if solution_callback.save_initial_solution
12✔
172
        solution_callback(integrator)
12✔
173
    end
174

175
    return nothing
12✔
176
end
177

178
# Save mesh for a general semidiscretization (default)
179
function save_mesh(semi::AbstractSemidiscretization, output_directory, timestep = 0)
40✔
180
    mesh, _, _, _ = mesh_equations_solver_cache(semi)
40✔
181

182
    if mesh.unsaved_changes
28✔
183
        # We only append the time step number to the mesh file name if it has
184
        # changed during the simulation due to AMR. We do not append it for
185
        # the first time step.
186
        if timestep == 0
12✔
187
            mesh.current_filename = save_mesh_file(mesh, output_directory)
12✔
188
        else
UNCOV
189
            mesh.current_filename = save_mesh_file(mesh, output_directory, timestep)
×
190
        end
191
        mesh.unsaved_changes = false
12✔
192
    end
193
    return mesh.current_filename
28✔
194
end
195

196
# Save mesh for a DGMultiMesh, which requires passing the `basis` as an argument to
197
# save_mesh_file
UNCOV
198
function save_mesh(semi::Union{SemidiscretizationHyperbolic{<:DGMultiMesh},
×
199
                               SemidiscretizationHyperbolicParabolic{<:DGMultiMesh}},
200
                   output_directory, timestep = 0)
UNCOV
201
    mesh, _, solver, _ = mesh_equations_solver_cache(semi)
×
202

UNCOV
203
    if mesh.unsaved_changes
×
204
        # We only append the time step number to the mesh file name if it has
205
        # changed during the simulation due to AMR. We do not append it for
206
        # the first time step.
UNCOV
207
        if timestep == 0
×
UNCOV
208
            mesh.current_filename = save_mesh_file(semi.mesh, solver.basis,
×
209
                                                   output_directory)
210
        else
211
            mesh.current_filename = save_mesh_file(semi.mesh, solver.basis,
×
212
                                                   output_directory, timestep)
213
        end
UNCOV
214
        mesh.unsaved_changes = false
×
215
    end
UNCOV
216
    return mesh.current_filename
×
217
end
218

219
# this method is called to determine whether the callback should be activated
220
function (solution_callback::SaveSolutionCallback)(u, t, integrator)
20✔
221
    @unpack interval_or_dt, save_final_solution = solution_callback
20✔
222

223
    # With error-based step size control, some steps can be rejected. Thus,
224
    #   `integrator.iter >= integrator.stats.naccept`
225
    #    (total #steps)       (#accepted steps)
226
    # We need to check the number of accepted steps since callbacks are not
227
    # activated after a rejected step.
228
    return interval_or_dt > 0 && (integrator.stats.naccept % interval_or_dt == 0 ||
40✔
229
            (save_final_solution && isfinished(integrator)))
230
end
231

232
# this method is called when the callback is activated
233
function (solution_callback::SaveSolutionCallback)(integrator)
16✔
234
    u_ode = integrator.u
16✔
235
    semi = integrator.p
16✔
236
    iter = integrator.stats.naccept
16✔
237

238
    @trixi_timeit timer() "I/O" begin
16✔
239
        # Call high-level functions that dispatch on semidiscretization type
240
        @trixi_timeit timer() "save mesh" save_mesh(semi,
16✔
241
                                                    solution_callback.output_directory,
242
                                                    iter)
243
        save_solution_file(semi, u_ode, solution_callback, integrator)
16✔
244
    end
245

246
    # avoid re-evaluating possible FSAL stages
247
    u_modified!(integrator, false)
16✔
248
    return nothing
16✔
249
end
250

251
@inline function save_solution_file(semi::AbstractSemidiscretization, u_ode,
32✔
252
                                    solution_callback,
253
                                    integrator; system = "")
254
    @unpack t, dt = integrator
16✔
255
    iter = integrator.stats.naccept
16✔
256

257
    element_variables = Dict{Symbol, Any}()
16✔
258
    @trixi_timeit timer() "get element variables" begin
16✔
259
        get_element_variables!(element_variables, u_ode, semi)
16✔
260
        callbacks = integrator.opts.callback
16✔
261
        if callbacks isa CallbackSet
16✔
262
            foreach(callbacks.continuous_callbacks) do cb
16✔
263
                return get_element_variables!(element_variables, u_ode, semi, cb;
×
264
                                              t = integrator.t, iter = iter)
265
            end
266
            foreach(callbacks.discrete_callbacks) do cb
16✔
267
                return get_element_variables!(element_variables, u_ode, semi, cb;
88✔
268
                                              t = integrator.t, iter = iter)
269
            end
270
        end
271
    end
272

273
    @trixi_timeit timer() "get node variables" get_node_variables!(solution_callback.node_variables,
16✔
274
                                                                   u_ode, semi)
275

276
    @trixi_timeit timer() "save solution" save_solution_file(u_ode, t, dt, iter, semi,
16✔
277
                                                             solution_callback,
278
                                                             element_variables,
279
                                                             solution_callback.node_variables,
280
                                                             system = system)
281

282
    return nothing
16✔
283
end
284

285
@inline function save_solution_file(u_ode, t, dt, iter,
32✔
286
                                    semi::AbstractSemidiscretization, solution_callback,
287
                                    element_variables = Dict{Symbol, Any}(),
288
                                    node_variables = Dict{Symbol, Any}();
289
                                    system = "")
290
    mesh, equations, solver, cache = mesh_equations_solver_cache(semi)
16✔
291
    u = wrap_array_native(u_ode, mesh, equations, solver, cache)
16✔
292
    save_solution_file(u, t, dt, iter, mesh, equations, solver, cache,
16✔
293
                       solution_callback,
294
                       element_variables,
295
                       node_variables; system = system)
296

297
    return nothing
16✔
298
end
299

300
# TODO: Taal refactor, move save_mesh_file?
301
# function save_mesh_file(mesh::TreeMesh, output_directory, timestep=-1) in io/io.jl
302

303
include("save_solution_dg.jl")
304
end # @muladd
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc