From 6a80d4de400da755f44fd95189af6aa45c5551dd Mon Sep 17 00:00:00 2001
From: Mark Olesen <Mark.Olesen@esi-group.com>
Date: Thu, 23 May 2024 09:55:06 +0200
Subject: [PATCH] ENH: reduce boundary face queries in streamFunction

BUG: streamFunction used uninitialized values for symmetry patches

- related to 8a8b5db977c3 changes (#3144)

ENH: improve robustness of surface field flattening

- vtk::surfaceFieldWriter
---
 .../output/ensightOutputVolFieldTemplates.C   |  36 +-
 .../vtk/output/foamVtkSurfaceFieldWriter.C    |  31 +-
 .../vtk/output/foamVtkSurfaceFieldWriter.H    |   8 +-
 .../volPointInterpolate.C                     |  38 +-
 .../field/streamFunction/streamFunction.C     | 332 ++++++++++--------
 5 files changed, 227 insertions(+), 218 deletions(-)

diff --git a/src/conversion/ensight/output/ensightOutputVolFieldTemplates.C b/src/conversion/ensight/output/ensightOutputVolFieldTemplates.C
index b940f11d23b..0a2cb59146e 100644
--- a/src/conversion/ensight/output/ensightOutputVolFieldTemplates.C
+++ b/src/conversion/ensight/output/ensightOutputVolFieldTemplates.C
@@ -49,7 +49,7 @@ bool Foam::ensightOutput::writeVolField
     bool parallel = Pstream::parRun();
 
     const fvMesh& mesh = vf.mesh();
-    const polyBoundaryMesh& bmesh = mesh.boundaryMesh();
+    const polyBoundaryMesh& pbm = mesh.boundaryMesh();
 
     const Map<ensightCells>& cellZoneParts = ensMesh.cellZoneParts();
     const Map<ensightFaces>& faceZoneParts = ensMesh.faceZoneParts();
@@ -69,13 +69,13 @@ bool Foam::ensightOutput::writeVolField
     {
         const ensightFaces& part = boundaryParts[patchId];
 
-        if (patchId < 0 || patchId >= bmesh.size())
+        if (patchId < 0 || patchId >= pbm.size())
         {
             // Future handling of combined patches?
             continue;
         }
 
-        const label patchStart = bmesh[patchId].start();
+        const label patchStart = pbm[patchId].start();
 
         // Either use a flat boundary field for all patches,
         // or patch-local face ids
@@ -107,34 +107,28 @@ bool Foam::ensightOutput::writeVolField
     // Flat boundary field
     // similar to volPointInterpolation::flatBoundaryField()
 
-    Field<Type> flat(mesh.nBoundaryFaces(), Zero);
+    Field<Type> flat(pbm.nFaces(), Foam::zero{});
 
-    const fvBoundaryMesh& bm = mesh.boundary();
     forAll(vf.boundaryField(), patchi)
     {
-        const polyPatch& pp = bm[patchi].patch();
-        const auto& bf = vf.boundaryField()[patchi];
+        const polyPatch& pp = pbm[patchi];
+        const auto& pfld = vf.boundaryField()[patchi];
 
-        if (isA<processorFvPatch>(bm[patchi]))
+        // Note: restrict transcribing to actual size of the patch field
+        // - handles "empty" patch type etc.
+
+        SubList<Type> slice(flat, pfld.size(), pp.offset());
+
+        if (isA<processorPolyPatch>(pp))
         {
             // Use average value for processor faces
             // own cell value = patchInternalField
             // nei cell value = evaluated boundary values
-            SubList<Type>
-            (
-                flat,
-                bf.size(),
-                pp.offset()
-            ) = (0.5 * (bf.patchInternalField() + bf));
+            slice = (0.5 * (pfld.patchInternalField() + pfld));
         }
-        else if (!isA<emptyFvPatch>(bm[patchi]))
+        else if (!isA<emptyPolyPatch>(pp))
         {
-            SubList<Type>
-            (
-                flat,
-                bf.size(),
-                pp.offset()
-            ) = bf;
+            slice = pfld;
         }
     }
 
diff --git a/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.C b/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.C
index 8bb1a1ee49e..c6494bd880d 100644
--- a/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.C
+++ b/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2016-2021 OpenCFD Ltd.
+    Copyright (C) 2016-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -26,35 +26,40 @@ License
 \*---------------------------------------------------------------------------*/
 
 #include "foamVtkSurfaceFieldWriter.H"
-#include "emptyFvsPatchFields.H"
 #include "fvsPatchFields.H"
 #include "surfaceFields.H"
 
-// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
 
-Foam::List<Foam::vector> Foam::vtk::surfaceFieldWriter::flattenBoundary
+namespace Foam
+{
+
+// Flatten boundary field values into a contiguous list
+template<class Type>
+static List<Type> flattenBoundary
 (
-    const surfaceVectorField& field
-) const
+    const GeometricField<Type, fvsPatchField, surfaceMesh>& field
+)
 {
-    // Boundary field - flatten
+    const polyBoundaryMesh& pbm = field.mesh().boundaryMesh();
 
-    List<vector> flat(mesh_.nBoundaryFaces(), Zero);
+    List<Type> flat(pbm.nFaces(), Foam::zero{});
 
     forAll(field.boundaryField(), patchi)
     {
-        const polyPatch& pp = mesh_.boundaryMesh()[patchi];
+        const polyPatch& pp = pbm[patchi];
         const auto& pfld = field.boundaryField()[patchi];
 
-        if (!isA<emptyFvsPatchVectorField>(pfld))
-        {
-            SubList<vector>(flat, pp.size(), pp.offset()) = pfld;
-        }
+        // Note: restrict transcribing to actual size of the patch field
+        // - handles "empty" patch type etc.
+        SubList<Type>(flat, pfld.size(), pp.offset()) = pfld;
     }
 
     return flat;
 }
 
+} // End namespace Foam
+
 
 // * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
 
diff --git a/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.H b/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.H
index 1dc5ca1c6d7..420fb0fca6d 100644
--- a/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.H
+++ b/src/conversion/vtk/output/foamVtkSurfaceFieldWriter.H
@@ -82,11 +82,9 @@ class surfaceFieldWriter
         label numberOfPoints_;
 
 
-    // Private Member Functions
-
-        //- Flatten boundary field values into a contiguous list
-        List<vector> flattenBoundary(const surfaceVectorField& field) const;
+public:
 
+    // Generated Methods
 
         //- No copy construct
         surfaceFieldWriter(const surfaceFieldWriter&) = delete;
@@ -95,8 +93,6 @@ class surfaceFieldWriter
         void operator=(const surfaceFieldWriter&) = delete;
 
 
-public:
-
     // Constructors
 
         //- Construct from mesh (default format INLINE_BASE64)
diff --git a/src/finiteVolume/interpolation/volPointInterpolation/volPointInterpolate.C b/src/finiteVolume/interpolation/volPointInterpolation/volPointInterpolate.C
index fac2318f22d..3ef984cf4d7 100644
--- a/src/finiteVolume/interpolation/volPointInterpolation/volPointInterpolate.C
+++ b/src/finiteVolume/interpolation/volPointInterpolation/volPointInterpolate.C
@@ -224,42 +224,34 @@ void Foam::volPointInterpolation::interpolateDimensionedInternalField
 
 
 template<class Type>
-Foam::tmp<Foam::Field<Type>> Foam::volPointInterpolation::flatBoundaryField
+Foam::tmp<Foam::Field<Type>>
+Foam::volPointInterpolation::flatBoundaryField
 (
     const GeometricField<Type, fvPatchField, volMesh>& vf
 ) const
 {
-    const fvMesh& mesh = vf.mesh();
-    const fvBoundaryMesh& bm = mesh.boundary();
+    const polyBoundaryMesh& pbm = vf.mesh().boundaryMesh();
 
-    auto tboundaryVals = tmp<Field<Type>>::New(mesh.nBoundaryFaces());
-    auto& boundaryVals = tboundaryVals.ref();
+    auto tboundaryVals = tmp<Field<Type>>::New(pbm.nFaces(), Foam::zero{});
+    auto& values = tboundaryVals.ref();
 
     forAll(vf.boundaryField(), patchi)
     {
-        label bFacei = bm[patchi].patch().start() - mesh.nInternalFaces();
+        const auto& pp = pbm[patchi];
+        const auto& pfld = vf.boundaryField()[patchi];
+
+        // Note: restrict transcribing to actual size of the patch field
+        // - handles "empty" patch type etc.
+
+        SubList<Type> slice(values, pfld.size(), pp.offset());
 
         if
         (
-           !isA<emptyFvPatch>(bm[patchi])
-        && !vf.boundaryField()[patchi].coupled()
+            !isA<emptyPolyPatch>(pp)
+         && !pfld.coupled()
         )
         {
-            SubList<Type>
-            (
-                boundaryVals,
-                vf.boundaryField()[patchi].size(),
-                bFacei
-            ) = vf.boundaryField()[patchi];
-        }
-        else
-        {
-            const polyPatch& pp = bm[patchi].patch();
-
-            forAll(pp, i)
-            {
-                boundaryVals[bFacei++] = Zero;
-            }
+            slice = pfld;
         }
     }
 
diff --git a/src/functionObjects/field/streamFunction/streamFunction.C b/src/functionObjects/field/streamFunction/streamFunction.C
index 741efa28d4a..9380b1591d0 100644
--- a/src/functionObjects/field/streamFunction/streamFunction.C
+++ b/src/functionObjects/field/streamFunction/streamFunction.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2016 OpenFOAM Foundation
-    Copyright (C) 2019-2023 OpenCFD Ltd.
+    Copyright (C) 2019-2024 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -77,9 +77,10 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
         pMesh,
         dimensionedScalar(phi.dimensions(), Zero)
     );
-    pointScalarField& streamFunction = tstreamFunction.ref();
+    auto& streamFunction = tstreamFunction.ref();
 
-    labelList visitedPoint(mesh_.nPoints(), Zero);
+
+    bitSet visitedPoint(mesh_.nPoints());
 
     label nVisited = 0;
     label nVisitedOld = 0;
@@ -87,10 +88,10 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
     const faceUList& faces = mesh_.faces();
     const pointField& points = mesh_.points();
 
-    label nInternalFaces = mesh_.nInternalFaces();
+    const label nInternalFaces = mesh_.nInternalFaces();
 
     vectorField unitAreas(mesh_.faceAreas());
-    unitAreas /= mag(unitAreas);
+    unitAreas.normalise();
 
     const polyPatchList& patches = mesh_.boundaryMesh();
 
@@ -104,44 +105,57 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
     {
         found = false;
 
+        // Check boundary faces first
         forAll(patches, patchi)
         {
-            const primitivePatch& bouFaces = patches[patchi];
+            const auto& pp = patches[patchi];
+            const auto& patchPhi = phi.boundaryField()[patchi];
+
+            // Skip empty, symmetry patches etc
+            if
+            (
+                patchPhi.empty()
+             || isType<emptyPolyPatch>(pp)
+             || isType<symmetryPlanePolyPatch>(pp)
+             || isType<symmetryPolyPatch>(pp)
+             || isType<wedgePolyPatch>(pp)
+            )
+            {
+                continue;
+            }
 
-            if (!isType<emptyPolyPatch>(patches[patchi]))
+            forAll(pp, facei)
             {
-                forAll(bouFaces, facei)
-                {
-                    if (magSqr(phi.boundaryField()[patchi][facei]) < SMALL)
-                    {
-                        const labelList& zeroPoints = bouFaces[facei];
+                const auto& f = pp[facei];
 
-                        // Zero flux face found
-                        found = true;
+                if (magSqr(patchPhi[facei]) < SMALL)
+                {
+                    // Zero flux face found
+                    found = true;
 
-                        forAll(zeroPoints, pointi)
+                    for (const label pointi : f)
+                    {
+                        if (visitedPoint.test(pointi))
                         {
-                            if (visitedPoint[zeroPoints[pointi]] == 1)
-                            {
-                                found = false;
-                                break;
-                            }
+                            found = false;
+                            break;
                         }
+                    }
 
-                        if (found)
-                        {
-                            Log << "        Zero face: patch: " << patchi
-                                << " face: " << facei << endl;
+                    if (found)
+                    {
+                        Log << "        Zero face: patch: " << patchi
+                            << " face: " << facei << endl;
 
-                            forAll(zeroPoints, pointi)
-                            {
-                                streamFunction[zeroPoints[pointi]] = 0;
-                                visitedPoint[zeroPoints[pointi]] = 1;
-                                nVisited++;
-                            }
+                        for (const label pointi : f)
+                        {
+                            visitedPoint.set(pointi);
+                            ++nVisited;
 
-                            break;
+                            streamFunction[pointi] = 0;
                         }
+
+                        break;
                     }
                 }
             }
@@ -152,20 +166,17 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
         if (!found)
         {
             Log << "        Zero flux boundary face not found. "
-                << "Using cell as a reference."
-                << endl;
-
-            const cellList& c = mesh_.cells();
+                << "Using cell as a reference." << endl;
 
-            forAll(c, ci)
+            for (const cell& c : mesh_.cells())
             {
-                labelList zeroPoints = c[ci].labels(mesh_.faces());
+                labelList zeroPoints = c.labels(mesh_.faces());
 
                 bool found = true;
 
-                forAll(zeroPoints, pointi)
+                for (const label pointi : zeroPoints)
                 {
-                    if (visitedPoint[zeroPoints[pointi]] == 1)
+                    if (visitedPoint.test(pointi))
                     {
                         found = false;
                         break;
@@ -174,11 +185,12 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
 
                 if (found)
                 {
-                    forAll(zeroPoints, pointi)
+                    for (const label pointi : zeroPoints)
                     {
-                        streamFunction[zeroPoints[pointi]] = 0.0;
-                        visitedPoint[zeroPoints[pointi]] = 1;
-                        nVisited++;
+                        visitedPoint.set(pointi);
+                        ++nVisited;
+
+                        streamFunction[pointi] = 0;
                     }
 
                     break;
@@ -201,132 +213,136 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
         {
             finished = true;
 
-            for (label facei = nInternalFaces; facei<faces.size(); facei++)
-            {
-                const labelList& curBPoints = faces[facei];
-                bool bPointFound = false;
+            scalar currentStreamValue(0);
+            point currentStreamPoint(Zero);
 
-                scalar currentBStream = 0.0;
-                vector currentBStreamPoint(0, 0, 0);
+            // Boundary faces first
+            forAll(patches, patchi)
+            {
+                const auto& pp = patches[patchi];
+                const auto& patchPhi = phi.boundaryField()[patchi];
+
+                // Skip empty, symmetry patches etc
+                if
+                (
+                    patchPhi.empty()
+                 || isType<emptyPolyPatch>(pp)
+                 || isType<symmetryPlanePolyPatch>(pp)
+                 || isType<symmetryPolyPatch>(pp)
+                 || isType<wedgePolyPatch>(pp)
+                )
+                {
+                    continue;
+                }
 
-                forAll(curBPoints, pointi)
+                forAll(pp, facei)
                 {
+                    const auto& f = pp[facei];
+
                     // Check if the point has been visited
-                    if (visitedPoint[curBPoints[pointi]] == 1)
+                    bool pointFound = false;
+
+                    for (const label pointi : f)
                     {
-                        // The point has been visited
-                        currentBStream = streamFunction[curBPoints[pointi]];
-                        currentBStreamPoint = points[curBPoints[pointi]];
+                        if (visitedPoint.test(pointi))
+                        {
+                            // The point has been visited
+                            currentStreamValue = streamFunction[pointi];
+                            currentStreamPoint = points[pointi];
 
-                        bPointFound = true;
+                            pointFound = true;
+                            break;
+                        }
+                    }
 
-                        break;
+                    if (!pointFound)
+                    {
+                        finished = false;
+                        continue;
                     }
-                }
 
-                if (bPointFound)
-                {
+
                     // Sort out other points on the face
-                    forAll(curBPoints, pointi)
+                    for (const label pointi : f)
                     {
-                        // Check if the point has been visited
-                        if (visitedPoint[curBPoints[pointi]] == 0)
+                        // If the point has not yet been visited
+                        if (!visitedPoint.test(pointi))
                         {
-                            label patchNo =
-                                mesh_.boundaryMesh().whichPatch(facei);
-
-                            if
+                            vector edgeHat =
                             (
-                                !isType<emptyPolyPatch>(patches[patchNo])
-                             && !isType<symmetryPlanePolyPatch>
-                                 (patches[patchNo])
-                             && !isType<symmetryPolyPatch>(patches[patchNo])
-                             && !isType<wedgePolyPatch>(patches[patchNo])
-                            )
-                            {
-                                label faceNo =
-                                    mesh_.boundaryMesh()[patchNo]
-                                   .whichFace(facei);
-
-                                vector edgeHat =
-                                    points[curBPoints[pointi]]
-                                  - currentBStreamPoint;
-                                edgeHat.replace(slabDir, 0);
-                                edgeHat.normalise();
+                                points[pointi] - currentStreamPoint
+                            );
+                            edgeHat.replace(slabDir, 0);
+                            edgeHat.normalise();
 
-                                vector nHat = unitAreas[facei];
+                            const vector& nHat = unitAreas[facei];
 
-                                if (edgeHat.y() > VSMALL)
-                                {
-                                    visitedPoint[curBPoints[pointi]] = 1;
-                                    nVisited++;
-
-                                    streamFunction[curBPoints[pointi]] =
-                                        currentBStream
-                                      + phi.boundaryField()[patchNo][faceNo]
-                                       *sign(nHat.x());
-                                }
-                                else if (edgeHat.y() < -VSMALL)
+                            if (edgeHat.y() > VSMALL)
+                            {
+                                visitedPoint.set(pointi);
+                                ++nVisited;
+
+                                streamFunction[pointi] =
+                                (
+                                    currentStreamValue
+                                  + patchPhi[facei]*sign(nHat.x())
+                                );
+                            }
+                            else if (edgeHat.y() < -VSMALL)
+                            {
+                                visitedPoint.set(pointi);
+                                ++nVisited;
+
+                                streamFunction[pointi] =
+                                (
+                                    currentStreamValue
+                                  - patchPhi[facei]*sign(nHat.x())
+                                );
+                            }
+                            else
+                            {
+                                if (edgeHat.x() > VSMALL)
                                 {
-                                    visitedPoint[curBPoints[pointi]] = 1;
-                                    nVisited++;
-
-                                    streamFunction[curBPoints[pointi]] =
-                                        currentBStream
-                                      - phi.boundaryField()[patchNo][faceNo]
-                                       *sign(nHat.x());
+                                    visitedPoint.set(pointi);
+                                    ++nVisited;
+
+                                    streamFunction[pointi] =
+                                    (
+                                        currentStreamValue
+                                      + patchPhi[facei]*sign(nHat.y())
+                                    );
                                 }
-                                else
+                                else if (edgeHat.x() < -VSMALL)
                                 {
-                                    if (edgeHat.x() > VSMALL)
-                                    {
-                                        visitedPoint[curBPoints[pointi]] = 1;
-                                        nVisited++;
-
-                                        streamFunction[curBPoints[pointi]] =
-                                            currentBStream
-                                          + phi.boundaryField()[patchNo][faceNo]
-                                           *sign(nHat.y());
-                                    }
-                                    else if (edgeHat.x() < -VSMALL)
-                                    {
-                                        visitedPoint[curBPoints[pointi]] = 1;
-                                        nVisited++;
-
-                                        streamFunction[curBPoints[pointi]] =
-                                            currentBStream
-                                          - phi.boundaryField()[patchNo][faceNo]
-                                           *sign(nHat.y());
-                                    }
+                                    visitedPoint.set(pointi);
+                                    ++nVisited;
+
+                                    streamFunction[pointi] =
+                                    (
+                                        currentStreamValue
+                                      - patchPhi[facei]*sign(nHat.y())
+                                    );
                                 }
                             }
                         }
                     }
                 }
-                else
-                {
-                    finished = false;
-                }
             }
 
-            for (label facei=0; facei<nInternalFaces; facei++)
+            // Internal faces next
+            for (label facei = 0; facei < nInternalFaces; ++facei)
             {
-                // Get the list of point labels for the face
-                const labelList& curPoints = faces[facei];
+                const auto& f = faces[facei];
 
                 bool pointFound = false;
 
-                scalar currentStream = 0.0;
-                point currentStreamPoint(0, 0, 0);
-
-                forAll(curPoints, pointi)
+                for (const label pointi : f)
                 {
                     // Check if the point has been visited
-                    if (visitedPoint[curPoints[pointi]] == 1)
+                    if (visitedPoint.test(pointi))
                     {
-                        // The point has been visited
-                        currentStream = streamFunction[curPoints[pointi]];
-                        currentStreamPoint = points[curPoints[pointi]];
+                        currentStreamValue = streamFunction[pointi];
+                        currentStreamPoint = points[pointi];
                         pointFound = true;
 
                         break;
@@ -336,36 +352,42 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
                 if (pointFound)
                 {
                     // Sort out other points on the face
-                    forAll(curPoints, pointi)
+                    for (const label pointi : f)
                     {
-                        // Check if the point has been visited
-                        if (visitedPoint[curPoints[pointi]] == 0)
+                        // If the point has not yet been visited
+                        if (!visitedPoint.test(pointi))
                         {
                             vector edgeHat =
-                                points[curPoints[pointi]] - currentStreamPoint;
+                            (
+                                points[pointi] - currentStreamPoint
+                            );
 
                             edgeHat.replace(slabDir, 0);
                             edgeHat.normalise();
 
-                            vector nHat = unitAreas[facei];
+                            const vector& nHat = unitAreas[facei];
 
                             if (edgeHat.y() > VSMALL)
                             {
-                                visitedPoint[curPoints[pointi]] = 1;
-                                nVisited++;
-
-                                streamFunction[curPoints[pointi]] =
-                                  currentStream
-                                + phi[facei]*sign(nHat.x());
+                                visitedPoint.set(pointi);
+                                ++nVisited;
+
+                                streamFunction[pointi] =
+                                (
+                                    currentStreamValue
+                                  + phi[facei]*sign(nHat.x())
+                                );
                             }
                             else if (edgeHat.y() < -VSMALL)
                             {
-                                visitedPoint[curPoints[pointi]] = 1;
-                                nVisited++;
-
-                                streamFunction[curPoints[pointi]] =
-                                  currentStream
-                                - phi[facei]*sign(nHat.x());
+                                visitedPoint.set(pointi);
+                                ++nVisited;
+
+                                streamFunction[pointi] =
+                                (
+                                    currentStreamValue
+                                  - phi[facei]*sign(nHat.x())
+                                );
                             }
                         }
                     }
@@ -397,7 +419,7 @@ Foam::tmp<Foam::pointScalarField> Foam::functionObjects::streamFunction::calc
     const scalar thickness = vector(slabNormal) & mesh_.bounds().span();
 
     streamFunction /= thickness;
-    streamFunction.boundaryFieldRef() = 0.0;
+    streamFunction.boundaryFieldRef() = Zero;
 
     return tstreamFunction;
 }
-- 
GitLab