Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions include/bout/sys/generator_context.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public:
BoutReal y() const { return get("y"); }
BoutReal z() const { return get("z"); }
BoutReal t() const { return get("t"); }
CELL_LOC location() const { return location_; }

/// Cell indices
int ix() const { return ix_; }
Expand Down Expand Up @@ -80,6 +81,7 @@ private:
int ix_{0};
int jy_{0};
int kz_{0};
CELL_LOC location_{CELL_CENTRE};

Mesh* localmesh{nullptr}; ///< The mesh on which the position is defined

Expand Down
1 change: 0 additions & 1 deletion manual/sphinx/user_docs/field_expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ sense:
- Unary algebraic operators such as ``sqrt``, ``abs``, ``exp``, ``log``,
``sin``, ``cos``, ``tan``, ``sinh``, ``cosh``, ``tanh``, and ``SQ``
- Simple conditionals with ``if_else`` and ``if_else_zero``
- Reductions such as ``min``, ``max``, and ``mean``

For example::

Expand Down
4 changes: 4 additions & 0 deletions manual/sphinx/user_docs/variable_init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ expressions.
+------------------------------------------+------------------------------------------------------+
| ``tan(x)`` | Tangent |
+------------------------------------------+------------------------------------------------------+
| ``unit_integral(x)`` | Scale ``x`` so that its mesh-weighted integral |
| | :math:`\sum x J\,dx\,dy\,dz` over ``RGN_NOBNDRY`` is |
| | :math:`1` |
+------------------------------------------+------------------------------------------------------+
| ``erf(x)`` | The error function |
+------------------------------------------+------------------------------------------------------+
| ``TanhHat(x, width, centre, steepness)`` | The hat function |
Expand Down
1 change: 1 addition & 0 deletions src/field/field_factory.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ FieldFactory::FieldFactory(Mesh* localmesh, Options* opt)
addGenerator("power", std::make_shared<FieldGenTwoArg<pow>>(nullptr, nullptr));

addGenerator("round", std::make_shared<FieldRound>(nullptr));
addGenerator("unit_integral", std::make_shared<FieldUnitIntegral>());

// Ballooning transform
addGenerator("ballooning", std::make_shared<FieldBallooning>(fieldmesh));
Expand Down
64 changes: 64 additions & 0 deletions src/field/fieldgenerators.cxx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

#include "fieldgenerators.hxx"

#include <bout/boutcomm.hxx>
#include <memory>

#include <bout/constants.hxx>
Expand Down Expand Up @@ -47,6 +48,69 @@ BoutReal FieldHeaviside::generate(const Context& ctx) {
return (gen->generate(ctx) > 0.0) ? 1.0 : 0.0;
}

FieldGeneratorPtr FieldUnitIntegral::clone(const std::list<FieldGeneratorPtr> args) {
if (args.size() != 1) {
throw ParseException(
"Incorrect number of arguments to unit_integral function. Expecting 1, got {:d}",
args.size());
}

return std::make_shared<FieldUnitIntegral>(args.front());
}

bool FieldUnitIntegral::cacheMatches(const Context& ctx) const {
return cache_valid && (cached_mesh == ctx.getMesh()) && (cached_time == ctx.t())
&& (cached_location == ctx.location());
}

void FieldUnitIntegral::populateCache(const Context& ctx) {
Mesh* localmesh = ctx.getMesh();
ASSERT0(localmesh != nullptr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "ASSERT0" is directly included [misc-include-cleaner]

src/field/fieldgenerators.cxx:3:

- #include <bout/boutcomm.hxx>
+ #include "bout/assert.hxx"
+ #include <bout/boutcomm.hxx>

Coordinates* coords = localmesh->getCoordinates(ctx.location());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "Coordinates" is directly included [misc-include-cleaner]

src/field/fieldgenerators.cxx:3:

- #include <bout/boutcomm.hxx>
+ #include "bout/coordinates.hxx"
+ #include <bout/boutcomm.hxx>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: pointee of variable 'coords' of type 'Coordinates *' can be declared 'const' [misc-const-correctness]

Suggested change
Coordinates* coords = localmesh->getCoordinates(ctx.location());
Coordinates const* coords = localmesh->getCoordinates(ctx.location());

if (coords == nullptr) {
throw BoutException("unit_integral function needs coordinates at {}",
toString(ctx.location()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "toString" is directly included [misc-include-cleaner]

                        toString(ctx.location()));
                        ^

}

cached_values = Field3D(localmesh).setLocation(ctx.location()).allocate();

BOUT_FOR(i, cached_values.getRegion("RGN_ALL")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "BOUT_FOR" is directly included [misc-include-cleaner]

src/field/fieldgenerators.cxx:3:

- #include <bout/boutcomm.hxx>
+ #include "bout/region.hxx"
+ #include <bout/boutcomm.hxx>

cached_values[i] = gen->generate(Context(i, ctx.location(), localmesh, ctx.t()));
}

BoutReal local_integral = 0.0;
BOUT_FOR(i, cached_values.getRegion("RGN_NOBNDRY")) {
local_integral += cached_values[i] * coords->J()[i] * coords->dx()[i]
* coords->dy()[i] * coords->dz()[i];
}

BoutReal integral = 0.0;
MPI_Allreduce(&local_integral, &integral, 1, MPI_DOUBLE, MPI_SUM, BoutComm::get());

if (integral == 0.0) {
throw BoutException("unit_integral function integral is zero");
}

BOUT_FOR(i, cached_values.getRegion("RGN_ALL")) { cached_values[i] /= integral; }

cached_mesh = localmesh;
cached_time = ctx.t();
cached_location = ctx.location();
cache_valid = true;
}

BoutReal FieldUnitIntegral::generate(const Context& ctx) {
if (!cacheMatches(ctx)) {
std::lock_guard<std::mutex> guard(cache_mutex);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "std::lock_guard" is directly included [misc-include-cleaner]

src/field/fieldgenerators.cxx:5:

+ #include <mutex>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "std::mutex" is directly included [misc-include-cleaner]

    std::lock_guard<std::mutex> guard(cache_mutex);
                         ^

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'guard' of type 'std::lock_guardstd::mutex' can be declared 'const' [misc-const-correctness]

Suggested change
std::lock_guard<std::mutex> guard(cache_mutex);
std::lock_guard<std::mutex> const guard(cache_mutex);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this AI by any chance? We don't have usual threads, we use OpenMP, so this should be BOUT_OMP(single) instead?

if (!cacheMatches(ctx)) {
populateCache(ctx);
}
}

ASSERT1(ctx.location() == cached_location);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: no header providing "ASSERT1" is directly included [misc-include-cleaner]

  ASSERT1(ctx.location() == cached_location);
  ^

return cached_values(ctx.ix(), ctx.jy(), ctx.kz());
}

//////////////////////////////////////////////////////////
// Ballooning transform
// Use a truncated Ballooning transform to enforce periodicity in y and z
Expand Down
24 changes: 24 additions & 0 deletions src/field/fieldgenerators.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,30 @@ private:
FieldGeneratorPtr gen;
};

/// Scale an expression so its volume integral over RGN_NOBNDRY is 1
class FieldUnitIntegral : public FieldGenerator {
public:
explicit FieldUnitIntegral(FieldGeneratorPtr g = nullptr) : gen(std::move(g)) {}

FieldGeneratorPtr clone(const std::list<FieldGeneratorPtr> args) override;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: parameter 'args' is const-qualified in the function declaration; const-qualification of parameters only has an effect in function definitions [readability-avoid-const-params-in-decls]

Suggested change
FieldGeneratorPtr clone(const std::list<FieldGeneratorPtr> args) override;
FieldGeneratorPtr clone(std::list<FieldGeneratorPtr> args) override;

BoutReal generate(const bout::generator::Context& pos) override;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'FieldUnitIntegral::generate' has a definition with different parameter names [readability-inconsistent-declaration-parameter-name]

  BoutReal generate(const bout::generator::Context& pos) override;
           ^
Additional context

src/field/fieldgenerators.cxx:101: the definition seen here

BoutReal FieldUnitIntegral::generate(const Context& ctx) {
                            ^

src/field/fieldgenerators.hxx:273: differing parameters are named here: ('pos'), in definition: ('ctx')

  BoutReal generate(const bout::generator::Context& pos) override;
           ^

std::string str() const override {
return std::string("unit_integral(") + gen->str() + std::string(")");
}

private:
void populateCache(const bout::generator::Context& ctx);
bool cacheMatches(const bout::generator::Context& ctx) const;

FieldGeneratorPtr gen;
Field3D cached_values{};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: initializer for member 'cached_values' is redundant [readability-redundant-member-init]

Suggested change
Field3D cached_values{};
Field3D cached_values;

Mesh* cached_mesh{nullptr};
BoutReal cached_time{0.0};
CELL_LOC cached_location{CELL_CENTRE};
bool cache_valid{false};
std::mutex cache_mutex;
};

//////////////////////////////////////////////////////////
// Ballooning transform
// Use a truncated Ballooning transform to enforce periodicity
Expand Down
5 changes: 3 additions & 2 deletions src/sys/generator_context.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace bout {
namespace generator {

Context::Context(int ix, int iy, int iz, CELL_LOC loc, Mesh* msh, BoutReal t)
: ix_(ix), jy_(iy), kz_(iz), localmesh(msh) {
: ix_(ix), jy_(iy), kz_(iz), location_(loc), localmesh(msh) {

parameters["x"] = (loc == CELL_XLOW) ? 0.5 * (msh->GlobalX(ix) + msh->GlobalX(ix - 1))
: msh->GlobalX(ix);
Expand All @@ -26,7 +26,8 @@ Context::Context(int ix, int iy, int iz, CELL_LOC loc, Mesh* msh, BoutReal t)
Context::Context(const BoundaryRegion* bndry, int iz, CELL_LOC loc, BoutReal t, Mesh* msh)
: // Add one to X index if boundary is in -x direction, so that XLOW is on the boundary
ix_((bndry->bx < 0) ? bndry->x + 1 : bndry->x),
jy_((bndry->by < 0) ? bndry->y + 1 : bndry->y), kz_(iz), localmesh(msh) {
jy_((bndry->by < 0) ? bndry->y + 1 : bndry->y), kz_(iz), location_(loc),
localmesh(msh) {

parameters["x"] = ((loc == CELL_XLOW) || (bndry->bx != 0))
? 0.5 * (msh->GlobalX(ix_) + msh->GlobalX(ix_ - 1))
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/field/test_field_factory.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "fake_mesh.hxx"
#include "test_extras.hxx"
#include "bout/bout_types.hxx"
#include "bout/boutcomm.hxx"
#include "bout/boutexception.hxx"
#include "bout/constants.hxx"
#include "bout/coordinates.hxx"
Expand All @@ -29,6 +30,43 @@
using namespace bout::globals;
using bout::generator::Context;

namespace {
BoutReal volumeIntegral(const Field3D& field) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that we don't already have a sum(Field3D) given that we have mean -- if we add that, then these two functions are just sum(field * coords->cell_volume()) and sum(coords->cell_volume()).

We could then use this in the generator implementation?

const auto* coords = field.getCoordinates();
if (coords == nullptr) {
throw BoutException("Field has no coordinates");
}

BoutReal local = 0.0;
BOUT_FOR(i, field.getRegion("RGN_NOBNDRY")) {
local +=
field[i] * coords->J()[i] * coords->dx()[i] * coords->dy()[i] * coords->dz()[i];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
field[i] * coords->J()[i] * coords->dx()[i] * coords->dy()[i] * coords->dz()[i];
field[i] * coords->cell_volume[i];

}

BoutReal global = 0.0;
MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, BoutComm::get());
return global;
}

BoutReal totalVolume(Mesh* localmesh, CELL_LOC location = CELL_CENTRE) {
auto* coords = localmesh->getCoordinates(location);
if (coords == nullptr) {
throw BoutException("Mesh has no coordinates");
}

BoutReal local = 0.0;
Field3D ones{localmesh};
ones.allocate();
BOUT_FOR(i, ones.getRegion("RGN_NOBNDRY")) {
local += coords->J()[i] * coords->dx()[i] * coords->dy()[i] * coords->dz()[i];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local += coords->J()[i] * coords->dx()[i] * coords->dy()[i] * coords->dz()[i];
local += coords->cell_volume[i];

}

BoutReal global = 0.0;
MPI_Allreduce(&local, &global, 1, MPI_DOUBLE, MPI_SUM, BoutComm::get());
return global;
}
} // namespace

// Reuse the "standard" fixture for FakeMesh
template <typename T>
class FieldFactoryCreationTest : public FakeMeshFixture {
Expand Down Expand Up @@ -60,6 +98,8 @@ using Fields = ::testing::Types<Field2D, Field3D>;

TYPED_TEST_SUITE(FieldFactoryCreationTest, Fields);

using FieldFactory3DCreationTest = FieldFactoryCreationTest<Field3D>;

TYPED_TEST(FieldFactoryCreationTest, CreateFromValueGenerator) {
auto value = BoutReal{4.};
auto output = this->create(generator(value));
Expand Down Expand Up @@ -179,6 +219,40 @@ TYPED_TEST(FieldFactoryCreationTest, CreateZ) {
EXPECT_TRUE(IsFieldEqual(output, expected));
}

TEST_F(FieldFactory3DCreationTest, CreateUnitIntegralConstant) {
const auto expected = 1.0 / totalVolume(mesh);

auto output = this->factory.create3D("unit_integral(1)");

EXPECT_TRUE(IsFieldEqual(output, expected));
EXPECT_NEAR(volumeIntegral(output), 1.0, 1e-12);
}

TEST_F(FieldFactory3DCreationTest, CreateUnitIntegralUsesCellVolume) {
Field2D dy{mesh};
dy.allocate();
BOUT_FOR(i, dy.getRegion("RGN_ALL")) { dy[i] = static_cast<BoutReal>(i.y() + 1); }
mesh->getCoordinates()->setDy(dy);

const auto expected = 1.0 / totalVolume(mesh);
auto output = this->factory.create3D("unit_integral(1)");

EXPECT_TRUE(IsFieldEqual(output, expected));
EXPECT_NEAR(volumeIntegral(output), 1.0, 1e-12);
}

TEST_F(FieldFactory3DCreationTest, CreateUnitIntegralRefreshesCachedParseAtNewTime) {
auto output_t0 =
this->factory.create3D("unit_integral(y + t)", nullptr, mesh, CELL_CENTRE, 0.0);
auto output_t1 =
this->factory.create3D("unit_integral(y + t)", nullptr, mesh, CELL_CENTRE, 1.0);

EXPECT_NEAR(volumeIntegral(output_t0), 1.0, 1e-12);
EXPECT_NEAR(volumeIntegral(output_t1), 1.0, 1e-12);
EXPECT_NE(output_t0(mesh->xstart, mesh->ystart, 0),
output_t1(mesh->xstart, mesh->ystart, 0));
}

TYPED_TEST(FieldFactoryCreationTest, CreateXStaggered) {
// Need this->mesh_staggered to access member of base FakeMeshFixture because
// derived FieldFactoryCreationTest is a template clas
Expand Down
Loading