diff --git a/src/functionObjects/lagrangian/Make/files b/src/functionObjects/lagrangian/Make/files index c7feb02e33ffcdbbcf385cd0be5ee13d40348ea1..8a65315b9adb9bfd649dd99e527e580e0e2331ea 100644 --- a/src/functionObjects/lagrangian/Make/files +++ b/src/functionObjects/lagrangian/Make/files @@ -4,5 +4,6 @@ icoUncoupledKinematicCloud/icoUncoupledKinematicCloud.C dsmcFields/dsmcFields.C vtkCloud/vtkCloud.C +vtkCloud/parcelSelectionDetail.C LIB = $(FOAM_LIBBIN)/liblagrangianFunctionObjects diff --git a/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.C b/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.C new file mode 100644 index 0000000000000000000000000000000000000000..d436f7f9385590fbf94044bde8a2fcaac1680739 --- /dev/null +++ b/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.C @@ -0,0 +1,416 @@ +/*---------------------------------------------------------------------------*\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | + \\ / A nd | Copyright (C) 2018 OpenCFD Ltd. + \\/ M anipulation | +------------------------------------------------------------------------------- +License + This file is part of OpenFOAM. + + OpenFOAM is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenFOAM is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>. + +\*---------------------------------------------------------------------------*/ + +#include "parcelSelectionDetail.H" +#include "scalarPredicates.H" +#include "labelField.H" +#include "scalarField.H" +#include "pointField.H" +#include "ListListOps.H" + +// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // + +const Foam::Enum +< + Foam::Detail::parcelSelection::actionType +> +Foam::Detail::parcelSelection::actionNames +({ + { actionType::ALL, "all" }, + { actionType::CLEAR, "clear" }, + { actionType::INVERT, "invert" }, + { actionType::ADD, "add" }, + { actionType::SUBTRACT, "subtract" }, + { actionType::SUBSET, "subset" }, + { actionType::IGNORE, "ignore" }, +}); + + +const Foam::Enum +< + Foam::Detail::parcelSelection::sourceType +> +Foam::Detail::parcelSelection::sourceNames +({ + { sourceType::FIELD, "field" }, + { sourceType::STRIDE, "stride" }, +}); + + +const Foam::Enum +< + Foam::Detail::parcelSelection::logicType +> Foam::Detail::parcelSelection::logicNames +({ + { logicType::AND, "and" }, + { logicType::OR, "or" }, +}); + + +// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // + +namespace Foam +{ + template<class Type, class Predicate, class AccessOp> + static void apply + ( + bitSet& selection, + const Detail::parcelSelection::actionType action, + const Predicate& accept, + const UList<Type>& list, + const AccessOp& aop + ) + { + using actionType = Detail::parcelSelection::actionType; + + const label len = selection.size(); + + switch (action) + { + case actionType::ADD: + { + for (label parceli = 0; parceli < len; ++parceli) + { + if (accept(aop(list[parceli]))) + { + selection.set(parceli); + } + } + } + break; + + case actionType::SUBTRACT: + { + for (label parceli = 0; parceli < len; ++parceli) + { + if (accept(aop(list[parceli]))) + { + selection.unset(parceli); + } + } + } + break; + + case actionType::SUBSET: + { + for (const label parceli : selection) + { + if (!accept(aop(list[parceli]))) + { + selection.unset(parceli); + } + } + } + break; + + default: + break; + } + } + +} // End namespace Foam + + +// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * // + +Foam::Detail::parcelSelection::parcelSelection() +: + parcelSelect_(), + parcelAddr_() +{} + + +// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * // + +bool Foam::Detail::parcelSelection::calculateFilter +( + const objectRegistry& obrTmp, + const bool log +) +{ + if (parcelSelect_.empty()) + { + parcelAddr_.clear(); + return false; + } + + // Start with all parcels unselected + + // Number of parcels (locally) + const auto* pointsPtr = obrTmp.findObject<vectorField>("position"); + label nParcels = pointsPtr->size(); + + parcelAddr_.reset(); + parcelAddr_.resize(nParcels); + + reduce(nParcels, sumOp<label>()); + + Log << "Applying parcel filtering to " << nParcels << " parcels" << nl; + + if (!nParcels) + { + parcelAddr_.clear(); + return false; + } + + // The unary function type(s) for testing a scalar. + // Allocate 3 slots. + // 0 is the test + // 1,2 are for storage of composite tests (eg, and/or logic) + predicates::scalars tests(3); + + for (const entry& dEntry : parcelSelect_) + { + if (!dEntry.isDict()) + { + WarningInFunction + << "Ignoring non-dictionary entry " + << dEntry << endl; + continue; + } + + const dictionary& dict = dEntry.dict(); + + // A very limited number of sources (stride, field) + // and actions (all add subtract subset) so handle manually + + auto action = actionNames.get("action", dict); + + // These ones we do directly + switch (action) + { + case actionType::ALL: + Log << "- select all" << nl; + parcelAddr_ = true; + continue; + break; + + case actionType::CLEAR: + Log << "- clear" << nl; + parcelAddr_ = false; + continue; + break; + + case actionType::INVERT: + Log << "- invert" << nl; + parcelAddr_.flip(); + continue; + break; + + case actionType::IGNORE: + continue; + break; + + default: + break; + } + + // The others need a source + // Need a source + const auto source = sourceNames.get("source", dict); + + switch (source) + { + case sourceType::STRIDE: + { + const label stride = dict.get<label>("stride"); + + const labelField& ids = + obrTmp.lookupObject<labelField>("origId"); + + Log << "- " << actionNames[action] + << " stride " << stride << nl; + + if (stride <= 0) + { + WarningInFunction + << nl + << "Ignoring bad value for stride=" << stride << nl + << endl; + } + else if (stride == 1) + { + // More efficient handling of stride 1, but should + // not really be using stride == 1. + switch (action) + { + case actionType::ADD: + parcelAddr_ = true; + break; + + case actionType::SUBTRACT: + parcelAddr_ = false; + break; + + default: + break; + } + } + else + { + // Using stride > 1 + apply + ( + parcelAddr_, + action, + [=](const label id) -> bool { return !(id % stride); }, + ids, + accessOp<label>() // pass-through + ); + } + } + break; + + case sourceType::FIELD: + { + const word fieldName(dict.get<word>("field")); + + const auto* labelFld = + obrTmp.findObject<labelField>(fieldName); + + const auto* scalarFld = + obrTmp.findObject<scalarField>(fieldName); + + const auto* vectorFld = + obrTmp.findObject<vectorField>(fieldName); + + + Log << "- " << actionNames[action] << " field " << fieldName; + + if (!labelFld && !scalarFld && !vectorFld) + { + WarningInFunction + << nl + << "No scalar/vector parcel field: " << fieldName + << " ignoring selection" << nl + << endl; + continue; + } + + const entry& e = dict.lookupEntry("accept", keyType::LITERAL); + + ITstream& is = e.stream(); + + if (4 == is.size()) + { + // 4 tokens: + // -> (op val) + + Tuple2<word,scalar> expr1(is); + e.checkITstream(is); + + tests.first() = predicates::scalars::operation(expr1); + + Log << " : " << expr1; + } + else if (9 == is.size()) + { + // 9 tokens: + // -> (op val) and (op val) + // -> (op val) or (op val) + + Tuple2<word,scalar> expr1(is); + word logicName(is); + Tuple2<word,scalar> expr2(is); + e.checkITstream(is); + + logicType logic = logicNames[logicName]; + tests[1] = predicates::scalars::operation(expr1); + tests[2] = predicates::scalars::operation(expr2); + + switch (logic) + { + case logicType::AND: + tests.first() = + predicates::scalars::andOp(tests[1], tests[2]); + break; + + case logicType::OR: + tests.first() = + predicates::scalars::orOp(tests[1], tests[2]); + break; + } + + Log << " : " << expr1 << ' ' << logicName << ' ' << expr2; + } + else + { + action = actionType::IGNORE; + + // Use the following to always provoke an error. + e.checkITstream(is); + } + + if (labelFld) + { + apply + ( + parcelAddr_, + action, + tests.first(), + *labelFld, + accessOp<label>() // pass-through + ); + } + else if (scalarFld) + { + apply + ( + parcelAddr_, + action, + tests.first(), + *scalarFld, + accessOp<scalar>() // pass-through + ); + } + else if (vectorFld) + { + apply + ( + parcelAddr_, + action, + tests.first(), + *vectorFld, + [](const vector& val) -> scalar + { + return Foam::mag(val); + } + ); + } + + Log << endl; + } + break; + + default: + break; + } + } + + return true; +} + + +// ************************************************************************* // diff --git a/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.H b/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.H new file mode 100644 index 0000000000000000000000000000000000000000..bf7f0f1276957baa50898b93fb5745e52fe6a903 --- /dev/null +++ b/src/functionObjects/lagrangian/vtkCloud/parcelSelectionDetail.H @@ -0,0 +1,221 @@ +/*---------------------------------------------------------------------------*\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | + \\ / A nd | Copyright (C) 2018 OpenCFD Ltd. + \\/ M anipulation | +------------------------------------------------------------------------------- +License + This file is part of OpenFOAM. + + OpenFOAM is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenFOAM is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>. + +Class + Foam::Detail::parcelSelection + +Description + Selection of parcels based on their objectRegistry entries. + Normally accessed via a dictionary entry. + + Example sub-dictionary entry + \verbatim + selection + { + stride + { + // every 10th parcelId + action add; + source stride; + stride 10; + } + injector + { + // Only output from injectorID == 1 + action subset; + source field; + field typeId; + accept (equal 1); + } + Umin + { + // Remove slow parcels + action subtract; + source field; + field U; + accept (less 1e-3); + } + diam + { + // Only particular diameter ranges + action subset; + source field; + field d; + accept (greater 1e-3) and (less 1e-3); + } + } + \endverbatim + + \heading Entry type + \table + Property | Description | Required | Default + action | all/clear/invert add/subtract/subset/ignore | yes | + source | field/stride | mostly | + \endtable + + \heading Stride source + \table + Property | Description | Required | Default + stride | The stride for the parcel id | yes | + \endtable + + \heading Field source + \table + Property | Description | Required | Default + field | The label/scalar/vector field name | yes | + accept | Acceptance or test criterion | yes | + \endtable + + The \c accept criterion has two forms: + -# single expression + - (expr) + -# composite expression + - (expr) or (expr) + - (expr) and (expr) + + The expressions are a (op scalar) pair that form a unary scalar + predicate. The \a op is one of the following: + - lt, less + - le, lessEq + - gt, greater + - ge, greaterEq + - eq, equal + - neq, notEqual + + For example, + \verbatim + accept (less 10); + accept (less 10) or (greater 100); + accept (ge 10) and (le 20); + \endverbatim + +See also + Foam::predicates::scalars + Foam::functionObjects::vtkCloud + +SourceFiles + parcelSelectionDetail.C + +\*---------------------------------------------------------------------------*/ + +#ifndef parcelSelectionDetail_H +#define parcelSelectionDetail_H + +#include "bitSet.H" +#include "Enum.H" +#include "objectRegistry.H" + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +namespace Foam +{ +namespace Detail +{ + +/*---------------------------------------------------------------------------*\ + Class Detail::parcelSelection Declaration +\*---------------------------------------------------------------------------*/ + +class parcelSelection +{ +public: + + //- Enumeration defining the valid selection actions + enum actionType + { + ALL, //!< "all" - select all parcels + CLEAR, //!< "clear" - clear the selection + INVERT, //!< "invert" - invert the selection + ADD, //!< "add" - parcel selection + SUBTRACT, //!< "subtract" - remove parcel selection + SUBSET, //!< "subset" - subset parcel selection + IGNORE, //!< "ignore" - dummy no-op + }; + + //- Names for the actionType + static const Enum<actionType> actionNames; + + + //- Enumeration defining the valid sources + enum sourceType + { + FIELD, //!< "field" - select based on field value + STRIDE //!< "stride" - select based on stride (parcel id) + }; + + //- Names for the sourceType + static const Enum<sourceType> sourceNames; + + + //- Enumeration defining and/or logic + enum logicType { AND, OR }; + + //- Names for the logicType + static const Enum<logicType> logicNames; + + +protected: + + // Protected data + + //- The filtered parcel addressing. Eg, for the current cloud. + dictionary parcelSelect_; + + //- The filtered parcel addressing. Eg, for the current cloud. + bitSet parcelAddr_; + + + // Protected Member Functions + + //- Calculate parcel selection filter. + // \return True if the filter is applicable + bool calculateFilter + ( + const objectRegistry& obrTmp, + const bool log = true + ); + +public: + + // Constructors + + //- Construct null + parcelSelection(); + + + //- Destructor + virtual ~parcelSelection() = default; + +}; + + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +} // End namespace Detail +} // End namespace Foam + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +#endif + +// ************************************************************************* // diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C index 840e6da70c5b7c3287f90e3d3c9c94095963cb07..b5fd758a7c03b13d5758ccb203bb5da2f0b0f1f6 100644 --- a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C +++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.C @@ -132,9 +132,23 @@ bool Foam::functionObjects::vtkCloud::writeCloud return false; } + applyFilter_ = calculateFilter(obrTmp, log); + reduce(applyFilter_, orOp<bool>()); + + + // Number of parcels (locally) + label nParcels = (applyFilter_ ? parcelAddr_.count() : pointsPtr->size()); + // Total number of parcels on all processes - const label nTotParcels = - returnReduce(pointsPtr->size(), sumOp<label>()); + const label nTotParcels = returnReduce(nParcels, sumOp<label>()); + + if (applyFilter_ && log) + { + // Report filtered/unfiltered count + Log << "After filtering using " << nTotParcels << '/' + << (returnReduce(pointsPtr->size(), sumOp<label>())) + << " parcels" << nl; + } if (pruneEmpty_ && !nTotParcels) { @@ -216,7 +230,14 @@ bool Foam::functionObjects::vtkCloud::writeCloud } - vtk::writeListParallel(format.ref(), *pointsPtr); + if (applyFilter_) + { + vtk::writeListParallel(format.ref(), *pointsPtr, parcelAddr_); + } + else + { + vtk::writeListParallel(format.ref(), *pointsPtr); + } if (Pstream::master()) @@ -333,6 +354,7 @@ Foam::functionObjects::vtkCloud::vtkCloud printf_(), useVerts_(false), pruneEmpty_(false), + applyFilter_(false), selectClouds_(), selectFields_(), directory_(), @@ -404,7 +426,6 @@ bool Foam::functionObjects::vtkCloud::read(const dictionary& dict) useVerts_ = dict.lookupOrDefault<bool>("cellData", false); pruneEmpty_ = dict.lookupOrDefault<bool>("prune", false); - selectClouds_.clear(); dict.readIfPresent("clouds", selectClouds_); @@ -417,7 +438,10 @@ bool Foam::functionObjects::vtkCloud::read(const dictionary& dict) selectFields_.clear(); dict.readIfPresent("fields", selectFields_); + selectFields_.uniq(); + // Actions to define selection + parcelSelect_ = dict.subOrEmptyDict("selection"); // Output directory diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H index 242d058d10ec6c6d22f0a333ba2db15c231e2650..256bbfa78237419bde6e80f5d198a5c91df60265 100644 --- a/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H +++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloud.H @@ -44,23 +44,56 @@ Description cloud myCloud; fields (T U rho); width 4; // file-padding + + selection + { + stride + { + // every 10th parcelId + action add; + source stride; + stride 10; + } + Umin + { + // Remove slow parcels + action subtract; + source field; + field U; + accept (less 1e-3); + } + diam + { + // Only particular diameter ranges + action subset; + source field; + field d; + accept (greater 1e-3) and (less 1e-3); + } + } } \endverbatim -Usage + \heading Basic Usage \table - Property | Description | Required | Default - type | Type name: vtkCloud | yes | - writeControl | Output control | recommended | timeStep - cloud | | no | defaultCloud - clouds | wordRe list of clouds | no | - fields | wordRe list of fields | no | - prune | Suppress writing of empty clouds | no | false + Property | Description | Required | Default + type | Type name: vtkCloud | yes | + clouds | List of clouds (name or regex) | no | + cloud | Cloud name | no | defaultCloud + fields | List of fields (name or regex) | no | + selection | Parcel selection control | no | empty-dict + \endtable + + \heading Output Options + \table + Property | Description | Required | Default + format | Format as ascii or binary | no | binary + precision | Write precision in ascii | no | same as IOstream + directory | The output directory name | no | postProcessing/NAME + width | Padding width for file name | no | 8 cellData | Emit cellData instead of pointData | no | false - directory | The output directory name | no | postProcessing/NAME - width | Padding width for file name | no | 8 - format | Format as ascii or binary | no | binary - precision | Write precision in ascii | no | same as IOstream + prune | Suppress writing of empty clouds | no | false + writeControl | Output control | recommended | timeStep \endtable The output filename and fields are added to the functionObjectProperties @@ -77,7 +110,17 @@ Usage } \endverbatim +Note + The selection dictionary can be used for finer control of the parcel + output. It contains a set of (add,subtract,subset,clear,invert) + selection actions and sources. + Omitting the selection dictionary is the same as specifying the + conversion of all parcels (in the selected clouds). + More syntax details are to be found in the corresponding + Foam::Detail::parcelSelection class. + See also + Foam::Detail::parcelSelection Foam::functionObjects::ensightWrite Foam::functionObjects::vtkWrite Foam::functionObjects::fvMeshFunctionObject @@ -93,6 +136,7 @@ SourceFiles #define functionObjects_vtkCloud_H #include "fvMeshFunctionObject.H" +#include "parcelSelectionDetail.H" #include "foamVtkOutputOptions.H" #include "foamVtkSeriesWriter.H" #include "wordRes.H" @@ -111,7 +155,8 @@ namespace functionObjects class vtkCloud : - public fvMeshFunctionObject + public fvMeshFunctionObject, + public Foam::Detail::parcelSelection { // Private data @@ -127,6 +172,9 @@ class vtkCloud //- Suppress writing of empty clouds bool pruneEmpty_; + //- Apply output filter (for the current cloud) + bool applyFilter_; + //- Requested names of clouds to process wordRes selectClouds_; diff --git a/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C b/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C index f68b666c9b16717008b86d8f90d8e082cb95b738..aed94728c8ec0d859d7ea61e6813ea37353f39e7 100644 --- a/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C +++ b/src/functionObjects/lagrangian/vtkCloud/vtkCloudTemplates.C @@ -88,7 +88,14 @@ Foam::wordList Foam::functionObjects::vtkCloud::writeFields } } - vtk::writeListParallel(format.ref(), values); + if (applyFilter_) + { + vtk::writeListParallel(format.ref(), values, parcelAddr_); + } + else + { + vtk::writeListParallel(format.ref(), values); + } if (Pstream::master()) { diff --git a/tutorials/lagrangian/reactingParcelFoam/filter/system/controlDict b/tutorials/lagrangian/reactingParcelFoam/filter/system/controlDict index 15290e5c12bfc004b8605e03e0a0f0ba0350e93e..e54c5d788b5948b91a14eb793300131942c40dac 100644 --- a/tutorials/lagrangian/reactingParcelFoam/filter/system/controlDict +++ b/tutorials/lagrangian/reactingParcelFoam/filter/system/controlDict @@ -54,6 +54,7 @@ maxDeltaT 1; functions { #include "vtkCloud" + #include "vtkWrite" } // ************************************************************************* // diff --git a/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkCloud b/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkCloud index 808f9d36d9543a9f7c3227e63b7d8f4836285dc8..ce153de26ee2f60a0a3eeb30f1e9bfca926dd881 100644 --- a/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkCloud +++ b/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkCloud @@ -6,6 +6,9 @@ cloudWrite libs ("liblagrangianFunctionObjects.so"); log true; + // Nothing happens before this anyhow + timeStart 0.5; + writeControl writeTime; // cloud reactingCloud1; @@ -15,16 +18,68 @@ cloudWrite fields ( U T d "Y.*" ); //- Output format (ascii | binary) - default = binary - // format binary; + // format ascii; - // format ascii; - // writePrecision 12; + // precision 12; - //- Suppress writing of empty clouds - default = false - // prune true; + // Suppress writing of empty clouds - default = false + prune true; - //- Output directory name - default = "VTK" + //- Output directory name - Default postProcessing // directory "VTK"; + + selection + { + all + { + action all; + } + + none + { + action clear; + } + + // Reduced number of output parcels + stride + { + action add; + source stride; + stride 4; + } + + T + { + action subset; + source field; + field T; + accept (greater 280) and (less 300); + } + + YH2O + { + action subset; + source field; + field YH2O(l); + accept (greater 0.5); + } + + diameter + { + action subset; + source field; + field d; + accept (greater 1e-10); + } + + Umin + { + action subtract; + source field; + field U; + accept (less 0.1); + } + } } diff --git a/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkWrite b/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkWrite new file mode 100644 index 0000000000000000000000000000000000000000..6e71e5f89efef72052fa29ed102c16f0018ddcbe --- /dev/null +++ b/tutorials/lagrangian/reactingParcelFoam/filter/system/vtkWrite @@ -0,0 +1,35 @@ +// -*- C++ -*- +// Minimal example of using the vtkWrite function object. +vtkWrite +{ + type vtkWrite; + libs ("libutilityFunctionObjects.so"); + log true; + + // Nothing happens before this anyhow + timeStart 0.4; + writeControl writeTime; + + boundary false; + + interpolate true; + + fields (U); + // format ascii; + + // Region of interest + selection + { + inletRegion + { + action add; + source zone; + zones (leftFluid); + } + } + + // Write cell ids as field - Default=true + writeIds false; +} + +// ************************************************************************* //