I think the easiest approach (as in: most readable), is replicate.
int r = 3;
int len = 1 + 2 * r;
const auto& squared_yx = Eigen::ArrayXf::LinSpaced(len, -r, r).square();
const auto& bcast = squared_yx.replicate(1, len);
Eigen::MatrixXf mask = (bcast + bcast.transpose()).sqrt();
Note that what you do is numerically unstable (for large r) and the hypot function exists to work around these issues. So even your python code could be better:
r = 3
y, x = numpy.ogrid[-r : r + 1, -r : r + 1]
mask = numpy.hypot(x, y)
To achieve the same in Eigen, do something like this:
const auto& yx = Eigen::ArrayXf::LinSpaced(len, -r, r);
const auto& bcast = yx.replicate(1, len);
Eigen::MatrixXf mask = bcast.binaryExpr(bcast.transpose(),
[](float x, float y) noexcept -> float {
return std::hypot(x, y);
});
Eigen's documentation on binaryExpr is currently broken, so this is hard to find.
To be fair, you will probably never run into stability issues in this particular case because you will run out of memory first. However, it'd still like to point this out because seeing a naive sqrt(x**2 + y**2) is always a bit of a red flag. Also, in Python hypot might still worth it from a performance point because it reduces the number of temporary memory allocations and function calls.
BinaryExpr
The documentation on binaryExpr is missing, I assume because the parser has trouble with Eigen's C++ code. In any case, one can find it indirectly as CwiseBinaryOp and similarly CwiseUnaryOp, CwiseNullaryOp and CwiseTernaryOp.
The use looks a bit weird but is pretty simple. It takes a functor (either a struct with operator(), a function pointer, or a lambda) and applies this element-wise.
The unary operation makes this pretty clear. If Eigen::Array.sin() didn't exist, you could write this:
array.unaryExpr([](double x) -> double { return std::sin(x); }) to achieve exactly the same effect.
The binary and ternary versions take one or two more Eigen expressions as the second and third argument to the function. That's what I did above. The nullary version is explained in the documentation in its own chapter.
Use of auto
Eigen is correct to warn about auto but only in that you have to know what you do. It is important to realize that auto on an Eigen expression just keeps the expression around. It does not evaluate it into a vector or matrix.
This is fine and very useful if you want to compose a complex expression that would be hard to read when put in a single statement. In my code above, there are no temporary memory allocations and no floating point computations take place until the final expression is assigned to the matrix.
As long as the programmer knows that these are expressions and not final matrices, everything is fine.
I think the main take-away is that use of auto with Eigen should be limited to short-lived (as in: inside a single function) scalar expressions. Any coding style that uses auto for everything will quickly break or be hard to read with Eigen. But it can be used safely and make the code more readable in the process without sacrificing performance in the same way as evaluating into matrices would.
As for why I chose const auto& instead of auto or const auto: Mostly force of habit that is unrelated to the task at hand. I mostly do it for instances like this:
const Eigen::Vector& Foo::get_bar();
void quz(Foo& foo)
{
const auto& bar = foo.get_bar();
}
Here, bar will remain a reference whereas auto would create a copy. If the return value is changed, everything stays valid.
Eigen::Vector Foo::get_bar();
void quz(Foo& foo)
{
const auto& bar = foo.get_bar();
}
Now a copy is created anyway. But everything continues to work because assigning the return value to a const-reference extends the lifetime of the object. So this may look like a dangling pointer, but it is not.