Skip to content

General plan and layout #42

@ranocha

Description

@ranocha

In GitLab by @ranocha on May 7, 2020, 17:07

Hi! I'm totally new to this project and haven't been involved in the discussions and the work resulting in the current version. Hence, I hesitate a bit to start discussions that can possibly necessitate huge changes to your workflow and/or the code. On the other hand, I couldn't find such discussions online (since they probably happened in person) and the resulting comments may be of interest for future users of Trixi.

My first impression of Trixi is that it's heavily inspired by monolithic Fortran codes like Flexi/Fluxo, which is totally natural, of course. In my opinion, a different approach might be useful in Julia: Instead of providing a single executable running a parameter file, an approach to create a library of tools might be of interest.
For example, if a student shall run some experiments with a slightly different initial condition, Trixi itself has to be modified, e.g. at https://gitlab.mi.uni-koeln.de/numsim/code/Trixi.jl/-/blob/master/src/equations/linearscalaradvection.jl#L46.
An alternative to parameter files could be to supply the parameters as pure Julia code using the Trixi library. In that case, students could write the initial condition as a Julia function without the need to modify Trixi itself. Since functions are first-class citizens in Julia, they can be passed around as arguments (or as part of structs).

The structure of Trixi using many modules and global variables reminds me of Fortran modules etc. While using constant global variables is okay (type stable) in Julia, passing parameters is more encouraged.

Another reminder of Fortran and co are the type annotations for function arguments. Unless they direct multiple dispatch, they shouldn't be used in Julia.

function calc_surface_flux!(surface_flux, neighbor_ids,
                            u_surfaces, dg::D, ::Val{true},
                            orientations)
   ...
end

is compiled to exactly the same machine code as

function calc_surface_flux!(surface_flux::Array{Float64, 4}, neighbor_ids::Matrix{Int},
                            u_surfaces::Array{Float64, 4}, dg::Dg, ::Val{true},
                            orientations::Vector{Int})
   ...
end

if the arguments are the same. The first version is more general, since the array type can be exchanged. For example, it is possible to use the same code to run a simulation with Float32 (or possibly some floating point type with higher accuracy), which can be useful sometimes. I prefer to include the additional kind of documentation provided by the type annotations in docstrings for the functions.

The multiple dispatch feature of Julia seems to be abused sometimes in the current version, e.g. in https://gitlab.mi.uni-koeln.de/numsim/code/Trixi.jl/-/blob/master/src/solvers/dg.jl#L1171. Such type-unstable code results in a possibly huge performance impact.
For example, I started from the current master branch with this git diff.

diff --git a/parameters_ec.toml b/parameters_ec.toml
index 8cd35d1..3bf5612 100644
--- a/parameters_ec.toml
+++ b/parameters_ec.toml
@@ -18,7 +18,7 @@ volume_flux_type = "chandrashekar_ec"
 # volume_flux_type = "central"
 # sources = 
 t_start = 0.0
-t_end   = 0.4
+t_end   = 4.0 # 0.4
 
 # restart = true
 # restart_filename = "out/restart_000100.h5"
