PolyFEM
Loading...
Searching...
No Matches
VarFormDiff.cpp
Go to the documentation of this file.
2
4
8
11
12#include <polysolve/linear/FEMSolver.hpp>
13
18// Below types in SolverData are forward declared, include them explicitly.
24
26
27#include <ipc/ipc.hpp>
28#include <ipc/potentials/friction_potential.hpp>
29
30#include <Eigen/Dense>
31
32#include <algorithm>
33#include <vector>
34#include <cassert>
35#include <vector>
36
37using namespace polyfem::basis;
38
39namespace polyfem
40{
41 namespace
42 {
43 void replace_rows_by_identity(StiffnessMatrix &reduced_mat, const StiffnessMatrix &mat, const std::vector<int> &rows)
44 {
45 reduced_mat.resize(mat.rows(), mat.cols());
46
47 std::vector<bool> mask(mat.rows(), false);
48 for (int i : rows)
49 mask[i] = true;
50
51 std::vector<Eigen::Triplet<double>> coeffs;
52 for (int k = 0; k < mat.outerSize(); ++k)
53 {
54 for (StiffnessMatrix::InnerIterator it(mat, k); it; ++it)
55 {
56 if (mask[it.row()])
57 {
58 if (it.row() == it.col())
59 coeffs.emplace_back(it.row(), it.col(), 1.0);
60 }
61 else
62 coeffs.emplace_back(it.row(), it.col(), it.value());
63 }
64 }
65 reduced_mat.setFromTriplets(coeffs.begin(), coeffs.end());
66 }
67
68 void compute_force_jacobian_prev(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, const int force_step, const int sol_step, StiffnessMatrix &hessian_prev)
69 {
70 assert(force_step > 0);
71 assert(force_step > sol_step);
72
73 if (varform.primary_assembler().is_linear() && !varform.is_contact_enabled())
74 {
75 hessian_prev = StiffnessMatrix(varform.primary_space().ndof(), varform.primary_space().ndof());
76 }
77 else
78 {
79 const Eigen::MatrixXd u = diff_cache.u(force_step);
80 const Eigen::MatrixXd u_prev = diff_cache.u(sol_step);
81 const double beta = time_integrator::BDF::betas(diff_cache.bdf_order(force_step) - 1);
82 const double dt = varform.solve_data()->time_integrator->dt();
83
84 hessian_prev = StiffnessMatrix(u.size(), u.size());
85 if (varform.get_problem().is_time_dependent())
86 {
87 if (varform.solve_data()->friction_form)
88 {
89 if (sol_step == force_step - 1)
90 {
91 Eigen::MatrixXd surface_solution_prev = varform.collision_mesh().vertices(utils::unflatten(u_prev, varform.get_mesh().dimension()));
92 Eigen::MatrixXd surface_solution = varform.collision_mesh().vertices(utils::unflatten(u, varform.get_mesh().dimension()));
93
94 // TODO: use the time integration to compute the velocity
95 const Eigen::MatrixXd surface_velocities = (surface_solution - surface_solution_prev) / dt;
96 const double dv_dut = -1 / dt;
97
98 if (const auto barrier_contact = dynamic_cast<const solver::BarrierContactForm *>(varform.solve_data()->contact_form.get()))
99 {
100 ipc::BarrierPotential bp = barrier_contact->barrier_potential();
101 bp.set_stiffness(barrier_contact->barrier_stiffness());
102 hessian_prev =
103 varform.solve_data()->friction_form->friction_potential().force_jacobian(
104 diff_cache.friction_collision_set(force_step),
105 varform.collision_mesh(),
106 varform.collision_mesh().rest_positions(),
107 /*lagged_displacements=*/surface_solution_prev,
108 surface_velocities,
109 bp,
110 ipc::FrictionPotential::DiffWRT::LAGGED_DISPLACEMENTS)
111 + varform.solve_data()->friction_form->friction_potential().force_jacobian(
112 diff_cache.friction_collision_set(force_step),
113 varform.collision_mesh(),
114 varform.collision_mesh().rest_positions(),
115 /*lagged_displacements=*/surface_solution_prev,
116 surface_velocities,
117 bp,
118 ipc::FrictionPotential::DiffWRT::VELOCITIES)
119 * dv_dut;
120 }
121
122 hessian_prev *= -1;
123
124 // {
125 // Eigen::MatrixXd X = collision_mesh.rest_positions();
126 // Eigen::VectorXd x = utils::flatten(surface_solution_prev);
127 // const double barrier_stiffness = solve_data.contact_form->barrier_stiffness();
128 // const double dhat = solve_data.contact_form->dhat();
129 // const double mu = solve_data.friction_form->mu();
130 // const double epsv = solve_data.friction_form->epsv();
131
132 // Eigen::MatrixXd fgrad;
133 // fd::finite_jacobian(
134 // x, [&](const Eigen::VectorXd &y) -> Eigen::VectorXd
135 // {
136 // Eigen::MatrixXd fd_Ut = utils::unflatten(y, surface_solution_prev.cols());
137
138 // ipc::TangentialCollisions fd_friction_constraints;
139 // ipc::NormalCollisions fd_constraints;
140 // fd_constraints.set_use_convergent_formulation(solve_data.contact_form->use_convergent_formulation());
141 // fd_constraints.set_enable_shape_derivatives(true);
142 // fd_constraints.build(collision_mesh, X + fd_Ut, dhat);
143
144 // fd_friction_constraints.build(
145 // collision_mesh, X + fd_Ut, fd_constraints, dhat, barrier_stiffness,
146 // mu);
147
148 // return fd_friction_constraints.compute_potential_gradient(collision_mesh, (surface_solution - fd_Ut) / dt, epsv);
149
150 // }, fgrad, fd::AccuracyOrder::SECOND, 1e-8);
151
152 // logger().trace("force Ut derivative error {} {}", (fgrad - hessian_prev).norm(), hessian_prev.norm());
153 // }
154
155 hessian_prev = varform.collision_mesh().to_full_dof(hessian_prev); // / (beta * dt) / (beta * dt);
156 }
157 else
158 {
159 // const double alpha = time_integrator::BDF::alphas(std::min(diff_cached.bdf_order(force_step), force_step) - 1)[force_step - sol_step - 1];
160 // Eigen::MatrixXd velocity = collision_mesh.map_displacements(utils::unflatten(diff_cached.v(force_step), collision_mesh.dim()));
161 // hessian_prev = diff_cached.friction_collision_set(force_step).compute_potential_hessian( //
162 // collision_mesh, velocity, solve_data.friction_form->epsv(), false) * (-alpha / beta / dt);
163
164 // hessian_prev = collision_mesh.to_full_dof(hessian_prev);
165 }
166 }
167
169 {
170
171 if (sol_step == force_step - 1)
172 {
173 StiffnessMatrix adhesion_hessian_prev(u.size(), u.size());
174
175 Eigen::MatrixXd surface_solution_prev = varform.collision_mesh().vertices(utils::unflatten(u_prev, varform.get_mesh().dimension()));
176 Eigen::MatrixXd surface_solution = varform.collision_mesh().vertices(utils::unflatten(u, varform.get_mesh().dimension()));
177
178 // TODO: use the time integration to compute the velocity
179 const Eigen::MatrixXd surface_velocities = (surface_solution - surface_solution_prev) / dt;
180 const double dv_dut = -1 / dt;
181
182 adhesion_hessian_prev =
183 varform.solve_data()->tangential_adhesion_form->tangential_adhesion_potential().force_jacobian(
184 diff_cache.tangential_adhesion_collision_set(force_step),
185 varform.collision_mesh(),
186 varform.collision_mesh().rest_positions(),
187 /*lagged_displacements=*/surface_solution_prev,
188 surface_velocities,
189 varform.solve_data()->normal_adhesion_form->normal_adhesion_potential(),
190 ipc::TangentialPotential::DiffWRT::LAGGED_DISPLACEMENTS)
191 + varform.solve_data()->tangential_adhesion_form->tangential_adhesion_potential().force_jacobian(
192 diff_cache.tangential_adhesion_collision_set(force_step),
193 varform.collision_mesh(),
194 varform.collision_mesh().rest_positions(),
195 /*lagged_displacements=*/surface_solution_prev,
196 surface_velocities,
197 varform.solve_data()->normal_adhesion_form->normal_adhesion_potential(),
198 ipc::TangentialPotential::DiffWRT::VELOCITIES)
199 * dv_dut;
200
201 adhesion_hessian_prev *= -1;
202
203 adhesion_hessian_prev = varform.collision_mesh().to_full_dof(adhesion_hessian_prev); // / (beta * dt) / (beta * dt);
204
205 hessian_prev += adhesion_hessian_prev;
206 }
207 }
208
209 if (varform.damping_assembler() && varform.damping_assembler()->is_valid() && sol_step == force_step - 1) // velocity in damping uses BDF1
210 {
211 utils::SparseMatrixCache mat_cache;
212 StiffnessMatrix damping_hessian_prev(u.size(), u.size());
213 varform.damping_prev_assembler()->assemble_hessian(varform.get_mesh().is_volume(), varform.primary_space().n_bases, false, varform.primary_space().basis_list(), varform.primary_space().geometry_basis_list(), varform.assembly_cache(), force_step * varform.get_args()["time"]["dt"].get<double>() + varform.get_args()["time"]["t0"].get<double>(), dt, u, u_prev, mat_cache, damping_hessian_prev);
214
215 hessian_prev += damping_hessian_prev;
216 }
217
218 if (sol_step == force_step - 1)
219 {
220 StiffnessMatrix body_force_hessian(u.size(), u.size());
221 varform.solve_data()->body_form->hessian_wrt_u_prev(u_prev, force_step * dt, body_force_hessian);
222 hessian_prev += body_force_hessian;
223 }
224 }
225 }
226 }
227
228 Eigen::MatrixXd solve_static_adjoint(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, const Eigen::MatrixXd &adjoint_rhs)
229 {
230
231 Eigen::MatrixXd b = adjoint_rhs;
232
233 Eigen::MatrixXd adjoint;
234 {
235 auto solver = polysolve::linear::Solver::create(varform.get_args()["solver"]["adjoint_linear"], adjoint_logger());
236
237 StiffnessMatrix A = diff_cache.gradu_h(0); // This should be transposed, but A is symmetric in hyper-elastic and diffusion problems
238
239 /*
240 For non-periodic problems, the adjoint solution p's size is the full size in NLProblem
241 For periodic problems, the adjoint solution p's size is the reduced size in NLProblem
242 */
243 if (!varform.is_homogenization())
244 {
245 adjoint.setZero(varform.primary_space().ndof(), adjoint_rhs.cols());
246 for (int i = 0; i < b.cols(); i++)
247 {
248 Eigen::VectorXd tmp = b.col(i);
249 tmp(varform.boundary_state().boundary_nodes).setZero();
250
251 Eigen::VectorXd x;
252 x.setZero(tmp.size());
253 dirichlet_solve(*solver, A, tmp, varform.boundary_state().boundary_nodes, x, A.rows(), "", false, false, false);
254
255 adjoint.col(i) = x;
256 adjoint(varform.boundary_state().boundary_nodes, i) = -b(varform.boundary_state().boundary_nodes, i);
257 }
258 }
259 else
260 {
261 solver->analyze_pattern(A, A.rows());
262 solver->factorize(A);
263
264 adjoint.setZero(adjoint_rhs.rows(), adjoint_rhs.cols());
265 for (int i = 0; i < b.cols(); i++)
266 {
267 Eigen::MatrixXd tmp = b.col(i);
268
269 Eigen::VectorXd x;
270 x.setZero(tmp.size());
271 solver->solve(tmp, x);
272 x.conservativeResize(adjoint.rows());
273
274 adjoint.col(i) = x;
275 }
276 }
277 }
278
279 return adjoint;
280 }
281
282 Eigen::MatrixXd solve_transient_adjoint(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, const Eigen::MatrixXd &adjoint_rhs)
283 {
284
285 const double dt = varform.get_args()["time"]["dt"];
286 const int time_steps = varform.get_args()["time"]["time_steps"];
287
288 int bdf_order = 1;
289 if (varform.get_args()["time"]["integrator"].is_string())
290 bdf_order = 1;
291 else if (varform.get_args()["time"]["integrator"]["type"] == "ImplicitEuler")
292 bdf_order = 1;
293 else if (varform.get_args()["time"]["integrator"]["type"] == "BDF")
294 bdf_order = varform.get_args()["time"]["integrator"]["steps"].get<int>();
295 else
296 log_and_throw_adjoint_error("Integrator type not supported for differentiability.");
297
298 assert(adjoint_rhs.cols() == time_steps + 1);
299
300 const int cols_per_adjoint = time_steps + 1;
301 Eigen::MatrixXd adjoints;
302 adjoints.setZero(varform.primary_space().ndof(), cols_per_adjoint * 2);
303
304 // set dirichlet rows of mass to identity
305 StiffnessMatrix reduced_mass;
306 replace_rows_by_identity(reduced_mass, varform.mass_matrix(), varform.boundary_state().boundary_nodes);
307
308 Eigen::MatrixXd sum_alpha_p, sum_alpha_nu;
309 for (int i = time_steps; i >= 0; --i)
310 {
311 {
312 sum_alpha_p.setZero(varform.primary_space().ndof(), 1);
313 sum_alpha_nu.setZero(varform.primary_space().ndof(), 1);
314
315 const int num = std::min(bdf_order, time_steps - i);
316
317 Eigen::VectorXd bdf_coeffs = Eigen::VectorXd::Zero(num);
318 for (int j = 0; j < bdf_order && i + j < time_steps; ++j)
319 bdf_coeffs(j) = -time_integrator::BDF::alphas(std::min(bdf_order - 1, i + j))[j];
320
321 sum_alpha_p = adjoints.middleCols(i + 1, num) * bdf_coeffs;
322 sum_alpha_nu = adjoints.middleCols(cols_per_adjoint + i + 1, num) * bdf_coeffs;
323 }
324
325 Eigen::VectorXd rhs_ = -reduced_mass.transpose() * sum_alpha_nu - adjoint_rhs.col(i);
326 for (int j = 1; j <= bdf_order; j++)
327 {
328 if (i + j > time_steps)
329 break;
330
331 StiffnessMatrix gradu_h_prev;
332 compute_force_jacobian_prev(varform, diff_cache, i + j, i, gradu_h_prev);
333 Eigen::VectorXd tmp = adjoints.col(i + j) * (time_integrator::BDF::betas(diff_cache.bdf_order(i + j) - 1) * dt);
334 tmp(varform.boundary_state().boundary_nodes).setZero();
335 rhs_ += -gradu_h_prev.transpose() * tmp;
336 }
337
338 if (i > 0)
339 {
340 double beta_dt = time_integrator::BDF::betas(diff_cache.bdf_order(i) - 1) * dt;
341
342 rhs_ += (1. / beta_dt) * (diff_cache.gradu_h(i) - reduced_mass).transpose() * sum_alpha_p;
343
344 {
345 StiffnessMatrix A = diff_cache.gradu_h(i).transpose();
346 Eigen::VectorXd b_ = rhs_;
347 b_(varform.boundary_state().boundary_nodes).setZero();
348
349 auto solver = polysolve::linear::Solver::create(varform.get_args()["solver"]["adjoint_linear"], adjoint_logger());
350
351 Eigen::VectorXd x;
352 dirichlet_solve(*solver, A, b_, varform.boundary_state().boundary_nodes, x, A.rows(), "", false, false, false);
353 adjoints.col(i + cols_per_adjoint) = x;
354 }
355
356 // TODO: generalize to BDFn
357 Eigen::VectorXd tmp = rhs_(varform.boundary_state().boundary_nodes);
358 if (i + 1 < cols_per_adjoint)
359 tmp += (-2. / beta_dt) * adjoints(varform.boundary_state().boundary_nodes, i + 1);
360 if (i + 2 < cols_per_adjoint)
361 tmp += (1. / beta_dt) * adjoints(varform.boundary_state().boundary_nodes, i + 2);
362
363 tmp -= (diff_cache.gradu_h(i).transpose() * adjoints.col(i + cols_per_adjoint))(varform.boundary_state().boundary_nodes);
364 adjoints(varform.boundary_state().boundary_nodes, i + cols_per_adjoint) = tmp;
365 adjoints.col(i) = beta_dt * adjoints.col(i + cols_per_adjoint) - sum_alpha_p;
366 }
367 else
368 {
369 adjoints.col(i) = -reduced_mass.transpose() * sum_alpha_p;
370 adjoints.col(i + cols_per_adjoint) = rhs_; // adjoint_nu[0] actually stores adjoint_mu[0]
371 }
372 }
373 return adjoints;
374 }
375
376 Eigen::MatrixXd solve_adjoint(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, const Eigen::MatrixXd &rhs)
377 {
378 if (varform.get_problem().is_time_dependent())
379 return solve_transient_adjoint(varform, diff_cache, rhs);
380 else
381 return solve_static_adjoint(varform, diff_cache, rhs);
382 }
383 } // namespace
384
385 void solve_adjoint_cached(const varform::DifferentiableVarForm &varform, DiffCache &diff_cache, const Eigen::MatrixXd &rhs)
386 {
387 diff_cache.cache_adjoints(solve_adjoint(varform, diff_cache, rhs));
388 }
389
397 Eigen::MatrixXd get_adjoint_mat(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, int type)
398 {
399 assert(diff_cache.adjoint_mat().size() > 0);
400
401 if (varform.get_problem().is_time_dependent())
402 {
403 if (type == 0)
404 return diff_cache.adjoint_mat().leftCols(diff_cache.adjoint_mat().cols() / 2);
405 else if (type == 1)
406 return diff_cache.adjoint_mat().middleCols(diff_cache.adjoint_mat().cols() / 2, diff_cache.adjoint_mat().cols() / 2);
407 else
408 log_and_throw_adjoint_error("Invalid adjoint type!");
409 }
410
411 return diff_cache.adjoint_mat();
412 }
413
414 void compute_surface_node_ids(const varform::DifferentiableVarForm &varform, const int surface_selection, std::vector<int> &node_ids)
415 {
416
417 node_ids = {};
418
419 const auto &gbases = varform.primary_space().geometry_basis_list();
420 for (const auto &lb : varform.boundary_state().total_local_boundary)
421 {
422 const int e = lb.element_id();
423 for (int i = 0; i < lb.size(); ++i)
424 {
425 const int primitive_global_id = lb.global_primitive_id(i);
426 const int boundary_id = varform.get_mesh().get_boundary_id(primitive_global_id);
427 const auto nodes = gbases[e].local_nodes_for_primitive(primitive_global_id, varform.get_mesh());
428
429 if (boundary_id == surface_selection)
430 {
431 for (long n = 0; n < nodes.size(); ++n)
432 {
433 const int g_id = gbases[e].bases[nodes(n)].global()[0].index;
434
435 if (std::count(node_ids.begin(), node_ids.end(), g_id) == 0)
436 node_ids.push_back(g_id);
437 }
438 }
439 }
440 }
441 }
442
443 void compute_total_surface_node_ids(const varform::DifferentiableVarForm &varform, std::vector<int> &node_ids)
444 {
445
446 node_ids = {};
447
448 const auto &gbases = varform.primary_space().geometry_basis_list();
449 for (const auto &lb : varform.boundary_state().total_local_boundary)
450 {
451 const int e = lb.element_id();
452 for (int i = 0; i < lb.size(); ++i)
453 {
454 const int primitive_global_id = lb.global_primitive_id(i);
455 const auto nodes = gbases[e].local_nodes_for_primitive(primitive_global_id, varform.get_mesh());
456
457 for (long n = 0; n < nodes.size(); ++n)
458 {
459 const int g_id = gbases[e].bases[nodes(n)].global()[0].index;
460
461 if (std::count(node_ids.begin(), node_ids.end(), g_id) == 0)
462 node_ids.push_back(g_id);
463 }
464 }
465 }
466 }
467
468 void compute_volume_node_ids(const varform::DifferentiableVarForm &varform, const int volume_selection, std::vector<int> &node_ids)
469 {
470
471 node_ids = {};
472
473 const auto &gbases = varform.primary_space().geometry_basis_list();
474 for (int e = 0; e < gbases.size(); e++)
475 {
476 const int body_id = varform.get_mesh().get_body_id(e);
477 if (body_id == volume_selection)
478 for (const auto &gbs : gbases[e].bases)
479 for (const auto &g : gbs.global())
480 node_ids.push_back(g.index);
481 }
482 }
483
484} // namespace polyfem
int x
Storage for additional data required by differntial code.
Definition DiffCache.hpp:22
int bdf_order(int step) const
Definition DiffCache.hpp:47
const ipc::TangentialCollisions & friction_collision_set(int step) const
const Eigen::MatrixXd & adjoint_mat() const
Definition DiffCache.hpp:42
Eigen::VectorXd u(int step) const
Definition DiffCache.hpp:63
void cache_adjoints(const Eigen::MatrixXd &adjoint_mat)
const StiffnessMatrix & gradu_h(int step) const
Definition DiffCache.hpp:85
const ipc::TangentialCollisions & tangential_adhesion_collision_set(int step) const
virtual bool is_linear() const =0
virtual bool is_time_dependent() const
Definition Problem.hpp:62
Eigen::MatrixXd assemble_hessian(const NonLinearAssemblerData &data) const override
virtual int get_body_id(const int primitive) const
Get the volume selection of an element (cell in 3d, face in 2d)
Definition Mesh.hpp:525
virtual int get_boundary_id(const int primitive) const
Get the boundary selection of an element (face in 3d, edge in 2d)
Definition Mesh.hpp:499
virtual bool is_volume() const =0
checks if mesh is volume
int dimension() const
utily for dimension
Definition Mesh.hpp:164
std::shared_ptr< solver::FrictionForm > friction_form
std::shared_ptr< solver::BodyForm > body_form
std::shared_ptr< solver::NormalAdhesionForm > normal_adhesion_form
std::shared_ptr< solver::ContactForm > contact_form
std::shared_ptr< time_integrator::ImplicitTimeIntegrator > time_integrator
std::shared_ptr< solver::TangentialAdhesionForm > tangential_adhesion_form
static double betas(const int i)
Retrieve the value of beta used for BDF with i steps.
Definition BDF.cpp:36
static const std::vector< double > & alphas(const int i)
Retrieve the alphas used for BDF with i steps.
Definition BDF.cpp:22
Optimization-facing interface implemented by differentiated VarForm adapters.
virtual const assembler::ViscousDampingPrev * damping_prev_assembler() const
virtual const assembler::ViscousDamping * damping_assembler() const
virtual const assembler::AssemblyValsCache & assembly_cache() const =0
virtual const assembler::Assembler & primary_assembler() const =0
virtual const VarFormBoundaryState & boundary_state() const =0
virtual const mesh::Mesh & get_mesh() const =0
virtual const StiffnessMatrix & mass_matrix() const =0
virtual solver::SolveData * solve_data()=0
virtual assembler::Problem & get_problem()=0
virtual bool is_contact_enabled() const =0
virtual const FESpace & primary_space() const =0
virtual const ipc::CollisionMesh & collision_mesh() const
const std::vector< basis::ElementBases > & geometry_basis_list() const
Definition FESpace.hpp:115
int n_bases
Number of globally indexed scalar basis functions in the space.
Definition FESpace.hpp:65
const std::vector< basis::ElementBases > & basis_list() const
Definition FESpace.hpp:109
list tmp
Definition p_bases.py:366
Eigen::MatrixXd unflatten(const Eigen::VectorXd &x, int dim)
Unflatten rowwises, so every dim elements in x become a row.
void compute_surface_node_ids(const varform::DifferentiableVarForm &varform, const int surface_selection, std::vector< int > &node_ids)
spdlog::logger & adjoint_logger()
Retrieves the current logger for adjoint.
Definition Logger.cpp:30
void compute_total_surface_node_ids(const varform::DifferentiableVarForm &varform, std::vector< int > &node_ids)
void compute_volume_node_ids(const varform::DifferentiableVarForm &varform, const int volume_selection, std::vector< int > &node_ids)
void log_and_throw_adjoint_error(const std::string &msg)
Definition Logger.cpp:79
Eigen::MatrixXd get_adjoint_mat(const varform::DifferentiableVarForm &varform, const DiffCache &diff_cache, int type)
Get adjoint parameter nu or p.
void solve_adjoint_cached(const varform::DifferentiableVarForm &varform, DiffCache &diff_cache, const Eigen::MatrixXd &rhs)
Eigen::SparseMatrix< double, Eigen::ColMajor > StiffnessMatrix
Definition Types.hpp:24
std::vector< mesh::LocalBoundary > total_local_boundary
Definition FESpace.hpp:154