Commit d5175529 authored by GILLES Sebastien's avatar GILLES Sebastien

#1473 Introduce the new input data to say the behaviour we want. So far it is...

#1473 Introduce the new input data to say the behaviour we want. So far it is introduced only for the hyperelastic model, and it does actually nothing besides reading the new data.
parent ad1f8f7c
......@@ -1030,6 +1030,10 @@
BE7C94991F605266003D2C52 /* GradPhiTauOrthoTauGradPhi.hxx in Headers */ = {isa = PBXBuildFile; fileRef = BE7C94931F605266003D2C52 /* GradPhiTauOrthoTauGradPhi.hxx */; };
BE7C949B1F605266003D2C52 /* GradPhiTauTauGradPhi.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BE7C94951F605266003D2C52 /* GradPhiTauTauGradPhi.hpp */; };
BE7C949C1F605266003D2C52 /* GradPhiTauTauGradPhi.hxx in Headers */ = {isa = PBXBuildFile; fileRef = BE7C94961F605266003D2C52 /* GradPhiTauTauGradPhi.hxx */; };
BE7E552622EF280E00BEA053 /* Parallelism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE7E552322EF280E00BEA053 /* Parallelism.cpp */; };
BE7E552822EF280E00BEA053 /* Parallelism.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BE7E552522EF280E00BEA053 /* Parallelism.hpp */; };
BE7E553A22EF2D0000BEA053 /* Parallelism.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BE7E553722EF2D0000BEA053 /* Parallelism.hpp */; };
BE7E553B22EF2D0000BEA053 /* Parallelism.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE7E553822EF2D0000BEA053 /* Parallelism.cpp */; };
BE83582B1FFD779E0009956E /* Sort.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BE83582A1FFD779E0009956E /* Sort.hpp */; };
BE83582D1FFD78DE0009956E /* Sort.hxx in Headers */ = {isa = PBXBuildFile; fileRef = BE83582C1FFD78DD0009956E /* Sort.hxx */; };
BE8553A91BBD77BF00DB109E /* DofSource.cpp in Sources */ = {isa = PBXBuildFile; fileRef = BE8553A31BBD77BF00DB109E /* DofSource.cpp */; };
......@@ -4119,6 +4123,10 @@
BE7DE82719CC537000D24C7D /* Op.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Op.hpp; sourceTree = "<group>"; };
BE7DE82A19CC541D00D24C7D /* Comm.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Comm.cpp; sourceTree = "<group>"; };
BE7DE82B19CC541D00D24C7D /* Comm.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Comm.hpp; sourceTree = "<group>"; };
BE7E552322EF280E00BEA053 /* Parallelism.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Parallelism.cpp; sourceTree = "<group>"; };
BE7E552522EF280E00BEA053 /* Parallelism.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Parallelism.hpp; sourceTree = "<group>"; };
BE7E553722EF2D0000BEA053 /* Parallelism.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Parallelism.hpp; sourceTree = "<group>"; };
BE7E553822EF2D0000BEA053 /* Parallelism.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Parallelism.cpp; sourceTree = "<group>"; };
BE7E68682065615100AA2FB3 /* InputData.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = InputData.hpp; sourceTree = "<group>"; };
BE7E68692065615100AA2FB3 /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = "<group>"; };
BE7E686A2065615100AA2FB3 /* Model.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Model.hpp; sourceTree = "<group>"; };
......@@ -7386,6 +7394,7 @@
children = (
BE4D0CA121A2E4F900E0D4E7 /* InputData.doxygen */,
BE4D0C5921A2E4F800E0D4E7 /* SourceList.cmake */,
BE7E552222EF278200BEA053 /* Parallelism */,
BE4D0D0D21A2E4F900E0D4E7 /* TimeManager */,
BE4D0CF621A2E4F900E0D4E7 /* Geometry */,
BE4D0C5B21A2E4F800E0D4E7 /* InitialCondition */,
......@@ -7733,7 +7742,6 @@
isa = PBXGroup;
children = (
BE4D0CF721A2E4F900E0D4E7 /* SourceList.cmake */,
BE4D0CF821A2E4F900E0D4E7 /* Impl */,
BE4D0D0B21A2E4F900E0D4E7 /* Mesh.hpp */,
BE4D0D0321A2E4F900E0D4E7 /* Mesh.hxx */,
BE4D0D0421A2E4F900E0D4E7 /* PseudoNormals.hpp */,
......@@ -7745,6 +7753,7 @@
BE4D0D0C21A2E4F900E0D4E7 /* Domain.hxx */,
BE4D0D0621A2E4F900E0D4E7 /* LightweightDomainList.hpp */,
BE4D0D0721A2E4F900E0D4E7 /* LightweightDomainList.hxx */,
BE4D0CF821A2E4F900E0D4E7 /* Impl */,
);
path = Geometry;
sourceTree = "<group>";
......@@ -7754,13 +7763,13 @@
children = (
BE4D0CF921A2E4F900E0D4E7 /* SourceList.cmake */,
BE4D0CFA21A2E4F900E0D4E7 /* Mesh.cpp */,
BE4D0CFF21A2E4F900E0D4E7 /* Mesh.hpp */,
BE4D0D0021A2E4F900E0D4E7 /* Domain.cpp */,
BE4D0CFB21A2E4F900E0D4E7 /* Domain.hpp */,
BE4D0D0121A2E4F900E0D4E7 /* PseudoNormals.cpp */,
BE4D0CFC21A2E4F900E0D4E7 /* PseudoNormals.hpp */,
BE4D0CFD21A2E4F900E0D4E7 /* LightweightDomainList.hpp */,
BE4D0CFE21A2E4F900E0D4E7 /* LightweightDomainList.cpp */,
BE4D0CFF21A2E4F900E0D4E7 /* Mesh.hpp */,
BE4D0D0021A2E4F900E0D4E7 /* Domain.cpp */,
BE4D0D0121A2E4F900E0D4E7 /* PseudoNormals.cpp */,
);
path = Impl;
sourceTree = "<group>";
......@@ -8609,6 +8618,25 @@
path = MacroEncapsulation;
sourceTree = "<group>";
};
BE7E552222EF278200BEA053 /* Parallelism */ = {
isa = PBXGroup;
children = (
BE7E552322EF280E00BEA053 /* Parallelism.cpp */,
BE7E552522EF280E00BEA053 /* Parallelism.hpp */,
BE7E553022EF2CE900BEA053 /* Internal */,
);
path = Parallelism;
sourceTree = "<group>";
};
BE7E553022EF2CE900BEA053 /* Internal */ = {
isa = PBXGroup;
children = (
BE7E553822EF2D0000BEA053 /* Parallelism.cpp */,
BE7E553722EF2D0000BEA053 /* Parallelism.hpp */,
);
path = Internal;
sourceTree = "<group>";
};
BE7E68642065615100AA2FB3 /* NonlinearMembrane */ = {
isa = PBXGroup;
children = (
......@@ -10881,6 +10909,7 @@
BE4D0D2F21A2E4F900E0D4E7 /* InitVertexMatching.hpp in Headers */,
BE4D0D3121A2E4F900E0D4E7 /* Section.hxx in Headers */,
BE4D0D5421A2E4F900E0D4E7 /* UsualDescription.hpp in Headers */,
BE7E553A22EF2D0000BEA053 /* Parallelism.hpp in Headers */,
BE4D0D2C21A2E4F900E0D4E7 /* InitVertexMatching.hxx in Headers */,
BE4D0D4921A2E4F900E0D4E7 /* ScalarTransientSource.hpp in Headers */,
BE64623B1AEE1AEF00D5162A /* GlobalVector.hxx in Headers */,
......@@ -10912,6 +10941,7 @@
BE8B67401CC0EA9100312399 /* FindFunctor.hxx in Headers */,
BEBE35B71C58FECD00392CCC /* GlobalDiagonalMatrix.hxx in Headers */,
BE4D0D6921A2E4F900E0D4E7 /* VolumicMass.hpp in Headers */,
BE7E552822EF280E00BEA053 /* Parallelism.hpp in Headers */,
BE4D0D8A21A2E4F900E0D4E7 /* Domain.hxx in Headers */,
BE4D0D6A21A2E4F900E0D4E7 /* VolumicMass.hxx in Headers */,
BE6AAE881AF7726600D420CB /* NumberingSubsetForMatrix.hxx in Headers */,
......@@ -12528,6 +12558,7 @@
files = (
BE4D0D5E21A2E4F900E0D4E7 /* HyperelasticBulk.cpp in Sources */,
BE4D0D5F21A2E4F900E0D4E7 /* Solid.cpp in Sources */,
BE7E553B22EF2D0000BEA053 /* Parallelism.cpp in Sources */,
BE4D0D7C21A2E4F900E0D4E7 /* LightweightDomainList.cpp in Sources */,
BE4D0D7E21A2E4F900E0D4E7 /* Domain.cpp in Sources */,
BE4D0D1C21A2E4F900E0D4E7 /* ReactionCoefficient.cpp in Sources */,
......@@ -12538,6 +12569,7 @@
BE4D0D7021A2E4F900E0D4E7 /* ElectricalActivation.cpp in Sources */,
BE4D0D4721A2E4F900E0D4E7 /* RectangularSourceTimeParameter.cpp in Sources */,
BE4D0D5B21A2E4F900E0D4E7 /* Viscosity.cpp in Sources */,
BE7E552622EF280E00BEA053 /* Parallelism.cpp in Sources */,
BE7818791E82BD5F00FF503D /* EnsightCaseReader.cpp in Sources */,
BE4D0D4321A2E4F900E0D4E7 /* Heart.cpp in Sources */,
BE4D0D7121A2E4F900E0D4E7 /* Fiber.cpp in Sources */,
//! \file
//
//
// Parallelism.cpp
// MoReFEM
//
// Created by sebastien on 29/07/2019.
//Copyright © 2019 Inria. All rights reserved.
//
#include "Utilities/String/EmptyString.hpp"
#include "Core/InputData/Instances/Parallelism/Internal/Parallelism.hpp"
namespace MoReFEM::Internal::InputDataNS::ParallelismNS
{
const std::string& Policy::NameInFile()
{
static std::string ret("policy");
return ret;
}
const std::string& Policy::Description()
{
static std::string ret("What should be done for a parallel run. There are 4 possibilities:\n"
" 'Precompute': Precompute the data for a later parallel run and stop once it's done.\n"
" 'ParallelNoWrite': Run the code in parallel without using any pre-processed data "
"and do not write down the processed data.\n"
" 'Parallel': Run the code in parallel without using any pre-processed data and write "
"down the processed data.\n"
" 'RunFromPreprocessed': Run the code in parallel using pre-processed data.");
return ret;
}
const std::string& Policy::Constraint()
{
static std::string ret("value_in(v, {'Precompute', 'ParallelNoWrite', 'Parallel', 'RunFromPreprocessed'})");
return ret;
}
const std::string& Policy::DefaultValue()
{
static std::string ret("'Parallel'");
return ret;
}
const std::string& Path::NameInFile()
{
static std::string ret("path");
return ret;
}
const std::string& Path::Description()
{
static std::string ret("If Policy is 'RunFromPreprocessed', path to the directory which contains the pre-"
"processed data.");
return ret;
}
const std::string& Path::Constraint()
{
return Utilities::EmptyString();
}
const std::string& Path::DefaultValue()
{
static std::string ret("''"); // not empty string on purpose: I want the default value to be this!
return ret;
}
} // namespace MoReFEM::Internal::InputDataNS::ParallelismNS
//! \file
//
//
// Parallelism.hpp
// MoReFEM
//
// Created by sebastien on 29/07/2019.
//Copyright © 2019 Inria. All rights reserved.
//
#ifndef MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_INTERNAL_x_PARALLELISM_HPP_
# define MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_INTERNAL_x_PARALLELISM_HPP_
# include <string>
namespace MoReFEM::Internal::InputDataNS::ParallelismNS
{
/*!
* \class doxygen_hide_parallelism_input_data_cases
*
* There are four cases:
* - "Precompute": Precompute the data for a later parallel run and stop once it's done.
* - "ParallelNoWrite", Run the code in parallel without using any pre-processed data and do not write down the processed data.
* - "Parallel", Run the code in parallel without using any pre-processed data and write down the processed data.
* - "RunFromPreprocessed": Run the code in parallel using pre-processed data.
*/
/*!
* \brief Choice of the parallel behaviour.
*
* \copydoc doxygen_hide_parallelism_input_data_cases
*/
struct Policy
{
//! Name of the input parameter in Lua input file.
static const std::string& NameInFile();
//! Description of the input parameter.
static const std::string& Description();
/*!
* \return Constraint to fulfill.
*
* Might be left empty; if not the format to respect is the \a LuaOptionFile one. Hereafter some text from \a LuaOptionFile example file:
*
* An age should be greater than 0 and less than, say, 150. It is possible
* to check it with a logical expression (written in Lua). The expression
* should be written with 'v' being the variable to be checked.
* \a constraint = "v >= 0 and v < 150"
*
* It is possible to check whether a variable is in a set of acceptable
* value. This is performed with 'value_in' (a Lua function defined by \a LuaOptionFile).
* \a constraint = "value_in(v, {'Messiah', 'Water Music'})"
*
* If a vector is retrieved, the constraint must be satisfied on every
* element of the vector.
*/
static const std::string& Constraint();
/*!
* \return Default value.
*
* This is intended to be used only when the class is used to create a default file; never when no value has been given
* in the input data file (doing so is too much error prone...)
*
* This is given as a string; if no default value return an empty string. The value must be \a LuaOptionFile-formatted.
*/
static const std::string& DefaultValue();
};
/*!
* \brief Path that might be useful (depending on the \a Policy choice).
*/
struct Path
{
//! Name of the input parameter in Lua input file.
static const std::string& NameInFile();
//! Description of the input parameter.
static const std::string& Description();
/*!
* \return Constraint to fulfill.
*
* Might be left empty; if not the format to respect is the \a LuaOptionFile one. Hereafter some text from \a LuaOptionFile example file:
*
* An age should be greater than 0 and less than, say, 150. It is possible
* to check it with a logical expression (written in Lua). The expression
* should be written with 'v' being the variable to be checked.
* \a constraint = "v >= 0 and v < 150"
*
* It is possible to check whether a variable is in a set of acceptable
* value. This is performed with 'value_in' (a Lua function defined by \a LuaOptionFile).
* \a constraint = "value_in(v, {'Messiah', 'Water Music'})"
*
* If a vector is retrieved, the constraint must be satisfied on every
* element of the vector.
*/
static const std::string& Constraint();
/*!
* \return Default value.
*
* This is intended to be used only when the class is used to create a default file; never when no value has been given
* in the input data file (doing so is too much error prone...)
*
* This is given as a string; if no default value return an empty string. The value must be \a LuaOptionFile-formatted.
*/
static const std::string& DefaultValue();
};
} // namespace MoReFEM::Internal::InputDataNS::ParallelismNS
#endif // MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_INTERNAL_x_PARALLELISM_HPP_
### ===================================================================================
### This file is generated automatically by Scripts/generate_cmake_source_list.py.
### Do not edit it manually!
### Convention is that:
### - When a CMake file is manually managed, it is named canonically CMakeLists.txt.
###. - When it is generated automatically, it is named SourceList.cmake.
### ===================================================================================
target_sources(${MOREFEM_CORE}
PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/Parallelism.cpp"
PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/Parallelism.hpp"
)
//! \file
//
//
// Parallelism.cpp
// MoReFEM
//
// Created by sebastien on 29/07/2019.
//Copyright © 2019 Inria. All rights reserved.
//
#include "Core/InputData/Instances/Parallelism/Parallelism.hpp"
namespace MoReFEM::InputDataNS
{
const std::string& Parallelism::GetName()
{
static std::string ret("Parallelism");
return ret;
}
} // namespace MoReFEM::InputDataNS
//! \file
//
//
// Parallelism.hpp
// MoReFEM
//
// Created by sebastien on 29/07/2019.
//Copyright © 2019 Inria. All rights reserved.
//
#ifndef MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_PARALLELISM_HPP_
# define MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_PARALLELISM_HPP_
# include <string>
# include "Core/InputData/Instances/Crtp/Section.hpp"
# include "Core/InputData/Instances/Parallelism/Internal/Parallelism.hpp"
namespace MoReFEM::InputDataNS
{
/*!
* \brief Input data which says how the program should do in its run.
*
* \copydoc doxygen_hide_parallelism_input_data_cases
*/
struct Parallelism
: public Crtp::Section<Parallelism, NoEnclosingSection>
{
/*!
* \brief Return the name of the section in the input parameter.
*
* e.g. 'Mesh1' for IndexT = 1.
*
* \return Name of the section in the input parameter.
*/
static const std::string& GetName();
//! Convenient alias.
using self = Parallelism;
//! Friendship to section parent.
using parent = Crtp::Section<self, NoEnclosingSection>;
//! \cond IGNORE_BLOCK_IN_DOXYGEN
friend parent;
//! \endcond IGNORE_BLOCK_IN_DOXYGEN
/*!
* \brief Choice of the parallel behaviour.
*
* \copydoc doxygen_hide_parallelism_input_data_cases
*/
struct Policy : public Crtp::InputData<Policy, self, std::string>,
public Internal::InputDataNS::ParallelismNS::Policy
{ };
/*!
* \brief Path that might be useful (depending on the \a Policy choice).
*/
struct Path : public Crtp::InputData<Path, self, std::string>,
public Internal::InputDataNS::ParallelismNS::Path
{ };
//! Alias to the tuple of structs.
using section_content_type = std::tuple
<
Policy,
Path
>;
private:
//! Content of the section.
section_content_type section_content_;
};
} // namespace MoReFEM::InputDataNS
#endif // MOREFEM_x_CORE_x_INPUT_DATA_x_INSTANCES_x_PARALLELISM_x_PARALLELISM_HPP_
### ===================================================================================
### This file is generated automatically by Scripts/generate_cmake_source_list.py.
### Do not edit it manually!
### Convention is that:
### - When a CMake file is manually managed, it is named canonically CMakeLists.txt.
###. - When it is generated automatically, it is named SourceList.cmake.
### ===================================================================================
target_sources(${MOREFEM_CORE}
PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/Parallelism.cpp"
PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/Parallelism.hpp"
)
include(${CMAKE_CURRENT_LIST_DIR}/Internal/SourceList.cmake)
......@@ -24,6 +24,7 @@ target_sources(${MOREFEM_CORE}
include(${CMAKE_CURRENT_LIST_DIR}/InitialCondition/SourceList.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/Reaction/SourceList.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/Parallelism/SourceList.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/FElt/SourceList.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/Interpolator/SourceList.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/Crtp/SourceList.cmake)
......
......@@ -27,6 +27,7 @@
# include "Core/InputData/Instances/TimeManager/TimeManager.hpp"
# include "Core/InputData/Instances/Geometry/Mesh.hpp"
# include "Core/InputData/Instances/Solver/Petsc.hpp"
# include "Core/InputData/Instances/Parallelism/Parallelism.hpp"
namespace MoReFEM
......@@ -38,25 +39,25 @@ namespace MoReFEM
//! \copydoc doxygen_hide_numbering_subset_enum
enum class NumberingSubsetIndex
enum class NumberingSubsetIndex
{
displacement = 1
};
//! \copydoc doxygen_hide_unknown_enum
enum class UnknownIndex
enum class UnknownIndex
{
displacement = 1
};
//! \copydoc doxygen_hide_mesh_enum
enum class MeshIndex
enum class MeshIndex
{
mesh = 1
};
//! \copydoc doxygen_hide_domain_enum
enum class DomainIndex
enum class DomainIndex
{
full_mesh = 1,
volume = 2,
......@@ -71,7 +72,7 @@ namespace MoReFEM
};
//! \copydoc doxygen_hide_felt_space_enum
enum class FEltSpaceIndex
enum class FEltSpaceIndex
{
volume = 1,
force = 2
......@@ -79,8 +80,7 @@ namespace MoReFEM
//! \copydoc doxygen_hide_solver_enum
enum class SolverIndex
enum class SolverIndex
{
solver = 1
};
......@@ -90,6 +90,7 @@ namespace MoReFEM
{
surfacic = 1
};
//! \copydoc doxygen_hide_input_data_tuple
using InputDataTuple = std::tuple
......@@ -121,6 +122,7 @@ namespace MoReFEM
InputDataNS::Solid::Kappa1,
InputDataNS::Solid::Kappa2,
InputDataNS::Parallelism,
InputDataNS::Result
>;
......
......@@ -518,6 +518,28 @@ Solid = {
} -- Solid
Parallelism = {
-- What should be done for a parallel run. There are 4 possibilities:
-- 'Precompute': Precompute the data for a later parallel run and stop once it's done.
-- 'ParallelNoWrite': Run the code in parallel without using any pre-processed data and do not write down
-- the processed data.
-- 'Parallel': Run the code in parallel without using any pre-processed data and write down the processed
-- data.
-- 'RunFromPreprocessed': Run the code in parallel using pre-processed data.
-- Expected format: "VALUE"
-- Constraint: value_in(v, {'Precompute', 'ParallelNoWrite', 'Parallel', 'RunFromPreprocessed'})
policy = 'Parallel',
-- If Policy is 'RunFromPreprocessed', path to the directory which contains the pre-processed data.
-- Expected format: "VALUE"
path = ''
} -- Parallelism
Result = {
......
......@@ -446,6 +446,28 @@ Solid = {
} -- Solid
Parallelism = {
-- What should be done for a parallel run. There are 4 possibilities:
-- 'Precompute': Precompute the data for a later parallel run and stop once it's done.
-- 'ParallelNoWrite': Run the code in parallel without using any pre-processed data and do not write down
-- the processed data.
-- 'Parallel': Run the code in parallel without using any pre-processed data and write down the processed
-- data.
-- 'RunFromPreprocessed': Run the code in parallel using pre-processed data.
-- Expected format: "VALUE"
-- Constraint: value_in(v, {'Precompute', 'ParallelNoWrite', 'Parallel', 'RunFromPreprocessed'})
policy = 'Parallel',
-- If Policy is 'RunFromPreprocessed', path to the directory which contains the pre-processed data.
-- Expected format: "VALUE"
path = ''
} -- Parallelism
Result = {
-- Directory in which all the results will be written. This path may use the environment variable
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment