-
Notifications
You must be signed in to change notification settings - Fork 248
Example: A simple Fluid Thermal coupled solver
Let's consider we wish to solve a simple fluid-thermal problem in which the velocity field obtained by a CFD analysis is to be employed as a basis in the related thermal convection-diffusion problem. You can download the example here.
Conceptually, the problem is a very simple one-way coupled problem, in which the temperature convection-diffusion problem is governed by the fluid flow. Provided that the same mesh is employed in the same domain, we would expect the solution to be equivalent to doing:
fluid_solver.Solve() #here we compute VELOCITY
thermal_solver.Solve() #here we use VELOCITY to convect the temperature.
Current tutorial is about implementing such behaviour within the Kratos framework.
In the previous part of the tutorial we discussed the existance of two objecs: a Stage and a Solver .
The implementation of the proposed coupled problem involves dealing with the physics of the problem. The time stepping involved as well as the application of the Boundary Conditions is equivalent to that of a "normal" CFD problem, we will thus use the standard Stage employed in the CFD solution and implement a new CoupledThermalSolver
An example of implementation of such a solver can be found here
however let's go step by step and consider the design of such a solver.
The first step is to load in the memory the libraries needed. This is achieved by doing
# Importing the Kratos Library
import KratosMultiphysics
# Check that applications were imported in the main script
KratosMultiphysics.CheckRegisteredApplications("FluidDynamicsApplication")
KratosMultiphysics.CheckRegisteredApplications("ConvectionDiffusionApplication")
# Import applications
import KratosMultiphysics.FluidDynamicsApplication as KratosCFD
import KratosMultiphysics.ConvectionDiffusionApplication as ConvDiff
note that the libraries should be imported first in the main script, we verify this by calling the CheckRegisteredApplications
to verify that the library is already loaded before entering in the current script.
The next step is to define a class implementing the behaviour we need. We do so by defining the class we want, as well as a CreateSolver
auxiliary function to be used in the object construction
def CreateSolver(main_model_part, custom_settings):
return CoupledFluidThermalSolver(main_model_part, custom_settings)
class CoupledFluidThermalSolver(object):
We now need to decide which solving technology shall be employed in solving the CFD and Thermal problems. Many possibilities exist to this end, since in principle any combination of transient solvers is viable.
The best way to go is thus to postpone the decision on the choice of the solvers to runtime, so that the user is allowed to pick the choice out of the existing possibilities. The concept here is simple: from the point of view of the coupling the two solvers are a black-box. We shall configure them as we would if we were to solve an uncoupled problem, by simply nesting the corresponding configuration file in the config being passed to the coupled solver. This is done in the class constructor by doing
def __init__(self, model, custom_settings):
self.model = model
default_settings = KratosMultiphysics.Parameters("""
{
"model_part_name" : "MainModelPart",
"solver_type" : "ThermallyCoupled",
"fluid_solver_settings": {
"solver_type": "navier_stokes_solver_vmsmonolithic",
"model_import_settings": {
"input_type": "mdpa",
"input_filename": "unknown_name"
}
},
"thermal_solver_settings": {
"solver_type": "Transient",
"analysis_type": "linear",
"model_import_settings": {
"input_type": "use_input_model_part",
"input_filename": "unknown_name"
},
"computing_model_part_name": "Thermal",
"material_import_settings": {
"materials_filename": "ThermicMaterials.json"
},
"convection_diffusion_variables": {
"density_variable": "DENSITY",
"diffusion_variable": "CONDUCTIVITY",
"unknown_variable": "TEMPERATURE",
"volume_source_variable": "HEAT_FLUX",
"surface_source_variable": "FACE_HEAT_FLUX",
"projection_variable": "PROJECTED_SCALAR1",
"convection_variable": "CONVECTION_VELOCITY",
"mesh_velocity_variable": "MESH_VELOCITY",
"transfer_coefficient_variable": "",
"velocity_variable": "VELOCITY",
"specific_heat_variable": "SPECIFIC_HEAT",
"reaction_variable": "REACTION_FLUX"
}
}
}""")
## Overwrite the default settings with user-provided parameters
self.settings = custom_settings
self.settings.ValidateAndAssignDefaults(default_settings)
model_part_name = self.settings["model_part_name"].GetString()
self.main_model_part = self.model[model_part_name]
import python_solvers_wrapper_fluid
self.fluid_solver = python_solvers_wrapper_fluid.CreateSolverByParameters(
self.main_model_part,
self.settings["fluid_solver_settings"],"OpenMP")
self.thermal_model_part = self.model.CreateModelPart("thermal_model_part")
modeler = KratosMultiphysics.ConnectivityPreserveModeler()
modeler.GenerateModelPart(
self.main_model_part,
self.thermal_model_part,
"Element2D3N",
"Condition2D2N")
import python_solvers_wrapper_convection_diffusion
self.thermal_solver = python_solvers_wrapper_convection_diffusion.CreateSolverByParameters(
self.thermal_model_part,
self.settings["thermal_solver_settings"],
"OpenMP")
If we take a detailed look, we observe that we added a variable default_settings
, which tells which will be the default configuration involved in the solution phase. In this specific case, the default settings has a structure of the type
default_settings = {
"solver_type" : ...here the name of the current solver ...,
"fluid_solver_settings": { ... CFD solver settings ... },
"thermal_solver_settings": {... thermal solver settings ... }
}
Such default settings are overridden by the user-provided options, passed in the constructor as settings
.
The user-provided settings may however contain wrong or unexpected input, or simply miss some information (for example not making a choice for the thermal solver).
We thus want to "validate" them against the defaults, which is achieved by the call:
self.settings.ValidateAndAssignDefaults(default_settings)
In this specific case this function verifies that the inputs are solver_type
,fluid_solver_settings
,thermal_solver_settings
and that they are respectively a string and two "nested parameters objects". The user-provided settings cannot provide any other entry. This allows to throw an error if a misspelled entry is provided as input.
In the case the input does not contain one of the given entries (for example the thermal_solver_settings) such entry is taken from the defaults.
An important feature of the ValidateAndAssignDefaults
is that it acts only on the outmost level. It does NOT do any validation on the subparameters.
The idea is that we can now construct the fluid_solver
and the thermal_solver
by passing the parameters.
This is done by the lines
import python_solvers_wrapper_fluid
self.fluid_solver = python_solvers_wrapper_fluid.CreateSolverByParameters(
self.main_model_part,
self.settings["fluid_solver_settings"],
"OpenMP")
import python_solvers_wrapper_convection_diffusion
self.thermal_solver = python_solvers_wrapper_convection_diffusion.CreateSolverByParameters(
self.thermal_model_part,
self.settings["thermal_solver_settings"],
"OpenMP")
note that here we pass to the factory functions the subparameters as taken from the self.settings . Validation of the relevant settings is thus demanded to each of the solvers, which will use hierarchically a mechanism similar to the one described here.
Aside of the details in any case the new solver now defined two inner solvers called
self.fluid_solver
self.thermal_solver
which are now ready for use. The rest of the class implementation now consists in simply calling the relevant functions for the new solver.
The new solver implements the solver API, and thus defines a number of functions. Let's take a look at them:
The AddVariables function allocated memory for the variables needed. Essentially any solver tells to the corresponding modelpart which variables need to be stored in the database. For example, the CFD solver will require VELOCITY
while the thermal solver will require TEMPERATURE
.
:warning: A hack is needed as of now, however this will be cleaned up in the next future
def AddVariables(self):
self.fluid_solver.AddVariables()
#this is a HACK: TODO: cleanup so that this is not needed in the future
#the problem is that variables are not added to the fluid_solver.MainModelPart
#who is in charge of creating the nodes
self.tmp = self.thermal_solver.main_model_part
self.thermal_solver.main_model_part = self.fluid_solver.main_model_part
self.thermal_solver.AddVariables()
#this is a HACK: TODO: cleanup so that this is not needed in the future
self.thermal_solver.main_model_part = self.tmp
The ImportModelPart
function is in charge of importing the modelparts involved.
by taking a look on the default settings, we can observe that the fluid settings contain
"fluid_solver_settings": {
...
"model_import_settings": {
"input_type": "mdpa",
"input_filename": "unknown_name"
}
}
while the thermal settings are slightly different:
"thermal_solver_settings": {
...
"model_import_settings": {
"input_type": "use_input_model_part",
}
...
}
this expresses that the fluid_model_part
will be read from a file (named input_filename
), while the thermal_solver
expects the termal_solver to be already available. This is so as instead of reading the thermal_model_part
from a file, we construct it on the basis of the fluid_solver
, so that it shares the same nodes.
This is achieved by the class
modeler = KratosMultiphysics.ConnectivityPreserveModeler()
which fills the thermal modelpart with new elements and conditions, preserving however the same nodes (Pointers) as for the source modelpart (the fluid) as well as the same connectivity.
The overall importing function thus looks
def ImportModelPart(self):
self.fluid_solver.ImportModelPart()
#here cloning the fluid modelpart to thermal_model_part so that the nodes are shared
modeler = KratosMultiphysics.ConnectivityPreserveModeler()
modeler.GenerateModelPart(
self.main_model_part,
self.thermal_model_part,
"Element2D3N",
"Condition2D2N")
self.thermal_solver.ImportModelPart()
The AddDofs
function simply adds the Dofs needed in the solution problem and presents no difficulty
def AddDofs(self):
self.fluid_solver.AddDofs()
self.thermal_solver.AddDofs()
Even though conceptually the solution process consists in calling consecutively the two solvers, the solve function is split in several steps within kratos.
The Initialize function in particular is designed to be called exactly once, same as the Check()
function.
def Initialize(self):
self.fluid_solver.Initialize()
self.thermal_solver.Initialize()
Each complete solution step is organized as
self.InitializeSolutionStep()
self.Predict()
self.SolveSolutionStep()
self.FinalizeSolutionStep()
where each of the substeps take into account the different solvers as in:
def InitializeSolutionStep(self):
self.fluid_solver.InitializeSolutionStep()
self.thermal_solver.InitializeSolutionStep()
def Predict(self):
self.fluid_solver.Predict()
self.thermal_solver.Predict()
def SolveSolutionStep(self):
self.fluid_solver.SolveSolutionStep()
self.thermal_solver.SolveSolutionStep()
def FinalizeSolutionStep(self):
self.fluid_solver.FinalizeSolutionStep()
self.thermal_solver.FinalizeSolutionStep()
Where the functions:
InitializeSolutionStep
-
FinalizeSolutionStep
are to be called EXACTLY ONCE per time step.
Indeed for a simple one-way coupled problem as the one at hand, this is exactly equivalent to calling the Solve
function for both solvers. The Solve
functions are however split into several substeps as for complex solvers two way coupled solvers one would do something equivalent to
def Solve(self):
#do once per time step
self.InitializeSolutionStep()
self.Predict()
#non linear loop
converged = False
while(not converged):
self.SolveSolutionStep()
...maybe here change something to make the problem two way coupled ...
converged = ...here a convergence_check
#do once per time step
self.FinalizeSolutionStep()
this subdivision is essential since by guaranteeing that FinalizeSolutionStep
is called just once, we provide a safe place for updating internal variables.
#Advancing in time The time stepping is dealt into the Stage. The time advancement from one step to the next is however done in the Solver. The releavant functions are:
The funciton GetMinimumBufferSize
telling how many steps in the past need to be preserved
def GetMinimumBufferSize(self):
buffer_size_fluid = self.fluid_solver.GetMinimumBufferSize()
buffer_size_thermal = self.thermal_solver.GetMinimumBufferSize()
return max(buffer_size_fluid, buffer_size_thermal)
and AdvanceInTime
which actually makes the time to progress.
it is important to make a remark here: since the nodes are shared between the modelparts the CloneTimeStep
function should be done only on one of the modelparts
def AdvanceInTime(self, current_time):
dt = self.ComputeDeltaTime()
new_time = current_time + dt
#NOTE: the cloning is done ONLY ONCE since the nodes are shared
self.main_model_part.CloneTimeStep(new_time)
self.main_model_part.ProcessInfo[KratosMultiphysics.STEP] += 1
return new_time
- Getting Kratos (Last compiled Release)
- Compiling Kratos
- Running an example from GiD
- Kratos input files and I/O
- Data management
- Solving strategies
- Manipulating solution values
- Multiphysics
- Video tutorials
- Style Guide
- Authorship of Kratos files
- Configure .gitignore
- How to configure clang-format
- How to use smart pointer in Kratos
- How to define adjoint elements and response functions
- Visibility and Exposure
- Namespaces and Static Classes
Kratos structure
Conventions
Solvers
Debugging, profiling and testing
- Compiling Kratos in debug mode
- Debugging Kratos using GDB
- Cross-debugging Kratos under Windows
- Debugging Kratos C++ under Windows
- Checking memory usage with Valgind
- Profiling Kratos with MAQAO
- Creating unitary tests
- Using ThreadSanitizer to detect OMP data race bugs
- Debugging Memory with ASAN
HOW TOs
- How to create applications
- Python Tutorials
- Kratos For Dummies (I)
- List of classes and variables accessible via python
- How to use Logger
- How to Create a New Application using cmake
- How to write a JSON configuration file
- How to Access DataBase
- How to use quaternions in Kratos
- How to do Mapping between nonmatching meshes
- How to use Clang-Tidy to automatically correct code
- How to use the Constitutive Law class
- How to use Serialization
- How to use GlobalPointerCommunicator
- How to use PointerMapCommunicator
- How to use the Geometry
- How to use processes for BCs
- How to use Parallel Utilities in futureproofing the code
- Porting to Pybind11 (LEGACY CODE)
- Porting to AMatrix
- How to use Cotire
- Applications: Python-modules
- How to run multiple cases using PyCOMPSs
- How to apply a function to a list of variables
- How to use Kratos Native sparse linear algebra
Utilities
Kratos API
Kratos Structural Mechanics API