diff --git a/src/equations/euler.jl b/src/equations/euler.jl
index c3978b9..5fdc582 100644
--- a/src/equations/euler.jl
+++ b/src/equations/euler.jl
@@ -670,62 +670,67 @@ function Equations.riemann!(surface_flux::AbstractArray{Float64, 1},
   calcflux1D!(f_ll, equation, rho_ll, rho_v1_ll, rho_v2_ll, rho_e_ll, orientation)
   calcflux1D!(f_rr, equation, rho_rr, rho_v1_rr, rho_v2_rr, rho_e_rr, orientation)
 
-  if equation.surface_flux_type == :laxfriedrichs
-    λ_max = max(v_mag_ll, v_mag_rr) + max(c_ll, c_rr)
-    surface_flux[1] = 1/2 * (f_ll[1] + f_rr[1]) - 1/2 * λ_max * (rho_rr    - rho_ll)
-    surface_flux[2] = 1/2 * (f_ll[2] + f_rr[2]) - 1/2 * λ_max * (rho_v1_rr - rho_v1_ll)
-    surface_flux[3] = 1/2 * (f_ll[3] + f_rr[3]) - 1/2 * λ_max * (rho_v2_rr - rho_v2_ll)
-    surface_flux[4] = 1/2 * (f_ll[4] + f_rr[4]) - 1/2 * λ_max * (rho_e_rr  - rho_e_ll)
-  elseif equation.surface_flux_type in (:central, :kennedygruber, :chandrashekar_ec, :yuichi)
-    symmetric_twopoint_flux!(surface_flux, Val(equation.surface_flux_type),
-                             equation, orientation,
-                             rho_ll, rho_v1_ll, rho_v2_ll, rho_e_ll,
-                             rho_rr, rho_v1_rr, rho_v2_rr, rho_e_rr)
-
-  elseif equation.surface_flux_type == :hllc
-    error("not yet implemented or tested")
-    v_tilde = (sqrt(rho_ll) * v_ll + sqrt(rho_rr) * v_rr) / (sqrt(rho_ll) + sqrt(rho_rr))
-    h_ll = (rho_e_ll + p_ll) / rho_ll
-    h_rr = (rho_e_rr + p_rr) / rho_rr
-    h_tilde = (sqrt(rho_ll) * h_ll + sqrt(rho_rr) * h_rr) / (sqrt(rho_ll) + sqrt(rho_rr))
-    c_tilde = sqrt((equation.gamma - 1) * (h_tilde - 1/2 * v_tilde^2))
-    s_ll = v_tilde - c_tilde
-    s_rr = v_tilde + c_tilde
-
-    if s_ll > 0
-      surface_flux[1, surface_id] = f_ll[1]
-      surface_flux[2, surface_id] = f_ll[2]
-      surface_flux[3, surface_id] = f_ll[3]
-    elseif s_rr < 0
-      surface_flux[1, surface_id] = f_rr[1]
-      surface_flux[2, surface_id] = f_rr[2]
-      surface_flux[3, surface_id] = f_rr[3]
-    else
-      s_star = ((p_rr - p_ll + rho_ll * v_ll * (s_ll - v_ll) - rho_rr * v_rr * (s_rr - v_rr))
-                / (rho_ll * (s_ll - v_ll) - rho_rr * (s_rr - v_rr)))
-      if s_ll <= 0 && 0 <= s_star
-        surface_flux[1, surface_id] = (f_ll[1] + s_ll *
-            (rho_ll * (s_ll - v_ll)/(s_ll - s_star) - rho_ll))
-        surface_flux[2, surface_id] = (f_ll[2] + s_ll *
-            (rho_ll * (s_ll - v_ll)/(s_ll - s_star) * s_star - rho_v_ll))
-        surface_flux[3, surface_id] = (f_ll[3] + s_ll *
-            (rho_ll * (s_ll - v_ll)/(s_ll - s_star) *
-            (rho_e_ll/rho_ll + (s_star - v_ll) * (s_star + rho_ll/(rho_ll * (s_ll - v_ll))))
-            - rho_e_ll))
-      else
-        surface_flux[1, surface_id] = (f_rr[1] + s_rr *
-            (rho_rr * (s_rr - v_rr)/(s_rr - s_star) - rho_rr))
-        surface_flux[2, surface_id] = (f_rr[2] + s_rr *
-            (rho_rr * (s_rr - v_rr)/(s_rr - s_star) * s_star - rho_v_rr))
-        surface_flux[3, surface_id] = (f_rr[3] + s_rr *
-            (rho_rr * (s_rr - v_rr)/(s_rr - s_star) *
-            (rho_e_rr/rho_rr + (s_star - v_rr) * (s_star + rho_rr/(rho_rr * (s_rr - v_rr))))
-            - rho_e_rr))
-      end
-    end
-  else
-    error("unknown Riemann solver '$(string(equation.surface_flux_type))'")
-  end
+  symmetric_twopoint_flux!(surface_flux, Val(:chandrashekar_ec),
+                           equation, orientation,
+                           rho_ll, rho_v1_ll, rho_v2_ll, rho_e_ll,
+                           rho_rr, rho_v1_rr, rho_v2_rr, rho_e_rr)
+
 end
 
 # Original riemann! implementation, non-optimized but easier to understand

If I understood the code correctly, the result should be the same since I use surface_flux_type = "chandrashekar_ec" in parameters_ec.toml. (Of course, this code is less general but it serves the purpose to demonstrate type stability in a simple way.)
Running the current master with this parameters_ec.toml, I get

              trixi                     Time                   Allocations      
                                ----------------------   -----------------------
        Tot / % measured:            18.1s / 99.5%            589MiB / 83.5%    

 Section                ncalls     time   %tot     avg     alloc   %tot      avg
 -------------------------------------------------------------------------------
 main loop                   1    18.0s   100%   18.0s    480MiB  97.5%   480MiB
   rhs                   1.75k    17.3s  95.8%  9.88ms   99.2MiB  20.2%  58.1KiB
     surface flux        1.75k    13.6s  75.5%  7.78ms   10.2MiB  2.06%  5.95KiB
     volume integral     1.75k    2.89s  16.0%  1.65ms   76.3MiB  15.5%  44.6KiB
     ...

With the type stable version from git diff, I get

        Tot / % measured:            5.22s / 90.0%            675MiB / 73.0%    

 Section                ncalls     time   %tot     avg     alloc   %tot      avg
 -------------------------------------------------------------------------------
 main loop                   1    4.70s   100%   4.70s    480MiB  97.5%   480MiB
   rhs                   1.75k    3.99s  84.8%  2.28ms   99.3MiB  20.2%  58.1KiB
     volume integral     1.75k    2.92s  62.2%  1.67ms   76.3MiB  15.5%  44.6KiB
     ...
     surface flux        1.75k    235ms  4.99%   134μs   10.2MiB  2.07%  5.97KiB

That impact is way more than I expected - I hope I didn't code bullshit here...
The additional git diff

diff --git a/src/equations/euler.jl b/src/equations/euler.jl
index c3978b9..b1f1b73 100644
--- a/src/equations/euler.jl
+++ b/src/equations/euler.jl
@@ -358,7 +358,9 @@ end
                                               equation::Euler,
                                               u::AbstractArray{Float64},
                                               element_id::Int, n_nodes::Int)
-  calcflux_twopoint!(f1, f2, f1_diag, f2_diag, Val(equation.volume_flux_type),
+  # calcflux_twopoint!(f1, f2, f1_diag, f2_diag, Val(equation.volume_flux_type),
+  #                    equation, u, element_id, n_nodes)
+  calcflux_twopoint!(f1, f2, f1_diag, f2_diag, Val(:chandrashekar_ec),
                      equation, u, element_id, n_nodes)
 end

improves the performance for the volume terms to

        Tot / % measured:            3.13s / 97.4%            576MiB / 83.2%    

 Section                ncalls     time   %tot     avg     alloc   %tot      avg
 -------------------------------------------------------------------------------
 main loop                   1    3.04s   100%   3.04s    467MiB  97.5%   467MiB
   rhs                   1.75k    2.25s  73.9%  1.29ms   85.6MiB  17.9%  50.1KiB
     volume integral     1.75k    1.20s  39.5%   687μs   62.6MiB  13.1%  36.6KiB
     ...
     surface flux        1.75k    237ms  7.79%   136μs   10.2MiB  2.13%  5.97KiB

A general solution to get type stable code in a library like approach could be to use parametric types and supply the fluxes as Julia functions instead of strings or symbols.

I saw the additional repositories Abraxas and trixi-tests but haven't looked at them yet. In Julia, it is pretty common to include some (unit) tests in every package which are triggered for every PR on GitHub (merge request on GitLab). In this way, some additional example code is provided and some basic functionality is guaranteed to work with new changes. While I didn't like that approach at the beginning (since I wasn't used to it), I really started to like it while developing more complex code, in particular if some time has passed between development cycles. I think it would be really nice if some small/basic tests are included here in test/runtests.jl etc. They could be triggered for every merge request as GitLab pipeline, I think. If Trixi shall be registered as Julia package (as hinted to in https://gitlab.mi.uni-koeln.de/numsim/code/Trixi.jl/-/issues/20#note_276), running tests and reporting coverage results (e.g. via Travis and Codecov/Coveralls) is usually seen as a requirement and sign of good coding standards.

Okay, this grew into a lot of text. I hope we can start a constructive discussion and improve Trixi, building on your nice work :)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions