OGS
CompareJacobiansJacobianAssembler.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Copyright (c) OpenGeoSys Community (opengeosys.org)
2// SPDX-License-Identifier: BSD-3-Clause
3
5
6#include <fstream>
7#include <limits>
8#include <sstream>
9
10#ifdef _OPENMP
11#include <omp.h>
12#endif
13
14#include "BaseLib/ConfigTree.h"
17
18namespace
19{
21template <typename T>
22 requires std::integral<T> || std::floating_point<T>
23void dump_py(std::ostream& fh, std::string const& var, T const val)
24{
25 fh << var << " = " << val << '\n';
26}
27
29template <typename Vec>
30void dump_py_vec(std::ostream& fh, std::string const& var, Vec const& val)
31{
32 fh << var << " = np.array([";
33 for (decltype(val.size()) i = 0; i < val.size(); ++i)
34 {
35 if (i != 0)
36 {
37 if (i % 8 == 0)
38 {
39 // Print at most eight entries on one line,
40 // indent with four spaces.
41 fh << ",\n ";
42 }
43 else
44 {
45 fh << ", ";
46 }
47 }
48 fh << val[i];
49 }
50 fh << "])\n";
51}
52
54void dump_py(std::ostream& fh, std::string const& var,
55 std::vector<double> const& val)
56{
57 dump_py_vec(fh, var, val);
58}
59
61template <typename Derived>
62void dump_py(std::ostream& fh, std::string const& var,
63 Eigen::ArrayBase<Derived> const& val,
64 std::integral_constant<int, 1> /*unused*/)
65{
66 dump_py_vec(fh, var, val);
67}
68
70template <typename Derived, int ColsAtCompileTime>
71void dump_py(std::ostream& fh, std::string const& var,
72 Eigen::ArrayBase<Derived> const& val,
73 std::integral_constant<int, ColsAtCompileTime> /*unused*/)
74{
75 fh << var << " = np.array([\n";
76 for (std::ptrdiff_t r = 0; r < val.rows(); ++r)
77 {
78 if (r != 0)
79 {
80 fh << ",\n";
81 }
82 fh << " [";
83 for (std::ptrdiff_t c = 0; c < val.cols(); ++c)
84 {
85 if (c != 0)
86 {
87 fh << ", ";
88 }
89 fh << val(r, c);
90 }
91 fh << "]";
92 }
93 fh << "])\n";
94}
95
97template <typename Derived>
98void dump_py(std::ostream& fh, std::string const& var,
99 Eigen::ArrayBase<Derived> const& val)
100{
101 dump_py(fh, var, val,
102 std::integral_constant<int, Derived::ColsAtCompileTime>{});
103}
104
106template <typename Derived>
107void dump_py(std::ostream& fh, std::string const& var,
108 Eigen::MatrixBase<Derived> const& val)
109{
110 dump_py(fh, var, val.array());
111}
112
114const std::string msg_fatal =
115 "The local matrices M or K or the local vectors b assembled with the two "
116 "different Jacobian assemblers differ.";
117
119auto signedAbsDiffRelDiff(auto const& mat_or_vec1, auto const& mat_or_vec2)
120{
121 auto abs_diff = (mat_or_vec2 - mat_or_vec1).array().eval();
122 auto const rel_diff =
123 (abs_diff == 0.0)
124 .select(
125 abs_diff,
126 2. * abs_diff /
127 (mat_or_vec1.cwiseAbs() + mat_or_vec2.cwiseAbs()).array())
128 .eval();
129 return std::pair{std::move(abs_diff), std::move(rel_diff)};
130}
131
132template <typename MatOrVec>
133auto oneIfAboveThresholdElseZero(MatOrVec&& mat_or_vec, double const threshold)
134{
135 return (mat_or_vec <= threshold)
136 .select(MatOrVec::Zero(mat_or_vec.rows(), mat_or_vec.cols()),
137 MatOrVec::Ones(mat_or_vec.rows(), mat_or_vec.cols()))
138 .eval();
139}
140
141// basic consistency check if something went terribly wrong
142bool isSimilar(auto const& mat_or_vec1, auto const& mat_or_vec2,
143 double const abs_tol, double const rel_tol)
144{
145 if (mat_or_vec1.size() == 0 || mat_or_vec2.size() == 0)
146 {
147 // either size is 0, ignore
148 return true;
149 }
150 if (mat_or_vec1.rows() != mat_or_vec2.rows() ||
151 mat_or_vec1.cols() != mat_or_vec2.cols())
152 {
153 return false;
154 }
155
156 auto const [abs_diff, rel_diff] =
157 signedAbsDiffRelDiff(mat_or_vec1, mat_or_vec2);
158 auto const abs_tol_exceeded = abs_diff.abs() > abs_tol;
159 auto const rel_tol_exceeded = rel_diff.abs() > rel_tol;
160
161 // similar if for no entry abs and rel tols are exceeded at the same time
162 return !(abs_tol_exceeded && rel_tol_exceeded).any();
163}
164
165} // anonymous namespace
166
167namespace ProcessLib
168{
169namespace detail
170{
173{
175
177 std::unique_ptr<AbstractJacobianAssembler>&& asm1,
178 std::unique_ptr<AbstractJacobianAssembler>&& asm2,
179 double abs_tol_Jac,
180 double rel_tol_Jac,
181 double abs_tol_res,
182 double rel_tol_res,
183 bool fail_on_error,
184 std::string const& log_file_path)
185 : asm1_{std::move(asm1)},
186 asm2_{std::move(asm2)},
187 abs_tol_Jac_{abs_tol_Jac},
188 rel_tol_Jac_{rel_tol_Jac},
189 abs_tol_res_{abs_tol_res},
190 rel_tol_res_{rel_tol_res},
191 fail_on_error_{fail_on_error},
192 log_file_{log_file_path}
193 {
194 log_file_.precision(std::numeric_limits<double>::max_digits10);
195 log_file_ << "#!/usr/bin/env python\n"
196 "import numpy as np\n"
197 "from numpy import nan\n"
198 << std::endl;
199 }
200
201 void assembleWithJacobian(std::size_t const mesh_item_id,
202 LocalAssemblerInterface& local_assembler,
203 double const t, double const dt,
204 std::vector<double> const& local_x,
205 std::vector<double> const& local_x_prev,
206 std::vector<double>& local_b_data,
207 std::vector<double>& local_Jac_data);
208
209private:
210 std::unique_ptr<AbstractJacobianAssembler> asm1_;
211 std::unique_ptr<AbstractJacobianAssembler> asm2_;
212
213 double const abs_tol_Jac_;
214 double const rel_tol_Jac_;
215 double const abs_tol_res_;
216 double const rel_tol_res_;
217
219 bool const fail_on_error_;
220
224 std::ofstream log_file_;
225
229 std::ptrdiff_t counter_ = -1;
230
231 unsigned iter_ = 0;
232};
233
235 std::size_t const mesh_item_id, LocalAssemblerInterface& local_assembler,
236 double const t, double const dt, std::vector<double> const& local_x,
237 std::vector<double> const& local_x_prev, std::vector<double>& local_b_data,
238 std::vector<double>& local_Jac_data)
239{
240 ++counter_;
241
242 auto const num_dof = local_x.size();
243
244 // First assembly -- the one whose results will be added to the global
245 // equation system finally.
246 asm1_->assembleWithJacobian(mesh_item_id, local_assembler, t, dt, local_x,
247 local_x_prev, local_b_data, local_Jac_data);
248
249 auto const local_b1 = MathLib::toVector(local_b_data);
250
251 std::vector<double> local_b_data2;
252 std::vector<double> local_Jac_data2;
253
254 // Second assembly -- used for checking only.
255 asm2_->assembleWithJacobian(mesh_item_id, local_assembler, t, dt, local_x,
256 local_x_prev, local_b_data2, local_Jac_data2);
257
258 auto const local_b2 = MathLib::toVector(local_b_data2);
259
260 auto const local_Jac1 = MathLib::toMatrix(local_Jac_data, num_dof, num_dof);
261 auto const local_Jac2 =
262 MathLib::toMatrix(local_Jac_data2, num_dof, num_dof);
263
264 auto const [abs_diff, rel_diff] =
265 signedAbsDiffRelDiff(local_Jac1, local_Jac2);
266
267 auto const abs_diff_mask =
268 oneIfAboveThresholdElseZero(abs_diff.abs(), abs_tol_Jac_);
269 auto const rel_diff_mask =
270 oneIfAboveThresholdElseZero(rel_diff.abs(), rel_tol_Jac_);
271
272 auto const abs_diff_OK = !abs_diff_mask.any();
273 auto const rel_diff_OK = !rel_diff_mask.any();
274
275 std::ostringstream msg_tolerance;
276 bool tol_exceeded = true;
277 bool fatal_error = false;
278
279 if (abs_diff_OK)
280 {
281 tol_exceeded = false;
282 }
283 else
284 {
285 msg_tolerance << "absolute tolerance of " << abs_tol_Jac_
286 << " exceeded";
287 }
288
289 if (rel_diff_OK)
290 {
291 tol_exceeded = false;
292 }
293 else
294 {
295 if (!msg_tolerance.str().empty())
296 {
297 msg_tolerance << " and ";
298 }
299
300 msg_tolerance << "relative tolerance of " << rel_tol_Jac_
301 << " exceeded";
302 }
303
304 fatal_error |= !isSimilar(local_b1, local_b2, abs_tol_res_, rel_tol_res_);
305
306 Eigen::VectorXd res1 = Eigen::VectorXd::Zero(num_dof);
307 auto const x = MathLib::toVector(local_x);
308 auto const x_dot = ((x - MathLib::toVector(local_x_prev)) / dt).eval();
309 if (local_b1.size() != 0)
310 {
311 res1.noalias() -= local_b1;
312 }
313
314 Eigen::VectorXd res2 = Eigen::VectorXd::Zero(num_dof);
315 if (local_b2.size() != 0)
316 {
317 res2.noalias() -= local_b2;
318 }
319
320 fatal_error |= !isSimilar(res1, res2, abs_tol_res_, rel_tol_res_);
321
322 if (tol_exceeded)
323 {
324 WARN("Compare Jacobians: {:s}", msg_tolerance.str());
325 }
326
327 bool const output = tol_exceeded || fatal_error;
328
329 if (output)
330 {
331 log_file_ << "\n### counter: " << std::to_string(counter_)
332 << ", t: " << t << ", element_id: " << mesh_item_id
333 << " (begin)\n";
334 }
335
336 if (fatal_error)
337 {
338 log_file_ << '\n'
339 << "#######################################################\n"
340 << "# FATAL ERROR: " << msg_fatal << '\n'
341 << "# You cannot expect any meaningful insights "
342 "from the Jacobian data printed below!\n"
343 << "# The reason for the mentioned differences "
344 "might be\n"
345 << "# (a) that the assembly routine has side "
346 "effects or\n"
347 << "# (b) that the assembly routines for b "
348 "themselves differ.\n"
349 << "#######################################################\n"
350 << '\n';
351 }
352
353 if (tol_exceeded)
354 {
355 log_file_ << "# " << msg_tolerance.str() << "\n\n";
356 }
357
358 if (output)
359 {
360 dump_py(log_file_, "counter", counter_);
361 dump_py(log_file_, "nonlinear_iteration", iter_);
362 dump_py(log_file_, "t", t);
363 dump_py(log_file_, "dt", dt);
364 dump_py(log_file_, "element_id", mesh_item_id);
365
366 log_file_ << '\n';
367
368 dump_py(log_file_, "num_dof", num_dof);
369 dump_py(log_file_, "abs_tol", abs_tol_Jac_);
370 dump_py(log_file_, "rel_tol", rel_tol_Jac_);
371
372 log_file_ << '\n';
373
374 dump_py(log_file_, "local_x", local_x);
375 dump_py(log_file_, "local_x_prev", local_x_prev);
376
377 log_file_ << '\n';
378
379 dump_py(log_file_, "Jacobian_1", local_Jac1);
380 dump_py(log_file_, "Jacobian_2", local_Jac2);
381
382 log_file_ << '\n';
383
384 log_file_ << "# Jacobian_2 - Jacobian_1\n";
385 dump_py(log_file_, "abs_diff", abs_diff);
386 log_file_ << "# max(|abs_diff|) = " << abs_diff.abs().maxCoeff()
387 << '\n';
388 log_file_ << "# Componentwise: 2 * abs_diff / (|Jacobian_1| + "
389 "|Jacobian_2|)\n";
390 dump_py(log_file_, "rel_diff", rel_diff);
391 log_file_ << "# max(|rel_diff|) = " << rel_diff.abs().maxCoeff()
392 << '\n';
393
394 log_file_ << '\n';
395
396 log_file_ << "# Masks: 0 ... tolerance met, 1 ... tolerance exceeded\n";
397 dump_py(log_file_, "abs_diff_mask", abs_diff_mask);
398 dump_py(log_file_, "rel_diff_mask", rel_diff_mask);
399
400 log_file_ << '\n';
401
402 dump_py(log_file_, "b_1", local_b_data);
403 dump_py(log_file_, "b_2", local_b_data2);
404 if (fatal_error && local_b1.size() == local_b2.size())
405 {
406 dump_py(log_file_, "delta_b", local_b2 - local_b1);
407 log_file_ << '\n';
408 }
409
410 dump_py(log_file_, "res_1", res1);
411 dump_py(log_file_, "res_2", res2);
412 if (fatal_error)
413 {
414 dump_py(log_file_, "delta_res", res2 - res1);
415 }
416
417 log_file_ << '\n';
418
419 log_file_ << "### counter: " << std::to_string(counter_) << " (end)\n";
420 }
421
422 if (fatal_error)
423 {
424 log_file_ << std::flush;
425 OGS_FATAL("{:s}", msg_fatal);
426 }
427
428 if (tol_exceeded && fail_on_error_)
429 {
430 log_file_ << std::flush;
431 OGS_FATAL(
432 "OGS failed, because the two Jacobian implementations returned "
433 "different results.");
434 }
435}
436} // namespace detail
437
439 std::unique_ptr<AbstractJacobianAssembler>&& asm1,
440 std::unique_ptr<AbstractJacobianAssembler>&& asm2, double abs_tol_Jac,
441 double rel_tol_Jac, double abs_tol_res, double rel_tol_res,
442 bool fail_on_error, std::string const& log_file_path)
443 : impl_{std::make_shared<detail::CompareJacobiansJacobianAssemblerImpl>(
444 std::move(asm1), std::move(asm2), abs_tol_Jac, rel_tol_Jac,
445 abs_tol_res, rel_tol_res, fail_on_error, log_file_path)}
446{
447}
448
450 std::shared_ptr<detail::CompareJacobiansJacobianAssemblerImpl> impl,
452 : impl_{std::move(impl)}
453{
454}
455
457 std::size_t const mesh_item_id, LocalAssemblerInterface& local_assembler,
458 double const t, double const dt, std::vector<double> const& local_x,
459 std::vector<double> const& local_x_prev, std::vector<double>& local_b_data,
460 std::vector<double>& local_Jac_data)
461{
462 impl_->assembleWithJacobian(mesh_item_id, local_assembler, t, dt, local_x,
463 local_x_prev, local_b_data, local_Jac_data);
464}
465
466std::unique_ptr<AbstractJacobianAssembler>
468{
469#ifdef _OPENMP
470 if (omp_get_thread_num() != 0)
471 {
472 OGS_FATAL(
473 "CompareJacobiansJacobianAssembler cannot be used concurrently. "
474 "Please restrict yourself to one assembly thread "
475 "(OGS_ASM_THREADS=1).");
476 }
477#endif
478
479 return std::make_unique<CompareJacobiansJacobianAssembler>(impl_, Key{});
480}
481
483 int const max_non_deformation_dofs_per_node) const
484{
485 impl_->asm1_->checkPerturbationSize(max_non_deformation_dofs_per_node);
486 impl_->asm2_->checkPerturbationSize(max_non_deformation_dofs_per_node);
487}
488
490 std::vector<int> const& non_deformation_component_ids)
491{
492 impl_->asm1_->setNonDeformationComponentIDs(non_deformation_component_ids);
493 impl_->asm2_->setNonDeformationComponentIDs(non_deformation_component_ids);
494}
495
498 std::vector<int> const& non_deformation_component_ids)
499{
500 impl_->asm1_->setNonDeformationComponentIDsNoSizeCheck(
501 non_deformation_component_ids);
502 impl_->asm2_->setNonDeformationComponentIDsNoSizeCheck(
503 non_deformation_component_ids);
504}
505
507{
508 return impl_->asm1_->needsPicardAssembly() ||
509 impl_->asm2_->needsPicardAssembly();
510}
511
513{
514 impl_->iter_ = iter;
515}
516
517std::unique_ptr<CompareJacobiansJacobianAssembler>
519{
520 // TODO doc script corner case: Parameter could occur at different
521 // locations.
523 config.checkConfigParameter("type", "CompareJacobians");
524
525 auto asm1 =
527 createJacobianAssembler(config.getConfigSubtree("jacobian_assembler"));
528
529 auto asm2 = createJacobianAssembler(
531 config.getConfigSubtree("reference_jacobian_assembler"));
532
534 auto const abs_tol = config.getConfigParameter<double>("abs_tol");
536 auto const rel_tol = config.getConfigParameter<double>("rel_tol");
537
539 auto const abs_tol_res = config.getConfigParameter<double>("abs_tol_res");
541 auto const rel_tol_res = config.getConfigParameter<double>("rel_tol_res");
542
544 auto const fail_on_error = config.getConfigParameter<bool>("fail_on_error");
545
547 auto const log_file = config.getConfigParameter<std::string>("log_file");
548
549 return std::make_unique<CompareJacobiansJacobianAssembler>(
550 std::move(asm1), std::move(asm2), abs_tol, rel_tol, abs_tol_res,
551 rel_tol_res, fail_on_error, log_file);
552}
553} // namespace ProcessLib
#define OGS_FATAL(...)
Definition Error.h:10
void WARN(fmt::format_string< Args... > fmt, Args &&... args)
Definition Logging.h:34
T getConfigParameter(std::string const &param) const
ConfigTree getConfigSubtree(std::string const &root) const
void checkConfigParameter(std::string const &param, std::string_view const value) const
void assembleWithJacobian(std::size_t const mesh_item_id, LocalAssemblerInterface &local_assembler, double const t, double const dt, std::vector< double > const &local_x, std::vector< double > const &local_x_prev, std::vector< double > &local_b_data, std::vector< double > &local_Jac_data) override
std::unique_ptr< AbstractJacobianAssembler > copy() const override
CompareJacobiansJacobianAssembler(std::unique_ptr< AbstractJacobianAssembler > &&asm1, std::unique_ptr< AbstractJacobianAssembler > &&asm2, double abs_tol_Jac, double rel_tol_Jac, double abs_tol_res, double rel_tol_res, bool fail_on_error, std::string const &log_file_path)
void setNonDeformationComponentIDsNoSizeCheck(std::vector< int > const &non_deformation_component_ids) override
std::shared_ptr< detail::CompareJacobiansJacobianAssemblerImpl > impl_
void checkPerturbationSize(int const max_non_deformation_dofs_per_node) const override
void setNonDeformationComponentIDs(std::vector< int > const &non_deformation_component_ids) override
Eigen::Map< const Vector > toVector(std::vector< double > const &data, Eigen::VectorXd::Index size)
Creates an Eigen mapped vector from the given data vector.
Eigen::Map< const Matrix > toMatrix(std::vector< double > const &data, Eigen::MatrixXd::Index rows, Eigen::MatrixXd::Index cols)
std::unique_ptr< CompareJacobiansJacobianAssembler > createCompareJacobiansJacobianAssembler(BaseLib::ConfigTree const &config)
std::unique_ptr< AbstractJacobianAssembler > createJacobianAssembler(std::optional< BaseLib::ConfigTree > const &config)
bool isSimilar(auto const &mat_or_vec1, auto const &mat_or_vec2, double const abs_tol, double const rel_tol)
auto signedAbsDiffRelDiff(auto const &mat_or_vec1, auto const &mat_or_vec2)
(Signed) absolute and (symmetric) relative difference as Eigen::Array
auto oneIfAboveThresholdElseZero(MatOrVec &&mat_or_vec, double const threshold)
void dump_py_vec(std::ostream &fh, std::string const &var, Vec const &val)
Dumps an arbitrary vector as a Python script snippet.
void dump_py(std::ostream &fh, std::string const &var, T const val)
Dumps a numeric value as a Python script snippet.
const std::string msg_fatal
Will be printed if some consistency error is detected.
bool const fail_on_error_
Whether to abort if the tolerances are exceeded.
void assembleWithJacobian(std::size_t const mesh_item_id, LocalAssemblerInterface &local_assembler, double const t, double const dt, std::vector< double > const &local_x, std::vector< double > const &local_x_prev, std::vector< double > &local_b_data, std::vector< double > &local_Jac_data)
CompareJacobiansJacobianAssemblerImpl(std::unique_ptr< AbstractJacobianAssembler > &&asm1, std::unique_ptr< AbstractJacobianAssembler > &&asm2, double abs_tol_Jac, double rel_tol_Jac, double abs_tol_res, double rel_tol_res, bool fail_on_error, std::string const &log_file_path)