Neko 1.99.1
A portable framework for high-order spectral element flow simulations
Loading...
Searching...
No Matches
Examples: Programming the user file

This directory contains tutorial files that demonstrate various aspects of programming with Neko. Each file provides examples and explanations for specific features, concepts, or workflows in Neko, helping users to customize and extend their simulations effectively.

The files don't necessarily need to be run, but executing prepare.sh will copy over a mesh and case file from the cylinder example, so you can use makeneko on a .f90 and run it.

<tt>user_file_template.f90</tt>

A user file template with stubs for every possible user subroutine.

! A template user file containing the user-defined functions
!
module user
use neko
implicit none
contains
! Register user-defined functions (see user_intf.f90)
subroutine user_setup(user)
type(user_t), intent(inout) :: user
user%startup => startup
user%initialize => initialize
user%initial_conditions => initial_conditions
user%mesh_setup => mesh_setup
user%compute => compute
user%finalize => finalize
user%source_term => source_term
user%dirichlet_conditions => dirichlet_conditions
user%material_properties => material_properties
end subroutine user_setup
subroutine startup(params)
type(json_file), intent(inout) :: params
end subroutine startup
subroutine initialize(time)
type(time_state_t), intent(in) :: time
end subroutine initialize
subroutine initial_conditions(scheme_name, fields)
character(len=*), intent(in) :: scheme_name
type(field_list_t), intent(inout) :: fields
end subroutine initial_conditions
subroutine mesh_setup(msh, time)
type(mesh_t), intent(inout) :: msh
type(time_state_t), intent(in) :: time
end subroutine mesh_setup
subroutine compute(time)
type(time_state_t), intent(in) :: time
end subroutine compute
subroutine finalize(time)
type(time_state_t), intent(in) :: time
end subroutine finalize
subroutine source_term(scheme_name, rhs, time)
character(len=*), intent(in) :: scheme_name
type(field_list_t), intent(inout) :: rhs
type(time_state_t), intent(in) :: time
end subroutine source_term
subroutine dirichlet_conditions(fields, bc, time)
type(field_list_t), intent(inout) :: fields
type(field_dirichlet_t), intent(in) :: bc
type(time_state_t), intent(in) :: time
end subroutine dirichlet_conditions
subroutine material_properties(scheme_name, properties, time)
character(len=*), intent(in) :: scheme_name
type(field_list_t), intent(inout) :: properties
type(time_state_t), intent(in) :: time
end subroutine material_properties
end module user
Master module.
Definition neko.f90:34
Implements the source_term_t type and a wrapper source_term_wrapper_t.
subroutine mesh_setup(msh, time)
subroutine initialize(time)
subroutine user_setup(user)
subroutine initial_conditions(scheme_name, fields)
subroutine compute(time)
subroutine material_properties(scheme_name, properties, time)
subroutine dirichlet_conditions(fields, bc, time)
subroutine startup(params)
subroutine finalize(time)

<tt>startup_and_json.f90</tt>

Demonstrates how to define user-specific routines and interact with the JSON parameter dictionary used for simulation configuration.

