Mechanics of Defect Evolution Library
Loading...
Searching...
No Matches
WeakProblem.h
Go to the documentation of this file.
1/* This file is part of MoDELib, the Mechanics Of Defects Evolution Library.
2 * Distributed under the GNU General Public License (GPL) v2.
3 */
4#ifndef model_WeakProblem_H_
5#define model_WeakProblem_H_
6#include <Eigen/Sparse>
7#include <Eigen/SVD>
8#include <LowRankBilinear.h>
9#include <algorithm>
10#include <cmath>
11#include <functional>
12#include <map>
13#include <memory>
14#include <stdexcept>
15#include <type_traits>
16#include <vector>
17
18namespace model
19{
21 {
22 bool useDirectSolver=true;
23 double tolerance=1.e-10;
24 double constraintTolerance=1.e-10;
25 Eigen::Index maxIterations=1000;
26 };
27
29 {
30 double relativeResidual=0.0;
31 Eigen::Index iterations=0;
33 };
34
35 // A snapshot of the bilinear coefficients is reused until reassembleMatrix().
36 // RHS values and constraints are read afresh at every solve. Not thread safe.
37 // Forms with lowRankPenaltyRows() use an indefinite bordered system; select
38 // solvers supporting indefinite matrices (e.g. SparseLU and BiCGSTAB, not LLT).
39 template<class DirectSolverType,class IterativeSolverType>
41 {
42 public:
43 using Matrix=typename DirectSolverType::MatrixType;
44 using Vector=Eigen::VectorXd;
45 using Triplet=Eigen::Triplet<double>;
46 static_assert(std::is_same_v<Matrix,typename IterativeSolverType::MatrixType>,
47 "WeakProblem solvers must use the same matrix type.");
48 static_assert(std::is_same_v<typename Matrix::Scalar,double>);
49
50 private:
51 struct Constraints
52 {
53 std::vector<Eigen::Index> reduced;
54 Vector offset;
55 Eigen::Index size=0;
56 Eigen::MatrixXd modes,affineNullspace,globalRows;
57 Vector affineOffset,globalValues;
58 std::map<size_t,double> dirichlet;
59 };
60 const WeakProblemOptions options;
61 std::shared_ptr<const void> fieldIdentity;
62 std::function<std::vector<Triplet>()> assemble;
63 std::function<Vector()> force;
64 std::function<Eigen::MatrixXd()> penalty;
65 std::function<Constraints()> constraints;
66 std::function<void(const Vector&)> commit;
67 std::vector<Triplet> fullTriplets;
68 std::vector<Eigen::Index> mapping;
69 Eigen::MatrixXd cachedModes;
70 Matrix reducedA;
71 Matrix solverA;
72 Eigen::MatrixXd fullPenalty;
73 Eigen::MatrixXd reducedPenalty;
74 Vector reducedB;
75 Eigen::MatrixXd affineModes;
76 Vector affineSolution;
77 Eigen::MatrixXd linearRows;
78 Eigen::MatrixXd reducedRows;
79 Vector linearValues;
80 DirectSolverType direct;
81 IterativeSolverType iterative;
82 bool assembled=false;
83 bool factorized=false;
84 size_t factorizations=0;
85
86 static bool sameMatrix(const Eigen::MatrixXd& a,const Eigen::MatrixXd& b)
87 {
88 return a.rows()==b.rows() && a.cols()==b.cols() && (a.array()==b.array()).all();
89 }
90
91 template<class Field>
92 Constraints makeConstraints(const Field& field) const
93 {
94 const auto& fe=field.fe();
95 constexpr int components=Field::dofPerNode;
96 const Eigen::Index n=field.gSize();
97 const Eigen::Index k=affineModes.cols();
98 if(k>0 && affineModes.rows()!=n)
99 {
100 throw std::invalid_argument("Affine FEM modes have the wrong number of DOFs.");
101 }
102 Constraints result;
103 result.offset=Vector::Zero(n);
104 result.modes=Eigen::MatrixXd::Zero(n,k);
105 if(k>0)
106 {
107 result.modes=affineModes;
108 }
109 result.reduced.resize(n,-1);
110 std::vector<size_t> root(n);
111 for(size_t node=0;node<fe.nodeSize();++node)
112 {
113 const size_t representative=fe.periodicRepresentative(node);
114 if(representative>=fe.nodeSize())
115 {
116 throw std::runtime_error("Invalid FEM periodic representative.");
117 }
118 const auto jump=(field.periodicGradient()*fe.periodicShift(node)).eval();
119 for(int c=0;c<components;++c)
120 {
121 root[node*components+c]=representative*components+c;
122 result.offset(node*components+c)=jump(c);
123 if(field.periodicNodalOffset().size())
124 {
125 result.offset(node*components+c)+=field.periodicNodalOffset()(node*components+c)
126 -field.periodicNodalOffset()(representative*components+c);
127 }
128 }
129 }
130 const Vector prescribedOffset=result.offset;
131 std::map<size_t,size_t> anchors;
132 for(const auto& bc : field.dirichletConditions())
133 {
134 if(bc.first>=root.size() || !std::isfinite(bc.second))
135 {
136 throw std::invalid_argument("Invalid FEM Dirichlet DOF or value.");
137 }
138 result.dirichlet.emplace(bc.first,bc.second);
139 anchors.emplace(root[bc.first],bc.first);
140 }
141 Eigen::MatrixXd coupling=Eigen::MatrixXd::Zero(result.dirichlet.size()-anchors.size(),k);
142 Vector values=Vector::Zero(coupling.rows());
143 Eigen::Index row=0;
144 for(const auto& bc : result.dirichlet)
145 {
146 const size_t anchor=anchors.at(root[bc.first]);
147 if(anchor!=bc.first)
148 {
149 // Two prescribed copies in one periodic class constrain the
150 // affine coefficients, rather than conflicting automatically.
151 if(k>0)
152 {
153 coupling.row(row)=affineModes.row(bc.first)-affineModes.row(anchor);
154 }
155 values(row)=bc.second-prescribedOffset(bc.first)-result.dirichlet.at(anchor)+prescribedOffset(anchor);
156 ++row;
157 }
158 }
159 result.affineOffset=Vector::Zero(k);
160 result.affineNullspace=Eigen::MatrixXd::Identity(k,k);
161 if(k>0 && coupling.rows()>0)
162 {
163 Eigen::JacobiSVD<Eigen::MatrixXd> svd(coupling,Eigen::ComputeThinU|Eigen::ComputeFullV);
164 svd.setThreshold(options.constraintTolerance);
165 result.affineOffset=svd.solve(values);
166 result.affineNullspace=svd.matrixV().rightCols(k-svd.rank());
167 }
168 const Vector mismatch=coupling*result.affineOffset-values;
169 for(Eigen::Index i=0;i<mismatch.size();++i)
170 {
171 const double scale=std::max({1.0,std::abs(values(i)),coupling.row(i).norm()*result.affineOffset.norm()});
172 if(std::abs(mismatch(i))>options.constraintTolerance*scale)
173 {
174 throw std::invalid_argument("Incompatible affine/periodic and nodal Dirichlet conditions.");
175 }
176 }
177 std::map<size_t,Eigen::Index> independent;
178 for(Eigen::Index i=0;i<n;++i)
179 {
180 const auto anchor=anchors.find(root[i]);
181 if(anchor!=anchors.end())
182 {
183 const size_t j=anchor->second;
184 result.offset(i)+=result.dirichlet.at(j)-prescribedOffset(j);
185 if(k>0)
186 {
187 result.modes.row(i)-=affineModes.row(j);
188 }
189 }
190 else
191 {
192 const auto entry=independent.emplace(root[i],result.size);
193 if(entry.second)
194 {
195 ++result.size;
196 }
197 result.reduced[i]=entry.first->second;
198 }
199 }
200 result.offset+=result.modes*result.affineOffset;
201 result.modes=(result.modes*result.affineNullspace).eval();
202 result.size+=result.modes.cols();
203 if(!result.offset.allFinite() || !result.modes.allFinite())
204 {
205 throw std::invalid_argument("Nonfinite FEM constraint elimination.");
206 }
207
208 // Check global affine-loading constraints against the eliminated
209 // Dirichlet conditions before factorization. Remove consistent
210 // redundant rows, otherwise their multipliers make the system singular.
211 result.globalRows=Eigen::MatrixXd::Zero(0,result.size);
212 result.globalValues=Vector::Zero(0);
213 if(linearRows.rows()>0)
214 {
215 if(linearRows.cols()!=n || linearValues.size()!=linearRows.rows())
216 {
217 throw std::invalid_argument("Invalid global FEM constraint size.");
218 }
219 Eigen::MatrixXd rows=Eigen::MatrixXd::Zero(linearRows.rows(),result.size);
220 for(Eigen::Index i=0;i<n;++i)
221 {
222 if(result.reduced[i]>=0)
223 {
224 rows.col(result.reduced[i])+=linearRows.col(i);
225 }
226 }
227 if(result.modes.cols()>0)
228 {
229 rows.rightCols(result.modes.cols())=linearRows*result.modes;
230 }
231 const Vector originalOffset=linearRows*result.offset;
232 Vector rhs=linearValues-originalOffset;
233 Vector normalizedValues=linearValues;
234 Vector normalizedOffset=originalOffset;
235 for(Eigen::Index i=0;i<rows.rows();++i)
236 {
237 const double scale=linearRows.row(i).norm();
238 if(scale>0.0)
239 {
240 rows.row(i)/=scale;
241 rhs(i)/=scale;
242 normalizedValues(i)/=scale;
243 normalizedOffset(i)/=scale;
244 }
245 }
246 Eigen::MatrixXd left=Eigen::MatrixXd::Identity(rows.rows(),rows.rows());
247 Eigen::Index rank=0;
248 if(result.size>0)
249 {
250 Eigen::JacobiSVD<Eigen::MatrixXd> svd(rows,Eigen::ComputeFullU);
251 const double largest=svd.singularValues().size()>0 ? svd.singularValues()(0) : 0.0;
252 if(largest>0.0)
253 {
254 // An eliminated constraint can leave roundoff-sized rows;
255 // compare against the original normalized row scale as well.
256 svd.setThreshold(options.constraintTolerance*std::max(1.0,rows.norm())/largest);
257 rank=svd.rank();
258 }
259 left=svd.matrixU();
260 }
261 if((left.rightCols(left.cols()-rank).transpose()*rhs).norm()>
262 options.constraintTolerance*std::max({1.0,normalizedValues.norm(),normalizedOffset.norm()}))
263 {
264 throw std::invalid_argument("Incompatible global affine constraints and nodal Dirichlet conditions.");
265 }
266 result.globalRows=left.leftCols(rank).transpose()*rows;
267 result.globalValues=left.leftCols(rank).transpose()*rhs;
268 }
269 return result;
270 }
271
272 bool prepare(const Constraints& c)
273 {
274 if(!assembled)
275 {
276 auto entries=assemble();
277 for(const auto& entry : entries)
278 {
279 if(entry.row()<0 || entry.col()<0 || entry.row()>=c.offset.size() ||
280 entry.col()>=c.offset.size() || !std::isfinite(entry.value()))
281 {
282 throw std::invalid_argument("Invalid FEM matrix triplet.");
283 }
284 }
285 fullTriplets=std::move(entries);
286 assembled=true;
287 }
288 if(factorized && mapping==c.reduced && sameMatrix(cachedModes,c.modes) && sameMatrix(reducedRows,c.globalRows))
289 {
290 return true;
291 }
292 factorized=false;
293 fullPenalty=penalty();
294 if(fullPenalty.cols()!=c.offset.size() || !fullPenalty.allFinite())
295 {
296 throw std::invalid_argument("Invalid low-rank FEM penalty.");
297 }
298 reducedPenalty=Eigen::MatrixXd::Zero(fullPenalty.rows(),c.size);
299 for(Eigen::Index i=0;i<c.offset.size();++i)
300 {
301 if(c.reduced[i]>=0)
302 {
303 reducedPenalty.col(c.reduced[i])+=fullPenalty.col(i);
304 }
305 }
306 const Eigen::Index modes=c.modes.cols();
307 const Eigen::Index nodal=c.size-modes;
308 if(modes>0)
309 {
310 reducedPenalty.rightCols(modes)=fullPenalty*c.modes;
311 }
312 if(!reducedPenalty.allFinite())
313 {
314 throw std::runtime_error("Nonfinite reduced FEM penalty.");
315 }
316 std::vector<Triplet> entries;
317 entries.reserve(fullTriplets.size());
318 for(const auto& entry : fullTriplets)
319 {
320 const auto row=c.reduced[entry.row()];
321 const auto col=c.reduced[entry.col()];
322 if(row>=0 && col>=0)
323 {
324 entries.emplace_back(row,col,entry.value());
325 }
326 }
327 if(modes>0)
328 {
329 Eigen::MatrixXd action=Eigen::MatrixXd::Zero(c.offset.size(),modes);
330 Eigen::MatrixXd transposeAction=action;
331 for(const auto& entry : fullTriplets)
332 {
333 action.row(entry.row())+=entry.value()*c.modes.row(entry.col());
334 transposeAction.row(entry.col())+=entry.value()*c.modes.row(entry.row());
335 }
336 for(Eigen::Index i=0;i<c.offset.size();++i)
337 {
338 if(c.reduced[i]>=0)
339 {
340 for(Eigen::Index j=0;j<modes;++j)
341 {
342 entries.emplace_back(c.reduced[i],nodal+j,action(i,j));
343 entries.emplace_back(nodal+j,c.reduced[i],transposeAction(i,j));
344 }
345 }
346 }
347 const Eigen::MatrixXd block=c.modes.transpose()*action;
348 for(Eigen::Index i=0;i<modes;++i)
349 {
350 for(Eigen::Index j=0;j<modes;++j)
351 {
352 entries.emplace_back(nodal+i,nodal+j,block(i,j));
353 }
354 }
355 }
356 reducedA.resize(c.size,c.size);
357 reducedA.setFromTriplets(entries.begin(),entries.end());
358 reducedA.prune([](Eigen::Index,Eigen::Index,double value)
359 {
360 return value!=0.0;
361 });
362 reducedA.makeCompressed();
363 for(Eigen::Index k=0;k<reducedA.nonZeros();++k)
364 {
365 if(!std::isfinite(reducedA.valuePtr()[k]))
366 {
367 throw std::runtime_error("Nonfinite reduced FEM matrix.");
368 }
369 }
370 // [K B^T; B -I] eliminates to K+B^T B without forming a dense outer product.
371 entries.clear();
372 for(Eigen::Index outer=0;outer<reducedA.outerSize();++outer)
373 {
374 for(typename Matrix::InnerIterator it(reducedA,outer);it;++it)
375 {
376 entries.emplace_back(it.row(),it.col(),it.value());
377 }
378 }
379 for(Eigen::Index row=0;row<reducedPenalty.rows();++row)
380 {
381 entries.emplace_back(c.size+row,c.size+row,-1.0);
382 for(Eigen::Index col=0;col<c.size;++col)
383 {
384 if(reducedPenalty(row,col)!=0.0)
385 {
386 entries.emplace_back(c.size+row,col,reducedPenalty(row,col));
387 entries.emplace_back(col,c.size+row,reducedPenalty(row,col));
388 }
389 }
390 }
391 reducedRows=c.globalRows;
392 for(Eigen::Index i=0;i<reducedRows.rows();++i)
393 {
394 for(Eigen::Index j=0;j<c.size;++j)
395 {
396 entries.emplace_back(c.size+reducedPenalty.rows()+i,j,reducedRows(i,j));
397 entries.emplace_back(j,c.size+reducedPenalty.rows()+i,reducedRows(i,j));
398 }
399 }
400 const Eigen::Index systemSize=c.size+reducedPenalty.rows()+reducedRows.rows();
401 solverA.resize(systemSize,systemSize);
402 solverA.setFromTriplets(entries.begin(),entries.end());
403 solverA.makeCompressed();
404 if(c.size>0)
405 {
406 if(solverA.nonZeros()==0)
407 {
408 throw std::runtime_error("WeakProblem has a zero matrix and unconstrained DOFs.");
409 }
410 if(options.useDirectSolver)
411 {
412 direct.compute(solverA);
413 if(direct.info()!=Eigen::Success)
414 {
415 throw std::runtime_error("WeakProblem direct factorization failed; check constraints and null modes.");
416 }
417 }
418 else
419 {
420 iterative.compute(solverA);
421 if(iterative.info()!=Eigen::Success)
422 {
423 throw std::runtime_error("WeakProblem iterative setup failed.");
424 }
425 }
426 ++factorizations;
427 }
428 mapping=c.reduced;
429 cachedModes=c.modes;
430 factorized=true;
431 return false;
432 }
433
434 public:
435 template<class BWF,class LWF>
436 WeakProblem(std::shared_ptr<BWF> bwf,std::shared_ptr<LWF> lwf,
437 WeakProblemOptions opts={}) : options(opts)
438 {
439 if(!bwf || !(options.tolerance>0.0) || !std::isfinite(options.tolerance) ||
440 !(options.constraintTolerance>=0.0) || !std::isfinite(options.constraintTolerance) ||
441 options.maxIterations<=0)
442 {
443 throw std::invalid_argument("Invalid WeakProblem form or options.");
444 }
445 fieldIdentity=bwf->field().ownership();
446 assemble=[bwf]() { return bwf->globalTriplets(); };
447 penalty=[bwf]() { return lowRankBilinearRows(*bwf); };
448 constraints=[this,bwf]() { return makeConstraints(bwf->field()); };
449 commit=[bwf](const Vector& x) { bwf->field().setFullDofVector(x); };
450 iterative.setTolerance(options.tolerance);
451 iterative.setMaxIterations(options.maxIterations);
452 setRightHandSide(std::move(lwf));
453 }
454 WeakProblem(const WeakProblem&)=delete;
458
459 template<class LWF>
460 void setRightHandSide(std::shared_ptr<LWF> lwf)
461 {
462 if(!lwf || lwf->field().ownership()!=fieldIdentity)
463 {
464 throw std::invalid_argument("WeakProblem forms must refer to the same trial instance.");
465 }
466 force=[lwf]() { return lwf->globalVector(); };
467 }
468
469 // Additional displacement modes are independent unknowns appended to the masters.
470 // Keep them fixed across RHS changes to reuse the factorization.
471 void setAffineModes(const Eigen::MatrixXd& modes)
472 {
473 if(!modes.allFinite())
474 {
475 throw std::invalid_argument("Nonfinite affine FEM modes.");
476 }
477 affineModes=modes;
478 factorized=false;
479 }
480 void setLinearConstraints(const Eigen::MatrixXd& rows)
481 {
482 if(!rows.allFinite())
483 {
484 throw std::invalid_argument("Nonfinite global FEM constraints.");
485 }
486 linearRows=rows;
487 linearValues=Vector::Zero(rows.rows());
488 factorized=false;
489 }
491 {
492 if(values.size()!=linearRows.rows() || !values.allFinite())
493 {
494 throw std::invalid_argument("Invalid global FEM constraint values.");
495 }
496 linearValues=values;
497 }
499 {
500 return affineSolution;
501 }
502
503 // Invalidate the snapshot after changing a bilinear coefficient.
504 // The next solve reassembles and recomputes the factorization.
506 {
507 assembled=false;
508 factorized=false;
509 }
510
512 {
513 const auto c=constraints();
514 const Vector b=force();
515 if(b.size()!=c.offset.size() || !b.allFinite())
516 {
517 throw std::invalid_argument("Invalid FEM right-hand side.");
518 }
520 result.reusedFactorization=prepare(c) && c.size>0;
521 Vector lifted=b;
522 for(const auto& entry : fullTriplets)
523 {
524 lifted(entry.row())-=entry.value()*c.offset(entry.col());
525 }
526 reducedB=Vector::Zero(c.size);
527 for(Eigen::Index i=0;i<b.size();++i)
528 {
529 if(c.reduced[i]>=0)
530 {
531 reducedB(c.reduced[i])+=lifted(i);
532 }
533 }
534 if(c.modes.cols()>0)
535 {
536 reducedB.tail(c.modes.cols())=c.modes.transpose()*lifted;
537 }
538 if(!reducedB.allFinite())
539 {
540 throw std::runtime_error("Nonfinite reduced FEM force vector.");
541 }
542 const Vector penaltyOffset=fullPenalty*c.offset;
543 Vector solverB(c.size+fullPenalty.rows()+reducedRows.rows());
544 solverB.head(c.size)=reducedB;
545 solverB.segment(c.size,fullPenalty.rows())=-penaltyOffset;
546 if(reducedRows.rows()>0)
547 {
548 solverB.tail(reducedRows.rows())=c.globalValues;
549 }
550 reducedB-=reducedPenalty.transpose()*penaltyOffset;
551 if(!solverB.allFinite() || !reducedB.allFinite())
552 {
553 throw std::runtime_error("Nonfinite low-rank FEM force correction.");
554 }
555 Vector solution=Vector::Zero(solverB.size());
556 if(c.size>0)
557 {
558 if(options.useDirectSolver)
559 {
560 solution=direct.solve(solverB);
561 if(direct.info()!=Eigen::Success)
562 {
563 throw std::runtime_error("WeakProblem direct solve failed.");
564 }
565 }
566 else
567 {
568 solution=iterative.solve(solverB);
569 result.iterations=iterative.iterations();
570 if(iterative.info()!=Eigen::Success)
571 {
572 throw std::runtime_error("WeakProblem iterative solve did not converge.");
573 }
574 }
575 result.relativeResidual=(solverA*solution-solverB).norm()/std::max(1.0,solverB.norm());
576 if(!solution.allFinite() || !std::isfinite(result.relativeResidual) ||
577 result.relativeResidual>options.tolerance)
578 {
579 throw std::runtime_error("WeakProblem residual check failed.");
580 }
581 }
582 Vector full=c.offset;
583 for(Eigen::Index i=0;i<full.size();++i)
584 {
585 if(c.reduced[i]>=0)
586 {
587 full(i)+=solution(c.reduced[i]);
588 }
589 }
590 const Vector coefficients=solution.segment(c.size-c.modes.cols(),c.modes.cols());
591 if(c.modes.cols()>0)
592 {
593 full+=c.modes*coefficients;
594 }
595 if(linearRows.rows()>0 && (linearRows*full-linearValues).norm()>options.tolerance*std::max(1.0,linearValues.norm()))
596 {
597 throw std::runtime_error("Global FEM constraint residual check failed.");
598 }
599 for(const auto& bc : c.dirichlet)
600 {
601 if(std::abs(full(bc.first)-bc.second)>options.constraintTolerance*std::max(1.0,std::abs(bc.second)))
602 {
603 throw std::runtime_error("Nodal Dirichlet residual check failed.");
604 }
605 }
606 const Vector recovered=c.affineOffset+c.affineNullspace*coefficients;
607 commit(full); // No field mutation until assembly and solve have succeeded.
608 affineSolution=recovered;
609 return result;
610 }
611
612 // Diagnostic only: materializing a global low-rank penalty can be dense.
614 {
615 if(reducedPenalty.rows()==0)
616 {
617 return reducedA;
618 }
619 const Eigen::MatrixXd product=reducedPenalty.transpose()*reducedPenalty;
620 return Matrix(reducedA+Matrix(product.sparseView()));
621 }
622 const Matrix& solverMatrix() const
623 {
624 return solverA;
625 }
627 {
628 return reducedB;
629 }
630 size_t factorizationCount() const
631 {
632 return factorizations;
633 }
634 };
635
636 template<class DirectSolverType=Eigen::SparseLU<Eigen::SparseMatrix<double>>,
637 class IterativeSolverType=Eigen::BiCGSTAB<Eigen::SparseMatrix<double>>,
638 class BWF,class LWF>
639 auto weakProblem(std::shared_ptr<BWF> bwf,std::shared_ptr<LWF> lwf,WeakProblemOptions options={})
640 {
641 return std::make_shared<WeakProblem<DirectSolverType,IterativeSolverType>>(std::move(bwf),std::move(lwf),options);
642 }
643}
644#endif
const Vector & affineCoefficients() const
WeakProblem(const WeakProblem &)=delete
WeakProblemResult solveWithMasterSlaveConstraints()
void setLinearConstraintValues(const Vector &values)
void setLinearConstraints(const Eigen::MatrixXd &rows)
WeakProblem & operator=(WeakProblem &&)=delete
WeakProblem & operator=(const WeakProblem &)=delete
void setRightHandSide(std::shared_ptr< LWF > lwf)
Matrix reducedMatrix() const
size_t factorizationCount() const
const Matrix & solverMatrix() const
const Vector & reducedForceVector() const
WeakProblem(WeakProblem &&)=delete
void setAffineModes(const Eigen::MatrixXd &modes)
WeakProblem(std::shared_ptr< BWF > bwf, std::shared_ptr< LWF > lwf, WeakProblemOptions opts={})
c
Definition int_R.m:15
a
Definition int_R.m:14
b
Definition int_R.m:16
x
Definition int_R.m:12
void field(QGridLayout *layout, int row, const QString &title, QWidget *input, const QString &description)
auto weakProblem(std::shared_ptr< BWF > bwf, std::shared_ptr< LWF > lwf, WeakProblemOptions options={})
auto eval(const std::shared_ptr< T > &t)
TrialFunction< 'c', 1, Space > Field
int j
left
int k
int i
result
REAL bc[4]