https://mooseframework.inl.gov
Loading...
Searching...
No Matches
XYCrossFieldSolver.C
Go to the documentation of this file.
1//* This file is part of the MOOSE framework
2//* https://mooseframework.inl.gov
3//*
4//* All rights reserved, see COPYRIGHT for full restrictions
5//* https://github.com/idaholab/moose/blob/master/COPYRIGHT
6//*
7//* Licensed under LGPL 2.1, please see LICENSE for details
8//* https://www.gnu.org/licenses/lgpl-2.1.html
9
10#include "XYCrossFieldSolver.h"
11
12#include "MooseError.h"
13
14#include "libmesh/dense_matrix.h"
15#include "libmesh/dense_vector.h"
16#include "libmesh/dof_map.h"
17#include "libmesh/elem.h"
18#include "libmesh/enum_elem_type.h"
19#include "libmesh/enum_fe_family.h"
20#include "libmesh/enum_order.h"
21#include "libmesh/enum_point_locator_type.h"
22#include "libmesh/enum_to_string.h"
23#include "libmesh/fe_base.h"
24#include "libmesh/fe_compute_data.h"
25#include "libmesh/fe_interface.h"
26#include "libmesh/fe_map.h"
27#include "libmesh/int_range.h"
28#include "libmesh/linear_implicit_system.h"
29#include "libmesh/linear_solver.h"
30#include "libmesh/node.h"
31#include "libmesh/numeric_vector.h"
32#include "libmesh/quadrature_gauss.h"
33#include "libmesh/sparse_matrix.h"
34#include "libmesh/utility.h"
35
36#include <algorithm>
37#include <cmath>
38
39using namespace libMesh;
40
41namespace
42{
45constexpr Real dirichlet_penalty = 1.0e10;
46
48constexpr Real linear_solver_tolerance = 1.0e-10;
49
51constexpr Real singular_magnitude = 0.1;
52
54constexpr Real minimum_magnitude = 1.0e-12;
55
57boundaryCrossField(const std::map<dof_id_type, Real> & boundary_tangent_angles)
58{
59 XYCrossFieldSolver::NodalCrossField boundary_cross_field;
60 for (const auto & [node_id, tangent_angle] : boundary_tangent_angles)
61 boundary_cross_field.emplace(node_id, std::polar(1.0, 4.0 * tangent_angle));
62 return boundary_cross_field;
63}
64}
65
67 const std::map<dof_id_type, Real> & boundary_tangent_angles)
68 : _communicator(MPI_COMM_SELF),
69 _mesh(_communicator),
70 _boundary_cross_field(boundaryCrossField(boundary_tangent_angles)),
71 _fe_type(FIRST, LAGRANGE),
72 _system(nullptr),
73 _real_variable(0),
74 _imaginary_variable(0),
75 _solved(false)
76{
77 // Solving across a distributed mesh is not supported here: only the local portion of one would
78 // be copied, which would silently solve on a fragment of the domain.
79 if (!background_mesh.is_replicated())
80 mooseError("XYCrossFieldSolver: The cross field background mesh must be replicated.");
81
82 // Node ids key the boundary data and every reported result, so they have to survive the copy onto
83 // the serial communicator.
85 _mesh.copy_nodes_and_elements(background_mesh);
87
88 for (const auto & elem : _mesh.element_ptr_range())
89 if (elem->type() != TRI3)
90 mooseError("XYCrossFieldSolver: The cross field background mesh must contain only TRI3 "
91 "elements, but it contains an element of type ",
92 Utility::enum_to_string(elem->type()),
93 ".");
94
95 for (const auto & [node_id, _] : _boundary_cross_field)
96 if (!_mesh.query_node_ptr(node_id))
97 mooseError("XYCrossFieldSolver: A boundary tangent angle was supplied for node ",
98 node_id,
99 ", which is not a node of the cross field background mesh.");
100
101 _equation_systems = std::make_unique<EquationSystems>(_mesh);
102 _system = &_equation_systems->add_system<LinearImplicitSystem>("cross_field");
106 _equation_systems->parameters.set<XYCrossFieldSolver *>("cross_field_solver") = this;
107 _equation_systems->init();
108
110 _point_locator->enable_out_of_mesh_mode();
111}
112
113void
115{
116 _equation_systems->parameters.set<Real>("linear solver tolerance") = linear_solver_tolerance;
117 _system->solve();
118
119 const auto reason = _system->get_linear_solver()->get_converged_reason();
120 if (reason < 0)
121 mooseError("XYCrossFieldSolver: The cross field solve failed to converge with reason: ",
123
125 _solved = true;
126}
127
128void
129XYCrossFieldSolver::assembleSystem(EquationSystems & es, const std::string & /*system_name*/)
130{
131 es.parameters.get<XYCrossFieldSolver *>("cross_field_solver")->assembleLaplace();
132}
133
134void
136{
137 const DofMap & dof_map = _system->get_dof_map();
138 std::unique_ptr<FEBase> fe(FEBase::build(_mesh.mesh_dimension(), _fe_type));
140 fe->attach_quadrature_rule(&qrule);
141
142 const std::vector<Real> & JxW = fe->get_JxW();
143 const std::vector<std::vector<RealGradient>> & dphi = fe->get_dphi();
144
146 DenseVector<Number> Fe_real;
147 DenseVector<Number> Fe_imaginary;
148 std::vector<dof_id_type> real_dof_indices;
149 std::vector<dof_id_type> imaginary_dof_indices;
150
151 auto & system_matrix = _system->get_system_matrix();
152
153 for (const auto & elem : _mesh.active_local_element_ptr_range())
154 {
155 fe->reinit(elem);
156 dof_map.dof_indices(elem, real_dof_indices, _real_variable);
157 dof_map.dof_indices(elem, imaginary_dof_indices, _imaginary_variable);
158
159 const auto n_dofs = real_dof_indices.size();
160 Ke.resize(n_dofs, n_dofs);
161 Fe_real.resize(n_dofs);
162 Fe_imaginary.resize(n_dofs);
163
164 for (const auto qp : make_range(qrule.n_points()))
165 for (const auto i : make_range(n_dofs))
166 for (const auto j : make_range(n_dofs))
167 Ke(i, j) += JxW[qp] * (dphi[i][qp] * dphi[j][qp]);
168
169 // The Dirichlet data is given per node, so the boundary condition is a penalty on the node's
170 // own equation rather than a side integral. A node shared by several elements accumulates the
171 // penalty once per element on both the diagonal and the right hand side, which leaves the value
172 // it pins unchanged.
173 mooseAssert(n_dofs == elem->n_nodes(),
174 "The nodal penalty indexes degrees of freedom by local node number, which linear "
175 "Lagrange on TRI3 must supply one of per element node.");
176 for (const auto i : make_range(n_dofs))
177 {
178 const auto boundary_value = _boundary_cross_field.find(elem->node_id(i));
179 if (boundary_value == _boundary_cross_field.end())
180 continue;
181
182 Ke(i, i) += dirichlet_penalty;
183 Fe_real(i) += dirichlet_penalty * boundary_value->second.real();
184 Fe_imaginary(i) += dirichlet_penalty * boundary_value->second.imag();
185 }
186
187 // Both components of z obey the same Laplace operator and couple only through the boundary
188 // data, so one element matrix serves both variables.
189 system_matrix.add_matrix(Ke, real_dof_indices);
190 system_matrix.add_matrix(Ke, imaginary_dof_indices);
191 _system->rhs->add_vector(Fe_real, real_dof_indices);
192 _system->rhs->add_vector(Fe_imaginary, imaginary_dof_indices);
193 }
194}
195
196void
198{
199 const auto system_number = _system->number();
200 const auto & solution = *_system->solution;
201
202 _nodal_cross_field.clear();
203 _singular_nodes.clear();
204
205 for (const auto & node : _mesh.node_ptr_range())
206 {
207 // The system lives on a serial communicator, so every degree of freedom is local here.
208 const std::complex<Real> z(solution(node->dof_number(system_number, _real_variable, 0)),
209 solution(node->dof_number(system_number, _imaginary_variable, 0)));
210 const Real magnitude = std::abs(z);
211
212 if (magnitude < singular_magnitude)
213 _singular_nodes.push_back(node->id());
214
215 // A vanishing z carries no direction at all, so fall back on the real axis instead of dividing
216 // by zero. Such a node is always singular by the test above.
217 const auto normalized =
218 magnitude > minimum_magnitude ? z / magnitude : std::complex<Real>(1.0, 0.0);
219 _nodal_cross_field.emplace(node->id(), normalized);
220 }
221
222 std::sort(_singular_nodes.begin(), _singular_nodes.end());
223}
224
225std::complex<Real>
227{
228 mooseAssert(_solved, "solve() must be called before the cross field can be queried.");
229
230 const Elem * elem = (*_point_locator)(point);
231 if (!elem)
232 mooseError("XYCrossFieldSolver: No element was found to contain point ", point);
233
234 const Point reference_point = FEMap::inverse_map(elem->dim(), elem, point);
235 FEComputeData fe_data(*_equation_systems, reference_point);
236 FEInterface::compute_data(elem->dim(), _fe_type, elem, fe_data);
237
238 mooseAssert(fe_data.shape.size() == elem->n_nodes(),
239 "Linear Lagrange on TRI3 must supply one shape function per element node.");
240
241 // Interpolating z rather than theta is what makes this well posed: theta is only defined modulo
242 // pi/2, so averaging nodal angles is wrong wherever the branch cut falls between the nodes.
243 std::complex<Real> interpolated(0.0, 0.0);
244 for (const auto i : index_range(fe_data.shape))
245 {
246 const Real shape_value = fe_data.shape[i];
247 interpolated += shape_value * libmesh_map_find(_nodal_cross_field, elem->node_id(i));
248 }
249
250 return interpolated;
251}
252
253Real
255{
256 return std::arg(interpolatedCrossField(point)) / 4.0;
257}
258
259std::pair<Point, Point>
261{
262 const Real angle = theta(point);
263 const Point u(std::cos(angle), std::sin(angle), 0.0);
264 return {u, Point(-u(1), u(0), 0.0)};
265}
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
Solves for a cross field, the smooth field of 4-fold symmetric directions that gives XYFrontalDelauna...
std::pair< Point, Point > crossFrame(const Point &point) const
static void assembleSystem(libMesh::EquationSystems &es, const std::string &system_name)
libMesh assembly callback; forwards to assembleLaplace() on the solver stashed in es.
std::map< dof_id_type, std::complex< Real > > NodalCrossField
Values of the complex cross field z at the nodes of the background mesh, keyed by node id.
const libMesh::FEType _fe_type
Linear Lagrange, the discretization of both components of z.
void solve()
Assemble and solve the two Laplace problems, then normalize the nodal field and collect the singular ...
unsigned int _real_variable
Variable number of the real part of z.
XYCrossFieldSolver(const libMesh::MeshBase &background_mesh, const std::map< dof_id_type, Real > &boundary_tangent_angles)
Build a cross field solver on a background triangulation.
std::unique_ptr< libMesh::PointLocatorBase > _point_locator
Locates the element containing a query point.
NodalCrossField _nodal_cross_field
Unit-magnitude nodal values of z, keyed by node id.
std::unique_ptr< libMesh::EquationSystems > _equation_systems
Holds the solve; kept past solve() because shape function evaluation needs it.
void assembleLaplace()
Assemble the Laplace operator for both variables and the nodal penalty Dirichlet conditions.
libMesh::ReplicatedMesh _mesh
Private copy of the background triangulation, sharing the caller's node ids.
const NodalCrossField _boundary_cross_field
Dirichlet value exp(4 i theta_t) of each boundary node, keyed by node id.
bool _solved
Whether solve() has produced the results the queries read.
void extractNodalCrossField()
Normalize the solved nodal values of z and record the nodes where z vanishes.
libMesh::LinearImplicitSystem * _system
System holding both components of z; owned by _equation_systems.
std::complex< Real > interpolatedCrossField(const Point &point) const
std::vector< dof_id_type > _singular_nodes
Ascending ids of the nodes where z vanishes.
unsigned int _imaginary_variable
Variable number of the imaginary part of z.
Real theta(const Point &point) const
void resize(const unsigned int new_m, const unsigned int new_n)
void resize(const unsigned int n)
void dof_indices(const Elem *const elem, std::vector< dof_id_type > &di) const
virtual unsigned int n_nodes() const=0
virtual unsigned short dim() const=0
dof_id_type node_id(const unsigned int i) const
NumericVector< Number > * rhs
std::vector< Number > shape
std::unique_ptr< FEGenericBase< Real > > build(const unsigned int dim, const FEType &fet)
static void compute_data(const unsigned int dim, const FEType &fe_t, const Elem *elem, FEComputeData &data)
static Point inverse_map(const unsigned int dim, const Elem *elem, const Point &p, const Real tolerance=TOLERANCE, const bool secure=true, const bool extra_checks=true)
Order default_quadrature_order() const
const SparseMatrix< Number > & get_system_matrix() const
virtual LinearSolver< Number > * get_linear_solver() const override
virtual void solve() override
virtual LinearConvergenceReason get_converged_reason() const=0
unsigned int mesh_dimension() const
virtual bool is_replicated() const
void allow_renumbering(bool allow)
void prepare_for_use(const bool skip_renumber_nodes_and_elements, const bool skip_find_neighbors)
virtual void add_vector(const T *v, const std::vector< numeric_index_type > &dof_indices)
const T & get(std::string_view) const
static std::unique_ptr< PointLocatorBase > build(PointLocatorType t, const MeshBase &mesh, const PointLocatorBase *master=nullptr)
unsigned int n_points() const
virtual const Node * query_node_ptr(const dof_id_type i) const override final
void attach_assemble_function(void fptr(EquationSystems &es, const std::string &name))
unsigned int add_variable(std::string_view var, const FEType &type, const std::set< subdomain_id_type > *const active_subdomains=nullptr)
std::unique_ptr< NumericVector< Number > > solution
const DofMap & get_dof_map() const
unsigned int number() const
virtual void copy_nodes_and_elements(const MeshBase &other_mesh, const bool skip_find_neighbors=false, dof_id_type element_id_offset=0, dof_id_type node_id_offset=0, unique_id_type unique_id_offset=0, std::unordered_map< subdomain_id_type, subdomain_id_type > *id_remapping=nullptr, const bool skip_preparation=false)
std::string enum_to_string(const T e)
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
auto index_range(const T &sizable)
TREE_LOCAL_ELEMENTS
DIE A HORRIBLE DEATH HERE typedef LIBMESH_DEFAULT_SCALAR_TYPE Real
IntRange< T > make_range(T beg, T end)