! This module demonstrates how to define user-specific routines in Neko. It
! provides an example of how to interact with the JSON parameter dictionary
! used for simulation configuration. The key points covered in this tutorial
! are:
!
! - Registering user-defined functions using the `user_setup` routine.
! - Using the `user_startup` routine to inspect and manipulate the JSON
! parameter dictionary before the simulation starts.
! - Extracting parameters from the JSON file using `json_get` and
! `json_get_or_default`, and `json_extract_object`.
! - Printing JSON objects using the `print` method.
! - Adding or modifying parameters in the JSON file using the `add` method.
! - Saving the JSON parameter dictionary for later use in the module.
!
! This tutorial highlights the flexibility of the JSON-based configuration
! system in Neko and demonstrates how to customize simulations by interacting
! with the JSON parameter dictionary.
! The user module always needs to be named "user"!
module user
! This use statement populates the scope of the module with all the types and
! public procedures defined in Neko. So, this is convenient, but your scope
! does get very polluted. It is generallys a good idea to `use` only the
! modules you need, and moreover specify what exactly you want from those
! modules using `use ..., only :`.
use neko
implicit none
! A module-scope variable to hold a copy of the case file JSON parameter
! dictionary.
type(json_file) :: case_params
! Some variable that we want to use in the user code.
real(kind=rp) :: some_variable
contains
! This is a special routine that registers the user-defined functions with
! Neko. Based on the inerface defined in user_intf.f90, we can register our
! user-defined implementations of the various routines. You do this by
! assigning procedure pointers to subroutines in the user module. Here, we
! register only the startup routine.
subroutine user_setup(user)
type(user_t), intent(inout) :: user
user%startup => startup
end subroutine user_setup
! The startup routine, provides the possibility to inspect and manipulate
! the JSON parameter dictionary before the simulation starts. The routine is
! called very early, before any of the solvers or simulation components are
! initialized. This is also a good place to set up some constants that are
! needed in the user code.
subroutine startup(params)
type(json_file), intent(inout) :: params
! Some auxillary variables to extract various type of parameters from the
! JSON
real(kind=rp) :: some_real
! Note that we need an allocatable when we extract strings or arrays.
! The json_fortran library will allocate the string or array for us.
character(len=:), allocatable :: some_string
real(kind=rp), allocatable :: some_real_array(:)
! We can extract an entire JSON object into a new json_file.
type(json_file) :: some_json_object
! Assign our constant to something. Can be based on JSON parameters
! extracted below. A common use case is to set material properties based
! on custom JSON entries in the case file, e.g. some non-dimensional number.
some_variable = 1.0_rp
! Neko relies on json_fortran to parse and manipulate the JSON file.
! In addition, there are some convenience subroutines in the json_utils
! module.
! Extract a string parameter. This will through an error if
! case.fluid.scheme does not exist in the JSON file.
call json_get(params, "case.fluid.scheme", some_string)
! Extract a real parameter, providing a default if it does not exist.
call json_get_or_default(params, "case.fluid.Re", some_real, 100.0_rp)
! Extract the object with the velocity initial conditions.
call json_extract_object(params, "case.fluid.initial_condition", &
some_json_object)
! We can print the contents to the console.
call some_json_object%print()
! We can use the json_fotran methods to manipulate the JSON.
! Here we add an additional parameter.
call params%add("case.fluid.initial_condition.my_param", 3.0_rp)
! Add can also be used to modify existing parameters.
some_real_array = [1.0_rp, 2.0_rp, 3.0_rp]
call params%add("case.fluid.initial_condition.value", some_real_array)
call params%add("case.end_time", 0.0_rp)
! Show the updated part of the JSON file.
call json_extract_object(params, "case.fluid.initial_condition", &
some_json_object)
call some_json_object%print()
! There are a few other json utilities in neko, as well as more methods in
! the json_fortran library. So, you can pretty much do anything you want
! with the case file, if you so need.
! We can save the params into our module-scope variable for later use.
case_params = params
end subroutine startup
end module user
real(kind=rp) some_variable

<tt>fields_vectors_math.f90</tt>

Explains how to work with the field_t and vector_t types in Neko, including mathematical operations and lifecycle management.

