Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Refactor Operators #198

Closed
wants to merge 84 commits into from
Closed

Refactor Operators #198

wants to merge 84 commits into from

Conversation

ftalbrecht
Copy link
Member

Remaining todos:

  • update functionals accordingly
  • make all c++ tests pass
  • update Python bindings

This completely rewrites the (global) operators to address several issues we've had over the time. This is the main change I want to have in dune-gdt before we write the paper. Once this is in, we can have a first release of a wheel.

Notes:

  • the Python bindings are not adapted yet and will not compile, I am first aiming for passing headercheck and C++ tests
  • the current documentation is not up to date, but I do not really don't know where to put it
    I was playing around with tutorials in Jupyter Notebook using xeus-cling for interactive C++ code and surrounding Markdown cells, which would be my favorite solutions (however, I get lots of segfaults as our code seems to be a bit too much still); doxygen feels clumsy and not-read-by-many to me. I will just start to explain some of the concepts here if it would help the rest of you and we'll see where this ends up, though.
  • I started to adapt @tobiasleibner advection-with-reconstruction operators, but did not try to compile them, all other operators should be working by now

The main changes are:

  • there is a cascade of three interfaces:
    • BilinearFormInterface, allowing for
      • apply2(v, u) and
      • norm(u), where u and v are grid functions;
    • ForwardOperatorInterface : public BilinearFormInterface, additionally allowing for
      • apply(u, range_vector), reading a grid function u and writing into a range_vector (plus convenience methods for DiscreteFunction and the like); and
    • OperatorInterface : public ForwardOperatorInterface, additionally allowing for
      • apply(source_vector, range_vector), reading a source_vector and writing into a range_vector,
        which then allows for
      • jacobian(...)
        and
      • apply_inverse(...)
        all based on vectors, plus the usual convenience methods.

(The main difference between ForwardOperatorInterface and OperatorInterface is, that the former acts on grid functions only and does not require a discrete source space, while the latter requires discrete source space but may additionally implement apply for grid functions.)

For each of the three interfaces there is exactly one default class

  • BilinearForm, allowing to += local element/intersection bilinear forms
  • ForwardOperator, allowing to += local element and intersection operators
  • Operator, allowing to += local element and intersection operators

which then implements the rest of the respective interface (including jacobian and apply_inverse using dampened newton).
Each of the three are appendable to the GridWalker, given all additional required information.

Each more general class allows to be converted to a more specific one given additional information, e.g.

  • the BilinearForm can be converted to a MatrixOperator given a matrix or a discrete source space and
  • the ForwardOperator can be converted to an Operator given a discrete source space.

Thus, there is now only one well-defined place where things happen. I.e., to assemble a bilinear form into a matrix, one cannot append the local bilinear forms to the MatrixOperator, but creates the BilinearForm first and then converts this to a MatrixOperator. In particular, one can still use the defined bilinear form as a product for arbitrary functions.

Some examples:

  • products can be easily built from bilinear forms as usual
  • all bilinear forms and forward operators can now be applied to everything a GridFunction can handle, see use of norms
  • an IPDG bilinear form assembled into a matrix (not the most elegant way of bilinear form to matrix conversion for efficient test code though)
  • arbitrary nonlinear operators can be built by appending local operators as usual

- Operator -> ForwardOperator
- DiscreteOperator -> Operator
@tobiasleibner
Copy link
Member

Looks sensible on a first (very short) look. Unfortunately, I won't have time to have a closer look at this in the next few weeks, but feel free to merge this already (with the hyperbolic tests disabled if necessary, I will then fix these in a follow-up PR).

Copy link
Member

@renefritze renefritze left a comment

Choose a reason for hiding this comment

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

Very happy with the overall new design.

General question: Can we make not be necessary to pass two seperate objects around controlling the logger state?

try {
tried_linear_solvers.push_back(linear_solver_type);
jacobian_solver.apply(
residual, update, {{"type", linear_solver_type}, {"precision", XT::Common::to_string(0.1 * precision)}});
Copy link
Member

Choose a reason for hiding this comment

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

0.1 is a magic number

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, do you not like magic numbers? :) As in: this should be configurable via the options?

Copy link
Member

Choose a reason for hiding this comment

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

I mean I like magic, just not in numbers maybe? 😉
If you didn't find it necessary to change this yet, maybe just make it a meaningfully named constant?

} catch (const XT::LA::Exceptions::linear_solver_failed&) {
}
}
DUNE_THROW_IF(!linear_solve_succeeded,
Copy link
Member

Choose a reason for hiding this comment

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

I would not expect that a newton algorithms internally tries a number of not-configurable linear solvers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Mhm, but this is here for a reason, Felix from a year ago could have told you! :)

For arbitrary nonlinear operators, it happens quite often that systems are barely solvable and that the choice of the linear solver might really make a difference. How should we proceed then?

We could add an option to the newton cfg about the linear solver, where something like "auto" is the current behavior. It's not that easy however, since we would also like to be able to pass the linear solver options as a sub cfg in the newton cfg. If the user selects a single linear solver this is easy.

However, a realistic scenario would be to specify my favorite direct solver and iterative solvers, say "superlu, amg", but how do I pass the respective opts as sub cfgs to the newton?

local_source_in_->bind(element);
local_range_in_->bind(element);
for (auto& data : element_data_) {
for (auto& data : bilinear_form_.element_data()) {
const auto& local_bilinear_form = *std::get<0>(data);
Copy link
Member

Choose a reason for hiding this comment

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

If you don't mind shifting the deref to the passing site, there's an alternate way to write this:

const auto&& [local_bilinear_form, filter] = data;

Copy link
Member Author

Choose a reason for hiding this comment

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

??? Do we now have tuple unpacking in C++? That would be amazing!

Copy link
Member

Choose a reason for hiding this comment

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

Well, yes and no. Look up structured bindings, got introduced with 17.

}


} // namespace GDT
} // namespace Dune

#include "matrix.hh"
Copy link
Member

Choose a reason for hiding this comment

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

If this is not here accidentally a comment explaining the reason would be good for the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree! Here on purpose. There is a comment at the top where the forward is, but another comment here will do.

return operator_.source_space();
this->assert_matching_range(range_vector);
VectorType u_update = range_vector.copy();
u_update.set_all(0.);
Copy link
Member

Choose a reason for hiding this comment

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

Could this be changed to init with 0 and correct size? Also in a few other places I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, agreed. We also have something like zeros_like I think.

DUNE_THROW_IF(!opts.has_key("type"), Exceptions::operator_error, "opts = " << opts);
const auto type = opts.get<std::string>("type");
if (type == "zero") {
LOG_(info) << "not adding zero jacobian ..." << std::endl;
Copy link
Member

Choose a reason for hiding this comment

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

Allow adding zero operators here, but throwing an error otherwise is intentional? If there's a structural reason for this, maybe add a note?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a default implementation in the interface for convenience. Ops with zero jacobian only need to implement available..., not jacobian(). So yes, some docs should be there.

1.);
return ret;
}
DUNE_THROW(Exceptions::operator_error, "This DiscreteOperator does not support jacobian(source_vector)!");
Copy link
Member

Choose a reason for hiding this comment

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

Unreachable code?

this->assert_matching_range(range_vector);
LOG_(info) << "applying " << this->num_ops() << " operators ..." << std::endl;
range_vector.set_all(0);
auto tmp_range_vector = range_vector;
Copy link
Member

Choose a reason for hiding this comment

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

see above

{
DUNE_THROW_IF(type != this->jacobian_options().at(0), Exceptions::operator_error, "type = " << type);
std::vector<XT::Common::Configuration> ret(1);
Copy link
Member

Choose a reason for hiding this comment

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

Could use plain Config here and return vector(cfg) instead.


/// \}

const std::list<std::pair<std::unique_ptr<LocalElementOperatorType>, std::unique_ptr<ElementFilterType>>>&
Copy link
Member

Choose a reason for hiding this comment

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

auto return?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we have a view on this (when to use auto)? The old 'auto is easier' vs. 'I can see what the type is' argument. Currently, I use auto when it gets too complicated, so this could apply here. Do we want to use it more often in general?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe use something like "more than one nesting level --> use auto" as a rule of thumb? If one can just use a type alias I still think it unnecessary/silly do use auto, yeah.

@ftalbrecht
Copy link
Member Author

General question: Can we make not be necessary to pass two seperate objects around controlling the logger state?

Yes please! Lets discuss this here.

@ftalbrecht
Copy link
Member Author

ftalbrecht commented Dec 11, 2020 via email

@renefritze
Copy link
Member

still awesome

I think the mail-in thingy misplaced you comment

@ftalbrecht
Copy link
Member Author

still awesome

I think the mail-in thingy misplaced you comment

True. Was referring to the tuple-unpack...

@ftalbrecht
Copy link
Member Author

I've just merged the required changes in dune-xt and updated the dune-gdt-super. So we can restart CI here once the new images are there and then start to tackle actual errors from this pr...

@tobiasleibner
Copy link
Member

I restarted the pipeline, the first error is that dune/gdt/operators/localizable-operator.hh is still included in several places though it seems to be gone/renamed.

@tobiasleibner
Copy link
Member

Moved to gitlab.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants