diff -ruN exercises/exercise-model/CMakeLists.txt exercises/solution/exercise-model/CMakeLists.txt --- exercises/exercise-model/CMakeLists.txt 2025-03-03 15:56:59.908625000 +0100 +++ exercises/solution/exercise-model/CMakeLists.txt 2025-03-03 15:56:59.911624948 +0100 @@ -3,5 +3,5 @@ dune_symlink_to_source_files(FILES images params.input) -dumux_add_test(NAME exercise_nonlineardiffusion +dumux_add_test(NAME exercise_nonlineardiffusion_sol SOURCES main.cc) diff -ruN exercises/exercise-model/main.cc exercises/solution/exercise-model/main.cc --- exercises/exercise-model/main.cc 2025-03-03 15:56:59.908625000 +0100 +++ exercises/solution/exercise-model/main.cc 2025-03-03 15:56:59.911624948 +0100 @@ -29,6 +29,8 @@ #include <dumux/nonlinear/newtonsolver.hh> #include <dumux/assembly/fvassembler.hh> +#include "model.hh" + /*! * \ingroup NonlinearDiffusion * \brief Test problem for image denoising using a nonlinear diffusion model @@ -78,7 +80,7 @@ struct NonlinearDiffusionTest { // TODO: We need to set our model by replacing ModelTypeTag - using InheritsFrom = std::tuple</*ModelTypeTag,*/BoxModel>; + using InheritsFrom = std::tuple<NonlinearDiffusionModel, BoxModel>; using Scalar = double; using Grid = Dune::YaspGrid<2>; @@ -136,63 +138,62 @@ // the problem for the boundary conditions, a solution vector and a grid variables instance. auto gridGeometry = std::make_shared<GridGeometry>(gridManager.grid().leafGridView()); - // TODO: Uncomment when the model is implemented - // using Scalar = GetPropType<TypeTag, Properties::Scalar>; - // using Problem = GetPropType<TypeTag, Properties::Problem>; - // using SolutionVector = GetPropType<TypeTag, Properties::SolutionVector>; - // using GridVariables = GetPropType<TypeTag, Properties::GridVariables>; - - // auto problem = std::make_shared<Problem>(gridGeometry); - // auto sol = createInitialSolution<SolutionVector>(*gridGeometry, imageData); - // auto gridVariables = std::make_shared<GridVariables>(problem, gridGeometry); - // gridVariables->init(sol); - - // // We initialize the VTK output module and write out the initial concentration field - // VtkOutputModule<GridVariables, SolutionVector> vtkWriter(*gridVariables, sol, problem->name()); - // vtkWriter.addVolumeVariable([](const auto& vv){ return vv.priVar(0); }, "imageIntensity"); - // vtkWriter.write(0.0); - - // // We instantiate time loop using start and end time as well as - // // the time step size from the parameter tree (`params.input`) - // auto timeLoop = std::make_shared<CheckPointTimeLoop<Scalar>>( - // getParam<Scalar>("TimeLoop.TStart", 0.0), - // getParam<Scalar>("TimeLoop.Dt"), - // getParam<Scalar>("TimeLoop.TEnd") - // ); - - // // Next, we choose the type of assembler, linear solver and nonlinear solver - // // and construct instances of these classes. - // using Assembler = FVAssembler<TypeTag, DiffMethod::numeric>; - // using LinearSolver = SSORCGIstlSolver<LinearSolverTraits<GridGeometry>, LinearAlgebraTraitsFromAssembler<Assembler>>; - // using Solver = Dumux::NewtonSolver<Assembler, LinearSolver>; - - // auto oldSol = sol; // copy the vector to store state of previous time step - // auto assembler = std::make_shared<Assembler>(problem, gridGeometry, gridVariables, timeLoop, oldSol); - // auto linearSolver = std::make_shared<LinearSolver>(gridGeometry->gridView(), gridGeometry->dofMapper()); - // Solver solver(assembler, linearSolver); - - // // time loop - // timeLoop->start(); do - // { - // // assemble & solve - // solver.solve(sol); - - // // make the new solution the old solution - // oldSol = sol; - // gridVariables->advanceTimeStep(); - - // // advance to the time loop to the next step - // timeLoop->advanceTimeStep(); - - // // write VTK output (writes out the concentration field) - // vtkWriter.write(timeLoop->time()); + using Scalar = GetPropType<TypeTag, Properties::Scalar>; + using Problem = GetPropType<TypeTag, Properties::Problem>; + using SolutionVector = GetPropType<TypeTag, Properties::SolutionVector>; + using GridVariables = GetPropType<TypeTag, Properties::GridVariables>; + + auto problem = std::make_shared<Problem>(gridGeometry); + auto sol = createInitialSolution<SolutionVector>(*gridGeometry, imageData); + auto gridVariables = std::make_shared<GridVariables>(problem, gridGeometry); + gridVariables->init(sol); + + // We initialize the VTK output module and write out the initial concentration field + VtkOutputModule<GridVariables, SolutionVector> vtkWriter(*gridVariables, sol, problem->name()); + vtkWriter.addVolumeVariable([](const auto& vv){ return vv.imageIntensity(); }, "imageIntensity"); + vtkWriter.write(0.0); + + // We instantiate time loop using start and end time as well as + // the time step size from the parameter tree (`params.input`) + auto timeLoop = std::make_shared<CheckPointTimeLoop<Scalar>>( + getParam<Scalar>("TimeLoop.TStart", 0.0), + getParam<Scalar>("TimeLoop.Dt"), + getParam<Scalar>("TimeLoop.TEnd") + ); + + // Next, we choose the type of assembler, linear solver and nonlinear solver + // and construct instances of these classes. + using Assembler = FVAssembler<TypeTag, DiffMethod::numeric>; + using LinearSolver = SSORCGIstlSolver<LinearSolverTraits<GridGeometry>, LinearAlgebraTraitsFromAssembler<Assembler>>; + using Solver = Dumux::NewtonSolver<Assembler, LinearSolver>; + + auto oldSol = sol; // copy the vector to store state of previous time step + auto assembler = std::make_shared<Assembler>(problem, gridGeometry, gridVariables, timeLoop, oldSol); + auto linearSolver = std::make_shared<LinearSolver>(gridGeometry->gridView(), gridGeometry->dofMapper()); + Solver solver(assembler, linearSolver); + + // time loop + timeLoop->start(); do + { + // assemble & solve + solver.solve(sol); + + // make the new solution the old solution + oldSol = sol; + gridVariables->advanceTimeStep(); + + // advance to the time loop to the next step + timeLoop->advanceTimeStep(); + + // write VTK output (writes out the concentration field) + vtkWriter.write(timeLoop->time()); - // // report statistics of this time step - // timeLoop->reportTimeStep(); + // report statistics of this time step + timeLoop->reportTimeStep(); - // } while (!timeLoop->finished()); + } while (!timeLoop->finished()); - // timeLoop->finalize(gridGeometry->gridView().comm()); + timeLoop->finalize(gridGeometry->gridView().comm()); return 0; }// end main diff -ruN exercises/exercise-model/model.hh exercises/solution/exercise-model/model.hh --- exercises/exercise-model/model.hh 1970-01-01 01:00:00.000000000 +0100 +++ exercises/solution/exercise-model/model.hh 2025-03-03 15:56:59.911624948 +0100 @@ -0,0 +1,210 @@ +// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +// vi: set et ts=4 sw=4 sts=4: +// +// SPDX-FileCopyrightInfo: Copyright © DuMux-Course contributors, see AUTHORS.md in root folder +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef DUMUX_EXERCISE_NONLINEAR_DIFFUSION_MODEL_HH +#define DUMUX_EXERCISE_NONLINEAR_DIFFUSION_MODEL_HH + +// In the file `model.hh`, we define the volume variables, the model equations, and +// set all default model properties. + +#include <dune/common/fvector.hh> +#include <dumux/common/math.hh> +#include <dumux/common/properties.hh> +#include <dumux/common/numeqvector.hh> +#include <dumux/common/volumevariables.hh> +#include <dumux/discretization/method.hh> + +// The property tag is simply an empty struct with the name `NonlinearDiffusionModel` +namespace Dumux::Properties::TTag { +//! The nonlinear diffusion model tag that we can specialize properties for +struct NonlinearDiffusionModel {}; +} // end namespace Dumux::Properties::TTag + +namespace Dumux { + +/*! + * \ingroup NonlinearDiffusionModel + * \brief Volume averaged quantities required by the nonlinear diffusion model. + * This contains the quantities which are related to a control volume + */ +template <class Traits> +class ImageDenoisingVolumeVariables +{ + using Scalar = typename Traits::PrimaryVariables::value_type; +public: + //! export the type used for the primary variables + using PrimaryVariables = typename Traits::PrimaryVariables; + using Indices = typename Traits::ModelTraits::Indices; + + template<class ElementSolution, class Problem, class Element, class SubControlVolume> + void update(const ElementSolution& elemSol, + const Problem& problem, + const Element& element, + const SubControlVolume& scv) + { + priVars_ = elemSol[scv.indexInElement()]; + } + + /*! + * \brief Returns the image intensity of the control volume. + */ + Scalar imageIntensity() const + { return priVars_[Indices::imageIntensityIdx]; } + + Scalar priVar(const int pvIdx) const + { return priVars_[pvIdx]; } + + const PrimaryVariables& priVars() const + { return priVars_; } + + Scalar extrusionFactor() const + { return 1.0; } + +private: + PrimaryVariables priVars_; +}; + +/*! + * \ingroup NonlinearDiffusionModel + * \brief Element-wise calculation of the residual and its derivatives + * for a nonlinear diffusion problem. + */ +template<class TypeTag> +class NonlinearDiffusionModelLocalResidual +: public GetPropType<TypeTag, Properties::BaseLocalResidual> +{ + using ParentType = GetPropType<TypeTag, Properties::BaseLocalResidual>; + using Scalar = GetPropType<TypeTag, Properties::Scalar>; + using Problem = GetPropType<TypeTag, Properties::Problem>; + using NumEqVector = Dumux::NumEqVector<GetPropType<TypeTag, Properties::PrimaryVariables>>; + + using GridVariables = GetPropType<TypeTag, Properties::GridVariables>; + using VolumeVariables = typename GridVariables::GridVolumeVariables::VolumeVariables; + using ElementVolumeVariables = typename GridVariables::GridVolumeVariables::LocalView; + using ElementFluxVariablesCache = typename GridVariables::GridFluxVariablesCache::LocalView; + + using GridGeometry = GetPropType<TypeTag, Properties::GridGeometry>; + using FVElementGeometry = typename GridGeometry::LocalView; + using SubControlVolume = typename GridGeometry::SubControlVolume; + using SubControlVolumeFace = typename GridGeometry::SubControlVolumeFace; + using GridView = typename GridGeometry::GridView; + using Element = typename GridView::template Codim<0>::Entity; + + using ModelTraits = GetPropType<TypeTag, Properties::ModelTraits>; + using Indices = typename ModelTraits::Indices; + static constexpr int dimWorld = GridView::dimensionworld; + +public: + using ParentType::ParentType; + + NumEqVector computeStorage(const Problem& problem, + const SubControlVolume& scv, + const VolumeVariables& volVars) const + { + NumEqVector storage; + storage[Indices::diffusionEqIdx] = volVars.imageIntensity(); + return storage; + } + + NumEqVector computeFlux(const Problem& problem, + const Element& element, + const FVElementGeometry& fvGeometry, + const ElementVolumeVariables& elemVolVars, + const SubControlVolumeFace& scvf, + const ElementFluxVariablesCache& elemFluxVarsCache) const + { + static_assert(DiscretizationMethods::isCVFE<typename GridGeometry::DiscretizationMethod>, + "This local residual is hard-coded to control-volume finite element schemes"); + + // Compute ∇c at the integration point of this sub control volume face. + const auto& fluxVarCache = elemFluxVarsCache[scvf]; + Dune::FieldVector<Scalar, dimWorld> gradConcentration(0.0); + for (const auto& scv : scvs(fvGeometry)) + { + const auto& volVars = elemVolVars[scv]; + // v.axpy(a, w) means v <- v + a*w + gradConcentration.axpy( + volVars.imageIntensity(), + fluxVarCache.gradN(scv.indexInElement()) + ); + } + + NumEqVector flux; + + const auto K = problem.conductance(); + const auto D = 1.0/(1.0 + gradConcentration.two_norm2()/(K*K)); + + // Compute the flux with `vtmv` (vector transposed times matrix times vector) or -n^T D ∇c A. + // The diffusion coefficient comes from the `problem` (see Part 2 of the example). + flux[Indices::diffusionEqIdx] = -1.0*vtmv( + scvf.unitOuterNormal(), D, gradConcentration + )*scvf.area(); + + return flux; + } +}; +} // end namespace Dumux + +// The model properties +namespace Dumux::Properties { + +// The type of the local residual is the class defined above +template<class TypeTag> +struct LocalResidual<TypeTag, TTag::NonlinearDiffusionModel> +{ using type = NonlinearDiffusionModelLocalResidual<TypeTag>; }; + +// The default scalar type is double +// we compute with double precision floating point numbers +template<class TypeTag> +struct Scalar<TypeTag, TTag::NonlinearDiffusionModel> +{ using type = double; }; + +// The model traits specify some information about our equation system. +template<class TypeTag> +struct ModelTraits<TypeTag, TTag::NonlinearDiffusionModel> +{ + struct type + { + struct Indices + { + static constexpr int imageIntensityIdx = 0; + static constexpr int diffusionEqIdx = 0; + }; + + static constexpr int numEq() { return 1; } + }; +}; + +// The primary variable vector has entries of type `Scalar` and is +// as large as the number of equations +template<class TypeTag> +struct PrimaryVariables<TypeTag, TTag::NonlinearDiffusionModel> +{ + using type = Dune::FieldVector< + GetPropType<TypeTag, Properties::Scalar>, + GetPropType<TypeTag, Properties::ModelTraits>::numEq() + >; +}; + +// The volume variables that are used in our model +template<class TypeTag> +struct VolumeVariables<TypeTag, TTag::NonlinearDiffusionModel> +{ + struct Traits + { + using ModelTraits + = GetPropType<TypeTag, Properties::ModelTraits>; + using PrimaryVariables + = GetPropType<TypeTag, Properties::PrimaryVariables>; + }; + + using type = ImageDenoisingVolumeVariables<Traits>; +}; + +} // end namespace Dumux::Properties + +#endif diff -ruN exercises/exercise-model/README.md exercises/solution/exercise-model/README.md --- exercises/exercise-model/README.md 2025-03-03 15:23:33.245567666 +0100 +++ exercises/solution/exercise-model/README.md 1970-01-01 01:00:00.000000000 +0100 @@ -1,195 +0,0 @@ -# Exercise Model (DuMuX course) -The aim of this exercise is it to learn how to set up a new model (new system of equations). -As an example, we implement a nonlinear diffusion equation model and apply it for image denoising. -In this exercise, we will only consider the bare minimum of classes to successfully assemble -and solve such a problem with DuMux. We also implement the model for a specific discretization method -(the Box method: a vertex-centered finite volume method also known as control-volume finite element method with piece-wise linear basis functions). - -## Equation and Problem set-up - -We consider a nonlinear diffusion equation on a domain $\Omega \subset \mathbb{R}^2$, with boundary $\partial\Omega$, and time interval $(0,T]$ given by - -```math -\begin{aligned} -\frac{\partial c}{\partial t} - \nabla \cdot (D(c) \nabla c) &= 0 && \mathrm{in}\; \Omega \times (0,T] \\ - -D(c) \nabla c \cdot \boldsymbol{n} &= 0 && \mathrm{on}\; \partial\Omega \times (0,T] \\ - c &= c^0 && \mathrm{for}\; t = 0, -\end{aligned} -``` -where for a position $\mathbf{x} \in \Omega$ and time $t$, $c(\mathbf{x},t)$ denotes the unknown evolving concentration field, $D(c)$ the solution-dependent diffusion coefficient, and $c^0$ the initial concentration field. - -In this exercise we want to model image denoising such that the primary variable $c$ corresponds to the **image intensity**, $c^0$ to the noisy image data, and $D$ is given (due to Perona & Malik), as -```math -D(c) = \frac{1}{1 + \left(\frac{|| \nabla c ||}{K}\right)^2}, -``` -with conductance $K$. - -## Task 1: Getting familiar with the diffusion example - -When implementing a new model it is advisable to built upon an already existing model that might be similar and only needs modifications. Since the above equation is a nonlinear diffusion equation, we can built upon the diffusion model implemented in the [diffusion example](https://git.iws.uni-stuttgart.de/dumux-repositories/dumux/-/tree/master/examples/diffusion). -The diffusion example also derives the discrete equations using the Box method as spatial discretization scheme. - -## Task 2: Set up the model - -:arrow_right: Copy the `model.hh` file from the diffusion example into `dumux-course/exercises/exercise-model` and choose appropriate class names. - -Do also not forget to change the [include guards](https://en.wikipedia.org/wiki/Include_guard) -at the beginning of header files (`DUMUX_EXAMPLES_DIFFUSION_MODEL_HH`). -Include guards have to be unique in the entire application that you compile. (Otherwise some -code will not be included.) An alternative is using [`#pragma once`](https://en.wikipedia.org/wiki/Pragma_once) -which is widely supported but not specified by the C++ standard. - -First, the goal is to get the simulation running and then add improvements. For this, it is important to have a -compiling test such that new changes can continuously be tested. -Thus, in the `computeFlux(...)` function of the local residual, you can start with a hard-coded diffusion coefficient of `1.0` (linear diffusion coefficient function). -(Replace `problem.diffusionCoefficient()` by `1.0` because our problem class in `main.cc` does not have a `diffusionCoefficient()` interface.) - -Each model also needs to define a model type tag for setting model-specific properties. - -:arrow_right: Rename the one of the diffusion model (`struct DiffusionModel {};`) to `NonlinearDiffusionModel`. -and use this new type tag for specializing properties, i.e. within the properties namespace - -```c++ -namespace Dumux::Properties::TTag { - -// Defined properties, replace DiffusionModel by NonlinearDiffusionModel - -} // end namespace Dumux::Properties::TTag -``` - -## Task 3: Set up the test case - -:arrow_right: Open the file `main.cc`. - -There you can find -1. The problem setup (implements the homogeneous Neumann boundary conditions (zero flux)) -2. Test case specific property definitions -3. A function for setting the initial intensity values by using the noisy image data -4. The main program defining all steps of the program - -For reading the image data (given as **pgm** file), the `NetPBMReader` reader is used -```c++ -// Read the image -const auto imageFileName = getParam<std::string>("ImageFile"); -const auto imageData = NetPBMReader::readPGM(imageFileName); -``` - -### 3.1: Make test type tag inherit properties from model type tag - -:arrow_right: Include the header `model.hh` <br> -:arrow_right: To use the new model for this test case, make -the `NonlinearDiffusionTest` type tag inherit properties from the model type tag - -```c++ -namespace Dumux::Properties::TTag { - -struct NonlinearDiffusionTest -{ - // TODO: Set new model by replacing ModelTypeTag - using InheritsFrom = std::tuple</*ModelTypeTag,*/BoxModel>; - - // Further property definitions -}; -} // end namespace Dumux::Properties::TTag -``` - -### 3.2: Uncomment the commented code in main file - -Without a model the previous code could not be compiled. -The parts of the main function that need a working model are commented. - -:arrow_right: Remove the code comments to enable all simulation steps. - -### 3.3: Run your application - -We hope everything is implemented correctly. - -:arrow_right: Compile and run the test case by executing - -```bash -cd build-cmake/exercises/exercise-model -make exercise_nonlineardiffusion -./exercise_nonlineardiffusion -``` - -:arrow_right: Fix any compiler errors and warnings. - -## Task 4: Nonlinear diffusion - -In the local residual class, instead of using a constant diffusion coefficient of `1.0`, we now -want to implement the nonlinear diffusion function specified above. With a `Dune::FieldVector` -you can get the 2-norm of the vector via `v.two_norm()` and its square with `v.two_norm2()`. (The reason -for having two different interfaces is that the second one is more efficient.) -Compute the diffusion coefficient and pass it to `vmtv` instead of `1.0`. -You can get the conductance $K$ via the problem interface `conductance`. - -:arrow_right: Add the nonlinear diffusion coefficient computation to the `computeFlux` function - -:arrow_right: Compare the `main.cc` with the `main.cc` of the diffusion example. Notice that we -use a Newton solver as we want to solve nonlinear equations. - -As the DuMux assembler uses numeric differentiation to approximate the Jacobian matrix, you do not have to implement -the derivatives of your residual by hand and implement them. This greatly simplifies implementing -nonlinear equations. - -## Task 5: Running the image denosing test case - -:arrow_right: Compile and run like above. - -With the conductance parameter `Problem.Conductance` (see `params.input`) you can regulate the preservation of edges. -For large conductance, you get almost linear diffusion (Gaussian blurring). For small conductances some edges -are preserved. The conductance acts as a threshold for the image intensity gradient detected as an edge feature. - -__The final result should look like this:__ -<figure> - <center> - <img src="../../slides/img/exercise_model_mri_denoise.gif" alt="denoising"/> - <figcaption> <b> Fig.1 </b> - Denosing of MRI image using nonlinear diffusion model.</figcaption> - </center> -</figure> - -## Task 6: Custom volume variables - -The volume variables represent variables related to control volumes. So far we have used -the `BasicVolumeVariables` class that only stores the primary variables at control volumes. -We have used the interface `priVar` to access the primary variables. In more complex models, -we often want to - -* assign names to our variables to make the local residual more readable -* evaluate constitutive laws and compute secondary variables from primary variables and use these in the local residual - -In this task we will implement a custom volume variable class, while in this simple case it will only be for the first reason. - -:arrow_right: Implement your own `ImageDenoisingVolumeVariables` class (either within a new header file or directly in `model.hh`). As basis you can use the `BasicVolumeVariables`, see `dumux/common/volumevariables.hh`. Copy the class into `model.hh` and rename it. - -:arrow_right: Add a function `imageIntensity()` that returns the first primary variable. - -:arrow_right: In order to use the new `ImageDenoisingVolumeVariables` class in your model, set in your model-specific properties (see `dumux-course/exercises/exercise-properties`): -```c++ -template<class TypeTag> -struct VolumeVariables<TypeTag, TTag::NonlinearDiffusionModel> -{ - struct Traits - { - using PrimaryVariables - = GetPropType<TypeTag, Properties::PrimaryVariables>; - }; - // TODO: set type to your class - using type = ...; -}; -``` - -You can now use the interface `imageIntensity` in your local residual instead of the generic `priVar` interface. - -In order to simplify the implementation of your custom volume variables, you can -also inherit from `BasicVolumeVariables` and only implement the additional interface. - -Another common modification is to add named indices to a model. -To this end, we typically add an `Indices` struct to the `ModelTraits` in which the primary variable -and equation indices are named (e.g. `static constexpr int imageIntensityIdx = 0;`). -The model traits can be passed to the volume variables class via the Traits class. - -:arrow_right: Have a look at the solution of the exercise to see how this is usually implemented. - -Names indices allow to address the entries of the local residual vector or the primary variables vector. -You will see names indices in every DuMux model.