libmesh icon indicating copy to clipboard operation
libmesh copied to clipboard

How to write values on elements in parallel?

Open bzjan opened this issue 3 years ago • 5 comments

Instead of writing the value of a function on the nodes, I would like to write it on the (triangular) elements. The idea is to for each element write the desired value on its (3) constituent nodes and then later divide the value on the node by the node degree.

// write 1's to vector
for(const auto & elem : system.get_mesh().active_local_element_ptr_range()){
	system.solution->add(elem->node_ptr(0)->dof_number(system.number(),0,0),1);
	system.solution->add(elem->node_ptr(1)->dof_number(system.number(),0,0),1);
	system.solution->add(elem->node_ptr(2)->dof_number(system.number(),0,0),1);
}
system.solution->close();
// create node degrees = averaging weights
avg_weights.resize(mesh.n_nodes());
for(const auto & elem : mesh.active_local_element_ptr_range()){
	for(const auto & i : elem->node_index_range()) avg_weights[ elem->node_ptr(i)->id() ]++;
}
mesh.comm().barrier();
mesh.comm().sum(avg_weights);
// weight values
for(const auto & idx : libMesh::index_range(*system.solution)){
	libMesh::Real avg_weight = avg_weights[ idx/system.n_vars() ];
	if(avg_weight>0) system.solution->set(idx, system.solution->el(idx)/avg_weight);
}
system.solution->close();

This works very well in serial (function is = 1 everywhere): good

but fails horribly in parallel (2 processors): bad

The miscounts occur along the boundary of the 2 processor domains.

After comparing the weight values in serial and parallel mode, it turns out that they are identical, so it seems that the problem is in the solution vector. Does close() not add up the the components of the vector across processors?

What am I missing to get this to work when running in parallel?

bzjan avatar Mar 11 '21 08:03 bzjan

The solution vector is of type PARALLEL, and can only return components owned by the processor you're querying it from. Even current_local_solution is of type GHOSTED and will only return semilocal components (typically those that are either on local elements or their neighbors. Normally we'd scream and die if you tried to ask for anything else, though. I'm guessing you're running in opt mode and we end up returning 0 or returning uninitialized data in that case? The first answer to "What do I do if I see a bug in opt mode" is always "Run it in devel or dbg mode instead".

I'm not sure I understand the motivation behind what you're doing, so it feels like you're asking an XY Problem and I'm only answering Y, but your proximal bug might be fixed as simply as doing an update() to current_local_solution, reading from that, and iterating over local elements rather than over global solution indices.

roystgnr avatar Mar 11 '21 15:03 roystgnr

for(const auto & idx : libMesh::index_range(*system.solution)){
	libMesh::Real avg_weight = avg_weights[ idx/system.n_vars() ];
	if(avg_weight>0) system.solution->set(idx, system.solution->el(idx)/avg_weight);
}

Looping over libMesh::index_range(*system.solution) will only hit the local indices of the parallel vector, so that part looks OK to me... you won't access values that you don't have in the solution vector. I'm a bit concerned about the indexing step:

libMesh::Real avg_weight = avg_weights[ idx/system.n_vars() ];

It seems that you assume a certain ordering of the DOFs here which might not be correct.

jwpeterson avatar Mar 11 '21 15:03 jwpeterson

I forgot about that bit of cleverness in index_range; please ignore my suggested fix.

John's almost certainly right about the real problem; the way we order DoFs in parallel depends on partitioning and certainly is never guaranteed to match the way we order nodes.

roystgnr avatar Mar 11 '21 15:03 roystgnr

Thanks for clearing the index issue up. I assumed that local index and node index were the same thing.

To viusalize that, I plotted them for serial and parallel code: Screenshot from 2021-03-12 01-11-54
Screenshot from 2021-03-12 01-12-15

Using node indices explicitly seems to work well. I now have:

// create node degrees = averaging weights
for(const auto & elem : mesh.active_local_element_ptr_range()){
	for(const auto & i : elem->node_index_range()) system.solution->add(elem->node_ptr(i)->dof_number(system.number(),1,0),1.0);
}
// averaging
for(const auto & local_node_ptr : mesh.local_node_ptr_range()){
	libMesh::Real avg_weight = system.solution->el(local_node_ptr->dof_number(system.number(),1,0));
	if(avg_weight>0) system.solution->set(
	local_node_ptr->dof_number(system.number(),0,0), system.solution->el(local_node_ptr->dof_number(system.number(),0,0))/avg_weight);
}
system.solution->close();

This gives the right solution: parallel_good

Is there a way of reducing/simplifying this code further?

bzjan avatar Mar 12 '21 06:03 bzjan

Is there a way of reducing/simplifying this code further?

I guess it depends a bit on the format of your input. In your example, you used the constant "1" but in practice I guess there will be a CONSTANT, MONOMIAL field variable that you want to average in this way? In that case, we do a very similar averaging process in EquationSystems::build_solution_vector() and EquationSystems::build_parallel_solution_vector() (see src/systems/equation_systems.C) so you may want to check whether that code would just work for your case as-is.

jwpeterson avatar Mar 12 '21 15:03 jwpeterson