Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
trixi-framework
GitHub Repository: trixi-framework/Trixi.jl
Path: blob/main/src/solvers/dgsem_tree/dg_1d_parabolic.jl
5591 views
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
# This method is called when a `SemidiscretizationHyperbolicParabolic` is constructed.
9
# It constructs the basic `cache` used throughout the simulation to compute
10
# the RHS etc.
11
function create_cache_parabolic(mesh::TreeMesh{1},
12
equations_hyperbolic::AbstractEquations,
13
dg::DG, n_elements, uEltype)
14
parabolic_container = init_parabolic_container_1d(nvariables(equations_hyperbolic),
15
nnodes(dg), n_elements,
16
uEltype)
17
18
cache_parabolic = (; parabolic_container)
19
20
return cache_parabolic
21
end
22
23
# This file collects all methods that have been updated to work with parabolic systems of equations
24
#
25
# assumptions: parabolic terms are of the form div(f(u, grad(u))) and
26
# will be discretized first order form as follows:
27
# 1. compute grad(u)
28
# 2. compute f(u, grad(u))
29
# 3. compute div(f(u, grad(u))) (i.e., the "regular" rhs! call)
30
# boundary conditions will be applied to both grad(u) and div(f(u, grad(u))).
31
function rhs_parabolic!(du, u, t, mesh::TreeMesh{1},
32
equations_parabolic::AbstractEquationsParabolic,
33
boundary_conditions_parabolic, source_terms_parabolic,
34
dg::DG, parabolic_scheme, cache, cache_parabolic)
35
@unpack parabolic_container = cache_parabolic
36
@unpack u_transformed, gradients, flux_parabolic = parabolic_container
37
38
# Convert conservative variables to a form more suitable for parabolic flux calculations
39
@trixi_timeit timer() "transform variables" begin
40
transform_variables!(u_transformed, u, mesh, equations_parabolic, dg,
41
cache)
42
end
43
44
# Compute the gradients of the transformed variables
45
@trixi_timeit timer() "calculate gradient" begin
46
calc_gradient!(gradients, u_transformed, t, mesh, equations_parabolic,
47
boundary_conditions_parabolic, dg,
48
parabolic_scheme, cache)
49
end
50
51
# Compute and store the parabolic fluxes
52
@trixi_timeit timer() "calculate parabolic fluxes" begin
53
calc_parabolic_fluxes!(flux_parabolic, gradients, u_transformed, mesh,
54
equations_parabolic, dg, cache)
55
end
56
57
# The remainder of this function is essentially a regular rhs! for
58
# parabolic equations (i.e., it computes the divergence of the parabolic fluxes)
59
#
60
# OBS! In `calc_parabolic_fluxes!`, the parabolic flux values at the volume nodes of each element have
61
# been computed and stored in `flux_parabolic`. In the following, we *reuse* (abuse) the
62
# `interfaces` and `boundaries` containers in `cache` to interpolate and store the
63
# *fluxes* at the element surfaces, as opposed to interpolating and storing the *solution* (as it
64
# is done in the hyperbolic operator). That is, `interfaces.u`/`boundaries.u` store *parabolic flux values*
65
# and *not the solution*. The advantage is that a) we do not need to allocate more storage, b) we
66
# do not need to recreate the existing data structure only with a different name, and c) we do not
67
# need to interpolate solutions *and* gradients to the surfaces.
68
69
# Reset du
70
@trixi_timeit timer() "reset ∂u/∂t" set_zero!(du, dg, cache)
71
72
# Calculate volume integral
73
# This calls the specialized version for the parabolic flux.
74
@trixi_timeit timer() "volume integral" begin
75
calc_volume_integral!(du, flux_parabolic, mesh, equations_parabolic, dg, cache)
76
end
77
78
# Prolong solution to interfaces
79
# This reuses `prolong2interfaces!` for the purely hyperbolic case.
80
@trixi_timeit timer() "prolong2interfaces" begin
81
prolong2interfaces!(cache, flux_parabolic, mesh, equations_parabolic, dg)
82
end
83
84
# Calculate interface fluxes.
85
# This calls the specialized version for the parabolic flux.
86
@trixi_timeit timer() "interface flux" begin
87
calc_interface_flux!(cache.elements.surface_flux_values,
88
mesh, equations_parabolic, dg,
89
parabolic_scheme, cache)
90
end
91
92
# Prolong solution to boundaries.
93
# This reuses `prolong2boundaries!` for the purely hyperbolic case.
94
@trixi_timeit timer() "prolong2boundaries" begin
95
prolong2boundaries!(cache, flux_parabolic, mesh, equations_parabolic, dg)
96
end
97
98
# Calculate boundary fluxes.
99
# This calls the specialized version for parabolic equations.
100
@trixi_timeit timer() "boundary flux" begin
101
calc_boundary_flux_divergence!(cache, t,
102
boundary_conditions_parabolic, mesh,
103
equations_parabolic,
104
dg.surface_integral, dg)
105
end
106
107
# Calculate surface integrals.
108
# This reuses `calc_surface_integral!` for the purely hyperbolic case.
109
@trixi_timeit timer() "surface integral" begin
110
calc_surface_integral!(nothing, du, u, mesh, equations_parabolic,
111
dg.surface_integral, dg, cache)
112
end
113
114
# Apply Jacobian from mapping to reference element
115
@trixi_timeit timer() "Jacobian" begin
116
apply_jacobian_parabolic!(du, mesh, equations_parabolic, dg, cache)
117
end
118
119
# Calculate source terms
120
@trixi_timeit timer() "source terms parabolic" begin
121
calc_sources_parabolic!(du, u, gradients, t, source_terms_parabolic,
122
equations_parabolic, dg, cache)
123
end
124
125
return nothing
126
end
127
128
# Transform solution variables prior to taking the gradient
129
# (e.g., conservative to primitive variables). Defaults to doing nothing.
130
# TODO: can we avoid copying data?
131
function transform_variables!(u_transformed, u, mesh::TreeMesh{1},
132
equations_parabolic::AbstractEquationsParabolic,
133
dg::DG, cache)
134
transformation = gradient_variable_transformation(equations_parabolic)
135
136
@threaded for element in eachelement(dg, cache)
137
# Calculate volume terms in one element
138
for i in eachnode(dg)
139
u_node = get_node_vars(u, equations_parabolic, dg, i, element)
140
u_transformed_node = transformation(u_node, equations_parabolic)
141
set_node_vars!(u_transformed, u_transformed_node, equations_parabolic, dg,
142
i, element)
143
end
144
end
145
146
return nothing
147
end
148
149
# This is the version used when calculating the divergence of the parabolic fluxes.
150
# Identical to weak-form volume integral/kernel for the purely hyperbolic case,
151
# except that the fluxes are here already precomputed in `calc_parabolic_fluxes!`
152
function calc_volume_integral!(du, flux_parabolic, mesh::TreeMesh{1},
153
equations_parabolic::AbstractEquationsParabolic,
154
dg::DGSEM, cache)
155
@unpack derivative_hat = dg.basis
156
157
@threaded for element in eachelement(dg, cache)
158
# Calculate volume terms in one element
159
for i in eachnode(dg)
160
flux_1_node = get_node_vars(flux_parabolic, equations_parabolic, dg, i,
161
element)
162
163
for ii in eachnode(dg)
164
multiply_add_to_node_vars!(du, derivative_hat[ii, i], flux_1_node,
165
equations_parabolic, dg, ii, element)
166
end
167
end
168
end
169
170
return nothing
171
end
172
173
# This is the version used when calculating the divergence of the parabolic fluxes
174
function calc_interface_flux!(surface_flux_values, mesh::TreeMesh{1},
175
equations_parabolic, dg::DG, parabolic_scheme,
176
cache)
177
@unpack neighbor_ids, orientations = cache.interfaces
178
179
@threaded for interface in eachinterface(dg, cache)
180
# Get neighboring elements
181
left_id = neighbor_ids[1, interface]
182
right_id = neighbor_ids[2, interface]
183
184
# Determine interface direction with respect to elements:
185
# orientation = 1: left -> 2, right -> 1
186
left_direction = 2 * orientations[interface]
187
right_direction = 2 * orientations[interface] - 1
188
189
# Get precomputed fluxes at interfaces
190
flux_ll, flux_rr = get_surface_node_vars(cache.interfaces.u,
191
equations_parabolic,
192
dg, interface)
193
194
# compute interface flux for the DG divergence
195
flux = flux_parabolic(flux_ll, flux_rr, Divergence(),
196
equations_parabolic, parabolic_scheme)
197
198
# Copy flux to left and right element storage
199
for v in eachvariable(equations_parabolic)
200
surface_flux_values[v, left_direction, left_id] = flux[v]
201
surface_flux_values[v, right_direction, right_id] = flux[v]
202
end
203
end
204
205
return nothing
206
end
207
208
function calc_parabolic_fluxes!(flux_parabolic, gradients, u_transformed,
209
mesh::TreeMesh{1},
210
equations_parabolic::AbstractEquationsParabolic,
211
dg::DG, cache)
212
@threaded for element in eachelement(dg, cache)
213
for i in eachnode(dg)
214
# Get solution and gradients
215
u_node = get_node_vars(u_transformed, equations_parabolic, dg, i, element)
216
gradients_1_node = get_node_vars(gradients, equations_parabolic, dg,
217
i, element)
218
219
# Calculate parabolic flux and store each component for later use
220
flux_parabolic_node = flux(u_node, (gradients_1_node,), 1,
221
equations_parabolic)
222
set_node_vars!(flux_parabolic, flux_parabolic_node, equations_parabolic, dg,
223
i, element)
224
end
225
end
226
227
return nothing
228
end
229
230
function calc_boundary_flux_gradient!(cache, t,
231
boundary_conditions_parabolic::BoundaryConditionPeriodic,
232
mesh::TreeMesh{1},
233
equations_parabolic::AbstractEquationsParabolic,
234
surface_integral, dg::DG)
235
return nothing
236
end
237
238
function calc_boundary_flux_divergence!(cache, t,
239
boundary_conditions_parabolic::BoundaryConditionPeriodic,
240
mesh::TreeMesh{1},
241
equations_parabolic::AbstractEquationsParabolic,
242
surface_integral, dg::DG)
243
return nothing
244
end
245
246
function calc_boundary_flux_gradient!(cache, t,
247
boundary_conditions_parabolic::NamedTuple,
248
mesh::TreeMesh{1}, # for dispatch only
249
equations_parabolic::AbstractEquationsParabolic,
250
surface_integral, dg::DG)
251
@unpack surface_flux_values = cache.elements
252
@unpack n_boundaries_per_direction = cache.boundaries
253
254
# Calculate indices
255
lasts = accumulate(+, n_boundaries_per_direction)
256
firsts = lasts - n_boundaries_per_direction .+ 1
257
258
# Calc boundary fluxes in each direction
259
calc_boundary_flux_by_direction_gradient!(surface_flux_values, t,
260
boundary_conditions_parabolic[1],
261
equations_parabolic, surface_integral,
262
dg, cache,
263
1, firsts[1], lasts[1])
264
calc_boundary_flux_by_direction_gradient!(surface_flux_values, t,
265
boundary_conditions_parabolic[2],
266
equations_parabolic, surface_integral,
267
dg, cache,
268
2, firsts[2], lasts[2])
269
270
return nothing
271
end
272
273
function calc_boundary_flux_by_direction_gradient!(surface_flux_values::AbstractArray{<:Any,
274
3},
275
t, boundary_condition,
276
equations_parabolic::AbstractEquationsParabolic,
277
surface_integral, dg::DG, cache,
278
direction, first_boundary,
279
last_boundary)
280
@unpack surface_flux = surface_integral
281
@unpack u, neighbor_ids, neighbor_sides, node_coordinates, orientations = cache.boundaries
282
283
@threaded for boundary in first_boundary:last_boundary
284
# Get neighboring element
285
neighbor = neighbor_ids[boundary]
286
287
# Get boundary flux
288
u_ll, u_rr = get_surface_node_vars(u, equations_parabolic, dg, boundary)
289
if neighbor_sides[boundary] == 1 # Element is on the left, boundary on the right
290
u_inner = u_ll
291
else # Element is on the right, boundary on the left
292
u_inner = u_rr
293
end
294
295
# TODO: revisit if we want more general boundary treatments.
296
# This assumes the gradient numerical flux at the boundary is the gradient variable,
297
# which is consistent with BR1, LDG.
298
flux_inner = u_inner
299
300
x = get_node_coords(node_coordinates, equations_parabolic, dg, boundary)
301
flux = boundary_condition(flux_inner, u_inner, orientations[boundary],
302
direction,
303
x, t, Gradient(), equations_parabolic)
304
305
# Copy flux to left and right element storage
306
for v in eachvariable(equations_parabolic)
307
surface_flux_values[v, direction, neighbor] = flux[v]
308
end
309
end
310
311
return nothing
312
end
313
314
function calc_boundary_flux_divergence!(cache, t,
315
boundary_conditions_parabolic::NamedTuple,
316
mesh::TreeMesh{1},
317
equations_parabolic::AbstractEquationsParabolic,
318
surface_integral, dg::DG)
319
@unpack surface_flux_values = cache.elements
320
@unpack n_boundaries_per_direction = cache.boundaries
321
322
# Calculate indices
323
lasts = accumulate(+, n_boundaries_per_direction)
324
firsts = lasts - n_boundaries_per_direction .+ 1
325
326
# Calc boundary fluxes in each direction
327
calc_boundary_flux_by_direction_divergence!(surface_flux_values, t,
328
boundary_conditions_parabolic[1],
329
equations_parabolic, surface_integral,
330
dg, cache,
331
1, firsts[1], lasts[1])
332
calc_boundary_flux_by_direction_divergence!(surface_flux_values, t,
333
boundary_conditions_parabolic[2],
334
equations_parabolic, surface_integral,
335
dg, cache,
336
2, firsts[2], lasts[2])
337
338
return nothing
339
end
340
341
function calc_boundary_flux_by_direction_divergence!(surface_flux_values::AbstractArray{<:Any,
342
3},
343
t,
344
boundary_condition,
345
equations_parabolic::AbstractEquationsParabolic,
346
surface_integral, dg::DG, cache,
347
direction, first_boundary,
348
last_boundary)
349
@unpack surface_flux = surface_integral
350
351
# Note: cache.boundaries.u contains the unsigned normal component (using "orientation", not "direction")
352
# of the parabolic flux, as computed in `prolong2boundaries!`
353
@unpack u, neighbor_ids, neighbor_sides, node_coordinates, orientations = cache.boundaries
354
355
@threaded for boundary in first_boundary:last_boundary
356
# Get neighboring element
357
neighbor = neighbor_ids[boundary]
358
359
# Get parabolic boundary fluxes
360
flux_ll, flux_rr = get_surface_node_vars(u, equations_parabolic, dg, boundary)
361
if neighbor_sides[boundary] == 1 # Element is on the left, boundary on the right
362
flux_inner = flux_ll
363
else # Element is on the right, boundary on the left
364
flux_inner = flux_rr
365
end
366
367
x = get_node_coords(node_coordinates, equations_parabolic, dg, boundary)
368
369
# TODO: add a field in `cache.boundaries` for gradient information.
370
# Here, we pass in `u_inner = nothing` since we overwrite cache.boundaries.u with gradient information.
371
# This currently works with Dirichlet/Neuman boundary conditions for LaplaceDiffusion2D and
372
# NoSlipWall/Adiabatic boundary conditions for CompressibleNavierStokesDiffusion2D as of 2022-6-27.
373
# It will not work with implementations which utilize `u_inner` to impose boundary conditions.
374
flux = boundary_condition(flux_inner, nothing, orientations[boundary],
375
direction,
376
x, t, Divergence(), equations_parabolic)
377
378
# Copy flux to left and right element storage
379
for v in eachvariable(equations_parabolic)
380
surface_flux_values[v, direction, neighbor] = flux[v]
381
end
382
end
383
384
return nothing
385
end
386
387
function calc_volume_integral_gradient!(gradients, u_transformed,
388
mesh::TreeMesh{1}, # for dispatch only
389
equations_parabolic::AbstractEquationsParabolic,
390
dg::DGSEM, cache)
391
@unpack derivative_hat = dg.basis
392
393
@threaded for element in eachelement(dg, cache)
394
# Calculate volume terms in one element,
395
# corresponds to `kernel` functions for the hyperbolic part of the flux
396
for i in eachnode(dg)
397
u_node = get_node_vars(u_transformed, equations_parabolic, dg,
398
i, element)
399
400
for ii in eachnode(dg)
401
multiply_add_to_node_vars!(gradients, derivative_hat[ii, i],
402
u_node, equations_parabolic, dg,
403
ii, element)
404
end
405
end
406
end
407
408
return nothing
409
end
410
411
function calc_interface_flux_gradient!(surface_flux_values,
412
mesh::TreeMesh{1},
413
equations_parabolic,
414
dg::DG, parabolic_scheme, cache)
415
@unpack neighbor_ids, orientations = cache.interfaces
416
417
@threaded for interface in eachinterface(dg, cache)
418
# Get neighboring elements
419
left_id = neighbor_ids[1, interface]
420
right_id = neighbor_ids[2, interface]
421
422
# Determine interface direction with respect to elements:
423
# orientation = 1: left -> 2, right -> 1
424
left_direction = 2 * orientations[interface]
425
right_direction = 2 * orientations[interface] - 1
426
427
# Call pointwise Riemann solver
428
u_ll, u_rr = get_surface_node_vars(cache.interfaces.u,
429
equations_parabolic, dg, interface)
430
431
flux = flux_parabolic(u_ll, u_rr, Gradient(),
432
equations_parabolic, parabolic_scheme)
433
434
# Copy flux to left and right element storage
435
for v in eachvariable(equations_parabolic)
436
surface_flux_values[v, left_direction, left_id] = flux[v]
437
# No sign flip needed for gradient computation because for parabolic terms,
438
# the normals are not embedded in `flux_` for gradient computations.
439
surface_flux_values[v, right_direction, right_id] = flux[v]
440
end
441
end
442
443
return nothing
444
end
445
446
function calc_surface_integral_gradient!(gradients,
447
mesh::TreeMesh{1}, # for dispatch only
448
equations_parabolic::AbstractEquationsParabolic,
449
dg::DGSEM, cache)
450
@unpack inverse_weights = dg.basis
451
@unpack surface_flux_values = cache.elements
452
453
# Note that all fluxes have been computed with outward-pointing normal vectors.
454
# We also use explicit assignments instead of `+=` to let `@muladd` turn these
455
# into FMAs (see comment at the top of the file).
456
factor = inverse_weights[1] # For LGL basis: Identical to weighted boundary interpolation at x = ±1
457
@threaded for element in eachelement(dg, cache)
458
for v in eachvariable(equations_parabolic)
459
# surface at -x
460
gradients[v, 1, element] = (gradients[v, 1, element] -
461
surface_flux_values[v, 1, element] *
462
factor)
463
464
# surface at +x
465
gradients[v, nnodes(dg), element] = (gradients[v, nnodes(dg), element] +
466
surface_flux_values[v, 2, element] *
467
factor)
468
end
469
end
470
471
return nothing
472
end
473
474
function calc_surface_integral_gradient!(gradients,
475
mesh::TreeMesh{1}, # for dispatch only
476
equations_parabolic::AbstractEquationsParabolic,
477
dg::DGSEM{<:GaussLegendreBasis}, cache)
478
@unpack boundary_interpolation_inverse_weights = dg.basis
479
@unpack surface_flux_values = cache.elements
480
481
# Note that all fluxes have been computed with outward-pointing normal vectors.
482
# We also use explicit assignments instead of `+=` to let `@muladd` turn these
483
# into FMAs (see comment at the top of the file).
484
@threaded for element in eachelement(dg, cache)
485
for v in eachvariable(equations_parabolic)
486
# Aliases for repeatedly accessed variables
487
surface_flux_minus = surface_flux_values[v, 1, element]
488
surface_flux_plus = surface_flux_values[v, 2, element]
489
for ii in eachnode(dg)
490
# surface at -x
491
gradients[v, ii, element] = (gradients[v, ii, element] -
492
surface_flux_minus *
493
boundary_interpolation_inverse_weights[ii,
494
1])
495
496
# surface at +x
497
gradients[v, ii, element] = (gradients[v, ii, element] +
498
surface_flux_plus *
499
boundary_interpolation_inverse_weights[ii,
500
2])
501
end
502
end
503
end
504
505
return nothing
506
end
507
508
# Calculate the gradient of the transformed variables
509
function calc_gradient!(gradients, u_transformed, t, mesh::TreeMesh{1},
510
equations_parabolic, boundary_conditions_parabolic,
511
dg::DG, parabolic_scheme, cache)
512
513
# Reset gradients
514
@trixi_timeit timer() "reset gradients" begin
515
set_zero!(gradients, dg, cache)
516
end
517
518
# Calculate volume integral
519
@trixi_timeit timer() "volume integral" begin
520
calc_volume_integral_gradient!(gradients, u_transformed,
521
mesh, equations_parabolic, dg, cache)
522
end
523
524
# Prolong solution to interfaces.
525
# This reusues `prolong2interfaces!` for the purely hyperbolic case.
526
@trixi_timeit timer() "prolong2interfaces" begin
527
prolong2interfaces!(cache, u_transformed, mesh,
528
equations_parabolic, dg)
529
end
530
531
# Calculate interface fluxes
532
@trixi_timeit timer() "interface flux" begin
533
@unpack surface_flux_values = cache.elements
534
calc_interface_flux_gradient!(surface_flux_values, mesh, equations_parabolic,
535
dg, parabolic_scheme, cache)
536
end
537
538
# Prolong solution to boundaries.
539
# This reuses `prolong2boundaries!` for the purely hyperbolic case.
540
@trixi_timeit timer() "prolong2boundaries" begin
541
prolong2boundaries!(cache, u_transformed, mesh, equations_parabolic, dg)
542
end
543
544
# Calculate boundary fluxes
545
@trixi_timeit timer() "boundary flux" begin
546
calc_boundary_flux_gradient!(cache, t,
547
boundary_conditions_parabolic, mesh,
548
equations_parabolic,
549
dg.surface_integral, dg)
550
end
551
552
# Calculate surface integrals
553
@trixi_timeit timer() "surface integral" begin
554
calc_surface_integral_gradient!(gradients, mesh, equations_parabolic,
555
dg, cache)
556
end
557
558
# Apply Jacobian from mapping to reference element
559
@trixi_timeit timer() "Jacobian" begin
560
apply_jacobian_parabolic!(gradients, mesh, equations_parabolic, dg,
561
cache)
562
end
563
564
return nothing
565
end
566
567
# Needed to *not* flip the sign of the inverse Jacobian.
568
# This is because the parabolic fluxes are assumed to be of the form
569
# `du/dt + df/dx = dg/dx + source(x,t)`,
570
# where f(u) is the inviscid flux and g(u) is the parabolic flux.
571
function apply_jacobian_parabolic!(du::AbstractArray, mesh::TreeMesh{1},
572
equations_parabolic::AbstractEquationsParabolic,
573
dg::DG, cache)
574
@unpack inverse_jacobian = cache.elements
575
576
@threaded for element in eachelement(dg, cache)
577
factor = inverse_jacobian[element]
578
579
for i in eachnode(dg)
580
for v in eachvariable(equations_parabolic)
581
du[v, i, element] *= factor
582
end
583
end
584
end
585
586
return nothing
587
end
588
589
# Need dimension specific version to avoid error at dispatching
590
function calc_sources_parabolic!(du, u, gradients, t, source_terms_parabolic::Nothing,
591
equations_parabolic::AbstractEquations{1}, dg::DG,
592
cache)
593
return nothing
594
end
595
596
function calc_sources_parabolic!(du, u, gradients, t, source_terms_parabolic,
597
equations_parabolic::AbstractEquations{1}, dg::DG,
598
cache)
599
@unpack node_coordinates = cache.elements
600
601
@threaded for element in eachelement(dg, cache)
602
for i in eachnode(dg)
603
u_local = get_node_vars(u, equations_parabolic, dg, i, element)
604
gradients_x_local = get_node_vars(gradients, equations_parabolic, dg, i,
605
element)
606
x_local = get_node_coords(node_coordinates, equations_parabolic, dg,
607
i, element)
608
du_local = source_terms_parabolic(u_local, (gradients_x_local,), x_local, t,
609
equations_parabolic)
610
add_to_node_vars!(du, du_local, equations_parabolic, dg, i, element)
611
end
612
end
613
614
return nothing
615
end
616
end # @muladd
617
618