Assembly
As explained in Section Complementary rotation the assembly procedure of the residual vector and of the Jacobian matrix must be tweaked in order to check whether an element has some but not all the unknown rotation vectors changed into their complementary vector.
To this end, one need to write a customized assembly class in C++, compile it, and make it visible to the python interface of Dolfin.
An excellent starting point is an existing assembly class. Thus, one can start copying
the files dolfin/fem/Assembler.h
and dolfin/fem/Assembler.cpp
into an unique file, say RotationAssembly.cpp
, and save it into the directory where this project lives.
Then, replace all the occurence of Assembler
class name with
RotationAssembler
to avoid any name clash.
The standard assembler class defines an assemble
method as
void assemble(GenericTensor& A,
const Form& a);
where A
is where Form a
should be assembled into.
The method assemble
performs some initialization and checks, and then calls
void assemble_cells(GenericTensor& A, const Form& a, UFC& ufc,
std::shared_ptr<const MeshFunction<std::size_t>> domains,
std::vector<double>* values);
void assemble_exterior_facets(GenericTensor& A, const Form& a,
UFC& ufc,
std::shared_ptr<const MeshFunction<std::size_t>> domains,
std::vector<double>* values);
void assemble_interior_facets(GenericTensor& A, const Form& a,
UFC& ufc,
std::shared_ptr<const MeshFunction<std::size_t>> domains,
std::shared_ptr<const MeshFunction<std::size_t>> cell_domains,
std::vector<double>* values);
and
void assemble_vertices(GenericTensor& A, const Form& a, UFC& ufc,
std::shared_ptr<const MeshFunction<std::size_t>> domains);
that loops over all the cells
, exterior_facets
, interior_facets
and vertices
of the mesh and ssemble into the GenricTensor A
the relative contributions
coming from Form a
.
The aim is to leave as much as possible unchanged. Looking into e.g.
assemble_cells
one easily identifies a loop over all the cells of the mesh
for (CellIterator cell(mesh); !cell.end(); ++cell)
{
....
}
Inside this loop the assembly is performed by collecting all the degree of freedom values
used by a given cell into a double**
stored inside the helper class variable UFC ufc
with
ufc.update(*cell, coordinate_dofs, ufc_cell,
integral->enabled_coefficients());
and then compute the contribution of the cell with this call
integral->tabulate_tensor(ufc.A.data(), ufc.w(),
coordinate_dofs.data(),
ufc_cell.orientation);
Here method ufc.w()
passes the double**
to tabulate_tensor
.
The result of the integral is stored into ufc.A.data()
, regardless of the rank of the form
that is being assembled. If the rank is 0, one would get only a scalar, for a rank one form
one would get a (dense) vector, and for a rank two form ufc.A.data()
the resulting vector would store
a dense matrix with row-major ordering.
At last, the result of the integral is assembled into the global tensor A
with
if (is_cell_functional)
(*values)[cell->index()] = ufc.A[0];
else
A.add_local(ufc.A.data(), dofs);
where dofs
stores the dofmap between the current cell DOFs and the global numbering
of the GenericTensor A
.
The assembly procedure needs to be modified between these three calls:
immediately after
ufc.update
it is possible to check whether not all of the cell rotation vectors has been transformed into its complement, and eventually modify in place the right rotation vectors ofufc.w()
by changing them again into their complement so that the rotation vector field can be interpolated.immediately after
integral->tabulate_tensor
is it instead possible to modify in place the values ofufc.A.data()
, as explained in Section Complementary rotation:if the form rank is equal to zero, do nothing;
if the form rank is equal to one then the equations that has as test function corresponding to the rotation vectors modified in the previous step should be multiplied on the left by \(\left(\partial\boldsymbol{\varphi}_C/\partial\boldsymbol{\varphi}\right)^T\);
if the form rank is equal to two then
the matrix rows that has a test function that corresponds to the rotation vectors modified in the previous step should be multiplied on the left by \(\left(\partial\boldsymbol{\varphi}_C/\partial\boldsymbol{\varphi}\right)^T\);
the matrix columns that has a trial function that corresponds to the rotation vectors modified in the previous step should be multiplied on the right by \(\left(\partial\boldsymbol{\varphi}_C/\partial\boldsymbol{\varphi}\right)\);
finally, one should add the term
(1)\[\frac{2\pi}{\varphi^3}\left[ \left( \boldsymbol{f}_i\otimes\boldsymbol{\varphi} + \boldsymbol{\varphi}\otimes\boldsymbol{f}_i \right) + (\boldsymbol{\varphi}\boldsymbol{f}_i) \left( \boldsymbol{I} - 3\ \frac{\boldsymbol{\varphi}}{\varphi}\otimes\frac{\boldsymbol{\varphi}}{\varphi} \right) \right]\]
To do so, one needs to know where the components of the rotation coefficients are stored inside
ufc.w()
, and what are the corresponding positions, if any, inside the test and trial functions.
The first step is to add a reference to the (sub)Function that approximates the rotation field
as a third argument of assemble
:
void assemble(GenericTensor& A,
const Form& a,
const Function& rotation_function);
Then, in the private section of the class, add the following declarations
bool walk_coefficients(std::size_t& n,
std::shared_ptr<const dolfin::FunctionSpace> func_space,
std::shared_ptr<const dolfin::FunctionSpace> rotation_space);
std::size_t value_size(std::shared_ptr<const dolfin::FunctionSpace> func_space);
struct rotation_coefficient_struct
{
std::size_t coefficient_dimension = 0;
std::size_t coef_idx = 0;
std::size_t rotation_value_size = 0;
std::size_t rotation_n_nodes = 0;
std::size_t start_rotation_index;
};
rotation_coefficient_struct rot_coefs;
rotation_coefficient_struct rot_args[2];
void find_coef_rotation_indices(const std::vector<std::shared_ptr<const GenericFunction>>& coefficients,
const Function& rotation_function);
void find_rotation_indices(rotation_coefficient_struct & rot,
const std::size_t i,
const std::shared_ptr<const dolfin::FunctionSpace> func_space,
const Function& rotation_function);
The structure rotation_coefficient_struct
is designed to sore the sought informations into
rot_coefs
for the coefficients of the form, rot_args[0]
for the test functions and rot_args[1]
for the trial functions:
rotation_coefficient_struct::coefficient_dimension
stores thespace_dimension
(the total number of DOfs) of the correspondingFiniteElement
;rotation_coefficient_struct::coef_idx
stores inside which sub element of the form’s function space therotation_function
is held;rotation_coefficient_struct::rotation_value_size
is thevalue_size
of the rotation function space (it should be equal to three)rotation_coefficient_struct::rotation_n_nodes
is the number ofnodes
of the finite element; it is equal torotation_coefficient_struct::coefficient_dimension
/rotation_coefficient_struct::rotation_value_size
;rotation_coefficient_struct::start_rotation_index
is the starting index of the rotation coefficients inside the local vector.
The local vector will store, starting from index start_rotation_index
,
rotation_n_nodes
values that corresponds to the first component of the rotation vector, then
rotation_n_nodes
values for the second components, and at last rotation_n_nodes
values for the third component.
These helper functions below can populate variables rot_coefs
and rot_args
by walking the form coefficients and arguments
void RotationAssembler::find_coef_rotation_indices(
const std::vector<std::shared_ptr<const GenericFunction>>& coefficients,
const Function& rotation_function)
{
for (std::size_t i = 0; i < coefficients.size(); i++)
{
const Function*const f = dynamic_cast<const Function*const>(coefficients[i].get());
if (f != nullptr) {
const std::shared_ptr<const dolfin::FunctionSpace> func_space = f->function_space();
find_rotation_indices(rot_coefs, i, func_space, rotation_function);
}
}
}
void RotationAssembler::find_rotation_indices(
rotation_coefficient_struct & rot,
const std::size_t i,
const std::shared_ptr<const FunctionSpace> func_space,
const Function& rotation_function)
{
rot.start_rotation_index = 0;
if (walk_coefficients(rot.start_rotation_index, func_space, rotation_function.function_space()))
{
rot.coefficient_dimension = func_space->element()->space_dimension();
rot.coef_idx = i;
rot.rotation_value_size = value_size(rotation_function.function_space());
rot.rotation_n_nodes = rotation_function.function_space()->element()->space_dimension() /
rot.rotation_value_size;
}
}
std::size_t RotationAssembler::value_size(std::shared_ptr<const dolfin::FunctionSpace> func_space)
{
std::size_t value_size = 1;
for (std::size_t d = 0; d < func_space->element()->value_rank(); d++)
value_size *= func_space->element()->value_dimension(d);
return value_size;
};
bool RotationAssembler::walk_coefficients(std::size_t& n,
std::shared_ptr<const dolfin::FunctionSpace> func_space,
std::shared_ptr<const dolfin::FunctionSpace> rotation_space)
{
bool found = false;
for (std::size_t i = 0; i < func_space->element()->num_sub_elements(); i++) {
if (func_space->sub(i) == rotation_space) {
if (value_size(func_space->sub(i)) != 3)
dolfin_error("RotationAssembler::walk_coefficients",
"correctly identify the rotation (sub)space",
"The size of rotation field is != 3: coef.sub(s_space).value_size() = %d != 3", value_size(func_space->sub(i)));
found = true;
break;
}
else if (func_space->sub(i)->element()->num_sub_elements() > 0)
{
found = walk_coefficients(n, func_space->sub(i), rotation_space);
}
else
{
n += func_space->sub(i)->dofmap()->max_element_dofs();
}
}
return found;
};
The variables can be populated inside assemble
by
// Find rotation (sub)space indices
find_coef_rotation_indices(coefficients, rotation_function);
for (std::size_t i = 0; i < ufc.form.rank(); ++i)
find_rotation_indices(rot_args[i], i, a.function_space(i), rotation_function);
At this point assemble_cell
can be modified. Let see first how can this be accomplished for forms up to rank 1.
First, immediately before tabulate_tensor
save the values of the rotation vectors inside Eigen::Vector3d save_phi[rot_coefs.rotation_n_nodes]
and check wheter some vector should be changed back to its complement. This is done
by checking whether the scalar product between the first node rotation vector and the subsequent ones is negative or not; note that this test somewhat limits the relative rotation allowed within a single element. Whether a rotation is changed into its complement or not is stored in
side the switched_node
vector, while switched_cell
is true
only if at least one rotation was transformed
// If the cell has switched rotation dofs switch then ...
bool switched_cell = false;
double * wr = ufc.w()[rot_coefs.coef_idx] + rot_coefs.start_rotation_index;
Eigen::Vector3d save_phi[rot_coefs.rotation_n_nodes];
bool switched_node[rot_coefs.rotation_n_nodes];
switched_node[0] = false;
constexpr double half_pi = std::acos(-1.) / 2.;
constexpr double pi2 = 2. * std::acos(-1.);
if (rot_coefs.rotation_value_size)
{
save_phi[0](0) = wr[0];
save_phi[0](1) = wr[0 + rot_coefs.rotation_n_nodes];
save_phi[0](2) = wr[0 + 2 * rot_coefs.rotation_n_nodes];
if (std::sqrt(save_phi[0].dot(save_phi[0])) > half_pi)
{
for (std::size_t n = 1; n < rot_coefs.rotation_n_nodes; n++)
{
Eigen::Vector3d& phi = *(save_phi + n);
phi(0) = wr[n];
phi(1) = wr[n + rot_coefs.rotation_n_nodes];
phi(2) = wr[n + 2 * rot_coefs.rotation_n_nodes];
switched_node[n] = (save_phi[0].dot(phi) < 0);
if (switched_node[n])
{
switched_cell = true;
double normphi = std::sqrt(phi.dot(phi));
double c = 1. - pi2 / normphi;
wr[n] = phi(0) * c;
wr[n + rot_coefs.rotation_n_nodes] = phi(1) * c;
wr[n + 2 * rot_coefs.rotation_n_nodes] = phi(2) * c;
}
}
}
}
It is now possible to call:
// Tabulate cell tensor
integral->tabulate_tensor(ufc.A.data(), ufc.w(),
coordinate_dofs.data(),
ufc_cell.orientation);
and transform the result
// Transform the tabulated values
if (switched_cell) {
for (std::size_t n = 0; n < rot_coefs.rotation_n_nodes; n++)
{
if (switched_node[n])
{
Eigen::Vector3d& phi = *(save_phi + n);
if (form_rank == 1 && rot_args[0].rotation_value_size)
{
const double normphi2 = phi.dot(phi);
const double normphi = std::sqrt(normphi2);
const double c1 = 1. - pi2 / normphi;
double * r = ufc.A.data() + rot_args[0].start_rotation_index + n;
const double c2 = (r[0] * phi(0) +
r[0 + rot_coefs.rotation_n_nodes] * phi(1) +
r[0 + 2 * rot_coefs.rotation_n_nodes] * phi(2)
) * pi2 / normphi2 / normphi;
r[0] = r[0] * c1 + phi(0) * c2;
r[0 + rot_coefs.rotation_n_nodes] = r[0 + rot_coefs.rotation_n_nodes] * c1 + phi(1) * c2;
r[0 + 2 * rot_coefs.rotation_n_nodes] = r[0 + 2 * rot_coefs.rotation_n_nodes] * c1 + phi(2) * c2;
}
}
}
}
before assembling it
// Add entries to global tensor. Either store values cell-by-cell
// (currently only available for functionals)
if (is_cell_functional)
(*values)[cell->index()] = ufc.A[0];
else
A.add_local(ufc.A.data(), dofs);
In order to use this new assembly class from within Python copy the file assembling.py
into the new file
rotation_assembling.py
.
In order to compile and link the new class add this code
from dolfin import compile_extension_module
from dolfin.fem.assembling import _create_dolfin_form, _create_tensor
pwd = os.path.dirname(os.path.abspath(__file__))
with open(pwd + "/RotationAssembler.cpp", "r") as f:
rotation_assembler_code = f.read()
rotation_assembler = compile_extension_module(rotation_assembler_code)
__all__ = ["r_assemble"]
Then add the new required argument rotation_function
by changing
def assemble(form,
tensor=None,
form_compiler_parameters=None,
add_values=False,
finalize_tensor=True,
keep_diagonal=False,
backend=None):
into
def r_assemble(form,
rotation_function,
tensor=None,
form_compiler_parameters=None,
add_values=False,
finalize_tensor=True,
keep_diagonal=False,
backend=None):
Then, istead of calling the standard Assembler
with
# Call C++ assemble function
assembler = cpp.Assembler()
assembler.add_values = add_values
assembler.finalize_tensor = finalize_tensor
assembler.keep_diagonal = keep_diagonal
assembler.assemble(tensor, dolfin_form)
one can call the new code with
# Call C++ assemble function
assembler = rotation_assembler.RotationAssembler()
assembler.add_values = add_values
assembler.finalize_tensor = finalize_tensor
assembler.keep_diagonal = keep_diagonal
assembler.assemble(tensor, dolfin_form, rotation_function)
# Convert to float for scalars
if dolfin_form.rank() == 0:
tensor = tensor.get_scalar_value()
# Return value
return tensor
Note however that, in order to assemble a rank two form one should assemble also the corresponding rank one. This can be accomplished by defining a new C++
RotationAssembleSystem
class that takes both forms as arguments
class RotationAssemblerSystem {
public:
void assemble(GenericTensor& A,
const Form& a,
GenericTensor& L,
const Form& b,
const Function& rotation_function);
/// Assemble tensor from given form over cells. This function is
/// provided for users who wish to build a customized assembler.
void assemble_cells(GenericTensor& A, const Form& a,
GenericTensor& L, const Form& b,
UFC& ufca,
UFC& ufcb,
std::shared_ptr<const MeshFunction<std::size_t>> domains,
std::vector<double>* values);
and so on.
The assemble_cell
code now computes both forms
// Tabulate cell tensor
integrala->tabulate_tensor(ufca.A.data(), ufca.w(),
coordinate_dofs.data(),
ufc_cell.orientation);
integralb->tabulate_tensor(ufcb.A.data(), ufcb.w(),
coordinate_dofs.data(),
ufc_cell.orientation);
storing into ufca
the local contribution from the rank two form a
, into ufcb
that from the rank 1 form b
.
If the cell needs a transoformation
// Transform the tabulated values
if (switched_cell) {
loop over all the nodes and, if a node was switched
for (std::size_t n = 0; n < rot_coefs.rotation_n_nodes; n++)
{
if (switched_node[n])
{
Eigen::Vector3d& phi = *(save_phi + n);
const double normphi2 = phi.dot(phi);
const double normphi = std::sqrt(normphi2);
const double pi2_normphi3 = pi2 / normphi2 / normphi;
const double c1 = 1. - pi2 / normphi;
If the trial function needs the transformation multiply the rank 2 tensor on the right by \(\left(\partial\boldsymbol{\varphi}_C/\partial\boldsymbol{\varphi}\right)\). Remember that the matrix is stored with a Row-Major ordering
// Rank 2 on the right (trial function)
if (rot_args[1].rotation_value_size)
{
//Row Major
for (std::size_t row = 0; row < dofs[0].size(); row++)
{
double * r = ufca.A.data() + row * dofs[1].size() + rot_args[1].start_rotation_index + n;
const double c2 = (r[0] * phi(0) +
r[0 + rot_coefs.rotation_n_nodes] * phi(1) +
r[0 + 2 * rot_coefs.rotation_n_nodes] * phi(2)
) * pi2_normphi3;
r[0] = r[0] * c1 + phi(0) * c2;
r[0 + rot_coefs.rotation_n_nodes] = r[0 + rot_coefs.rotation_n_nodes] * c1 + phi(1) * c2;
r[0 + 2 * rot_coefs.rotation_n_nodes] = r[0 + 2 * rot_coefs.rotation_n_nodes] * c1 + phi(2) * c2;
}
}
If the test function needs the transformation multiply the rank 2 tensor on the left by \(\left(\partial\boldsymbol{\varphi}_C/\partial\boldsymbol{\varphi}\right)^T\)
if (rot_args[0].rotation_value_size)
{
// Rank 2 on the left (test function)
double * rb = ufcb.A.data() + rot_args[0].start_rotation_index + n;
Eigen::Vector3d res(rb[0], rb[0 + rot_coefs.rotation_n_nodes], rb[0 + 2 * rot_coefs.rotation_n_nodes]);
for (std::size_t col = 0; col < dofs[1].size(); col++)
{
double * r = ufca.A.data() + col + dofs[1].size() * (rot_args[0].start_rotation_index + n);
double c2 = (r[0] * phi(0) +
r[0 + dofs[1].size() * rot_coefs.rotation_n_nodes] * phi(1) +
r[0 + 2 * dofs[1].size() * rot_coefs.rotation_n_nodes] * phi(2)
) * pi2_normphi3;
r[0] = r[0] * c1 + phi(0) * c2;
r[0 + dofs[1].size() * rot_coefs.rotation_n_nodes] =
r[0 + dofs[1].size() * rot_coefs.rotation_n_nodes] * c1 + phi(1) * c2;
r[0 + 2 * dofs[1].size() * rot_coefs.rotation_n_nodes] =
r[0 + 2 * dofs[1].size() * rot_coefs.rotation_n_nodes] * c1 + phi(2) * c2;
}
Still inside the test function block, having at hand the residual it is possible to compute the second order tensor (1) into
the Eigen::Matrix3d K
variable, and add it to the tensor at the right locations
Eigen::Vector3d n_phi = phi / normphi;
Eigen::Matrix3d K = (res * n_phi.transpose() + n_phi * res.transpose() +
n_phi.dot(res) * (Eigen::Matrix3d().setIdentity() - n_phi * n_phi.transpose() * 3.)
) * (pi2 / normphi2);
double * r = ufca.A.data() + (rot_args[1].start_rotation_index + n) * dofs[1].size() + rot_args[1].start_rotation_index + n;
for (std::size_t ir = 0; ir < 3; ir++)
for (std::size_t ic = 0; ic < 3; ic++)
r[0 + dofs[1].size() * (ir * rot_coefs.rotation_n_nodes) + ic * rot_coefs.rotation_n_nodes] -= K(ir, ic);
Finally, tranform the rank one form
// Rank 1 on the left (test function)
const double c2 = res.dot(phi) * pi2 / normphi2 / normphi;
rb[0] = res(0) * c1 + phi(0) * c2;
rb[0 + rot_coefs.rotation_n_nodes] = res(1) * c1 + phi(1) * c2;
rb[0 + 2 * rot_coefs.rotation_n_nodes] = res(2) * c1 + phi(2) * c2;
}
and assemble the result into the GenericTensor``s ``A
and L
// Add entries to global tensor.
A.add_local(ufca.A.data(), dofs);
L.add_local(ufcb.A.data(), dofs);
The new class can be called from Python by extending again rotation_assembly.py
from dolfin import compile_extension_module
from dolfin.fem.assembling import _create_dolfin_form, _create_tensor
pwd = os.path.dirname(os.path.abspath(__file__))
with open(pwd + "/RotationAssembler.cpp", "r") as f:
rotation_assembler_code = f.read()
rotation_assembler = compile_extension_module(rotation_assembler_code)
pwd = os.path.dirname(os.path.abspath(__file__))
with open(pwd + "/RotationAssemblerSystem.cpp", "r") as f:
rotation_assembler_system_code = f.read()
rotation_assembler_system = compile_extension_module(rotation_assembler_system_code)
__all__ = ["r_assemble", "r_assemble_system"]
and adding a function
def r_assemble_system(forma, formb,
rotation_function,
tensora=None,
tensorb=None,
form_compiler_parameters=None,
add_values=False,
finalize_tensor=True,
keep_diagonal=False,
backend=None):
that calls rotation_assembler_system
# Create dolfin Form object referencing all data needed by assembler
dolfin_forma = _create_dolfin_form(forma, form_compiler_parameters)
dolfin_formb = _create_dolfin_form(formb, form_compiler_parameters)
# Create tensor
comma = dolfin_forma.mesh().mpi_comm()
commb = dolfin_formb.mesh().mpi_comm()
tensora = _create_tensor(comma, forma, dolfin_forma.rank(), backend, tensora)
tensorb = _create_tensor(commb, formb, dolfin_formb.rank(), backend, tensorb)
# Call C++ assemble function
assembler = rotation_assembler_system.RotationAssemblerSystem()
assembler.add_values = add_values
assembler.finalize_tensor = finalize_tensor
assembler.keep_diagonal = keep_diagonal
assembler.assemble(tensora, dolfin_forma, tensorb, dolfin_formb, rotation_function)
# Return value
return (tensora, tensorb)
Methods assemble_exterior_facets
, assemble_interior_facets
and assemble_vertices
need to be modified too. Right now we skip them because either they are not used in this example or they do
evaluate the rotation function only at a node.