! This tutorial demonstrates how to work with fields and vectors in Neko. It
! covers the following key points:
!
! - Initializing and working with `field_t` and `vector_t` types.
! - Performing mathematical operations on fields and vectors, including:
! - Using overloaded operators for basic operations.
! - Accessing and modifying field data directly on the CPU.
! - Using low-level math routines for device and CPU compatibility.
! - Leveraging the `field_math` module for backend-agnostic computations.
! - Demonstrating the lifecycle of user-defined objects:
! - Initializing fields and vectors in `user_init_modules`.
! - Performing custom calculations in `user_check`.
! - Cleaning up allocated resources in `user_finalize_modules`.
!
! This tutorial highlights the flexibility of Neko's field and vector types,
! as well as the tools provided for efficient and portable numerical
! computations across different backends.
module user
use neko
implicit none
! One of the most important types in Neko is the field_t type. It is used to
! represent the solution fields in the simulation.
type(field_t) :: my_field
! A vector is essentially a 1D array that handles the storage and association
! between data on the host and the device.
type(vector_t) :: vec
contains
! Register user-defined functions (see user_intf.f90)
subroutine user_setup(user)
type(user_t), intent(inout) :: user
user%startup => startup
user%initialize => initialize
user%compute => compute
user%finalize => finalize
end subroutine user_setup
! We will use the user_startup routine to manipulate the end time.
subroutine startup(params)
type(json_file), intent(inout) :: params
call params%add("case.end_time", 0.0_rp)
end subroutine startup
! The user_init_modules routine is called after all the objects used to run
! the simulation are created. Thus, we can initialize user variables that,
! for example, depend on the mesh or the fluid solver.
subroutine initialize(time)
type(time_state_t), intent(in) :: time
! Like almost all other Neko types, the field_t has an init method that is
! effectively a constructor. Typically, we use a dofmap_t object and a
! a name to call the init method. The dofmap_t is usually fetched from the
! solver, like a `fluid_scheme_t`. To do this, we make use of the special
! neko_user_access object, which is a singleton that provides access to
! various objects used in the simulation.
call my_field%init(neko_user_access%case%fluid%dm_Xh, "my_field")
! The actual values of the field are stored in the x array or the x_d
! pointer. The x_d pointer is used to access the data on the GPU. The x
! array has a rank of 4, corresponding to GLL points in 3 dimensions, and
! the number of elements in the mesh (i,j,k,e).
! Some operators are overloaded for the field_t type. For example, we can
! assign it to a scalar. At construction, the values are set to zero.
my_field = 1.0_rp
! The situation for vector is similar, but we only need to provide the size
! of the array we want in the constructor.
call vec%init(50)
vec = 1.0_rp
end subroutine initialize
! This routine is run at the end of each time step. It is mean to hold
! arbitrary calculations that the user wants to do.
subroutine compute(time)
type(time_state_t), intent(in) :: time
integer :: i
! Let us consider doing some simple math on our field. Here we just add 1
! to the value at each GLL point. Note that we use a linear index to loop
! over the entire field, and use %x(i,1,1,1). This is very common in Neko,
! and relies on contiguous memory layout. Sometimes you will see 4D arrays
! passed to dummy arguments that are 1D, 2D, etc. This is possible for the
! same reason.
do i = 1, my_field%size() ! <- Number of GLL points.
my_field%x(i,1,1,1) = my_field%x(i,1,1,1) * 2.0_rp
end do
! There is an issue with the above code: it only works on the CPU. You can
! not write GPU kernels as a user. Therefore, Neko has a whole range of
! low-level math functions that can be used. You can find them in math.f90.
! Corresponding routines for the GPU are in device_math.f90, and
! field_math.f90 contains wrappers that dispatch correctly to either a CPU
! or device routine based on the current backend. The names of the routines
! are taken from Nek5000, so if you are familiar with that code, you will
! find it easy to use.
! A generic way to perform the computation above would be
if (neko_bcknd_device .eq. 1) then ! <- Check if we are on the device.
call device_cmult(my_field%x_d, 2.0_rp, my_field%size())
else
call cmult(my_field%x, 2.0_rp, my_field%size())
end if
! Alternatively, (and more concisely) we can use the field_math module,
call field_cmult(my_field, 2.0_rp)
! For vector_t there are no math wrappers, so the latter approach is the
! one to go for. The vector_t does overload some operators though, so use
! those when possible.
end subroutine compute
! If you declare objects at module scope that need to be destroyed, you can
! do it here. In our case it is the my_field field.
subroutine finalize(time)
type(time_state_t), intent(in) :: time
! All types the allocate memory in Neko have a free method, which is a
! destructor.
call my_field%free()
end subroutine finalize
end module user

<tt>registries.f90</tt>

Shows how to use registries in Neko to manage fields and temporary data efficiently.

! This tutorial demonstrates how to use registries in Neko to manage fields and
! temporary data. It covers the following key points:
!
! - Registering user-defined routines using the `user_setup` subroutine.
! - Accessing and managing fields using the `neko_field_registry`:
! - Retrieving existing fields registered by solvers (e.g., velocity fields).
! - Registering new fields for use across modules or for output purposes.
! - Using the `neko_scratch_registry` to manage temporary fields:
! - Requesting temporary fields for intermediate calculations.
! - Reliquishing temporary fields to free resources.
!
module user
use neko
implicit none
contains
! Register user-defined functions (see user_intf.f90)
subroutine user_setup(user)
type(user_t), intent(inout) :: user
user%startup => startup
user%initialize => initialize
user%compute => compute
end subroutine user_setup
! We will use the startup routine to manipulate the end time.
subroutine startup(params)
type(json_file), intent(inout) :: params
call params%add("case.end_time", 0.0_rp)
end subroutine startup
subroutine initialize(time)
type(time_state_t), intent(in) :: time
type(field_t), pointer :: my_field_ptr
! Sometimes we need fields to be accessible across different modules. This
! can include fields that we create in the user module. To that end, Neko
! provides a special singleton object of type `field_registry_t`. The name
! of the singleton is `neko_field_registry`. Solvers register their solution
! fields there, for example.
! Here we can grab the x-velocity from the fluid_scheme_t, which is
! registered as "u" in the registry.
my_field_ptr => neko_field_registry%get_field("u")
! We can also register new fields. For that we need a dofmap_t object, just
! like when we initialize a field. Once in the registry, the field is
! available for access and manipulation in other modules.
! Practical example: you may want to run some calculations on this field and
! then use the field_writer simcomp to output it to disk during the
! simulation. The field_writer looks for fields in the registry, given their
! names.
call neko_field_registry%add_field(my_field_ptr%dof, "my_field")
end subroutine initialize
subroutine compute(time)
type(time_state_t), intent(in) :: time
integer :: temp_index
type(field_t), pointer :: temp_field_ptr
type(field_t), pointer :: u, v
! Sometimes we need some temporary fields to perform certain calculations.
! Instead of declaring them inside a subroutine, Neko provides yet another
! singleton: `neko_scratch_registry`, of type `scratch_registry_t`. The
! advantage of using it is saving memory and time to allocate things.
! We get a temporary by using a field_t pointer and an index.
call neko_scratch_registry%request_field(temp_field_ptr, temp_index)
u => neko_field_registry%get_field("u")
v => neko_field_registry%get_field("v")
! Perform some operations and use the temporary for something
call field_cfill(temp_field_ptr, 1.0_rp)
call field_add2(temp_field_ptr, u)
v = temp_field_ptr
! At the end of the subroutine, the temporary field needs to be reliquished.
! This is where the integer index comes in.
call neko_scratch_registry%relinquish_field(temp_index)
end subroutine compute
end module user

<tt>output.f90</tt>

Shows how to output simulation data to .fld and .csv files.

