libMesh
variational_smoother_system.C
Go to the documentation of this file.
1 // The libMesh Finite Element Library.
2 // Copyright (C) 2002-2025 Benjamin S. Kirk, John W. Peterson, Roy H. Stogner
3 
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 // Lesser General Public License for more details.
13 
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 
18 #include "libmesh/variational_smoother_system.h"
19 
20 #include "libmesh/elem.h"
21 #include "libmesh/fe_base.h"
22 #include "libmesh/fe_interface.h"
23 #include "libmesh/fem_context.h"
24 #include "libmesh/mesh.h"
25 #include "libmesh/quadrature.h"
26 #include "libmesh/string_to_enum.h"
27 #include "libmesh/utility.h"
28 #include "libmesh/numeric_vector.h"
29 #include "libmesh/parallel_ghost_sync.h"
30 
31 // C++ includes
32 #include <functional> // std::reference_wrapper
33 
34 namespace libMesh
35 {
36 
38 
39 void VariationalSmootherSystem::assembly (bool get_residual,
40  bool get_jacobian,
41  bool apply_heterogeneous_constraints,
42  bool apply_no_constraints)
43 {
44  // Update the mesh based on the current variable values
45  auto & mesh = this->get_mesh();
46  this->solution->close();
47 
48  for (auto * node : mesh.local_node_ptr_range())
49  {
50  for (const auto d : make_range(mesh.mesh_dimension()))
51  {
52  const auto dof_id = node->dof_number(this->number(), d, 0);
53  // Update mesh
54  (*node)(d) = libmesh_real((*current_local_solution)(dof_id));
55  }
56  }
57 
58  SyncNodalPositions sync_object(mesh);
59  Parallel::sync_dofobject_data_by_id (mesh.comm(), mesh.nodes_begin(), mesh.nodes_end(), sync_object);
60 
61  FEMSystem::assembly(get_residual, get_jacobian, apply_heterogeneous_constraints, apply_no_constraints);
62 }
63 
65 {
66  auto & mesh = this->get_mesh();
67  const auto & elem_orders = mesh.elem_default_orders();
68  libmesh_error_msg_if(elem_orders.size() != 1,
69  "The variational smoother cannot be used for mixed-order meshes!");
70  const auto fe_order = *elem_orders.begin();
71  // Add a variable for each dimension of the mesh
72  // "r0" for x, "r1" for y, "r2" for z
73  for (const auto & d : make_range(mesh.mesh_dimension()))
74  this->add_variable ("r" + std::to_string(d), fe_order);
75 
76  // Do the parent's initialization after variables are defined
78 
79  // Set the current_local_solution to the current mesh for the initial guess
80  this->solution->close();
81 
82  for (auto * node : mesh.local_node_ptr_range())
83  {
84  for (const auto d : make_range(mesh.mesh_dimension()))
85  {
86  const auto dof_id = node->dof_number(this->number(), d, 0);
87  // Update solution
88  (*solution).set(dof_id, (*node)(d));
89  }
90  }
91 
93 }
94 
96 {
97  std::unique_ptr<DiffContext> con = this->build_context();
98  FEMContext & femcontext = cast_ref<FEMContext &>(*con);
99  this->init_context(femcontext);
100 
101  const auto & mesh = this->get_mesh();
102 
103  Real elem_averaged_det_J_sum = 0.;
104 
105  // Make pre-requests before reinit() for efficiency in
106  // --enable-deprecated builds, and to avoid errors in
107  // --disable-deprecated builds.
108  const auto & fe_map = femcontext.get_element_fe(0)->get_fe_map();
109  const auto & JxW = fe_map.get_JxW();
110 
111  for (const auto * elem : mesh.active_local_element_ptr_range())
112  {
113  femcontext.pre_fe_reinit(*this, elem);
114  femcontext.elem_fe_reinit();
115 
116  const auto elem_integrated_det_J = std::accumulate(JxW.begin(), JxW.end(), 0.);
117  const auto ref_elem_vol = elem->reference_elem()->volume();
118  elem_averaged_det_J_sum += elem_integrated_det_J / ref_elem_vol;
119  }
120 
121  // Get contributions from elements on other processors
122  mesh.comm().sum(elem_averaged_det_J_sum);
123 
124  _ref_vol = elem_averaged_det_J_sum / mesh.n_active_elem();
125 }
126 
127 
128 
130 {
131  FEMContext & c = cast_ref<FEMContext &>(context);
132 
133  FEBase * my_fe = nullptr;
134 
135  // Now make sure we have requested all the data
136  // we need to build the system.
137 
138  // We might have a multi-dimensional mesh
139  const std::set<unsigned char> & elem_dims =
140  c.elem_dimensions();
141 
142  for (const auto & dim : elem_dims)
143  {
144  c.get_element_fe( 0, my_fe, dim );
145  my_fe->get_nothing();
146 
147  auto & fe_map = my_fe->get_fe_map();
148  fe_map.get_dxyzdxi();
149  fe_map.get_dxyzdeta();
150  fe_map.get_dxyzdzeta();
151  fe_map.get_JxW();
152 
153  c.get_side_fe( 0, my_fe, dim );
154  my_fe->get_nothing();
155  }
156 
157  FEMSystem::init_context(context);
158 }
159 
160 
162  DiffContext & context)
163 {
164  FEMContext & c = cast_ref<FEMContext &>(context);
165 
166  const Elem & elem = c.get_elem();
167 
168  // First we get some references to cell-specific data that
169  // will be used to assemble the linear system.
170 
171  const auto & fe_map = c.get_element_fe(0)->get_fe_map();
172 
173  const auto & dxyzdxi = fe_map.get_dxyzdxi();
174  const auto & dxyzdeta = fe_map.get_dxyzdeta();
175  const auto & dxyzdzeta = fe_map.get_dxyzdzeta();
176 
177  const auto & dphidxi_map = fe_map.get_dphidxi_map();
178  const auto & dphideta_map = fe_map.get_dphideta_map();
179  const auto & dphidzeta_map = fe_map.get_dphidzeta_map();
180 
181  unsigned int dim = c.get_dim();
182 
183  unsigned int x_var = 0, y_var = 1, z_var = 2;
184  // In the case of lower dimensions, we will not access z (1D/2D) or y (1D)
185  if (dim < 3)
186  {
187  z_var = 0;
188  if (dim < 2)
189  y_var = 0;
190  }
191 
192  // The subvectors and submatrices we need to fill:
193  // system residual
194  std::reference_wrapper<DenseSubVector<Number>> F[3] =
195  {
196  c.get_elem_residual(x_var),
197  c.get_elem_residual(y_var),
198  c.get_elem_residual(z_var)
199  };
200  // system jacobian
201  std::reference_wrapper<DenseSubMatrix<Number>> K[3][3] =
202  {
203  {c.get_elem_jacobian(x_var, x_var), c.get_elem_jacobian(x_var, y_var), c.get_elem_jacobian(x_var, z_var)},
204  {c.get_elem_jacobian(y_var, x_var), c.get_elem_jacobian(y_var, y_var), c.get_elem_jacobian(y_var, z_var)},
205  {c.get_elem_jacobian(z_var, x_var), c.get_elem_jacobian(z_var, y_var), c.get_elem_jacobian(z_var, z_var)}
206  };
207 
208  // Quadrature info
209  const auto quad_weights = c.get_element_qrule().get_weights();
210 
211  // Integrate the distortion-dilation metric over the reference element
212  for (const auto qp : index_range(quad_weights))
213  {
214  // Compute quantities needed to evaluate the distortion-dilation metric
215  // and its gradient and Hessian.
216  // The metric will be minimized when it's gradient with respect to the node
217  // locations (R) is zero. For Newton's method, minimizing the gradient
218  // requires computation of the gradient's Jacobian with respect to R.
219  // The Jacobian of the distortion-dilation metric's gradientis the Hessian
220  // of the metric.
221  //
222  // Note that the term "Jacobian" has two meanings in this mesh smoothing
223  // application. The first meaning refers to the Jacobian w.r.t R of the
224  // gradient w.r.t. R, (i.e., the Hessian of the metric). This is the K
225  // variable defined above. This is also the Jacobian the 'request_jacobian'
226  // variable refers to. The second mesning refers to the Jacobian of the
227  // physical-to-reference element mapping. This is the Jacobian used to
228  // compute the distorion-dilation metric.
229  //
230  // Grab the physical-to-reference mapping Jacobian matrix (i.e., "S") at this qp
231  RealTensor S;
232  // RealTensors are always 3x3, so we will fill any dimensions above dim
233  // with 1s on the diagonal. This indicates a 1 to 1 relationship between
234  // the physical and reference elements in these extra dimensions.
235  switch (dim)
236  {
237  case 1:
238  S = RealTensor(
239  dxyzdxi[qp](0), 0, 0,
240  0, 1, 0,
241  0, 0, 1
242  );
243  break;
244 
245  case 2:
246  S = RealTensor(
247  dxyzdxi[qp](0), dxyzdeta[qp](0), 0,
248  dxyzdxi[qp](1), dxyzdeta[qp](1), 0,
249  0, 0, 1
250  );
251  break;
252 
253  case 3:
254  S = RealTensor(
255  dxyzdxi[qp],
256  dxyzdeta[qp],
257  dxyzdzeta[qp]
258  ).transpose(); // Note the transposition!
259  break;
260 
261  default:
262  libmesh_error_msg("Unsupported dimension.");
263  }
264 
265  // Compute quantities needed for the smoothing algorithm
266 
267  // determinant
268  const Real det = S.det();
269  const Real det_sq = det * det;
270  const Real det_cube = det_sq * det;
271 
272  const Real ref_vol_sq = _ref_vol * _ref_vol;
273 
274  // trace of S^T * S
275  // DO NOT USE RealTensor.tr for the trace, it will NOT be correct for
276  // 1D and 2D meshes because of our hack of putting 1s in the diagonal of
277  // S for the extra dimensions
278  const RealTensor ST_S = S.transpose() * S;
279  Real tr = 0.0;
280  for (const auto i : make_range(dim))
281  tr += ST_S(i, i);
282  const Real tr_div_dim = tr / dim;
283 
284  // Precompute pow(tr_div_dim, 0.5 * dim - x) for x = 0, 1, 2
285  const Real half_dim = 0.5 * dim;
286  const std::vector<Real> trace_powers{
287  std::pow(tr_div_dim, half_dim),
288  std::pow(tr_div_dim, half_dim - 1.),
289  std::pow(tr_div_dim, half_dim - 2.),
290  };
291 
292  // inverse of S
293  const RealTensor S_inv = S.inverse();
294  // inverse transpose of S
295  const RealTensor S_inv_T = S_inv.transpose();
296 
297  // Identity matrix
298  const RealTensor I(1, 0, 0,
299  0, 1, 0,
300  0, 0, 1);
301 
302  // The chi function allows us to handle degenerate elements
303  // When the element is not degenerate (i.e., det(S) > 0), we can set
304  // _epsilon_squared to zero to get chi(det(S)) = det(S)
305  const Real epsilon_sq = det > 0. ? 0. : _epsilon_squared;
306  const Real chi = 0.5 * (det + std::sqrt(epsilon_sq + det_sq));
307  const Real chi_sq = chi * chi;
308  const Real sqrt_term = std::sqrt(epsilon_sq + det_sq);
309  // dchi(x) / dx
310  const Real chi_prime = 0.5 * (1. + det / sqrt_term);
311  const Real chi_prime_sq = chi_prime * chi_prime;
312  // d2chi(x) / dx2
313  const Real chi_2prime = 0.5 * (1. / sqrt_term - det_sq / std::pow(sqrt_term, 3));
314 
315  // Distortion metric (beta)
316  //const Real beta = trace_powers[0] / chi;
317  const RealTensor dbeta_dS = (trace_powers[1] / chi) * S - (trace_powers[0] / chi_sq * chi_prime * det) * S_inv_T;
318 
319  // Dilation metric (mu)
320  //const Real mu = 0.5 * (_ref_vol + det_sq / _ref_vol) / chi;
321  // We represent d mu / dS as alpha(S) * S^-T, where alpha is a scalar function
322  const Real alpha = (-chi_prime * det_cube + 2. * det_sq * chi - ref_vol_sq * det * chi_prime) / (2. * _ref_vol * chi_sq);
323  const RealTensor dmu_dS = alpha * S_inv_T;
324 
325  // Combined metric (E)
326  //const Real E = (1. - _dilation_weight) * beta + _dilation_weight * mu;
327  const RealTensor dE_dS = (1. - _dilation_weight) * dbeta_dS + _dilation_weight * dmu_dS;
328 
329  // This vector is useful in computing dS/dR below
330  std::vector<std::vector<std::vector<Real>>> dphi_maps = {dphidxi_map, dphideta_map, dphidzeta_map};
331 
332  // Compute residual (i.e., the gradient of the combined metric w.r.t node locations)
333  // Recall that when the gradient (residual) is zero, the combined metric is minimized
334  for (const auto l : elem.node_index_range())
335  {
336  for (const auto var_id : make_range(dim))
337  {
338  // Build dS/dR, the derivative of the physical-to-reference mapping Jacobin w.r.t. the mesh node locations
339  RealTensor dS_dR = RealTensor(0);
340  for (const auto jj : make_range(dim))
341  dS_dR(var_id, jj) = dphi_maps[jj][l][qp];
342 
343  // Residual contribution. The contraction of dE/dS and dS/dR gives us
344  // the gradient we are looking for, dE/dR
345  F[var_id](l) += quad_weights[qp] * dE_dS.contract(dS_dR);
346  }// for var_id
347  }// for l
348 
349 
350  if (request_jacobian)
351  {
352  // Compute jacobian of the smoothing system (i.e., the Hessian of the
353  // combined metric w.r.t. the mesh node locations)
354 
355  // Precompute coefficients to be applied to each component of tensor
356  // products in the loops below. At first glance, these coefficients look
357  // like gibberish, but everything in the Hessian has been verified by
358  // taking finite differences of the gradient. We should probably write
359  // down the derivations of the gradient and Hessian somewhere...
360 
361  // Recall that above, dbeta_dS takes the form:
362  // d(beta)/dS = c1(S) * S - c2(S) * S_inv_T,
363  // where c1 and c2 are scalar-valued functions.
364  const std::vector<Real> d2beta_dS2_coefs = {
365  //Part 1: scaler coefficients of d(c1 * S) / dS
366  //
367  // multiplies I[i,a] x I[j,b]
368  trace_powers[1] / chi,
369  // multiplies S[a,b] x S[i,j]
370  ((dim - 2.) / dim) * trace_powers[2] / chi,
371  // multiplies S_inv[b,a] * S[i,j]
372  -(trace_powers[1] / chi_sq) * chi_prime * det,
373  //
374  //Part 2: scaler coefficients of d(-c2 * S_inv_T) / dS
375  //
376  // multiplies S[a,b] x S_inv[j,i]
377  -(trace_powers[1] / chi_sq) * chi_prime * det,
378  // multiplies S_inv[b,a] x S_inv[j,i]
379  trace_powers[0] * (det / chi_sq)
380  * ((2. * chi_prime_sq / chi - chi_2prime) * det - chi_prime),
381  // multiplies S_inv[b,i] x S_inv[j,a]
382  (trace_powers[0] / chi_sq) * chi_prime * det,
383  };
384 
385 
386  // d alpha / dS has the form c(S) * S^-T, where c is the scalar coefficient defined below
387  const Real dalpha_dS_coef = (det / (2. * _ref_vol * chi_sq))
388  * (-4. * _ref_vol * alpha * chi * chi_prime - chi_2prime * det_cube
389  - chi_prime * det_sq + 4 * chi * det - ref_vol_sq * (chi_prime + det * chi_2prime));
390 
391  for (const auto l: elem.node_index_range()) // Contribution to Hessian from node l
392  {
393  for (const auto var_id1 : make_range(dim)) // Contribution from each x/y/z component of node l
394  {
395  // Build dS/dR_l, the derivative of the physical-to-reference mapping Jacobin w.r.t.
396  // the l-th node
397  RealTensor dS_dR_l = RealTensor(0);
398  for (const auto ii : make_range(dim))
399  dS_dR_l(var_id1, ii) = dphi_maps[ii][l][qp];
400 
401  for (const auto p: elem.node_index_range()) // Contribution to Hessian from node p
402  {
403  for (const auto var_id2 : make_range(dim)) // Contribution from each x/y/z component of node p
404  {
405  // Build dS/dR_l, the derivative of the physical-to-reference mapping Jacobin w.r.t.
406  // the p-th node
407  RealTensor dS_dR_p = RealTensor(0);
408  for (const auto jj : make_range(dim))
409  dS_dR_p(var_id2, jj) = dphi_maps[jj][p][qp];
410 
411  Real d2beta_dR2 = 0.;
412  Real d2mu_dR2 = 0.;
413  // Perform tensor contraction
414  for (const auto i : make_range(dim))
415  {
416  for (const auto j : make_range(dim))
417  {
418  for (const auto a : make_range(dim))
419  {
420  for (const auto b : make_range(dim))
421  {
422  // Nasty tensor products to be multiplied by d2beta_dS2_coefs to get d2(beta) / dS2
423  const std::vector<Real> d2beta_dS2_tensor_contributions =
424  {
425  I(i,a) * I(j,b),
426  S(a,b) * S(i,j),
427  S_inv(b,a) * S(i,j),
428  S(a,b) * S_inv(j,i),
429  S_inv(b,a) * S_inv(j,i),
430  S_inv(j,a) * S_inv(b,i),
431  };
432 
433  // Combine precomputed coefficients with tensor products to get d2(beta) / dS2
434  Real d2beta_dS2 = 0.;
435  for (const auto comp_id : index_range(d2beta_dS2_coefs))
436  {
437  const Real contribution = d2beta_dS2_coefs[comp_id] * d2beta_dS2_tensor_contributions[comp_id];
438  d2beta_dS2 += contribution;
439  }
440 
441  // Incorporate tensor product portion to get d2(mu) / dS2
442  const Real d2mu_dS2 = dalpha_dS_coef * S_inv(b,a) * S_inv(j,i) - alpha * S_inv(b,i) * S_inv(j,a);
443 
444  // Chain rule to change d/dS to d/dR
445  d2beta_dR2 += d2beta_dS2 * dS_dR_l(a, b) * dS_dR_p(i, j);
446  d2mu_dR2 += d2mu_dS2 * dS_dR_l(a, b) * dS_dR_p(i, j);
447 
448  }// for b
449  }// for a
450  }// for j
451  }// for i, end tensor contraction
452 
453  // Jacobian contribution
454  K[var_id1][var_id2](l, p) += quad_weights[qp] * ((1. - _dilation_weight) * d2beta_dR2 + _dilation_weight * d2mu_dR2);
455 
456  }// for var_id2
457  }// for p
458  }// for var_id1
459  }// for l
460  }
461  } // end of the quadrature point qp-loop
462 
463  return request_jacobian;
464 }
465 
466 } // namespace libMesh
T libmesh_real(T a)
virtual std::unique_ptr< DiffContext > build_context() override
Builds a FEMContext object with enough information to do evaluations on each element.
Definition: fem_system.C:1346
virtual void init_data() override
Initializes the member data fields associated with the system, so that, e.g., assemble() may be used...
const DenseMatrix< Number > & get_elem_jacobian() const
Const accessor for element Jacobian.
Definition: diff_context.h:274
This class provides all data required for a physics package (e.g.
Definition: diff_context.h:55
void get_side_fe(unsigned int var, FEGenericBase< OutputShape > *&fe) const
Accessor for edge/face (2D/3D) finite element object for variable var for the largest dimension in th...
Definition: fem_context.h:317
virtual void pre_fe_reinit(const System &, const Elem *e)
Reinitializes local data vectors/matrices on the current geometric element.
Definition: fem_context.C:1683
const Elem & get_elem() const
Accessor for current Elem object.
Definition: fem_context.h:908
unsigned int dim
virtual void init_context(DiffContext &) override
Definition: fem_system.C:1370
TypeTensor< T > transpose() const
Definition: type_tensor.h:1050
This is the base class from which all geometric element types are derived.
Definition: elem.h:94
virtual void assembly(bool get_residual, bool get_jacobian, bool apply_heterogeneous_constraints=false, bool apply_no_constraints=false) override
Assembly method to update the mesh based on the smoother solve.
MeshBase & mesh
const std::vector< Real > & get_weights() const
Definition: quadrature.h:168
RealTensorValue RealTensor
The libMesh namespace provides an interface to certain functionality in the library.
const MeshBase & get_mesh() const
Definition: system.h:2358
Real _dilation_weight
The relative weight to give the dilation metric. The distortion metric is given weight 1 - _dilation_...
unsigned char get_dim() const
Accessor for cached mesh dimension.
Definition: fem_context.h:936
virtual void elem_fe_reinit(const std::vector< Point > *const pts=nullptr)
Reinitializes interior FE objects on the current geometric element.
Definition: fem_context.C:1477
unsigned int number() const
Definition: system.h:2350
T pow(const T &x)
Definition: utility.h:328
std::unique_ptr< NumericVector< Number > > solution
Data structure to hold solution values.
Definition: system.h:1593
virtual void init_data() override
Initializes the member data fields associated with the system, so that, e.g., assemble() may be used...
Definition: fem_system.C:873
This class provides all data required for a physics package (e.g.
Definition: fem_context.h:62
unsigned int add_variable(std::string_view var, const FEType &type, const std::set< subdomain_id_type > *const active_subdomains=nullptr)
Adds the variable var to the list of variables for this system.
Definition: system.C:1357
virtual bool element_time_derivative(bool request_jacobian, libMesh::DiffContext &context) override
Adds the time derivative contribution on elem to elem_residual.
void sync_dofobject_data_by_id(const Communicator &comm, const Iterator &range_begin, const Iterator &range_end, SyncFunctor &sync)
Request data about a range of ghost dofobjects uniquely identified by their id.
CompareTypes< T, T2 >::supertype contract(const TypeTensor< T2 > &) const
Multiply 2 tensors together to return a scalar, i.e.
Definition: type_tensor.h:1268
Real _ref_vol
The reference volume for each element.
const DenseVector< Number > & get_elem_residual() const
Const accessor for element residual.
Definition: diff_context.h:242
const Real _epsilon_squared
The small nonzero constant to prevent zero denominators (degenerate elements only) ...
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
TypeTensor< T > inverse() const
Definition: type_tensor.h:1080
IntRange< T > make_range(T beg, T end)
The 2-parameter make_range() helper function returns an IntRange<T> when both input parameters are of...
Definition: int_range.h:140
std::unique_ptr< NumericVector< Number > > current_local_solution
All the values I need to compute my contribution to the simulation at hand.
Definition: system.h:1605
virtual void init_context(libMesh::DiffContext &context) override
void get_element_fe(unsigned int var, FEGenericBase< OutputShape > *&fe) const
Accessor for interior finite element object for variable var for the largest dimension in the mesh...
Definition: fem_context.h:277
virtual void assembly(bool get_residual, bool get_jacobian, bool apply_heterogeneous_constraints=false, bool apply_no_constraints=false) override
Prepares matrix or rhs for matrix assembly.
Definition: fem_system.C:880
auto index_range(const T &sizable)
Helper function that returns an IntRange<std::size_t> representing all the indices of the passed-in v...
Definition: int_range.h:117
This class forms the foundation from which generic finite elements may be derived.
const QBase & get_element_qrule() const
Accessor for element interior quadrature rule for the dimension of the current _elem.
Definition: fem_context.h:802
const std::set< unsigned char > & elem_dimensions() const
Definition: fem_context.h:951
This class defines a tensor in LIBMESH_DIM dimensional Real or Complex space.