New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FEValuesViews::RenumberedView #15819
Conversation
/rebuild |
5364cc6
to
2ce44e5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few comments, but I have to go now.
* according to the given renumbering vectors. | ||
*/ | ||
template <typename ViewType> | ||
class ReorderedView : public Subscriptor |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the other views are not derived from Subscriptor
, but are very lightweight classes. Any reason to go a different route here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no. Sorry. Just a habit. I'll remove the subscriptor derivation.
* object, where the degrees of freedom and quadrature points are renumbered | ||
* according to the given renumbering vectors. | ||
*/ | ||
template <typename ViewType> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is only a single tutorial program (step-81) that ever names these FEValuesViews
objects. (And I'm going to write a patch eventually to change that.) Rather, we only ever advertise using the FEValuesExtractors
explicitly, and you happen to get a view when you use fe_values[...]
.
Have you considered whether one could use the extractor type as a template argument instead, to stick with things that typically appear in user programs? You could then value a
using VewType = decltype(declval<FEValues<...>>()[declval<ExtractorType>()]);
to obtain the corresponding view type.
I would prefer something like this, because the extractor types are so much more widely used. You'd have to make dim,spacedim
template arguments as well, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's exactly the plan. The FEValuesCoupling object will take extractors and return a ReorderedView
, pretty much like FEValues
does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. This isn't a class one is supposed to create objects of by hand. Then yes, this makes sense.
/** | ||
* Helper function that constructs a unique name for a container, based on a | ||
* string prefix, on its size, and on the type stored in the container. | ||
*/ | ||
template <typename Number> | ||
std::string | ||
get_unique_container_name(const std::string &prefix, | ||
const unsigned int size, | ||
const Number & exemplar_number) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The presence of this function surprises me, reading just through the class declaration (not having gotten to the implementation). Can you add to the documentation what the function is used for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've actually moved this in the RenumberingData
struct, which is now external, and shared by all Views with the same renumbering indices.
/** | ||
* Store a reference to the underlying view. | ||
*/ | ||
const ViewType &view; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just make a copy. We don't generally create views as objects with storage, but only as temporaries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, on second thought, we generally take them from a cache. So perhaps this design is right. Maybe say that FEValues
keeps a cache of these objects and so we can safely keep a reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copying them is also forbidden.
This class may be used in the following way:
the call to |
6494df2
to
16d6a4a
Compare
@bangerth I'll update the other PR in a minute, so that you can have an idea of how this is used, but basically this is how it will look like:
In the example above, |
Any opinions on naming? |
c4104c2
to
7a86faf
Compare
* the corresponding shape function. | ||
* | ||
* An example of the renumbering ordering is the following. Assume that you | ||
* have an FE_Q(1) finite element spac in dimension two (i.e., four degrees |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...in space dimension two...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finite element space in dimension two.
* have an FE_Q(1) finite element spac in dimension two (i.e., four degrees | |
* have an FE_Q(1) finite element space in dimension two (i.e., four degrees |
* object should look like this: | ||
* @code | ||
* ... | ||
* RenumberingData data({{0, 1, numbers::invalid_unsigned_int, 2, 3}}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But could it also be {0,1,2,3}?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. In this case it would simply be the identity renumbering.
More later -- off to my plane now. Then a week of vacation in Greece ;-) |
Enjoy! |
Anybody willing to revive the discussion here? |
1949dce
to
70cb5f8
Compare
70cb5f8
to
eb4bd21
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. I don't understand yet in which context this should be used but I guess I will see that once reviewing #15773.
Any other opinions? Let's wait at most another 2 days so that @luca-heltai can continue with the other patches. We can still make modifications on master 👍 |
The other patches are in the second PR, where I show how to use this for coupled bulk surface problems, BEM, and fractional Laplacian. |
Would you mind rebasing this to see whether the tests still all succeed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through this one more time and did not find much with the exception that the class is not thread-safe. That's a shame because we like to do assembly in parallel. I don't think it should be too difficult to address.
RenumberingData( | ||
const unsigned int n_inner_dofs, | ||
const unsigned int n_inner_quadrature_points, | ||
const std::vector<unsigned int> &dof_renumbering = {}, | ||
const std::vector<unsigned int> &quadrature_renumbering = {}) | ||
: n_inner_dofs(n_inner_dofs) | ||
, n_dofs(dof_renumbering.empty() ? n_inner_dofs : dof_renumbering.size()) | ||
, n_inner_quadrature_points(n_inner_quadrature_points) | ||
, n_quadrature_points(quadrature_renumbering.empty() ? | ||
n_inner_quadrature_points : | ||
quadrature_renumbering.size()) | ||
, dof_renumbering(dof_renumbering) | ||
, quadrature_renumbering(quadrature_renumbering) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks to me like the usual approach would be to either provide the first two arguments, or to provide the two vectors and make sure that the first two arguments are used consistently. Have you considered splitting this constructor into two constructors instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, these two are somwhat independent: I could have a renumbering vector with smaller size w.r.t. inner dofs, i.e., extract only vertex dofs of a Q2, or I could integrate on a subface using an iterated quadrature as inner quadrature. I have no way to infer the first two arguments from the second two arguments.
{ | ||
// Check that the renumbering vectors are valid. | ||
#ifdef DEBUG | ||
// While for dofs we admit invalid values, this is not the case for | ||
// quadrature points. | ||
for (const auto i : dof_renumbering) | ||
Assert(i < n_inner_dofs || i == numbers::invalid_unsigned_int, | ||
ExcIndexRange(i, 0, n_inner_dofs)); | ||
|
||
for (const auto q : quadrature_renumbering) | ||
AssertIndexRange(q, n_inner_quadrature_points) | ||
#endif | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're implementing all of the other functions out-of-line. Can you do the same with this function?
template <typename Number> | ||
inline std::string | ||
RenumberingData::get_unique_container_name( | ||
const std::string &prefix, | ||
const unsigned int size, | ||
const Number &exemplar_number) const | ||
{ | ||
return prefix + "_" + Utilities::int_to_string(size) + "_" + | ||
Utilities::type_to_string(exemplar_number); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is part of the public
interface of the class. Looking through where it is used, it seems like it's only actually used in RenumberedView
. What do you think about making the function private
and making RenumberedView
a friend? This seems to be an implementation detail that does not need to be part of the public interface of the structure.
const auto inner_shape_function = data.dof_renumbering.empty() ? | ||
shape_function : | ||
data.dof_renumbering[shape_function]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a shame we're still stuck with C++17. This would be so much nicer to write by storing the vector as a std::optional
that either holds the vector or nothing, and then using the monadic operations...
const auto name = data.get_unique_container_name( | ||
"RenumberedView::outer_to_inner_values", | ||
data.n_inner_quadrature_points, | ||
outer_values[0]); | ||
auto &inner_values = | ||
data.data_storage.get() | ||
.template get_or_add_object_with_name<std::vector<ValueType>>( | ||
name, data.n_inner_quadrature_points); | ||
return inner_values; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pertains to the comment above regarding the mutex: We generally consider views themselves as stateless; the state is in the object being viewed. As a consequence using views is thread-safe is the underlying container is accessed in a thread-safe manner (for example because you're only ever reading).
However, here you have a state that you store in the data
object. You need to guard all accesses to the data.data_storage
access with a mutex and because data
is a reference to another object, that mutex must be part of the data
object, not the view object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separately, I don't yet understand what you use this function for, but don't you have to make sure that inner_values
actually has some content before you return it? What get_or_add_object_with_name()
returns here is a vector with data.n_inner_quadrature_points
elements, but no content. Is that what you want/need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The content is updated later. This method is used to get a reference to a vector of values (either the same as the input vector, if they have the same dimension and no renumbering must occur), which is then filled, i.e.,
auto &inner_values = outer_to_inner_values(values);
view.get_function_values(fe_function, inner_values);
inner_to_outer_values(inner_values, values);
inner_values
is filled by get_function_values
, and then reordered by inner_to_outer_values
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understood that later: You're just using this as a temp vector, but don't want to allocate/deallocate memory every time, right?
auto &inner_dofs = data.data_storage.get() | ||
.template get_or_add_object_with_name<VectorType>( | ||
name, data.n_inner_dofs); | ||
for (unsigned int i = 0; i < data.n_dofs; ++i) | ||
{ | ||
const auto inner_i = data.dof_renumbering[i]; | ||
if (inner_i != numbers::invalid_unsigned_int) | ||
{ | ||
AssertIndexRange(inner_i, data.n_inner_dofs); | ||
inner_dofs[inner_i] = outer_dofs[i]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, if you get to this function a second time, don't you want to skip the initialization of the inner_dofs
array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. Calling this function is done to translate outer degrees of freedom into inner degrees of freedom before calling get_function_values_from_dof_values(..., inner_dofs)
, so it needs the outer dofs passed as argument, i.e., this how we use this:
const auto &inner_dof_values = outer_to_inner_dofs(dof_values);
auto &inner_gradients = outer_to_inner_values(gradients);
view.get_function_gradients_from_local_dof_values(inner_dof_values,
inner_gradients);
inner_to_outer_values(inner_gradients, gradients);
it uses the renumbering to provide the get_function_*_from_local_dof_values
with sensible input data.
In other words, outer_to_inner_dofs
fills the output vector, while outer_to_inner_values
only provides a reference to a vector of the right size (which is filled later by get_function_*_from_local_dof_values
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I had missed that you're already doing the copying from outer_dofs
here. These will be different every time, so the code block doesn't just initialize the object, but actually do the work on it.
auto &inner_values = outer_to_inner_values(values); | ||
view.get_function_values(fe_function, inner_values); | ||
inner_to_outer_values(inner_values, values); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This too is not thread-safe. You're using a temporary vector. That vector needs to be guarded (perhaps by a different mutex than the data.data_storage
object, or perhaps not, but it needs to be guarded).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue in the functions below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole point of using a Threads::ThreadLocalStorage<GeneralDataStorage>
was to make this thread safe. The outer_to_inner_values
and inner_to_outer_values
are, in fact, both thread safe (each thread only access its own ThreadLocalStorage
object). The vector is cached (it is created if it does not exists, and stored in the (local to thread) GeneralDataStorage
object for future use). Why should this be not thread safe?
eb4bd21
to
dba6907
Compare
ebaf11e
to
8c2a897
Compare
I'm not sure I understand why this should not be thread safe. The only object that could be non-thread safe is the Can you elaborate on why this could be non-thread safe? |
8c2a897
to
d557770
Compare
On my way to work today I was thinking about this patch again and realized that I had seen the use of But this doesn't work in general. It assumes that the entire workflow using the thread local object happens on a single thread (clearly the case) but also that it is not interrupted by another task, using the same object, running on the same thread. This is why we have a list of these objects in I don't know whether that can happen in your case. You need to convince yourself that between getting the object out of the thread-local cache and using it, there can not be a place where you call into the task scheduler. The case that made us find the problem was a call to |
Well, I don't think it can. We could be on the safe side, by making the |
Let me describe the problem differently: It is not enough to make these objects thread-local. They need to be task-local. That's because you can have multiple tasks running on the same thread at the same time. The typical case looks like this:
Here, the call to
The approach used in
|
Options, in order to get this merged in the not so far future:
|
The way the class is used now is the following. Inside each task a new (and independent from other tasks) CouplingFEValues<dim> cfv(fe_values1, fe_values2, quad_coupling_type, dof_coupling_type); This constructor is the one that builds the reordering vectors, and figures out how to match dofs and q points. This is the place where the cache is created and stored. Unfortunately we don't know here what type of number we'll need in the cache, and I don't really know how to get rid of it, since the temporary vectors are needed inside functions that are templated on the number type. I think that the major point is that the The rationale of the class is that inside each loop, I do not want to initialize new vectors, i.e., once I have the extractors // Extractor on left cell
const auto v1 = cfv.left(FEValuesExtractor::Scalar(0));
const auto p1 = cfv.left(FEValuesExtractor::Scalar(1));
// Extractor on right cell
const auto u2 = cfv.right(FEValuesExtractor::Scalar(0));
const auto q2 = cfv.right(FEValuesExtractor::Scalar(1)); I want to be able to use the same syntax we are used to, i.e., I want to get a new view that is cheap to construct on the fly. We cannot do what we currently do with When I use the views, I want the temporary vectors to be built only the first time, and reused all other times: for (const unsigned int q : cfv.quadrature_point_indices()) {
const auto &[x_q,y_q] = cfv.quadrature_point(q);
for (const unsigned int i : cfv.coupling_dof_indices())
{
const auto &v_i = cfv[v1].value(i, q); // <- only on the first call this will be initialized. It is local to each task
const auto &p_i = cfv[p1].value(i, q); // <- and this is a different cache as well
for (const unsigned int j : cfv.coupling_dof_indices())
{
const auto &u_j = cfv[u2].value(j, q);
const auto &q_j = cfv[q2].value(j, q);
local_matrix(i, j) += (K1(x_q, y_q) * v_i * u_j +
K2(x_q, y_q) * p_i * q_j) *
cfv[0].JxW(q) *
cfv[1].JxW(q);
}
}
} If you think it would be safer, I can create a mutex for each vector size and number type that has been created, and make sure access is only made through a mutex. I did not want to do this, since these maybe accessed inside triple loops (as above). If you have suggestions on how to achieve the design above, I'm open to change what is required. My argument for the thread safety of these classes is that |
OK, thanks for taking the time to explain this. Let's merge this -- we can think about this some more later on. It is true that if you create the |
Part of #15773.