! This tutorial demonstrates how to output simulation data to .fld and .csv
! files in Neko. It covers:
!
! - Writing single fields and lists of fields to .fld files using `fld_file_t`.
! - Controlling output precision for .fld files.
! - Writing data to .csv files using `csv_file_t` and `vector_t`.
! - Managing output across simulation steps and cleaning up resources.
module user
use neko
implicit none
! Custom fields we will output. We add the target attribute to make sure we
! can point to them.
type(field_t), target :: my_field1
type(field_t), target :: my_field2
! A custom vector for CSV output
type(vector_t) :: vec
contains
! Register user-defined functions (see user_intf.f90)
subroutine user_setup(user)
type(user_t), intent(inout) :: user
user%startup => startup
user%initialize => initialize
user%finalize => finalize
end subroutine user_setup
! We will use the startup routine to manipulate the end time.
subroutine startup(params)
type(json_file), intent(inout) :: params
call params%add("case.end_time", 0.0_rp)
end subroutine startup
subroutine initialize(time)
type(time_state_t), intent(in) :: time
! A writer for .fld files
type(fld_file_t) :: fld_writer
! Storage for a list of fields, two in our case
type(field_list_t) :: field_pair
! A writer for CSV files
type(csv_file_t) :: csv_writer
!
! WRITING FLD FILES
!
! Initialize the fields and set the values to 1.0 and 2.0
call my_field1%init(neko_user_access%case%fluid%dm_Xh, "my_field1")
call my_field2%init(neko_user_access%case%fluid%dm_Xh, "my_field2")
call field_cfill(my_field1, 1.0_rp)
call field_cfill(my_field2, 2.0_rp)
! Initialize the writer using the filename
call fld_writer%init("my_output.fld")
! Here is how we can output a single field to the fld file.
! The field is written as the pressure field.
call fld_writer%write(my_field1)
! We can also pack more fields inside a single file. To that end we need to
! pass a field_list_t object to the writer. The field_list_t is a list of
! field pointers.
! Initialize the field_list_t object with the number of fields we want to
! pack.
call field_pair%init(2)
! Assign the fields to the list, the first parameter is the index of the
! field in the list, the second is the field itself.
call field_pair%assign_to_field(1, my_field1)
call field_pair%assign_to_field(2, my_field2)
! Write the list of fields to a different file, they will be put in the
! pressure and temperature inside the fld.
call fld_writer%init("my_output2.fld")
call fld_writer%write(field_pair)
! If one has 3 fields in the list, they will instead be written as velocity
! components. Further adding 1 will populate the pressure with the first
! field and then the next 3 will be the velocity components. Adding a fifth
! field will put it into temperature, and all consecutive fields will be
! scalars.
! Finally, note that you can set the precision of the output by calling
call fld_writer%set_precision(dp) ! <- sets double precision output
call fld_writer%set_precision(sp) ! <- sets single precision output
!
! WRITING CSV FILES
!
! A vector_t can be used to pass values to a CSV file writer. Let us init
! one to have 5 elements and set them to 3.0.
call vec%init(5)
vec = 3.0_rp
! We initialize the CSV writer just like the fld writer.
call csv_writer%init("my_output.csv")
! The writer can optionally add a header string to the file.
call csv_writer%set_header("# p1, p2, p3, p4, p5")
! This will write one line to the CSV.
call csv_writer%write(vec)
! Let us add another line. A common scenario is that you will update a
! vector at each time step, e.f. in user_check and then append the result
! to the CSV file.
vec = 4.0_rp
call csv_writer%write(vec)
! Note that the CSV writer will append to file, if it exists, so you have
! to take care of that manually. Finally, note that one can also use
! matrix_t objects to write to CSV files, in case you have 2D data.
end subroutine initialize
subroutine finalize(time)
type(time_state_t), intent(in) :: time
! Don't forget to free the objects you initialized
call my_field1%free()
call my_field2%free()
call vec%free()
end subroutine finalize
end module user

<tt>custom_types.f90</tt>

Shows how to add a new run-time-selectable type to Neko, using a source term as an example.

! Copyright (c) 2025, The Neko Authors
! All rights reserved.
!
! Redistribution and use in source and binary forms, with or without
! modification, are permitted provided that the following conditions
! are met:
!
! * Redistributions of source code must retain the above copyright
! notice, this list of conditions and the following disclaimer.
!
! * Redistributions in binary form must reproduce the above
! copyright notice, this list of conditions and the following
! disclaimer in the documentation and/or other materials provided
! with the distribution.
!
! * Neither the name of the authors nor the names of its
! contributors may be used to endorse or promote products derived
! from this software without specific prior written permission.
!
! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
! "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
! LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
! FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
! COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
! INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
! BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
! LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
! LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
! ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
! POSSIBILITY OF SUCH DAMAGE.
!
!! After compiling this file with makeneko, you can add the following to your
!! JSON file to use this custom source term:
!! "source_terms": [
!! {
!! "type": "my_source_term",
!! "greeting": "Hello there!"
!!
!! }
!! ],
!! NOTE: the module name must be the same as the file name sans the extension.
use num_types, only : rp
use json_module, only : json_file
! These imports are needed for registering our new type with Neko
use source_term, only : source_term_t, register_source_term, &
use coefs, only : coef_t
implicit none
private
type, public, extends(source_term_t) :: my_source_term_t
! We will read this greeting from the JSON file.
character(len=:), allocatable :: greeting
contains
procedure, pass(this) :: init => my_source_term_init_from_json
procedure, pass(this) :: free => my_source_term_free
procedure, pass(this) :: compute_ => my_source_term_compute
! Have to make this public!
contains
subroutine my_source_term_init_from_json(this, json, fields, coef, &
variable_name)
class(my_source_term_t), intent(inout) :: this
type(json_file), intent(inout) :: json
type(field_list_t), intent(in), target :: fields
type(coef_t), intent(in), target :: coef
character(len=*), intent(in) :: variable_name
real(kind=rp) :: start_time, end_time
call json_get_or_default(json, "start_time", start_time, 0.0_rp)
call json_get_or_default(json, "end_time", end_time, huge(0.0_rp))
! Start fresh
call this%free()
! Initialize the base class source_term_t
call this%init_base(fields, coef, start_time, end_time)
call json_get(json, "greeting", this%greeting)
subroutine my_source_term_free(this)
class(my_source_term_t), intent(inout) :: this
if (allocated(this%greeting)) then
deallocate(this%greeting)
end if
call this%free_base()
end subroutine my_source_term_free
subroutine my_source_term_compute(this, time)
class(my_source_term_t), intent(inout) :: this
type(time_state_t), intent(in) :: time
write(*,*) this%greeting
end subroutine my_source_term_compute
! The name of this routine follows a convention: the name of the module +
! register_types. This is where we call the register_source_term routine, or
! a similar routine for other types. You can register as much types as you
! want in this routine, in case you have several types in the module.
! Just a helper variable
procedure(source_term_allocate), pointer :: allocator
! Based on this the name of the source term will be "my_source_term",
! This is what you set in the JSON file, in the type keyword
call register_source_term("my_source_term", allocator)
! This thing does nothing except allocate a polymorphic object to our new
! custom type.
subroutine my_source_term_allocate(obj)
class(source_term_t), allocatable, intent(inout) :: obj
allocate(my_source_term_t::obj)
end subroutine my_source_term_allocate
end module custom_types
Retrieves a parameter by name or assigns a provided default value. In the latter case also adds the m...
Retrieves a parameter by name or throws an error.
Source term factory. Both constructs and initializes the object.
Coefficients.
Definition coef.f90:34
In this module we implement a custom source term, my_source_term and prep it for being recognized by ...
subroutine my_source_term_compute(this, time)
Will just bring our greeting to the console.
subroutine my_source_term_allocate(obj)
subroutine, public custom_types_register_types()
subroutine my_source_term_free(this)
Destructor.
subroutine my_source_term_init_from_json(this, json, fields, coef, variable_name)
The common constructor using a JSON object.
Utilities for retrieving parameters from the case files.
integer, parameter, public rp
Global precision used in computations.
Definition num_types.f90:12
Module with things related to the simulation time.
Coefficients defined on a given (mesh, ) tuple. Arrays use indices (i,j,k,e): element e,...
Definition coef.f90:55
field_list_t, To be able to group fields together
Base abstract type for source terms.
A struct that contains all info about the time, expand as needed.